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}