use std::sync::Arc; use domain::jobs::JobKind; use domain::ports::{Cipher, Mailer, SettingsRepository}; use domain::settings::{SmtpSettings, KEY_SMTP, SECRET_KEYS}; use domain::DomainError; use crate::scheduler::validate_cron; use crate::vuln_service::{DEFAULT_NOTIFY_MIN_SEVERITY, KEY_NOTIFY_MIN_SEVERITY}; use domain::vuln::Severity; pub struct SettingsService { repo: Arc, cipher: Arc, mailer: Arc, } impl SettingsService { pub fn new( repo: Arc, cipher: Arc, mailer: Arc, ) -> Self { Self { repo, cipher, mailer, } } async fn get(&self, key: &str) -> Result, DomainError> { let Some(raw) = self.repo.get(key).await? else { return Ok(None); }; if SECRET_KEYS.contains(&key) { self.cipher.decrypt(&raw).map(Some) } else { Ok(Some(raw)) } } async fn set(&self, key: &str, value: &str) -> Result<(), DomainError> { let stored = if SECRET_KEYS.contains(&key) { self.cipher.encrypt(value)? } else { value.to_string() }; self.repo.set(key, &stored).await } pub async fn smtp(&self) -> Result, DomainError> { match self.get(KEY_SMTP).await? { Some(json) => serde_json::from_str(&json) .map(Some) .map_err(|e| DomainError::Storage(e.to_string())), None => Ok(None), } } pub async fn set_smtp(&self, smtp: SmtpSettings) -> Result<(), DomainError> { smtp.validate()?; let json = serde_json::to_string(&smtp).map_err(|e| DomainError::Storage(e.to_string()))?; self.set(KEY_SMTP, &json).await } /// Send a mail to the configured recipients (or `to` if given) using the stored SMTP settings. pub async fn send_mail( &self, to: Option>, subject: &str, body: &str, ) -> Result<(), DomainError> { let smtp = self .smtp() .await? .ok_or_else(|| DomainError::Validation("smtp is not configured".into()))?; let to = to.unwrap_or_else(|| smtp.notify_to.clone()); if to.is_empty() { return Err(DomainError::Validation("no recipients configured".into())); } self.mailer.send(&smtp, &to, subject, body).await } /// Cron expression (6 fields, seconds first) for a scheduled job kind, or None if disabled. pub async fn schedule(&self, kind: JobKind) -> Result, DomainError> { match self.get(&schedule_key(kind)).await? { Some(v) if v.is_empty() => Ok(None), Some(v) => Ok(Some(v)), None => Ok(kind.default_schedule().map(String::from)), } } pub async fn set_schedule( &self, kind: JobKind, cron: Option, ) -> Result<(), DomainError> { let value = match cron.map(|c| c.trim().to_string()).filter(|c| !c.is_empty()) { Some(c) => { validate_cron(&c)?; c } None => String::new(), }; self.set(&schedule_key(kind), &value).await } } impl SettingsService { pub async fn notify_min_severity(&self) -> Result { Ok(self .get(KEY_NOTIFY_MIN_SEVERITY) .await? .map(|s| Severity::parse(&s)) .unwrap_or(DEFAULT_NOTIFY_MIN_SEVERITY)) } pub async fn set_notify_min_severity(&self, severity: Severity) -> Result<(), DomainError> { self.set(KEY_NOTIFY_MIN_SEVERITY, severity.as_str()).await } } fn schedule_key(kind: JobKind) -> String { format!("schedule.{}", kind.as_str()) }