prvng_platform/crates/control-center/src/simple_config.rs

337 lines
10 KiB
Rust

use std::path::Path;
use serde::{Deserialize, Serialize};
use tracing::info;
use crate::error::{infrastructure, ControlCenterError, Result};
/// Main configuration structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub server: ServerConfig,
pub database: DatabaseConfig,
pub jwt: JwtConfig,
pub rate_limiting: RateLimitConfig,
pub cors: CorsConfig,
pub security: SecurityConfig,
pub logging: LoggingConfig,
}
/// Server configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
pub workers: Option<usize>,
pub keep_alive: Option<u64>,
pub max_connections: Option<usize>,
}
/// Database configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseConfig {
pub url: String,
pub namespace: String,
pub database: String,
pub username: Option<String>,
pub password: Option<String>,
}
/// JWT configuration
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct JwtConfig {
#[serde(default)]
pub issuer: String,
#[serde(default)]
pub audience: String,
#[serde(default)]
pub access_token_expiration_hours: i64,
#[serde(default)]
pub refresh_token_expiration_hours: i64,
#[serde(default)]
pub private_key_pem: String,
#[serde(default)]
pub public_key_pem: String,
}
/// Rate limiting configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateLimitConfig {
pub max_requests: u32,
pub window_seconds: u64,
pub per_ip: bool,
pub global: bool,
}
/// CORS configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CorsConfig {
pub allowed_origins: Vec<String>,
pub allowed_methods: Vec<String>,
pub allowed_headers: Vec<String>,
pub expose_headers: Vec<String>,
pub max_age: u64,
pub allow_credentials: bool,
}
/// Security configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityConfig {
pub session_cleanup_interval_minutes: u64,
pub max_sessions_per_user: Option<usize>,
pub password_min_length: usize,
pub password_require_special_chars: bool,
pub password_require_numbers: bool,
pub password_require_uppercase: bool,
pub failed_login_lockout_attempts: Option<u32>,
pub failed_login_lockout_duration_minutes: Option<u64>,
}
/// Logging configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
pub level: String,
pub format: String,
pub file_path: Option<String>,
pub max_file_size: Option<String>,
pub max_files: Option<usize>,
}
impl Default for Config {
fn default() -> Self {
// Generate default JWT keys
let key_pair = crate::services::jwt::generate_rsa_key_pair()
.expect("Failed to generate default RSA key pair");
Self {
server: ServerConfig::default(),
database: DatabaseConfig::default(),
jwt: JwtConfig {
issuer: "control-center".to_string(),
audience: "control-center-api".to_string(),
access_token_expiration_hours: 1,
refresh_token_expiration_hours: 24 * 7,
private_key_pem: key_pair.private_key_pem,
public_key_pem: key_pair.public_key_pem,
},
rate_limiting: RateLimitConfig::default(),
cors: CorsConfig::default(),
security: SecurityConfig::default(),
logging: LoggingConfig::default(),
}
}
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
host: "0.0.0.0".to_string(),
port: 9012,
workers: None,
keep_alive: Some(75),
max_connections: Some(1000),
}
}
}
impl Default for DatabaseConfig {
fn default() -> Self {
Self {
url: "127.0.0.1:8000".to_string(),
namespace: "control_center".to_string(),
database: "main".to_string(),
username: Some("root".to_string()),
password: Some("root".to_string()),
}
}
}
impl Default for RateLimitConfig {
fn default() -> Self {
Self {
max_requests: 100,
window_seconds: 60,
per_ip: true,
global: false,
}
}
}
impl Default for CorsConfig {
fn default() -> Self {
Self {
allowed_origins: vec!["http://localhost:3000".to_string()],
allowed_methods: vec![
"GET".to_string(),
"POST".to_string(),
"PUT".to_string(),
"DELETE".to_string(),
"PATCH".to_string(),
"OPTIONS".to_string(),
],
allowed_headers: vec![
"content-type".to_string(),
"authorization".to_string(),
"accept".to_string(),
"x-requested-with".to_string(),
"x-session-id".to_string(),
],
expose_headers: vec![
"x-total-count".to_string(),
"x-rate-limit-remaining".to_string(),
"x-rate-limit-limit".to_string(),
"x-rate-limit-reset".to_string(),
],
max_age: 86400, // 24 hours
allow_credentials: true,
}
}
}
impl Default for SecurityConfig {
fn default() -> Self {
Self {
session_cleanup_interval_minutes: 60, // 1 hour
max_sessions_per_user: Some(5),
password_min_length: 8,
password_require_special_chars: false,
password_require_numbers: false,
password_require_uppercase: false,
failed_login_lockout_attempts: Some(5),
failed_login_lockout_duration_minutes: Some(15),
}
}
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: "info".to_string(),
format: "json".to_string(),
file_path: None,
max_file_size: Some("100MB".to_string()),
max_files: Some(10),
}
}
}
impl Config {
/// Load configuration from file
pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let content = std::fs::read_to_string(path).map_err(|e| {
ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
format!("Failed to read config file: {}", e),
))
})?;
let config: Config = toml::from_str(&content).map_err(|e| {
ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
format!("Failed to parse config file: {}", e),
))
})?;
config.validate()?;
info!("Configuration loaded successfully");
Ok(config)
}
/// Load configuration from environment variables and files
pub fn load() -> Result<Self> {
// Try to load from file first
let config_paths = [
"config.toml",
"config/config.toml",
"/etc/control-center/config.toml",
];
for path in &config_paths {
if Path::new(path).exists() {
info!("Loading configuration from: {}", path);
return Self::load_from_file(path);
}
}
// If no config file found, use defaults
info!("No config file found, using defaults");
let config = Self::default();
config.validate()?;
Ok(config)
}
/// Validate configuration
pub fn validate(&self) -> Result<()> {
// Validate server configuration
if self.server.port == 0 {
return Err(ControlCenterError::Infrastructure(
infrastructure::InfrastructureError::Configuration(
"Server port must be greater than 0".to_string(),
),
));
}
// Validate JWT configuration
if self.jwt.access_token_expiration_hours == 0 {
return Err(ControlCenterError::Infrastructure(
infrastructure::InfrastructureError::Configuration(
"JWT access token expiration must be greater than 0".to_string(),
),
));
}
// Validate security configuration
if self.security.password_min_length < 4 {
return Err(ControlCenterError::Infrastructure(
infrastructure::InfrastructureError::Configuration(
"Password minimum length must be at least 4".to_string(),
),
));
}
// Validate logging level
match self.logging.level.to_lowercase().as_str() {
"trace" | "debug" | "info" | "warn" | "error" => {}
_ => {
return Err(ControlCenterError::Infrastructure(
infrastructure::InfrastructureError::Configuration(
"Invalid logging level. Must be one of: trace, debug, info, warn, error"
.to_string(),
),
));
}
}
Ok(())
}
/// Save configuration to file
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let content = toml::to_string_pretty(self).map_err(|e| {
ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
format!("Failed to serialize config: {}", e),
))
})?;
std::fs::write(path, content).map_err(|e| {
ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
format!("Failed to write config file: {}", e),
))
})?;
Ok(())
}
/// Get server bind address
pub fn bind_address(&self) -> String {
format!("{}:{}", self.server.host, self.server.port)
}
}
/// Helper function to create a default config file
pub fn create_default_config_file<P: AsRef<Path>>(path: P) -> Result<()> {
let config = Config::default();
config.save_to_file(path)?;
info!("Default configuration file created");
Ok(())
}