Skip to main content

shared/settings/
mod.rs

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        // Intentionally not writing oobe_step, persisting 'register' in a write will cause a lot of weird issues
331        // The seperate settings set_oobe_step should be used to manage this setting instead
332
333        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
776// SAFETY: Just wrapping Arc
777unsafe 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        // SAFETY: Just wrapping Arc
790        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}