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}