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