//! Configuration management for Veza Rust services //! //! This module provides centralized configuration management with validation //! and environment variable support. use serde::{Deserialize, Serialize}; use std::env; use tracing::{info, warn}; /// Main configuration trait for Veza services pub trait VezaConfig: Default + Clone + Serialize { /// Load configuration from environment variables fn from_env() -> crate::VezaResult { let mut config = Self::default(); config.load_from_env()?; config.validate()?; Ok(config) } /// Load configuration from environment variables fn load_from_env(&mut self) -> crate::VezaResult<()>; /// Validate configuration fn validate(&self) -> crate::VezaResult<()>; /// Get configuration as JSON for debugging fn to_json(&self) -> crate::VezaResult { Ok(serde_json::to_string_pretty(self)?) } } /// Database configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DatabaseConfig { pub url: String, pub max_connections: u32, pub min_connections: u32, pub connect_timeout: u64, pub idle_timeout: u64, pub acquire_timeout: u64, } impl Default for DatabaseConfig { fn default() -> Self { Self { url: "postgresql://veza:password@localhost:5432/veza_db".to_string(), max_connections: 20, min_connections: 5, connect_timeout: 5, idle_timeout: 600, acquire_timeout: 10, } } } impl VezaConfig for DatabaseConfig { fn load_from_env(&mut self) -> crate::VezaResult<()> { if let Ok(url) = env::var("DATABASE_URL") { self.url = url; } if let Ok(max_conn) = env::var("DATABASE_MAX_CONNECTIONS") { self.max_connections = max_conn.parse() .map_err(|_| crate::VezaError::Config("Invalid DATABASE_MAX_CONNECTIONS".to_string()))?; } if let Ok(min_conn) = env::var("DATABASE_MIN_CONNECTIONS") { self.min_connections = min_conn.parse() .map_err(|_| crate::VezaError::Config("Invalid DATABASE_MIN_CONNECTIONS".to_string()))?; } if let Ok(timeout) = env::var("DATABASE_CONNECT_TIMEOUT") { self.connect_timeout = timeout.parse() .map_err(|_| crate::VezaError::Config("Invalid DATABASE_CONNECT_TIMEOUT".to_string()))?; } if let Ok(timeout) = env::var("DATABASE_IDLE_TIMEOUT") { self.idle_timeout = timeout.parse() .map_err(|_| crate::VezaError::Config("Invalid DATABASE_IDLE_TIMEOUT".to_string()))?; } if let Ok(timeout) = env::var("DATABASE_ACQUIRE_TIMEOUT") { self.acquire_timeout = timeout.parse() .map_err(|_| crate::VezaError::Config("Invalid DATABASE_ACQUIRE_TIMEOUT".to_string()))?; } Ok(()) } fn validate(&self) -> crate::VezaResult<()> { if self.url.is_empty() { return Err(crate::VezaError::Config("Database URL cannot be empty".to_string())); } if self.max_connections < self.min_connections { return Err(crate::VezaError::Config("Max connections must be >= min connections".to_string())); } if self.max_connections == 0 { return Err(crate::VezaError::Config("Max connections must be > 0".to_string())); } Ok(()) } } /// Redis configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RedisConfig { pub url: String, pub max_connections: u32, pub connection_timeout: u64, pub command_timeout: u64, pub retry_attempts: u32, } impl Default for RedisConfig { fn default() -> Self { Self { url: "redis://localhost:6379".to_string(), max_connections: 10, connection_timeout: 5, command_timeout: 3, retry_attempts: 3, } } } impl VezaConfig for RedisConfig { fn load_from_env(&mut self) -> crate::VezaResult<()> { if let Ok(url) = env::var("REDIS_URL") { self.url = url; } if let Ok(max_conn) = env::var("REDIS_MAX_CONNECTIONS") { self.max_connections = max_conn.parse() .map_err(|_| crate::VezaError::Config("Invalid REDIS_MAX_CONNECTIONS".to_string()))?; } if let Ok(timeout) = env::var("REDIS_CONNECTION_TIMEOUT") { self.connection_timeout = timeout.parse() .map_err(|_| crate::VezaError::Config("Invalid REDIS_CONNECTION_TIMEOUT".to_string()))?; } if let Ok(timeout) = env::var("REDIS_COMMAND_TIMEOUT") { self.command_timeout = timeout.parse() .map_err(|_| crate::VezaError::Config("Invalid REDIS_COMMAND_TIMEOUT".to_string()))?; } if let Ok(retries) = env::var("REDIS_RETRY_ATTEMPTS") { self.retry_attempts = retries.parse() .map_err(|_| crate::VezaError::Config("Invalid REDIS_RETRY_ATTEMPTS".to_string()))?; } Ok(()) } fn validate(&self) -> crate::VezaResult<()> { if self.url.is_empty() { return Err(crate::VezaError::Config("Redis URL cannot be empty".to_string())); } if self.max_connections == 0 { return Err(crate::VezaError::Config("Redis max connections must be > 0".to_string())); } Ok(()) } } /// Server configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ServerConfig { pub host: String, pub port: u16, pub workers: Option, pub keep_alive: u64, pub max_connections: u32, pub timeout: u64, } impl Default for ServerConfig { fn default() -> Self { Self { host: "0.0.0.0".to_string(), port: 8080, workers: None, keep_alive: 30, max_connections: 1000, timeout: 30, } } } impl VezaConfig for ServerConfig { fn load_from_env(&mut self) -> crate::VezaResult<()> { if let Ok(host) = env::var("SERVER_HOST") { self.host = host; } if let Ok(port) = env::var("SERVER_PORT") { self.port = port.parse() .map_err(|_| crate::VezaError::Config("Invalid SERVER_PORT".to_string()))?; } if let Ok(workers) = env::var("SERVER_WORKERS") { self.workers = Some(workers.parse() .map_err(|_| crate::VezaError::Config("Invalid SERVER_WORKERS".to_string()))?); } if let Ok(keep_alive) = env::var("SERVER_KEEP_ALIVE") { self.keep_alive = keep_alive.parse() .map_err(|_| crate::VezaError::Config("Invalid SERVER_KEEP_ALIVE".to_string()))?; } if let Ok(max_conn) = env::var("SERVER_MAX_CONNECTIONS") { self.max_connections = max_conn.parse() .map_err(|_| crate::VezaError::Config("Invalid SERVER_MAX_CONNECTIONS".to_string()))?; } if let Ok(timeout) = env::var("SERVER_TIMEOUT") { self.timeout = timeout.parse() .map_err(|_| crate::VezaError::Config("Invalid SERVER_TIMEOUT".to_string()))?; } Ok(()) } fn validate(&self) -> crate::VezaResult<()> { if self.host.is_empty() { return Err(crate::VezaError::Config("Server host cannot be empty".to_string())); } if self.port == 0 { return Err(crate::VezaError::Config("Server port must be > 0".to_string())); } if self.max_connections == 0 { return Err(crate::VezaError::Config("Server max connections must be > 0".to_string())); } Ok(()) } } /// JWT configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JwtConfig { pub secret: String, pub access_token_ttl: u64, pub refresh_token_ttl: u64, pub issuer: String, pub audience: String, } impl Default for JwtConfig { fn default() -> Self { Self { secret: "your-super-secret-jwt-key".to_string(), access_token_ttl: 3600, // 1 hour refresh_token_ttl: 604800, // 7 days issuer: "veza".to_string(), audience: "veza-users".to_string(), } } } impl VezaConfig for JwtConfig { fn load_from_env(&mut self) -> crate::VezaResult<()> { if let Ok(secret) = env::var("JWT_SECRET") { self.secret = secret; } if let Ok(ttl) = env::var("JWT_ACCESS_TOKEN_TTL") { self.access_token_ttl = ttl.parse() .map_err(|_| crate::VezaError::Config("Invalid JWT_ACCESS_TOKEN_TTL".to_string()))?; } if let Ok(ttl) = env::var("JWT_REFRESH_TOKEN_TTL") { self.refresh_token_ttl = ttl.parse() .map_err(|_| crate::VezaError::Config("Invalid JWT_REFRESH_TOKEN_TTL".to_string()))?; } if let Ok(issuer) = env::var("JWT_ISSUER") { self.issuer = issuer; } if let Ok(audience) = env::var("JWT_AUDIENCE") { self.audience = audience; } Ok(()) } fn validate(&self) -> crate::VezaResult<()> { if self.secret.is_empty() { return Err(crate::VezaError::Config("JWT secret cannot be empty".to_string())); } if self.secret.len() < 32 { warn!("JWT secret is shorter than recommended 32 characters"); } if self.access_token_ttl == 0 { return Err(crate::VezaError::Config("JWT access token TTL must be > 0".to_string())); } if self.refresh_token_ttl == 0 { return Err(crate::VezaError::Config("JWT refresh token TTL must be > 0".to_string())); } if self.issuer.is_empty() { return Err(crate::VezaError::Config("JWT issuer cannot be empty".to_string())); } if self.audience.is_empty() { return Err(crate::VezaError::Config("JWT audience cannot be empty".to_string())); } Ok(()) } } /// Rate limiting configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RateLimitConfig { pub enabled: bool, pub global_limit: u32, pub global_window: u64, pub per_user_limit: u32, pub per_user_window: u64, pub per_ip_limit: u32, pub per_ip_window: u64, } impl Default for RateLimitConfig { fn default() -> Self { Self { enabled: true, global_limit: 1000, global_window: 60, per_user_limit: 100, per_user_window: 60, per_ip_limit: 60, per_ip_window: 60, } } } impl VezaConfig for RateLimitConfig { fn load_from_env(&mut self) -> crate::VezaResult<()> { if let Ok(enabled) = env::var("RATE_LIMIT_ENABLED") { self.enabled = enabled.parse() .map_err(|_| crate::VezaError::Config("Invalid RATE_LIMIT_ENABLED".to_string()))?; } if let Ok(limit) = env::var("RATE_LIMIT_GLOBAL_LIMIT") { self.global_limit = limit.parse() .map_err(|_| crate::VezaError::Config("Invalid RATE_LIMIT_GLOBAL_LIMIT".to_string()))?; } if let Ok(window) = env::var("RATE_LIMIT_GLOBAL_WINDOW") { self.global_window = window.parse() .map_err(|_| crate::VezaError::Config("Invalid RATE_LIMIT_GLOBAL_WINDOW".to_string()))?; } if let Ok(limit) = env::var("RATE_LIMIT_PER_USER_LIMIT") { self.per_user_limit = limit.parse() .map_err(|_| crate::VezaError::Config("Invalid RATE_LIMIT_PER_USER_LIMIT".to_string()))?; } if let Ok(window) = env::var("RATE_LIMIT_PER_USER_WINDOW") { self.per_user_window = window.parse() .map_err(|_| crate::VezaError::Config("Invalid RATE_LIMIT_PER_USER_WINDOW".to_string()))?; } if let Ok(limit) = env::var("RATE_LIMIT_PER_IP_LIMIT") { self.per_ip_limit = limit.parse() .map_err(|_| crate::VezaError::Config("Invalid RATE_LIMIT_PER_IP_LIMIT".to_string()))?; } if let Ok(window) = env::var("RATE_LIMIT_PER_IP_WINDOW") { self.per_ip_window = window.parse() .map_err(|_| crate::VezaError::Config("Invalid RATE_LIMIT_PER_IP_WINDOW".to_string()))?; } Ok(()) } fn validate(&self) -> crate::VezaResult<()> { if self.global_limit == 0 { return Err(crate::VezaError::Config("Global rate limit must be > 0".to_string())); } if self.global_window == 0 { return Err(crate::VezaError::Config("Global rate limit window must be > 0".to_string())); } if self.per_user_limit == 0 { return Err(crate::VezaError::Config("Per-user rate limit must be > 0".to_string())); } if self.per_user_window == 0 { return Err(crate::VezaError::Config("Per-user rate limit window must be > 0".to_string())); } if self.per_ip_limit == 0 { return Err(crate::VezaError::Config("Per-IP rate limit must be > 0".to_string())); } if self.per_ip_window == 0 { return Err(crate::VezaError::Config("Per-IP rate limit window must be > 0".to_string())); } Ok(()) } } /// Logging configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LoggingConfig { pub level: String, pub format: String, pub file: Option, pub max_size: u64, pub max_files: u32, pub compress: bool, } impl Default for LoggingConfig { fn default() -> Self { Self { level: "info".to_string(), format: "json".to_string(), file: None, max_size: 100 * 1024 * 1024, // 100MB max_files: 5, compress: true, } } } impl VezaConfig for LoggingConfig { fn load_from_env(&mut self) -> crate::VezaResult<()> { if let Ok(level) = env::var("LOG_LEVEL") { self.level = level; } if let Ok(format) = env::var("LOG_FORMAT") { self.format = format; } if let Ok(file) = env::var("LOG_FILE") { self.file = Some(file); } if let Ok(size) = env::var("LOG_MAX_SIZE") { self.max_size = size.parse() .map_err(|_| crate::VezaError::Config("Invalid LOG_MAX_SIZE".to_string()))?; } if let Ok(files) = env::var("LOG_MAX_FILES") { self.max_files = files.parse() .map_err(|_| crate::VezaError::Config("Invalid LOG_MAX_FILES".to_string()))?; } if let Ok(compress) = env::var("LOG_COMPRESS") { self.compress = compress.parse() .map_err(|_| crate::VezaError::Config("Invalid LOG_COMPRESS".to_string()))?; } Ok(()) } fn validate(&self) -> crate::VezaResult<()> { let valid_levels = ["trace", "debug", "info", "warn", "error"]; if !valid_levels.contains(&self.level.as_str()) { return Err(crate::VezaError::Config(format!("Invalid log level: {}", self.level))); } let valid_formats = ["json", "text"]; if !valid_formats.contains(&self.format.as_str()) { return Err(crate::VezaError::Config(format!("Invalid log format: {}", self.format))); } if self.max_size == 0 { return Err(crate::VezaError::Config("Log max size must be > 0".to_string())); } if self.max_files == 0 { return Err(crate::VezaError::Config("Log max files must be > 0".to_string())); } Ok(()) } } /// Load configuration from environment with validation pub fn load_config() -> crate::VezaResult { let config = T::from_env()?; info!("Configuration loaded successfully"); Ok(config) } /// Load configuration from file pub fn load_config_from_file(path: &str) -> crate::VezaResult where T: for<'de> Deserialize<'de>, { let content = std::fs::read_to_string(path) .map_err(|e| crate::VezaError::Config(format!("Failed to read config file {}: {}", path, e)))?; let config: T = serde_json::from_str(&content) .map_err(|e| crate::VezaError::Config(format!("Failed to parse config file {}: {}", path, e)))?; config.validate()?; info!("Configuration loaded from file: {}", path); Ok(config) }