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