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