app_settings_service.rs
415 lignes · 13308 octets
use crate::error::AppError; use crate::models::app_settings; use chrono::Utc; use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, Set}; use std::env; use uuid::Uuid; /// Service for managing application settings /// /// This service provides business logic for global application settings /// stored as key-value pairs with traceability. pub struct AppSettingsService; impl AppSettingsService { /// Helper: get boolean setting with fallback keys (first found wins) pub async fn get_bool_with_fallback( db: &DatabaseConnection, keys: &[&str], default: bool, ) -> Result<bool, AppError> { for key in keys { if let Some(setting) = Self::get_setting(db, key).await? { return Ok(setting.value.parse::<bool>().unwrap_or(default)); } } Ok(default) } /// Helper: get boolean setting with default fallback pub async fn get_bool( db: &DatabaseConnection, key: &str, default: bool, ) -> Result<bool, AppError> { if let Some(setting) = Self::get_setting(db, key).await? { return Ok(setting.value.parse::<bool>().unwrap_or(default)); } Ok(default) } /// Get a setting by key /// /// # Arguments /// /// * `db` - Database connection /// * `key` - Setting key /// /// # Returns /// /// Setting if found, None otherwise pub async fn get_setting( db: &DatabaseConnection, key: &str, ) -> Result<Option<app_settings::Model>, AppError> { let setting = app_settings::Entity::find() .filter(app_settings::Column::Key.eq(key)) .one(db) .await?; Ok(setting) } /// Set or update a setting /// /// # Arguments /// /// * `db` - Database connection /// * `key` - Setting key /// * `value` - Setting value /// * `updated_by` - UUID of the user who updated this setting /// /// # Returns /// /// The created or updated setting model pub async fn set_setting( db: &DatabaseConnection, key: String, value: String, updated_by: Option<Uuid>, ) -> Result<app_settings::Model, AppError> { let existing = app_settings::Entity::find() .filter(app_settings::Column::Key.eq(&key)) .one(db) .await?; let now = Utc::now(); if let Some(setting) = existing { // Update existing setting let mut active_setting: app_settings::ActiveModel = setting.into(); active_setting.value = Set(value); active_setting.updated_by = Set(updated_by); active_setting.updated_at = Set(now); let result = active_setting.update(db).await?; Ok(result) } else { // Create new setting let setting = app_settings::ActiveModel { id: Set(Uuid::new_v4()), key: Set(key), value: Set(value), description: Set(None), updated_by: Set(updated_by), updated_at: Set(now), }; let result = setting.insert(db).await?; Ok(result) } } /// Get all settings /// /// # Arguments /// /// * `db` - Database connection /// /// # Returns /// /// Vector of all settings pub async fn get_all_settings( db: &DatabaseConnection, ) -> Result<Vec<app_settings::Model>, AppError> { let settings = app_settings::Entity::find().all(db).await?; Ok(settings) } /// Get server configuration from AppSettings. /// /// This method reads configuration from AppSettings using the standardized key format /// `{app_id}:server_config:{config_name}`. Returns the first matching configuration found. /// /// # Arguments /// /// * `db` - Database connection /// * `config_name` - Name of the configuration (e.g., "max_body_size") /// /// # Returns /// /// `Ok(Some(value))` if configuration found, `Ok(None)` if not found /// /// # Errors /// /// Returns `AppError` if database query fails /// /// # Example /// /// ```rust,ignore /// let max_size = AppSettingsService::get_server_config(&db, "max_body_size").await?; /// ``` pub async fn get_server_config( db: &DatabaseConnection, config_name: &str, ) -> Result<Option<String>, AppError> { // Search for all settings matching the pattern *:server_config:{config_name} // Use SQL LIKE pattern: %:server_config:{config_name} let pattern = format!("%:server_config:{}", config_name); let settings = app_settings::Entity::find() .filter(app_settings::Column::Key.like(&pattern)) .all(db) .await .map_err(AppError::Database)?; // Return the first matching configuration Ok(settings.into_iter().next().map(|s| s.value)) } /// Set server configuration in AppSettings. /// /// Stores configuration using the standardized key format /// `{app_id}:server_config:{config_name}`. /// /// # Arguments /// /// * `db` - Database connection /// * `app_id` - ID of the application /// * `config_name` - Name of the configuration (e.g., "max_body_size") /// * `value` - Configuration value (as string) /// /// # Returns /// /// The created or updated setting model /// /// # Errors /// /// Returns `AppError` if database operation fails /// /// # Example /// /// ```rust,ignore /// AppSettingsService::set_server_config(&db, "files", "max_body_size", "10737418240").await?; /// ``` pub async fn set_server_config( db: &DatabaseConnection, app_id: &str, config_name: &str, value: String, ) -> Result<app_settings::Model, AppError> { let key = format!("{}:server_config:{}", app_id, config_name); Self::set_setting(db, key, value, None).await } /// Initialize default settings /// /// This function creates default application settings if they don't exist. /// It's idempotent and should be called at application startup. /// /// # Arguments /// /// * `db` - Database connection /// /// # Returns /// /// Ok(()) if successful pub async fn initialize_default_settings(db: &DatabaseConnection) -> Result<(), AppError> { // Get app domain from environment or use default let app_domain = env::var("APP_DOMAIN").unwrap_or_else(|_| "localhost".to_string()); // List of default settings to initialize let default_settings = vec![ ( "app_domain", app_domain.as_str(), Some("Application domain name"), ), ( "allow_registration", "false", Some("Allow user registration"), ), ( "validation_email_required", "true", Some("Require email validation to login"), ), ( "audit_log_level", "INFO", Some("Audit log level: DEBUG, INFO, WARN, ERROR"), ), // Audience measurement. The script URL is deliberately absent: // it feeds the CSP, which is frozen at startup, so it stays an // environment/builder setting. See `config::analytics`. ( "analytics_enabled", "false", Some("Enable the audience-measurement script"), ), ( "analytics_site_id", "", Some("Site identifier handed to the analytics vendor"), ), ( "analytics_attributes", "{}", Some("Extra data-* attributes for the analytics script (JSON object)"), ), ( "password_min_length", "8", Some("Minimum password length (6-128)"), ), ( "password_require_uppercase", "false", Some("Require uppercase letters in password"), ), ( "password_require_lowercase", "false", Some("Require lowercase letters in password"), ), ( "password_require_digits", "false", Some("Require digits in password"), ), ( "password_require_special", "false", Some("Require special characters in password"), ), ( "password_change_require_email", "true", Some("Require email confirmation for password changes"), ), ( "password_expiration_enabled", "false", Some("Enable password expiration"), ), ( "password_expiration_days", "0", Some("Password expiration duration in days (0 = disabled, 1-365)"), ), ( "password_expiration_alert_enabled", "false", Some("Enable email alerts for password expiration"), ), ( "password_expiration_alert_days_before", "7", Some("Days before expiration to send alert"), ), ( "audit_log_actions", r#"["create","update","delete","reset_password"]"#, Some("Actions to log in audit_logs table (JSON array)"), ), ( "oauth_enabled", "false", Some("Enable OAuth/OpenID Connect authentication"), ), ( "oauth_google_enabled", "false", Some("Enable Google OAuth provider"), ), ( "oauth_github_enabled", "false", Some("Enable GitHub OAuth provider"), ), ( "oauth_discord_enabled", "false", Some("Enable Discord OAuth provider"), ), ( "oauth_microsoft_enabled", "false", Some("Enable Microsoft OAuth provider"), ), ( "oauth_redirect_base_url", "", Some( "Base URL for OAuth redirect callbacks (fallback to OAUTH_REDIRECT_BASE_URL env var)", ), ), ( "oauth_auto_register", "true", Some("Auto-create user account on first OAuth login if email not found"), ), ( "oauth_link_existing_account", "true", Some("Auto-link OAuth account to existing user with same verified email"), ), ("oauth_google_client_id", "", Some("Google OAuth Client ID")), ( "oauth_google_client_secret", "", Some("Google OAuth Client Secret (encrypted)"), ), ("oauth_github_client_id", "", Some("GitHub OAuth Client ID")), ( "oauth_github_client_secret", "", Some("GitHub OAuth Client Secret (encrypted)"), ), ( "oauth_discord_client_id", "", Some("Discord OAuth Client ID"), ), ( "oauth_discord_client_secret", "", Some("Discord OAuth Client Secret (encrypted)"), ), ( "oauth_microsoft_client_id", "", Some("Microsoft OAuth Client ID"), ), ( "oauth_microsoft_client_secret", "", Some("Microsoft OAuth Client Secret (encrypted)"), ), ( "oauth_microsoft_tenant", "", Some("Microsoft Azure AD Tenant ID"), ), ]; // Check if settings already exist before creating for (key, value, description) in default_settings { let existing = Self::get_setting(db, key).await?; if existing.is_none() { let active_setting = app_settings::ActiveModel { id: Set(Uuid::new_v4()), key: Set(key.to_string()), value: Set(value.to_string()), description: Set(description.map(|s| s.to_string())), updated_by: Set(None), updated_at: Set(Utc::now()), }; active_setting.insert(db).await?; } } Ok(()) } }
GitRust