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 secrets;
28mod telemetry;
29mod templates;
30mod upstream_oauth2;
31
32pub use self::{
33 account::AccountConfig,
34 branding::BrandingConfig,
35 captcha::{CaptchaConfig, CaptchaServiceKind},
36 clients::{ClientAuthMethodConfig, ClientConfig, ClientsConfig},
37 database::{DatabaseConfig, PgSslMode},
38 email::{EmailConfig, EmailSmtpMode, EmailTransportKind},
39 experimental::{ExperimentalConfig, SessionLimitConfig as ExperimentalSessionLimitConfig},
40 http::{
41 BindConfig as HttpBindConfig, HttpConfig, ListenerConfig as HttpListenerConfig,
42 Resource as HttpResource, TlsConfig as HttpTlsConfig, UnixOrTcp,
43 },
44 matrix::{HomeserverKind, MatrixConfig},
45 oauth::OAuthConfig,
46 passwords::{
47 Algorithm as PasswordAlgorithm, HashingScheme as PasswordHashingScheme, PasswordsConfig,
48 },
49 policy::PolicyConfig,
50 rate_limiting::RateLimitingConfig,
51 secrets::SecretsConfig,
52 telemetry::{
53 MetricsConfig, MetricsExporterKind, Propagator, TelemetryConfig, TracingConfig,
54 TracingExporterKind,
55 },
56 templates::TemplatesConfig,
57 upstream_oauth2::{
58 ClaimsImports as UpstreamOAuth2ClaimsImports, DiscoveryMode as UpstreamOAuth2DiscoveryMode,
59 EmailImportPreference as UpstreamOAuth2EmailImportPreference,
60 ImportAction as UpstreamOAuth2ImportAction,
61 OnBackchannelLogout as UpstreamOAuth2OnBackchannelLogout,
62 OnConflict as UpstreamOAuth2OnConflict, PkceMethod as UpstreamOAuth2PkceMethod,
63 Provider as UpstreamOAuth2Provider, ResponseMode as UpstreamOAuth2ResponseMode,
64 TokenAuthMethod as UpstreamOAuth2TokenAuthMethod, UpstreamOAuth2Config,
65 },
66};
67use crate::util::ConfigurationSection;
68
69#[derive(Debug, Serialize, Deserialize, JsonSchema)]
71pub struct RootConfig {
72 #[serde(default, skip_serializing_if = "ClientsConfig::is_default")]
74 pub clients: ClientsConfig,
75
76 #[serde(default)]
78 pub http: HttpConfig,
79
80 #[serde(default)]
82 pub database: DatabaseConfig,
83
84 #[serde(default, skip_serializing_if = "TelemetryConfig::is_default")]
86 pub telemetry: TelemetryConfig,
87
88 #[serde(default, skip_serializing_if = "TemplatesConfig::is_default")]
90 pub templates: TemplatesConfig,
91
92 #[serde(default)]
94 pub email: EmailConfig,
95
96 pub secrets: SecretsConfig,
98
99 #[serde(default)]
101 pub passwords: PasswordsConfig,
102
103 pub matrix: MatrixConfig,
105
106 #[serde(default, skip_serializing_if = "PolicyConfig::is_default")]
108 pub policy: PolicyConfig,
109
110 #[serde(default, skip_serializing_if = "RateLimitingConfig::is_default")]
113 pub rate_limiting: RateLimitingConfig,
114
115 #[serde(default, skip_serializing_if = "UpstreamOAuth2Config::is_default")]
117 pub upstream_oauth2: UpstreamOAuth2Config,
118
119 #[serde(default, skip_serializing_if = "BrandingConfig::is_default")]
121 pub branding: BrandingConfig,
122
123 #[serde(default, skip_serializing_if = "CaptchaConfig::is_default")]
125 pub captcha: CaptchaConfig,
126
127 #[serde(default, skip_serializing_if = "AccountConfig::is_default")]
130 pub account: AccountConfig,
131
132 #[serde(default, skip_serializing_if = "OAuthConfig::is_default")]
134 pub oauth: OAuthConfig,
135
136 #[serde(default, skip_serializing_if = "ExperimentalConfig::is_default")]
138 pub experimental: ExperimentalConfig,
139}
140
141impl ConfigurationSection for RootConfig {
142 fn validate(
143 &self,
144 figment: &figment::Figment,
145 ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
146 self.clients.validate(figment)?;
147 self.http.validate(figment)?;
148 self.database.validate(figment)?;
149 self.telemetry.validate(figment)?;
150 self.templates.validate(figment)?;
151 self.email.validate(figment)?;
152 self.passwords.validate(figment)?;
153 self.secrets.validate(figment)?;
154 self.matrix.validate(figment)?;
155 self.policy.validate(figment)?;
156 self.rate_limiting.validate(figment)?;
157 self.upstream_oauth2.validate(figment)?;
158 self.branding.validate(figment)?;
159 self.captcha.validate(figment)?;
160 self.account.validate(figment)?;
161 self.oauth.validate(figment)?;
162 self.experimental.validate(figment)?;
163
164 Ok(())
165 }
166}
167
168impl RootConfig {
169 pub async fn generate<R>(mut rng: R) -> anyhow::Result<Self>
175 where
176 R: RngExt + Send,
177 {
178 Ok(Self {
179 clients: ClientsConfig::default(),
180 http: HttpConfig::default(),
181 database: DatabaseConfig::default(),
182 telemetry: TelemetryConfig::default(),
183 templates: TemplatesConfig::default(),
184 email: EmailConfig::default(),
185 passwords: PasswordsConfig::default(),
186 secrets: SecretsConfig::generate(&mut rng).await?,
187 matrix: MatrixConfig::generate(&mut rng),
188 policy: PolicyConfig::default(),
189 rate_limiting: RateLimitingConfig::default(),
190 upstream_oauth2: UpstreamOAuth2Config::default(),
191 branding: BrandingConfig::default(),
192 captcha: CaptchaConfig::default(),
193 account: AccountConfig::default(),
194 oauth: OAuthConfig::default(),
195 experimental: ExperimentalConfig::default(),
196 })
197 }
198
199 #[must_use]
201 pub fn test() -> Self {
202 Self {
203 clients: ClientsConfig::default(),
204 http: HttpConfig::default(),
205 database: DatabaseConfig::default(),
206 telemetry: TelemetryConfig::default(),
207 templates: TemplatesConfig::default(),
208 passwords: PasswordsConfig::default(),
209 email: EmailConfig::default(),
210 secrets: SecretsConfig::test(),
211 matrix: MatrixConfig::test(),
212 policy: PolicyConfig::default(),
213 rate_limiting: RateLimitingConfig::default(),
214 upstream_oauth2: UpstreamOAuth2Config::default(),
215 branding: BrandingConfig::default(),
216 captcha: CaptchaConfig::default(),
217 account: AccountConfig::default(),
218 oauth: OAuthConfig::default(),
219 experimental: ExperimentalConfig::default(),
220 }
221 }
222}
223
224#[expect(missing_docs)]
226#[derive(Debug, Deserialize)]
227pub struct AppConfig {
228 #[serde(default)]
229 pub http: HttpConfig,
230
231 #[serde(default)]
232 pub database: DatabaseConfig,
233
234 #[serde(default)]
235 pub templates: TemplatesConfig,
236
237 #[serde(default)]
238 pub email: EmailConfig,
239
240 pub secrets: SecretsConfig,
241
242 #[serde(default)]
243 pub passwords: PasswordsConfig,
244
245 pub matrix: MatrixConfig,
246
247 #[serde(default)]
248 pub policy: PolicyConfig,
249
250 #[serde(default)]
251 pub rate_limiting: RateLimitingConfig,
252
253 #[serde(default)]
254 pub branding: BrandingConfig,
255
256 #[serde(default)]
257 pub captcha: CaptchaConfig,
258
259 #[serde(default)]
260 pub account: AccountConfig,
261
262 #[serde(default)]
263 pub oauth: OAuthConfig,
264
265 #[serde(default)]
266 pub experimental: ExperimentalConfig,
267}
268
269impl ConfigurationSection for AppConfig {
270 fn validate(
271 &self,
272 figment: &figment::Figment,
273 ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
274 self.http.validate(figment)?;
275 self.database.validate(figment)?;
276 self.templates.validate(figment)?;
277 self.email.validate(figment)?;
278 self.passwords.validate(figment)?;
279 self.secrets.validate(figment)?;
280 self.matrix.validate(figment)?;
281 self.policy.validate(figment)?;
282 self.rate_limiting.validate(figment)?;
283 self.branding.validate(figment)?;
284 self.captcha.validate(figment)?;
285 self.account.validate(figment)?;
286 self.oauth.validate(figment)?;
287 self.experimental.validate(figment)?;
288
289 Ok(())
290 }
291}
292
293#[expect(missing_docs)]
295#[derive(Debug, Deserialize)]
296pub struct SyncConfig {
297 #[serde(default)]
298 pub database: DatabaseConfig,
299
300 pub secrets: SecretsConfig,
301
302 #[serde(default)]
303 pub clients: ClientsConfig,
304
305 #[serde(default)]
306 pub upstream_oauth2: UpstreamOAuth2Config,
307}
308
309impl ConfigurationSection for SyncConfig {
310 fn validate(
311 &self,
312 figment: &figment::Figment,
313 ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
314 self.database.validate(figment)?;
315 self.secrets.validate(figment)?;
316 self.clients.validate(figment)?;
317 self.upstream_oauth2.validate(figment)?;
318
319 Ok(())
320 }
321}
322
323#[derive(Clone, Debug)]
328pub enum ClientSecret {
329 File(Utf8PathBuf),
331
332 Value(String),
334}
335
336#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
338pub struct ClientSecretRaw {
339 #[schemars(with = "Option<String>")]
343 #[serde(skip_serializing_if = "Option::is_none")]
344 client_secret_file: Option<Utf8PathBuf>,
345
346 #[serde(skip_serializing_if = "Option::is_none")]
349 client_secret: Option<String>,
350}
351
352impl ClientSecret {
353 pub async fn value(&self) -> anyhow::Result<String> {
361 Ok(match self {
362 ClientSecret::File(path) => tokio::fs::read_to_string(path).await?.trim().to_owned(),
363 ClientSecret::Value(client_secret) => client_secret.clone(),
364 })
365 }
366}
367
368impl TryFrom<ClientSecretRaw> for Option<ClientSecret> {
369 type Error = anyhow::Error;
370
371 fn try_from(value: ClientSecretRaw) -> Result<Self, Self::Error> {
372 match (value.client_secret, value.client_secret_file) {
373 (None, None) => Ok(None),
374 (None, Some(path)) => Ok(Some(ClientSecret::File(path))),
375 (Some(client_secret), None) => Ok(Some(ClientSecret::Value(client_secret))),
376 (Some(_), Some(_)) => {
377 bail!("Cannot specify both `client_secret` and `client_secret_file`")
378 }
379 }
380 }
381}
382
383impl From<Option<ClientSecret>> for ClientSecretRaw {
384 fn from(value: Option<ClientSecret>) -> Self {
385 match value {
386 Some(ClientSecret::File(path)) => ClientSecretRaw {
387 client_secret_file: Some(path),
388 client_secret: None,
389 },
390 Some(ClientSecret::Value(client_secret)) => ClientSecretRaw {
391 client_secret_file: None,
392 client_secret: Some(client_secret),
393 },
394 None => ClientSecretRaw {
395 client_secret_file: None,
396 client_secret: None,
397 },
398 }
399 }
400}