use anyhow::Result; use serde::{Deserialize, Serialize}; use std::path::Path; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MarkBaseConfig { pub server: ServerConfig, pub postgresql: PostgreSQLConfig, pub authentication: AuthenticationConfig, pub test: TestConfig, pub logging: LoggingConfig, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ServerConfig { pub host: String, pub port: u16, pub log_level: String, pub auth_db_path: String, pub users_db_dir: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PostgreSQLConfig { pub host: String, pub port: u16, pub user: String, pub password: String, pub database: String, pub connection_pool_size: u8, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuthenticationConfig { pub bcrypt_cost: u32, pub token_validity_hours: u8, pub session_storage: String, pub max_sessions_per_user: u8, pub default_user: String, pub default_password: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TestConfig { pub users: Vec, pub password: String, pub login_test_iterations: u16, pub verify_test_iterations: u16, pub api_test_iterations: u16, pub performance_report: bool, pub output_format: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LoggingConfig { pub level: String, pub file_path: String, pub console_output: bool, pub structured_logging: bool, } impl MarkBaseConfig { pub fn load(path: &Path) -> Result { let content = std::fs::read_to_string(path)?; let config: MarkBaseConfig = toml::from_str(&content)?; Ok(config) } pub fn save(&self, path: &Path) -> Result<()> { let content = toml::to_string_pretty(self)?; std::fs::write(path, content)?; Ok(()) } pub fn default_config() -> Self { Self { server: ServerConfig { host: "127.0.0.1".to_string(), port: 11438, log_level: "info".to_string(), auth_db_path: "data/auth.sqlite".to_string(), users_db_dir: "data/users".to_string(), }, postgresql: PostgreSQLConfig { host: "127.0.0.1".to_string(), port: 5432, user: "sftpgo".to_string(), password: "sftpgo_pass_2026".to_string(), database: "sftpgo".to_string(), connection_pool_size: 5, }, authentication: AuthenticationConfig { bcrypt_cost: 10, token_validity_hours: 24, session_storage: "memory".to_string(), max_sessions_per_user: 5, default_user: "demo".to_string(), default_password: "demo123".to_string(), }, test: TestConfig { users: vec!["warren".to_string(), "momentry".to_string(), "demo".to_string()], password: "demo123".to_string(), login_test_iterations: 10, verify_test_iterations: 100, api_test_iterations: 50, performance_report: true, output_format: "markdown".to_string(), }, logging: LoggingConfig { level: "info".to_string(), file_path: "logs/markbase.log".to_string(), console_output: true, structured_logging: false, }, } } pub fn merge_env(&mut self) { if let Ok(host) = std::env::var("MB_HOST") { self.server.host = host; } if let Ok(port) = std::env::var("MB_PORT") { if let Ok(p) = port.parse() { self.server.port = p; } } if let Ok(log_level) = std::env::var("MB_LOG_LEVEL") { self.server.log_level = log_level; } if let Ok(pg_host) = std::env::var("PG_HOST") { self.postgresql.host = pg_host; } if let Ok(pg_port) = std::env::var("PG_PORT") { if let Ok(p) = pg_port.parse() { self.postgresql.port = p; } } if let Ok(pg_user) = std::env::var("PG_USER") { self.postgresql.user = pg_user; } if let Ok(pg_password) = std::env::var("PG_PASSWORD") { self.postgresql.password = pg_password; } if let Ok(pg_database) = std::env::var("PG_DATABASE") { self.postgresql.database = pg_database; } if let Ok(bcrypt_cost) = std::env::var("MB_BCRYPT_COST") { if let Ok(c) = bcrypt_cost.parse() { self.authentication.bcrypt_cost = c; } } if let Ok(token_hours) = std::env::var("MB_TOKEN_VALIDITY_HOURS") { if let Ok(h) = token_hours.parse() { self.authentication.token_validity_hours = h; } } } pub fn get(&self, key: &str) -> Option { match key { "server.host" => Some(self.server.host.clone()), "server.port" => Some(self.server.port.to_string()), "server.log_level" => Some(self.server.log_level.clone()), "server.auth_db_path" => Some(self.server.auth_db_path.clone()), "server.users_db_dir" => Some(self.server.users_db_dir.clone()), "postgresql.host" => Some(self.postgresql.host.clone()), "postgresql.port" => Some(self.postgresql.port.to_string()), "postgresql.user" => Some(self.postgresql.user.clone()), "postgresql.password" => Some(self.postgresql.password.clone()), "postgresql.database" => Some(self.postgresql.database.clone()), "postgresql.connection_pool_size" => Some(self.postgresql.connection_pool_size.to_string()), "authentication.bcrypt_cost" => Some(self.authentication.bcrypt_cost.to_string()), "authentication.token_validity_hours" => Some(self.authentication.token_validity_hours.to_string()), "authentication.session_storage" => Some(self.authentication.session_storage.clone()), "authentication.max_sessions_per_user" => Some(self.authentication.max_sessions_per_user.to_string()), "authentication.default_user" => Some(self.authentication.default_user.clone()), "authentication.default_password" => Some(self.authentication.default_password.clone()), "test.users" => Some(serde_json::to_string(&self.test.users).unwrap_or_default()), "test.password" => Some(self.test.password.clone()), "test.login_test_iterations" => Some(self.test.login_test_iterations.to_string()), "test.verify_test_iterations" => Some(self.test.verify_test_iterations.to_string()), "test.api_test_iterations" => Some(self.test.api_test_iterations.to_string()), "test.performance_report" => Some(self.test.performance_report.to_string()), "test.output_format" => Some(self.test.output_format.clone()), "logging.level" => Some(self.logging.level.clone()), "logging.file_path" => Some(self.logging.file_path.clone()), "logging.console_output" => Some(self.logging.console_output.to_string()), "logging.structured_logging" => Some(self.logging.structured_logging.to_string()), _ => None, } } pub fn set(&mut self, key: &str, value: &str) -> Result<()> { match key { "server.host" => self.server.host = value.to_string(), "server.port" => self.server.port = value.parse()?, "server.log_level" => self.server.log_level = value.to_string(), "server.auth_db_path" => self.server.auth_db_path = value.to_string(), "server.users_db_dir" => self.server.users_db_dir = value.to_string(), "postgresql.host" => self.postgresql.host = value.to_string(), "postgresql.port" => self.postgresql.port = value.parse()?, "postgresql.user" => self.postgresql.user = value.to_string(), "postgresql.password" => self.postgresql.password = value.to_string(), "postgresql.database" => self.postgresql.database = value.to_string(), "postgresql.connection_pool_size" => self.postgresql.connection_pool_size = value.parse()?, "authentication.bcrypt_cost" => self.authentication.bcrypt_cost = value.parse()?, "authentication.token_validity_hours" => self.authentication.token_validity_hours = value.parse()?, "authentication.session_storage" => self.authentication.session_storage = value.to_string(), "authentication.max_sessions_per_user" => self.authentication.max_sessions_per_user = value.parse()?, "authentication.default_user" => self.authentication.default_user = value.to_string(), "authentication.default_password" => self.authentication.default_password = value.to_string(), "test.password" => self.test.password = value.to_string(), "test.login_test_iterations" => self.test.login_test_iterations = value.parse()?, "test.verify_test_iterations" => self.test.verify_test_iterations = value.parse()?, "test.api_test_iterations" => self.test.api_test_iterations = value.parse()?, "test.performance_report" => self.test.performance_report = value.parse()?, "test.output_format" => self.test.output_format = value.to_string(), "logging.level" => self.logging.level = value.to_string(), "logging.file_path" => self.logging.file_path = value.to_string(), "logging.console_output" => self.logging.console_output = value.parse()?, "logging.structured_logging" => self.logging.structured_logging = value.parse()?, _ => return Err(anyhow::anyhow!("Invalid config key: {}", key)), } Ok(()) } pub fn validate(&self) -> Result<()> { if self.server.port < 1024 { return Err(anyhow::anyhow!("Invalid server port: {}. Must be >= 1024", self.server.port)); } if self.postgresql.port == 0 { return Err(anyhow::anyhow!("Invalid PostgreSQL port: {}", self.postgresql.port)); } if self.authentication.bcrypt_cost < 4 || self.authentication.bcrypt_cost > 31 { return Err(anyhow::anyhow!("Invalid bcrypt_cost: {}. Must be 4-31", self.authentication.bcrypt_cost)); } if self.authentication.token_validity_hours == 0 { return Err(anyhow::anyhow!("Invalid token_validity_hours: {}. Must be >= 1", self.authentication.token_validity_hours)); } if self.test.users.is_empty() { return Err(anyhow::anyhow!("test.users must not be empty")); } Ok(()) } }