1use anyhow::bail;
9use camino::Utf8PathBuf;
10use rand::RngExt;
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13
14mod account;
15mod branding;
16mod captcha;
17mod clients;
18mod database;
19mod email;
20mod experimental;
21mod http;
22mod matrix;
23mod oauth;
24mod passwords;
25mod policy;
26mod rate_limiting;
27mod room_invites;
28mod secrets;
29mod telemetry;
30mod templates;
31mod upstream_oauth2;
32
33pub use self::{
34 account::AccountConfig,
35 branding::BrandingConfig,
36 captcha::{CaptchaConfig, CaptchaServiceKind},
37 clients::{ClientAuthMethodConfig, ClientConfig, ClientsConfig},
38 database::{DatabaseConfig, PgSslMode},
39 email::{EmailConfig, EmailSmtpMode, EmailTransportKind},
40 experimental::{ExperimentalConfig, SessionLimitConfig as ExperimentalSessionLimitConfig},
41 http::{
42 BindConfig as HttpBindConfig, HttpConfig, ListenerConfig as HttpListenerConfig,
43 Resource as HttpResource, TlsConfig as HttpTlsConfig, UnixOrTcp,
44 },
45 matrix::{HomeserverKind, MatrixConfig},
46 oauth::OAuthConfig,
47 passwords::{
48 Algorithm as PasswordAlgorithm, HashingScheme as PasswordHashingScheme, PasswordsConfig,
49 },
50 policy::PolicyConfig,
51 rate_limiting::RateLimitingConfig,
52 room_invites::RoomInvitesConfig,
53 secrets::SecretsConfig,
54 telemetry::{
55 MetricsConfig, MetricsExporterKind, Propagator, TelemetryConfig, TracingConfig,
56 TracingExporterKind,
57 },
58 templates::TemplatesConfig,
59 upstream_oauth2::{
60 ClaimsImports as UpstreamOAuth2ClaimsImports, DiscoveryMode as UpstreamOAuth2DiscoveryMode,
61 EmailImportPreference as UpstreamOAuth2EmailImportPreference,
62 ImportAction as UpstreamOAuth2ImportAction,
63 OnBackchannelLogout as UpstreamOAuth2OnBackchannelLogout,
64 OnConflict as UpstreamOAuth2OnConflict, PkceMethod as UpstreamOAuth2PkceMethod,
65 Provider as UpstreamOAuth2Provider, ResponseMode as UpstreamOAuth2ResponseMode,
66 TokenAuthMethod as UpstreamOAuth2TokenAuthMethod, UpstreamOAuth2Config,
67 },
68};
69use crate::util::ConfigurationSection;
70
71#[derive(Debug, Serialize, Deserialize, JsonSchema)]
73pub struct RootConfig {
74 #[serde(default, skip_serializing_if = "ClientsConfig::is_default")]
76 pub clients: ClientsConfig,
77
78 #[serde(default)]
80 pub http: HttpConfig,
81
82 #[serde(default)]
84 pub database: DatabaseConfig,
85
86 #[serde(default, skip_serializing_if = "TelemetryConfig::is_default")]
88 pub telemetry: TelemetryConfig,
89
90 #[serde(default, skip_serializing_if = "TemplatesConfig::is_default")]
92 pub templates: TemplatesConfig,
93
94 #[serde(default)]
96 pub email: EmailConfig,
97
98 pub secrets: SecretsConfig,
100
101 #[serde(default)]
103 pub passwords: PasswordsConfig,
104
105 pub matrix: MatrixConfig,
107
108 #[serde(default, skip_serializing_if = "PolicyConfig::is_default")]
110 pub policy: PolicyConfig,
111
112 #[serde(default, skip_serializing_if = "RateLimitingConfig::is_default")]
115 pub rate_limiting: RateLimitingConfig,
116
117 #[serde(default, skip_serializing_if = "UpstreamOAuth2Config::is_default")]
119 pub upstream_oauth2: UpstreamOAuth2Config,
120
121 #[serde(default, skip_serializing_if = "BrandingConfig::is_default")]
123 pub branding: BrandingConfig,
124
125 #[serde(default, skip_serializing_if = "CaptchaConfig::is_default")]
127 pub captcha: CaptchaConfig,
128
129 #[serde(default, skip_serializing_if = "AccountConfig::is_default")]
132 pub account: AccountConfig,
133
134 #[serde(default, skip_serializing_if = "OAuthConfig::is_default")]
136 pub oauth: OAuthConfig,
137
138 #[serde(default, skip_serializing_if = "RoomInvitesConfig::is_default")]
140 pub room_invites: RoomInvitesConfig,
141
142 #[serde(default, skip_serializing_if = "ExperimentalConfig::is_default")]
144 pub experimental: ExperimentalConfig,
145}
146
147impl ConfigurationSection for RootConfig {
148 fn validate(
149 &self,
150 figment: &figment::Figment,
151 ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
152 self.clients.validate(figment)?;
153 self.http.validate(figment)?;
154 self.database.validate(figment)?;
155 self.telemetry.validate(figment)?;
156 self.templates.validate(figment)?;
157 self.email.validate(figment)?;
158 self.passwords.validate(figment)?;
159 self.secrets.validate(figment)?;
160 self.matrix.validate(figment)?;
161 self.policy.validate(figment)?;
162 self.rate_limiting.validate(figment)?;
163 self.upstream_oauth2.validate(figment)?;
164 self.branding.validate(figment)?;
165 self.captcha.validate(figment)?;
166 self.account.validate(figment)?;
167 self.oauth.validate(figment)?;
168 self.room_invites.validate(figment)?;
169 self.experimental.validate(figment)?;
170
171 Ok(())
172 }
173}
174
175impl RootConfig {
176 pub async fn generate<R>(mut rng: R) -> anyhow::Result<Self>
182 where
183 R: RngExt + Send,
184 {
185 Ok(Self {
186 clients: ClientsConfig::default(),
187 http: HttpConfig::default(),
188 database: DatabaseConfig::default(),
189 telemetry: TelemetryConfig::default(),
190 templates: TemplatesConfig::default(),
191 email: EmailConfig::default(),
192 passwords: PasswordsConfig::default(),
193 secrets: SecretsConfig::generate(&mut rng).await?,
194 matrix: MatrixConfig::generate(&mut rng),
195 policy: PolicyConfig::default(),
196 rate_limiting: RateLimitingConfig::default(),
197 upstream_oauth2: UpstreamOAuth2Config::default(),
198 branding: BrandingConfig::default(),
199 captcha: CaptchaConfig::default(),
200 account: AccountConfig::default(),
201 oauth: OAuthConfig::default(),
202 room_invites: RoomInvitesConfig::default(),
203 experimental: ExperimentalConfig::default(),
204 })
205 }
206
207 #[must_use]
209 pub fn test() -> Self {
210 Self {
211 clients: ClientsConfig::default(),
212 http: HttpConfig::default(),
213 database: DatabaseConfig::default(),
214 telemetry: TelemetryConfig::default(),
215 templates: TemplatesConfig::default(),
216 passwords: PasswordsConfig::default(),
217 email: EmailConfig::default(),
218 secrets: SecretsConfig::test(),
219 matrix: MatrixConfig::test(),
220 policy: PolicyConfig::default(),
221 rate_limiting: RateLimitingConfig::default(),
222 upstream_oauth2: UpstreamOAuth2Config::default(),
223 branding: BrandingConfig::default(),
224 captcha: CaptchaConfig::default(),
225 account: AccountConfig::default(),
226 oauth: OAuthConfig::default(),
227 room_invites: RoomInvitesConfig::default(),
228 experimental: ExperimentalConfig::default(),
229 }
230 }
231}
232
233#[expect(missing_docs)]
235#[derive(Debug, Deserialize)]
236pub struct AppConfig {
237 #[serde(default)]
238 pub http: HttpConfig,
239
240 #[serde(default)]
241 pub database: DatabaseConfig,
242
243 #[serde(default)]
244 pub templates: TemplatesConfig,
245
246 #[serde(default)]
247 pub email: EmailConfig,
248
249 pub secrets: SecretsConfig,
250
251 #[serde(default)]
252 pub passwords: PasswordsConfig,
253
254 pub matrix: MatrixConfig,
255
256 #[serde(default)]
257 pub policy: PolicyConfig,
258
259 #[serde(default)]
260 pub rate_limiting: RateLimitingConfig,
261
262 #[serde(default)]
263 pub branding: BrandingConfig,
264
265 #[serde(default)]
266 pub captcha: CaptchaConfig,
267
268 #[serde(default)]
269 pub account: AccountConfig,
270
271 #[serde(default)]
272 pub oauth: OAuthConfig,
273
274 #[serde(default)]
275 pub room_invites: RoomInvitesConfig,
276
277 #[serde(default)]
278 pub experimental: ExperimentalConfig,
279}
280
281impl ConfigurationSection for AppConfig {
282 fn validate(
283 &self,
284 figment: &figment::Figment,
285 ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
286 self.http.validate(figment)?;
287 self.database.validate(figment)?;
288 self.templates.validate(figment)?;
289 self.email.validate(figment)?;
290 self.passwords.validate(figment)?;
291 self.secrets.validate(figment)?;
292 self.matrix.validate(figment)?;
293 self.policy.validate(figment)?;
294 self.rate_limiting.validate(figment)?;
295 self.branding.validate(figment)?;
296 self.captcha.validate(figment)?;
297 self.account.validate(figment)?;
298 self.oauth.validate(figment)?;
299 self.room_invites.validate(figment)?;
300 self.experimental.validate(figment)?;
301
302 Ok(())
303 }
304}
305
306#[expect(missing_docs)]
308#[derive(Debug, Deserialize)]
309pub struct SyncConfig {
310 #[serde(default)]
311 pub database: DatabaseConfig,
312
313 pub secrets: SecretsConfig,
314
315 #[serde(default)]
316 pub clients: ClientsConfig,
317
318 #[serde(default)]
319 pub upstream_oauth2: UpstreamOAuth2Config,
320}
321
322impl ConfigurationSection for SyncConfig {
323 fn validate(
324 &self,
325 figment: &figment::Figment,
326 ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
327 self.database.validate(figment)?;
328 self.secrets.validate(figment)?;
329 self.clients.validate(figment)?;
330 self.upstream_oauth2.validate(figment)?;
331
332 Ok(())
333 }
334}
335
336#[derive(Clone, Debug)]
341pub enum ClientSecret {
342 File(Utf8PathBuf),
344
345 Value(String),
347}
348
349#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
351pub struct ClientSecretRaw {
352 #[schemars(with = "Option<String>")]
356 #[serde(skip_serializing_if = "Option::is_none")]
357 client_secret_file: Option<Utf8PathBuf>,
358
359 #[serde(skip_serializing_if = "Option::is_none")]
362 client_secret: Option<String>,
363}
364
365impl ClientSecret {
366 pub async fn value(&self) -> anyhow::Result<String> {
374 Ok(match self {
375 ClientSecret::File(path) => tokio::fs::read_to_string(path).await?.trim().to_owned(),
376 ClientSecret::Value(client_secret) => client_secret.clone(),
377 })
378 }
379}
380
381impl TryFrom<ClientSecretRaw> for Option<ClientSecret> {
382 type Error = anyhow::Error;
383
384 fn try_from(value: ClientSecretRaw) -> Result<Self, Self::Error> {
385 match (value.client_secret, value.client_secret_file) {
386 (None, None) => Ok(None),
387 (None, Some(path)) => Ok(Some(ClientSecret::File(path))),
388 (Some(client_secret), None) => Ok(Some(ClientSecret::Value(client_secret))),
389 (Some(_), Some(_)) => {
390 bail!("Cannot specify both `client_secret` and `client_secret_file`")
391 }
392 }
393 }
394}
395
396impl From<Option<ClientSecret>> for ClientSecretRaw {
397 fn from(value: Option<ClientSecret>) -> Self {
398 match value {
399 Some(ClientSecret::File(path)) => ClientSecretRaw {
400 client_secret_file: Some(path),
401 client_secret: None,
402 },
403 Some(ClientSecret::Value(client_secret)) => ClientSecretRaw {
404 client_secret_file: None,
405 client_secret: Some(client_secret),
406 },
407 None => ClientSecretRaw {
408 client_secret_file: None,
409 client_secret: None,
410 },
411 }
412 }
413}