diff --git a/src/config.rs b/src/config.rs index 8be07d8..4230b71 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1 +1,249 @@ -// Config module — Task 2 +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::{Path, PathBuf}; + +const MAX_ALERTS: usize = 500; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Thresholds { + pub cpu_percent: f64, + pub ram_percent: f64, + pub disk_percent: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProcessConfig { + pub name: String, + pub pattern: String, + pub memory_threshold_mb: f64, + pub enabled: bool, + pub alert_on_down: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct SmtpConfig { + pub server: String, + pub port: u16, + pub use_tls: bool, + pub username: String, + pub password: String, + pub from_email: String, + pub to_emails: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserStatusThresholds { + pub active_minutes: u64, + pub inactive_minutes: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AdminConfig { + pub username: String, + pub password_hash: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub secret_key: String, + pub port: u16, + pub check_interval_minutes: u64, + pub alert_cooldown_minutes: u64, + pub thresholds: Thresholds, + pub processes: Vec, + pub smtp: SmtpConfig, + pub amadea_log_path: String, + pub user_status_thresholds: UserStatusThresholds, + pub admin: AdminConfig, +} + +impl Default for Config { + fn default() -> Self { + Config { + secret_key: generate_secret_key(), + port: 5000, + check_interval_minutes: 1, + alert_cooldown_minutes: 30, + thresholds: Thresholds { + cpu_percent: 90.0, + ram_percent: 85.0, + disk_percent: 90.0, + }, + processes: vec![ + ProcessConfig { + name: "JVM".into(), + pattern: "java".into(), + memory_threshold_mb: 0.0, + enabled: true, + alert_on_down: true, + }, + ProcessConfig { + name: "Nginx".into(), + pattern: "nginx".into(), + memory_threshold_mb: 0.0, + enabled: false, + alert_on_down: false, + }, + ProcessConfig { + name: "Amadea Web 8 x64".into(), + pattern: "amadea".into(), + memory_threshold_mb: 0.0, + enabled: true, + alert_on_down: true, + }, + ], + smtp: SmtpConfig { + port: 587, + use_tls: true, + ..Default::default() + }, + amadea_log_path: r"C:\ProgramData\ISoft\Amadea Web 8 x64\data\logs".into(), + user_status_thresholds: UserStatusThresholds { + active_minutes: 5, + inactive_minutes: 30, + }, + admin: AdminConfig { + username: "admin".into(), + password_hash: bcrypt::hash("admin", bcrypt::DEFAULT_COST) + .unwrap_or_default(), + }, + } + } +} + +fn generate_secret_key() -> String { + use rand::Rng; + let mut rng = rand::thread_rng(); + (0..32).map(|_| format!("{:02x}", rng.gen::())).collect() +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Alert { + pub timestamp: String, + #[serde(rename = "type")] + pub alert_type: String, + pub key: String, + pub message: String, + pub value: f64, + pub threshold: f64, + pub hostname: String, +} + +pub struct ConfigManager { + config_file: PathBuf, + alerts_file: PathBuf, + pub config: Config, +} + +impl ConfigManager { + pub fn new() -> Self { + let exe_dir = std::env::current_exe() + .unwrap_or_default() + .parent() + .unwrap_or(Path::new(".")) + .to_path_buf(); + Self::new_with_dir(exe_dir.join("data")) + } + + pub fn new_with_dir(data_dir: PathBuf) -> Self { + fs::create_dir_all(&data_dir).ok(); + let config_file = data_dir.join("config.json"); + let alerts_file = data_dir.join("alerts.json"); + + let config = if config_file.exists() { + fs::read_to_string(&config_file) + .ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default() + } else { + let cfg = Config::default(); + let json = serde_json::to_string_pretty(&cfg).unwrap_or_default(); + fs::write(&config_file, &json).ok(); + cfg + }; + + ConfigManager { + config_file, + alerts_file, + config, + } + } + + pub fn save(&self) { + let json = serde_json::to_string_pretty(&self.config).unwrap_or_default(); + fs::write(&self.config_file, json).ok(); + } + + pub fn update(&mut self, config: Config) { + self.config = config; + self.save(); + } + + pub fn load_alerts(&self) -> Vec { + if !self.alerts_file.exists() { + return vec![]; + } + fs::read_to_string(&self.alerts_file) + .ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default() + } + + pub fn save_alert(&self, alert: Alert) { + let mut alerts = self.load_alerts(); + alerts.insert(0, alert); + alerts.truncate(MAX_ALERTS); + let json = serde_json::to_string_pretty(&alerts).unwrap_or_default(); + fs::write(&self.alerts_file, json).ok(); + } + + pub fn clear_alerts(&self) { + fs::write(&self.alerts_file, "[]").ok(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_config_has_expected_values() { + let cfg = Config::default(); + assert_eq!(cfg.port, 5000); + assert_eq!(cfg.thresholds.cpu_percent, 90.0); + assert_eq!(cfg.thresholds.ram_percent, 85.0); + assert_eq!(cfg.thresholds.disk_percent, 90.0); + assert_eq!(cfg.check_interval_minutes, 1); + assert_eq!(cfg.alert_cooldown_minutes, 30); + assert_eq!(cfg.processes.len(), 3); + assert_eq!(cfg.admin.username, "admin"); + } + + #[test] + fn config_serializes_and_deserializes() { + let cfg = Config::default(); + let json = serde_json::to_string(&cfg).unwrap(); + let cfg2: Config = serde_json::from_str(&json).unwrap(); + assert_eq!(cfg.port, cfg2.port); + assert_eq!(cfg.admin.username, cfg2.admin.username); + } + + #[test] + fn save_alert_truncates_at_500() { + let dir = tempfile::tempdir().unwrap(); + let cm = ConfigManager::new_with_dir(dir.path().to_path_buf()); + for i in 0..510u32 { + cm.save_alert(Alert { + timestamp: format!("2026-01-01T00:00:{:02}", i % 60), + alert_type: "threshold".into(), + key: format!("cpu_{}", i), + message: format!("msg {}", i), + value: i as f64, + threshold: 90.0, + hostname: "test".into(), + }); + } + let alerts = cm.load_alerts(); + assert_eq!(alerts.len(), 500); + } +}