Files
Infrastruktur-Monitoring-Sy…/backend/crates/application/src/settings_service.rs
Dennis Nemec 5c6e09ad10
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / ui (push) Has been cancelled
WP-20/21: contract and failing tests for vulnerability management
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 22:40:48 +02:00

126 lines
3.8 KiB
Rust

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<dyn SettingsRepository>,
cipher: Arc<dyn Cipher>,
mailer: Arc<dyn Mailer>,
}
impl SettingsService {
pub fn new(
repo: Arc<dyn SettingsRepository>,
cipher: Arc<dyn Cipher>,
mailer: Arc<dyn Mailer>,
) -> Self {
Self {
repo,
cipher,
mailer,
}
}
async fn get(&self, key: &str) -> Result<Option<String>, 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<Option<SmtpSettings>, 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<Vec<String>>,
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<Option<String>, 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<String>,
) -> 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<Severity, DomainError> {
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())
}