337 lines
10 KiB
Rust
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(())
|
|
}
|