1use crate::{
2 cap::CapFilesystem,
3 extensions::settings::{
4 ExtensionSettings, ExtensionSettingsDeserializer, SettingsDeserializeExt,
5 SettingsDeserializer, SettingsSerializeExt, SettingsSerializer,
6 },
7 prelude::{AsyncOptionExt, StringExt},
8};
9use compact_str::ToCompactString;
10use garde::Validate;
11use serde::{Deserialize, Serialize};
12use std::{
13 collections::{BTreeMap, HashMap},
14 ops::{Deref, DerefMut},
15 path::Path,
16 str::FromStr,
17 sync::{
18 Arc, LazyLock,
19 atomic::{AtomicUsize, Ordering},
20 },
21};
22use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard, Semaphore, SemaphorePermit};
23use utoipa::ToSchema;
24
25pub mod activity;
26pub mod app;
27pub mod ratelimits;
28pub mod server;
29pub mod user;
30pub mod webauthn;
31
32#[derive(ToSchema, Serialize, Deserialize, Clone)]
33#[serde(tag = "type", rename_all = "snake_case")]
34pub enum RouteOrderItem {
35 Route {
36 path: compact_str::CompactString,
37 },
38 Divider {
39 name: Option<compact_str::CompactString>,
40 name_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
41 },
42 Redirect {
43 name: compact_str::CompactString,
44 name_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
45 destination: compact_str::CompactString,
46 },
47}
48
49#[derive(ToSchema, Validate, Serialize, Deserialize, Clone, Copy)]
50#[serde(rename_all = "snake_case")]
51pub enum TlsMode {
52 None,
53 StartTls,
54 ImplicitTls,
55}
56
57#[derive(ToSchema, Validate, Serialize, Deserialize, Clone)]
58#[serde(tag = "type", rename_all = "snake_case")]
59pub enum StorageDriver {
60 Filesystem {
61 #[garde(length(chars, min = 1, max = 255))]
62 path: compact_str::CompactString,
63 },
64 S3 {
65 #[garde(length(chars, min = 1, max = 255), url)]
66 public_url: compact_str::CompactString,
67 #[garde(length(chars, min = 1, max = 512))]
68 access_key: compact_str::CompactString,
69 #[garde(length(chars, min = 1, max = 512))]
70 secret_key: compact_str::CompactString,
71 #[garde(length(chars, min = 1, max = 63))]
72 bucket: compact_str::CompactString,
73 #[garde(length(chars, min = 1, max = 63))]
74 region: compact_str::CompactString,
75 #[garde(length(chars, min = 1, max = 255))]
76 endpoint: compact_str::CompactString,
77 #[garde(skip)]
78 path_style: bool,
79 },
80}
81
82impl StorageDriver {
83 pub async fn get_cap_filesystem(
84 &self,
85 relative_path: impl AsRef<Path>,
86 ) -> Option<Result<CapFilesystem, std::io::Error>> {
87 match self {
88 StorageDriver::Filesystem { path } => {
89 Some(CapFilesystem::async_new(Path::new(path).join(relative_path.as_ref())).await)
90 }
91 _ => None,
92 }
93 }
94}
95
96#[derive(ToSchema, Validate, Serialize, Deserialize, Clone)]
97#[serde(tag = "type", rename_all = "snake_case")]
98pub enum MailMode {
99 None,
100 Smtp {
101 #[garde(length(chars, min = 1, max = 255))]
102 host: compact_str::CompactString,
103 #[garde(skip)]
104 port: u16,
105 #[garde(length(chars, min = 1, max = 255))]
106 username: Option<compact_str::CompactString>,
107 #[garde(length(chars, min = 1, max = 255))]
108 password: Option<compact_str::CompactString>,
109 #[garde(skip)]
110 tls_mode: TlsMode,
111 #[garde(skip)]
112 #[serde(default)]
113 skip_cert_validation: bool,
114
115 #[garde(length(chars, min = 1, max = 255), email)]
116 from_address: compact_str::CompactString,
117 #[garde(length(chars, min = 1, max = 255))]
118 from_name: Option<compact_str::CompactString>,
119 },
120 Sendmail {
121 #[garde(length(chars, min = 1, max = 255))]
122 command: compact_str::CompactString,
123
124 #[garde(length(chars, min = 1, max = 255), email)]
125 from_address: compact_str::CompactString,
126 #[garde(length(chars, min = 1, max = 255))]
127 from_name: Option<compact_str::CompactString>,
128 },
129 Filesystem {
130 #[garde(length(chars, min = 1, max = 255))]
131 path: compact_str::CompactString,
132
133 #[garde(length(chars, min = 1, max = 255), email)]
134 from_address: compact_str::CompactString,
135 #[garde(length(chars, min = 1, max = 255))]
136 from_name: Option<compact_str::CompactString>,
137 },
138}
139
140#[derive(ToSchema, Validate, Serialize, Deserialize, Clone)]
141#[serde(tag = "type", rename_all = "snake_case")]
142pub enum CaptchaProvider {
143 None,
144 Turnstile {
145 #[garde(length(chars, min = 1, max = 255))]
146 site_key: compact_str::CompactString,
147 #[garde(length(chars, min = 1, max = 255))]
148 secret_key: compact_str::CompactString,
149 },
150 Recaptcha {
151 #[garde(skip)]
152 v3: bool,
153 #[garde(length(chars, min = 1, max = 255))]
154 site_key: compact_str::CompactString,
155 #[garde(length(chars, min = 1, max = 255))]
156 secret_key: compact_str::CompactString,
157 },
158 Hcaptcha {
159 #[garde(length(chars, min = 1, max = 255))]
160 site_key: compact_str::CompactString,
161 #[garde(length(chars, min = 1, max = 255))]
162 secret_key: compact_str::CompactString,
163 },
164 FriendlyCaptcha {
165 #[garde(length(chars, min = 1, max = 255))]
166 site_key: compact_str::CompactString,
167 #[garde(length(chars, min = 1, max = 255))]
168 api_key: compact_str::CompactString,
169 },
170}
171
172impl CaptchaProvider {
173 pub fn to_public_provider<'a>(&'a self) -> PublicCaptchaProvider<'a> {
174 match &self {
175 CaptchaProvider::None => PublicCaptchaProvider::None,
176 CaptchaProvider::Turnstile { site_key, .. } => PublicCaptchaProvider::Turnstile {
177 site_key: site_key.as_str(),
178 },
179 CaptchaProvider::Recaptcha { v3, site_key, .. } => PublicCaptchaProvider::Recaptcha {
180 v3: *v3,
181 site_key: site_key.as_str(),
182 },
183 CaptchaProvider::Hcaptcha { site_key, .. } => PublicCaptchaProvider::Hcaptcha {
184 site_key: site_key.as_str(),
185 },
186 CaptchaProvider::FriendlyCaptcha { site_key, .. } => {
187 PublicCaptchaProvider::FriendlyCaptcha {
188 site_key: site_key.as_str(),
189 }
190 }
191 }
192 }
193
194 pub fn to_csp_script_src(&self) -> &'static str {
195 match self {
196 CaptchaProvider::None => "",
197 CaptchaProvider::Turnstile { .. } => "https://challenges.cloudflare.com",
198 CaptchaProvider::Recaptcha { .. } => {
199 "https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/"
200 }
201 CaptchaProvider::Hcaptcha { .. } => "https://hcaptcha.com https://*.hcaptcha.com",
202 CaptchaProvider::FriendlyCaptcha { .. } => "",
203 }
204 }
205
206 pub fn to_csp_style_src(&self) -> &'static str {
207 match self {
208 CaptchaProvider::None => "",
209 CaptchaProvider::Turnstile { .. } => "",
210 CaptchaProvider::Recaptcha { .. } => "",
211 CaptchaProvider::Hcaptcha { .. } => "https://hcaptcha.com https://*.hcaptcha.com",
212 CaptchaProvider::FriendlyCaptcha { .. } => "",
213 }
214 }
215}
216
217#[derive(ToSchema, Serialize, Deserialize, Clone)]
218#[serde(tag = "type", rename_all = "snake_case")]
219pub enum PublicCaptchaProvider<'a> {
220 None,
221 Turnstile { site_key: &'a str },
222 Recaptcha { v3: bool, site_key: &'a str },
223 Hcaptcha { site_key: &'a str },
224 FriendlyCaptcha { site_key: &'a str },
225}
226
227#[derive(ToSchema, Serialize, Deserialize)]
228pub struct AppSettings {
229 pub telemetry_uuid: Option<uuid::Uuid>,
230 #[schema(value_type = Option<String>)]
231 pub telemetry_cron_schedule: Option<cron::Schedule>,
232 pub oobe_step: Option<compact_str::CompactString>,
233
234 pub storage_driver: StorageDriver,
235 pub mail_mode: MailMode,
236 pub captcha_provider: CaptchaProvider,
237
238 #[schema(inline)]
239 pub app: app::AppSettingsApp,
240 #[schema(inline)]
241 pub webauthn: webauthn::AppSettingsWebauthn,
242 #[schema(inline)]
243 pub server: server::AppSettingsServer,
244 #[schema(inline)]
245 pub user: user::AppSettingsUser,
246 #[schema(inline)]
247 pub activity: activity::AppSettingsActivity,
248 #[schema(inline)]
249 pub ratelimits: ratelimits::AppSettingsRatelimits,
250
251 #[serde(skip)]
252 pub extensions: HashMap<&'static str, ExtensionSettings>,
253}
254
255impl AppSettings {
256 pub fn get_extension_settings<T: 'static>(
257 &self,
258 ext_identifier: &str,
259 ) -> Result<&T, anyhow::Error> {
260 let ext_settings = self
261 .extensions
262 .get(ext_identifier)
263 .ok_or_else(|| anyhow::anyhow!("failed to find extension settings"))?;
264
265 (&**ext_settings as &dyn std::any::Any)
266 .downcast_ref::<T>()
267 .ok_or_else(|| anyhow::anyhow!("failed to downcast extension settings"))
268 }
269
270 pub fn get_mut_extension_settings<T: 'static>(
271 &mut self,
272 ext_identifier: &str,
273 ) -> Result<&mut T, anyhow::Error> {
274 let ext_settings = self
275 .extensions
276 .get_mut(ext_identifier)
277 .ok_or_else(|| anyhow::anyhow!("failed to find extension settings"))?;
278
279 (&mut **ext_settings as &mut dyn std::any::Any)
280 .downcast_mut::<T>()
281 .ok_or_else(|| anyhow::anyhow!("failed to downcast extension settings"))
282 }
283
284 pub fn find_extension_settings<T: 'static>(&self) -> Result<&T, anyhow::Error> {
285 for ext_settings in self.extensions.values() {
286 if let Some(downcasted) = (&**ext_settings as &dyn std::any::Any).downcast_ref::<T>() {
287 return Ok(downcasted);
288 }
289 }
290
291 Err(anyhow::anyhow!("failed to find extension settings"))
292 }
293
294 pub fn find_mut_extension_settings<T: 'static>(&mut self) -> Result<&mut T, anyhow::Error> {
295 for ext_settings in self.extensions.values_mut() {
296 if let Some(downcasted) =
297 (&mut **ext_settings as &mut dyn std::any::Any).downcast_mut::<T>()
298 {
299 return Ok(downcasted);
300 }
301 }
302
303 Err(anyhow::anyhow!("failed to find extension settings"))
304 }
305}
306
307#[async_trait::async_trait]
308impl SettingsSerializeExt for AppSettings {
309 async fn serialize(
310 &self,
311 mut serializer: SettingsSerializer,
312 ) -> Result<SettingsSerializer, anyhow::Error> {
313 let database = serializer.database.clone();
314
315 serializer = serializer
316 .write_raw_setting(
317 "telemetry_uuid",
318 self.telemetry_uuid
319 .as_ref()
320 .map(|u| u.to_compact_string())
321 .unwrap_or_default(),
322 )
323 .write_raw_setting(
324 "telemetry_cron_schedule",
325 self.telemetry_cron_schedule
326 .as_ref()
327 .map(|s| s.to_compact_string())
328 .unwrap_or_default(),
329 );
330 match &self.storage_driver {
334 StorageDriver::Filesystem { path } => {
335 serializer = serializer
336 .write_raw_setting("storage_driver", "filesystem")
337 .write_raw_setting("storage_filesystem_path", &**path);
338 }
339 StorageDriver::S3 {
340 public_url,
341 access_key,
342 secret_key,
343 bucket,
344 region,
345 endpoint,
346 path_style,
347 } => {
348 serializer = serializer
349 .write_raw_setting("storage_driver", "s3")
350 .write_raw_setting("storage_s3_public_url", &**public_url)
351 .write_raw_setting(
352 "storage_s3_access_key",
353 base32::encode(
354 base32::Alphabet::Z,
355 &database.encrypt(access_key.clone()).await?,
356 ),
357 )
358 .write_raw_setting(
359 "storage_s3_secret_key",
360 base32::encode(
361 base32::Alphabet::Z,
362 &database.encrypt(secret_key.clone()).await?,
363 ),
364 )
365 .write_raw_setting("storage_s3_bucket", &**bucket)
366 .write_raw_setting("storage_s3_region", &**region)
367 .write_raw_setting("storage_s3_endpoint", &**endpoint)
368 .write_raw_setting("storage_s3_path_style", path_style.to_compact_string());
369 }
370 }
371
372 match &self.mail_mode {
373 MailMode::None => {
374 serializer = serializer.write_raw_setting("mail_mode", "none");
375 }
376 MailMode::Smtp {
377 host,
378 port,
379 username,
380 password,
381 tls_mode,
382 skip_cert_validation,
383 from_address,
384 from_name,
385 } => {
386 serializer = serializer
387 .write_raw_setting("mail_mode", "smtp")
388 .write_raw_setting("mail_smtp_host", &**host)
389 .write_raw_setting("mail_smtp_port", port.to_compact_string())
390 .write_raw_setting(
391 "mail_smtp_username",
392 if let Some(u) = username {
393 base32::encode(base32::Alphabet::Z, &database.encrypt(u.clone()).await?)
394 } else {
395 "".into()
396 },
397 )
398 .write_raw_setting(
399 "mail_smtp_password",
400 if let Some(p) = password {
401 base32::encode(base32::Alphabet::Z, &database.encrypt(p.clone()).await?)
402 } else {
403 "".into()
404 },
405 )
406 .write_raw_setting(
407 "mail_smtp_tls_mode",
408 match tls_mode {
409 TlsMode::None => "none",
410 TlsMode::StartTls => "starttls",
411 TlsMode::ImplicitTls => "implicit_tls",
412 },
413 )
414 .write_raw_setting(
415 "mail_smtp_skip_cert_validation",
416 skip_cert_validation.to_compact_string(),
417 )
418 .write_raw_setting("mail_smtp_from_address", &**from_address)
419 .write_raw_setting(
420 "mail_smtp_from_name",
421 from_name.clone().unwrap_or_default(),
422 );
423 }
424 MailMode::Sendmail {
425 command,
426 from_address,
427 from_name,
428 } => {
429 serializer = serializer
430 .write_raw_setting("mail_mode", "sendmail")
431 .write_raw_setting("mail_sendmail_command", &**command)
432 .write_raw_setting("mail_sendmail_from_address", &**from_address)
433 .write_raw_setting(
434 "mail_sendmail_from_name",
435 from_name.clone().unwrap_or_default(),
436 );
437 }
438 MailMode::Filesystem {
439 path,
440 from_address,
441 from_name,
442 } => {
443 serializer = serializer
444 .write_raw_setting("mail_mode", "filesystem")
445 .write_raw_setting("mail_filesystem_path", &**path)
446 .write_raw_setting("mail_filesystem_from_address", &**from_address)
447 .write_raw_setting(
448 "mail_filesystem_from_name",
449 from_name.clone().unwrap_or_default(),
450 );
451 }
452 }
453
454 match &self.captcha_provider {
455 CaptchaProvider::None => {
456 serializer = serializer.write_raw_setting("captcha_provider", "none");
457 }
458 CaptchaProvider::Turnstile {
459 site_key,
460 secret_key,
461 } => {
462 serializer = serializer
463 .write_raw_setting("captcha_provider", "turnstile")
464 .write_raw_setting("turnstile_site_key", &**site_key)
465 .write_raw_setting("turnstile_secret_key", &**secret_key);
466 }
467 CaptchaProvider::Recaptcha {
468 v3,
469 site_key,
470 secret_key,
471 } => {
472 serializer = serializer
473 .write_raw_setting("captcha_provider", "recaptcha")
474 .write_raw_setting("recaptcha_v3", v3.to_compact_string())
475 .write_raw_setting("recaptcha_site_key", &**site_key)
476 .write_raw_setting("recaptcha_secret_key", &**secret_key);
477 }
478 CaptchaProvider::Hcaptcha {
479 site_key,
480 secret_key,
481 } => {
482 serializer = serializer
483 .write_raw_setting("captcha_provider", "hcaptcha")
484 .write_raw_setting("hcaptcha_site_key", &**site_key)
485 .write_raw_setting("hcaptcha_secret_key", &**secret_key);
486 }
487 CaptchaProvider::FriendlyCaptcha { site_key, api_key } => {
488 serializer = serializer
489 .write_raw_setting("captcha_provider", "friendlycaptcha")
490 .write_raw_setting("friendlycaptcha_site_key", &**site_key)
491 .write_raw_setting("friendlycaptcha_api_key", &**api_key);
492 }
493 }
494
495 serializer = serializer
496 .nest("app", &self.app)
497 .await?
498 .nest("webauthn", &self.webauthn)
499 .await?
500 .nest("server", &self.server)
501 .await?
502 .nest("user", &self.user)
503 .await?
504 .nest("activity", &self.activity)
505 .await?
506 .nest("ratelimits", &self.ratelimits)
507 .await?;
508
509 for (ext_identifier, ext_settings) in self.extensions.iter() {
510 serializer = serializer.nest(ext_identifier, ext_settings).await?;
511 }
512
513 Ok(serializer)
514 }
515}
516
517pub(crate) static SETTINGS_DESER_EXTENSIONS: LazyLock<
518 parking_lot::RwLock<HashMap<&'static str, ExtensionSettingsDeserializer>>,
519> = LazyLock::new(|| parking_lot::RwLock::new(HashMap::new()));
520
521pub struct AppSettingsDeserializer;
522
523#[async_trait::async_trait]
524impl SettingsDeserializeExt for AppSettingsDeserializer {
525 async fn deserialize_boxed(
526 &self,
527 mut deserializer: SettingsDeserializer<'_>,
528 ) -> Result<ExtensionSettings, anyhow::Error> {
529 let mut extensions = HashMap::new();
530
531 let extension_deserializers = {
532 let ext_deser_lock = SETTINGS_DESER_EXTENSIONS.read();
533
534 ext_deser_lock
535 .iter()
536 .map(|(k, v)| (*k, v.clone()))
537 .collect::<Vec<_>>()
538 };
539
540 for (ext_identifier, ext_deserializer) in extension_deserializers {
541 let settings_deserializer = SettingsDeserializer::new(
542 deserializer.database.clone(),
543 deserializer.nest_prefix(ext_identifier),
544 deserializer.settings,
545 );
546
547 let ext_settings = ext_deserializer
548 .deserialize_boxed(settings_deserializer)
549 .await?;
550 extensions.insert(ext_identifier, ext_settings);
551 }
552
553 Ok(Box::new(AppSettings {
554 telemetry_uuid: deserializer
555 .take_raw_setting("telemetry_uuid")
556 .and_then(|s| uuid::Uuid::from_str(&s).ok()),
557 telemetry_cron_schedule: deserializer
558 .take_raw_setting("telemetry_cron_schedule")
559 .and_then(|s| cron::Schedule::from_str(&s).ok()),
560 oobe_step: match deserializer.take_raw_setting("oobe_step") {
561 Some(step) if step.is_empty() => None,
562 Some(step) => Some(step),
563 None => {
564 if crate::models::user::User::count(&deserializer.database).await > 0 {
565 None
566 } else {
567 Some("register".into())
568 }
569 }
570 },
571 storage_driver: match deserializer.take_raw_setting("storage_driver").as_deref() {
572 Some("s3") => StorageDriver::S3 {
573 public_url: deserializer
574 .take_raw_setting("storage_s3_public_url")
575 .unwrap_or_else(|| "https://your-s3-bucket.s3.amazonaws.com".into()),
576 access_key: if let Some(access_key) =
577 deserializer.take_raw_setting("storage_s3_access_key")
578 {
579 base32::decode(base32::Alphabet::Z, &access_key)
580 .map(|encrypted| deserializer.database.decrypt(encrypted))
581 .awaited()
582 .await
583 .transpose()?
584 .unwrap_or_else(|| "your-access-key".into())
585 } else {
586 "your-access-key".into()
587 },
588 secret_key: if let Some(secret_key) =
589 deserializer.take_raw_setting("storage_s3_secret_key")
590 {
591 base32::decode(base32::Alphabet::Z, &secret_key)
592 .map(|encrypted| deserializer.database.decrypt(encrypted))
593 .awaited()
594 .await
595 .transpose()?
596 .unwrap_or_else(|| "your-secret-key".into())
597 } else {
598 "your-secret-key".into()
599 },
600 bucket: deserializer
601 .take_raw_setting("storage_s3_bucket")
602 .unwrap_or_else(|| "your-s3-bucket".into()),
603 region: deserializer
604 .take_raw_setting("storage_s3_region")
605 .unwrap_or_else(|| "us-east-1".into()),
606 endpoint: deserializer
607 .take_raw_setting("storage_s3_endpoint")
608 .unwrap_or_else(|| "https://s3.amazonaws.com".into()),
609 path_style: deserializer
610 .take_raw_setting("storage_s3_path_style")
611 .map(|s| s == "true")
612 .unwrap_or(false),
613 },
614 _ => StorageDriver::Filesystem {
615 path: deserializer
616 .take_raw_setting("storage_filesystem_path")
617 .unwrap_or_else(|| {
618 if std::env::consts::OS == "windows" {
619 "C:\\calagopus_data".into()
620 } else {
621 "/var/lib/calagopus".into()
622 }
623 }),
624 },
625 },
626 mail_mode: match deserializer.take_raw_setting("mail_mode").as_deref() {
627 Some("smtp") => MailMode::Smtp {
628 host: deserializer
629 .take_raw_setting("mail_smtp_host")
630 .unwrap_or_else(|| "smtp.example.com".into()),
631 port: deserializer
632 .take_raw_setting("mail_smtp_port")
633 .and_then(|s| s.parse().ok())
634 .unwrap_or(587),
635 username: if let Some(username) = deserializer
636 .take_raw_setting("mail_smtp_username")
637 .and_then(|s| s.into_optional())
638 {
639 base32::decode(base32::Alphabet::Z, &username)
640 .map(|encrypted| deserializer.database.decrypt(encrypted))
641 .awaited()
642 .await
643 .transpose()?
644 } else {
645 None
646 },
647 password: if let Some(password) = deserializer
648 .take_raw_setting("mail_smtp_password")
649 .and_then(|s| s.into_optional())
650 {
651 base32::decode(base32::Alphabet::Z, &password)
652 .map(|encrypted| deserializer.database.decrypt(encrypted))
653 .awaited()
654 .await
655 .transpose()?
656 } else {
657 None
658 },
659 tls_mode: match deserializer
660 .take_raw_setting("mail_smtp_tls_mode")
661 .as_deref()
662 {
663 Some("none") => TlsMode::None,
664 Some("starttls") => TlsMode::StartTls,
665 Some("implicit_tls") => TlsMode::ImplicitTls,
666 _ => TlsMode::StartTls,
667 },
668 skip_cert_validation: deserializer
669 .take_raw_setting("mail_smtp_skip_cert_validation")
670 .map(|s| s == "true")
671 .unwrap_or(false),
672 from_address: deserializer
673 .take_raw_setting("mail_smtp_from_address")
674 .unwrap_or_else(|| "[email protected]".into()),
675 from_name: deserializer.take_raw_setting("mail_smtp_from_name"),
676 },
677 Some("sendmail") => MailMode::Sendmail {
678 command: deserializer
679 .take_raw_setting("mail_sendmail_command")
680 .unwrap_or_else(|| "sendmail".into()),
681 from_address: deserializer
682 .take_raw_setting("mail_sendmail_from_address")
683 .unwrap_or_else(|| "[email protected]".into()),
684 from_name: deserializer.take_raw_setting("mail_sendmail_from_name"),
685 },
686 Some("filesystem") => MailMode::Filesystem {
687 path: deserializer
688 .take_raw_setting("mail_filesystem_path")
689 .unwrap_or_else(|| "/var/lib/calagopus/mail".into()),
690 from_address: deserializer
691 .take_raw_setting("mail_filesystem_from_address")
692 .unwrap_or_else(|| "[email protected]".into()),
693 from_name: deserializer.take_raw_setting("mail_filesystem_from_name"),
694 },
695 _ => MailMode::None,
696 },
697 captcha_provider: match deserializer.take_raw_setting("captcha_provider").as_deref() {
698 Some("turnstile") => CaptchaProvider::Turnstile {
699 site_key: deserializer
700 .take_raw_setting("turnstile_site_key")
701 .unwrap_or_default(),
702 secret_key: deserializer
703 .take_raw_setting("turnstile_secret_key")
704 .unwrap_or_default(),
705 },
706 Some("recaptcha") => CaptchaProvider::Recaptcha {
707 v3: deserializer
708 .take_raw_setting("recaptcha_v3")
709 .map(|s| s == "true")
710 .unwrap_or(false),
711 site_key: deserializer
712 .take_raw_setting("recaptcha_site_key")
713 .unwrap_or_default(),
714 secret_key: deserializer
715 .take_raw_setting("recaptcha_secret_key")
716 .unwrap_or_default(),
717 },
718 Some("hcaptcha") => CaptchaProvider::Hcaptcha {
719 site_key: deserializer
720 .take_raw_setting("hcaptcha_site_key")
721 .unwrap_or_default(),
722 secret_key: deserializer
723 .take_raw_setting("hcaptcha_secret_key")
724 .unwrap_or_default(),
725 },
726 Some("friendlycaptcha") => CaptchaProvider::FriendlyCaptcha {
727 site_key: deserializer
728 .take_raw_setting("friendlycaptcha_site_key")
729 .unwrap_or_default(),
730 api_key: deserializer
731 .take_raw_setting("friendlycaptcha_api_key")
732 .unwrap_or_default(),
733 },
734 _ => CaptchaProvider::None,
735 },
736 app: deserializer
737 .nest("app", &app::AppSettingsAppDeserializer)
738 .await?,
739 webauthn: deserializer
740 .nest("webauthn", &webauthn::AppSettingsWebauthnDeserializer)
741 .await?,
742 server: deserializer
743 .nest("server", &server::AppSettingsServerDeserializer)
744 .await?,
745 user: deserializer
746 .nest("user", &user::AppSettingsUserDeserializer)
747 .await?,
748 activity: deserializer
749 .nest("activity", &activity::AppSettingsActivityDeserializer)
750 .await?,
751 ratelimits: deserializer
752 .nest("ratelimits", &ratelimits::AppSettingsRatelimitsDeserializer)
753 .await?,
754 extensions,
755 }))
756 }
757}
758
759pub struct SettingsReadGuard<'a> {
760 settings: RwLockReadGuard<'a, SettingsBuffer>,
761}
762
763impl Deref for SettingsReadGuard<'_> {
764 type Target = AppSettings;
765
766 fn deref(&self) -> &Self::Target {
767 &self.settings.settings
768 }
769}
770
771const INDEX_HTML: &str = include_str!("../../../frontend/dist/index.html");
772
773#[derive(Clone)]
774pub struct ArcedIndexHtml(Arc<String>);
775
776unsafe impl arc_swap::RefCnt for ArcedIndexHtml {
778 type Base = String;
779
780 fn into_ptr(me: Self) -> *mut Self::Base {
781 arc_swap::RefCnt::into_ptr(me.0)
782 }
783
784 fn as_ptr(me: &Self) -> *mut Self::Base {
785 arc_swap::RefCnt::as_ptr(&me.0)
786 }
787
788 unsafe fn from_ptr(ptr: *const Self::Base) -> Self {
789 Self(unsafe { arc_swap::RefCnt::from_ptr(ptr) })
791 }
792}
793
794impl AsRef<str> for ArcedIndexHtml {
795 fn as_ref(&self) -> &str {
796 &self.0
797 }
798}
799
800impl AsRef<[u8]> for ArcedIndexHtml {
801 fn as_ref(&self) -> &[u8] {
802 self.0.as_bytes()
803 }
804}
805
806pub struct SettingsWriteGuard<'a> {
807 parent: &'a Settings,
808 settings: Option<RwLockWriteGuard<'a, SettingsBuffer>>,
809 _writer_token: SemaphorePermit<'a>,
810}
811
812impl<'a> SettingsWriteGuard<'a> {
813 pub async fn save(mut self) -> Result<(), crate::database::DatabaseError> {
814 let mut settings_guard = self.settings.take().ok_or_else(|| {
815 crate::database::DatabaseError::Any(anyhow::anyhow!(
816 "settings have already been saved or dropped"
817 ))
818 })?;
819
820 let (keys, values) = SettingsSerializeExt::serialize(
821 &settings_guard.settings,
822 SettingsSerializer::new(self.parent.database.clone(), ""),
823 )
824 .await?
825 .into_parts();
826
827 sqlx::query!(
828 "INSERT INTO settings (key, value)
829 SELECT * FROM UNNEST($1::text[], $2::text[])
830 ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value",
831 &keys as &[compact_str::CompactString],
832 &values as &[compact_str::CompactString]
833 )
834 .execute(self.parent.database.write())
835 .await?;
836
837 settings_guard.expires = std::time::Instant::now() + std::time::Duration::from_secs(60);
838
839 let _ = self
840 .parent
841 .cached_index
842 .fetch_update(Ordering::Release, Ordering::Relaxed, |i| Some((i + 1) % 2));
843
844 let mut environment = minijinja::Environment::new();
845 environment.set_auto_escape_callback(|_| minijinja::AutoEscape::Html);
846 environment.add_global(
847 "settings",
848 minijinja::Value::from_serialize(&settings_guard.settings),
849 );
850
851 let rendered_index_html = environment
852 .render_str(INDEX_HTML, minijinja::context! {})
853 .map_err(anyhow::Error::new)?;
854 self.parent
855 .rendered_index_html
856 .store(ArcedIndexHtml(Arc::new(rendered_index_html)));
857
858 Ok(())
859 }
860
861 pub fn censored(&self) -> serde_json::Value {
862 let settings = self.settings.as_ref().expect("settings have been dropped");
863 let mut json = serde_json::to_value(&settings.settings).unwrap();
864
865 fn censor_values(key: &str, value: &mut serde_json::Value) {
866 match value {
867 serde_json::Value::Object(map) => {
868 for (k, v) in map.iter_mut() {
869 censor_values(k, v);
870 }
871 }
872 serde_json::Value::String(s) if key.contains("password") => {
873 *s = "*".repeat(s.len());
874 }
875 _ => {}
876 }
877 }
878
879 censor_values("", &mut json);
880
881 json
882 }
883}
884
885impl Deref for SettingsWriteGuard<'_> {
886 type Target = AppSettings;
887
888 fn deref(&self) -> &Self::Target {
889 &self
890 .settings
891 .as_ref()
892 .expect("settings have been dropped")
893 .settings
894 }
895}
896
897impl DerefMut for SettingsWriteGuard<'_> {
898 fn deref_mut(&mut self) -> &mut Self::Target {
899 &mut self
900 .settings
901 .as_mut()
902 .expect("settings have been dropped")
903 .settings
904 }
905}
906
907struct SettingsBuffer {
908 settings: AppSettings,
909 expires: std::time::Instant,
910}
911
912pub struct Settings {
913 cached: [RwLock<SettingsBuffer>; 2],
914 cached_index: AtomicUsize,
915 write_serializing: Semaphore,
916
917 rendered_index_html: arc_swap::ArcSwapAny<ArcedIndexHtml>,
918
919 database: Arc<crate::database::Database>,
920}
921
922impl Settings {
923 async fn fetch_settings(
924 database: &Arc<crate::database::Database>,
925 ) -> Result<AppSettings, anyhow::Error> {
926 let rows = sqlx::query!("SELECT * FROM settings")
927 .fetch_all(database.read())
928 .await?;
929
930 let mut map = HashMap::new();
931 for row in rows {
932 map.insert(row.key.into(), row.value.into());
933 }
934
935 let boxed = SettingsDeserializeExt::deserialize_boxed(
936 &AppSettingsDeserializer,
937 SettingsDeserializer::new(database.clone(), "", &mut map),
938 )
939 .await?;
940
941 Ok(*(boxed as Box<dyn std::any::Any>)
942 .downcast::<AppSettings>()
943 .expect("settings has invalid type"))
944 }
945
946 pub async fn new(database: Arc<crate::database::Database>) -> Result<Self, anyhow::Error> {
947 let (s1, s2) = tokio::try_join!(
948 Self::fetch_settings(&database),
949 Self::fetch_settings(&database)
950 )?;
951
952 let mut environment = minijinja::Environment::new();
953 environment.set_auto_escape_callback(|_| minijinja::AutoEscape::Html);
954 environment.add_global("settings", minijinja::Value::from_serialize(&s1));
955
956 let rendered_index_html = environment.render_str(INDEX_HTML, minijinja::context! {})?;
957 let rendered_index_html =
958 arc_swap::ArcSwapAny::new(ArcedIndexHtml(Arc::new(rendered_index_html)));
959
960 Ok(Self {
961 cached: [
962 RwLock::new(SettingsBuffer {
963 settings: s1,
964 expires: std::time::Instant::now() + std::time::Duration::from_secs(60),
965 }),
966 RwLock::new(SettingsBuffer {
967 settings: s2,
968 expires: std::time::Instant::now() + std::time::Duration::from_secs(60),
969 }),
970 ],
971 cached_index: AtomicUsize::new(0),
972 write_serializing: Semaphore::new(1),
973 rendered_index_html,
974 database,
975 })
976 }
977
978 #[inline]
979 pub fn get_rendered_index_html(&self) -> ArcedIndexHtml {
980 self.rendered_index_html.load_full()
981 }
982
983 pub async fn get(&self) -> Result<SettingsReadGuard<'_>, anyhow::Error> {
984 let now = std::time::Instant::now();
985
986 let index = self.cached_index.load(Ordering::Acquire);
987 {
988 let guard = self.cached[index % 2].read().await;
989 if now < guard.expires {
990 return Ok(SettingsReadGuard { settings: guard });
991 }
992 }
993
994 let _write_token = self.write_serializing.acquire().await?;
995
996 let index = self.cached_index.load(Ordering::Acquire);
997 let current_buffer = &self.cached[index % 2];
998
999 if now < current_buffer.read().await.expires {
1000 return Ok(SettingsReadGuard {
1001 settings: current_buffer.read().await,
1002 });
1003 }
1004
1005 let start = std::time::Instant::now();
1006 tracing::info!("settings cache expired, reloading from database");
1007
1008 let settings = Self::fetch_settings(&self.database).await?;
1009 let mut guard = current_buffer.write().await;
1010 guard.settings = settings;
1011 guard.expires = now + std::time::Duration::from_secs(60);
1012
1013 drop(guard);
1014
1015 tracing::info!(
1016 "reloaded settings from database in {} ms",
1017 start.elapsed().as_millis()
1018 );
1019
1020 Ok(SettingsReadGuard {
1021 settings: current_buffer.read().await,
1022 })
1023 }
1024
1025 pub async fn get_as<F: FnOnce(&AppSettings) -> T, T>(&self, f: F) -> Result<T, anyhow::Error> {
1026 let settings = self.get().await?;
1027 Ok(f(&settings))
1028 }
1029
1030 pub async fn get_webauthn(&self) -> Result<webauthn_rs::Webauthn, anyhow::Error> {
1031 let settings = self.get().await?;
1032
1033 Ok(webauthn_rs::WebauthnBuilder::new(
1034 &settings.webauthn.rp_id,
1035 &settings.webauthn.rp_origin.parse()?,
1036 )?
1037 .rp_name(&settings.app.name)
1038 .build()?)
1039 }
1040
1041 pub async fn get_mut(&self) -> Result<SettingsWriteGuard<'_>, anyhow::Error> {
1042 let writer_token = self.write_serializing.acquire().await?;
1043
1044 let active_index = self.cached_index.load(Ordering::Acquire);
1045 let inactive_index = (active_index + 1) % 2;
1046 let inactive_buffer = &self.cached[inactive_index];
1047
1048 let mut guard = inactive_buffer.write().await;
1049
1050 guard.settings = Self::fetch_settings(&self.database).await?;
1051
1052 Ok(SettingsWriteGuard {
1053 parent: self,
1054 settings: Some(guard),
1055 _writer_token: writer_token,
1056 })
1057 }
1058
1059 pub async fn set_oobe_step(
1060 &self,
1061 oobe_step: Option<compact_str::CompactString>,
1062 ) -> Result<(), crate::database::DatabaseError> {
1063 sqlx::query(
1064 "INSERT INTO settings (key, value) VALUES ('oobe_step', $1)
1065 ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value",
1066 )
1067 .bind(oobe_step.map(|s| s.to_string()).unwrap_or_default())
1068 .execute(self.database.write())
1069 .await?;
1070
1071 self.invalidate_cache().await;
1072
1073 Ok(())
1074 }
1075
1076 pub async fn invalidate_cache(&self) {
1077 let Ok(_lock) = self.write_serializing.acquire().await else {
1078 return;
1079 };
1080 let index = self.cached_index.load(Ordering::Acquire);
1081 self.cached[index % 2].write().await.expires = std::time::Instant::now();
1082 }
1083}