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