use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::fs::OpenOptions; use std::io::Write; use std::path::PathBuf; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuditLogEntry { timestamp: DateTime, operation: String, config_type: String, key: String, old_value: String, new_value: String, user: String, ip_address: Option, } pub struct AuditLogger { log_path: PathBuf, } impl AuditLogger { pub fn new(log_path: &str) -> Self { Self { log_path: PathBuf::from(log_path), } } pub fn default() -> Self { Self::new("logs/config_audit.log") } pub fn log_config_change( &self, config_type: &str, key: &str, old_value: &str, new_value: &str, user: &str, ip_address: Option<&str>, ) -> anyhow::Result<()> { let entry = AuditLogEntry { timestamp: Utc::now(), operation: "edit".to_string(), config_type: config_type.to_string(), key: key.to_string(), old_value: old_value.to_string(), new_value: new_value.to_string(), user: user.to_string(), ip_address: ip_address.map(|s| s.to_string()), }; self.write_entry(&entry)?; log::info!( "Audit: {} config {} changed from '{}' to '{}' by {}", config_type, key, old_value, new_value, user ); Ok(()) } pub fn log_config_validate( &self, config_type: &str, result: &str, user: &str, ip_address: Option<&str>, ) -> anyhow::Result<()> { let entry = AuditLogEntry { timestamp: Utc::now(), operation: "validate".to_string(), config_type: config_type.to_string(), key: "validation".to_string(), old_value: "".to_string(), new_value: result.to_string(), user: user.to_string(), ip_address: ip_address.map(|s| s.to_string()), }; self.write_entry(&entry)?; Ok(()) } fn write_entry(&self, entry: &AuditLogEntry) -> anyhow::Result<()> { // Create logs directory if not exists if let Some(parent) = self.log_path.parent() { if !parent.exists() { std::fs::create_dir_all(parent)?; } } // Open file in append mode let mut file = OpenOptions::new() .create(true) .append(true) .open(&self.log_path)?; // Write JSON line let json = serde_json::to_string(entry)?; file.write_all(json.as_bytes())?; file.write_all(b"\n")?; Ok(()) } pub fn read_recent_entries(&self, limit: usize) -> anyhow::Result> { if !self.log_path.exists() { return Ok(Vec::new()); } let content = std::fs::read_to_string(&self.log_path)?; let entries: Vec = content .lines() .filter_map(|line| serde_json::from_str(line).ok()) .collect(); // Return last N entries let start = if entries.len() > limit { entries.len() - limit } else { 0 }; Ok(entries[start..].to_vec()) } }