package config import ( "context" "errors" "fmt" "os" "strconv" "strings" "time" "veza-backend-api/internal/database" "veza-backend-api/internal/email" "veza-backend-api/internal/eventbus" // Import the eventbus package "veza-backend-api/internal/metrics" "veza-backend-api/internal/middleware" "veza-backend-api/internal/repositories" "veza-backend-api/internal/services" "veza-backend-api/internal/workers" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" "go.uber.org/zap" ) // Config contient toute la configuration de l'application type Config struct { // Base de données Database *database.Database // Redis RedisClient *redis.Client // Services SessionService *services.SessionService AuditService *services.AuditService TOTPService *services.TOTPService UploadValidator *services.UploadValidator CacheService *services.CacheService PlaylistService *services.PlaylistService PermissionService *services.PermissionService JWTService *services.JWTService UserService *services.UserService // Middlewares RateLimiter *middleware.RateLimiter SimpleRateLimiter *middleware.SimpleRateLimiter // Rate limiter simple (T0015) EndpointLimiter *middleware.EndpointLimiter AuthMiddleware *middleware.AuthMiddleware // Logger Logger *zap.Logger // Metrics (T0020) ErrorMetrics *metrics.ErrorMetrics // Secrets Provider (T0037) SecretsProvider SecretsProvider // Config Watcher (T0040) ConfigWatcher *ConfigWatcher // Configuration Env string // Environnement: development, test, production (P0-SECURITY) AppPort int // Port pour le serveur HTTP (T0031) JWTSecret string JWTIssuer string // T0204: Issuer claim validation (P1-SECURITY) JWTAudience string // T0204: Audience claim validation (P1-SECURITY) ChatJWTSecret string // Secret pour les tokens WebSocket Chat RedisURL string RedisEnable bool // Enable/Disable Redis DatabaseURL string UploadDir string // Répertoire d'upload StreamServerURL string // URL du serveur de streaming ChatServerURL string // URL du serveur de chat CORSOrigins []string // Liste des origines CORS autorisées // Sentry configuration SentryDsn string // DSN Sentry pour error tracking SentryEnvironment string // Environnement Sentry (dev, staging, prod) SentrySampleRateErrors float64 // Sample rate pour les erreurs (0.0-1.0) SentrySampleRateTransactions float64 // Sample rate pour les transactions (0.0-1.0) RateLimitLimit int // Limite de requêtes pour le rate limiter simple RateLimitWindow int // Fenêtre de temps en secondes pour le rate limiter simple AuthRateLimitLoginAttempts int // Max login attempts (PR-3) AuthRateLimitLoginWindow int // Login rate limit window in minutes (PR-3) HandlerTimeout time.Duration // Global handler timeout (PR-6) LogLevel string // Niveau de log (T0027) DBMaxRetries int DBRetryInterval time.Duration MaxConcurrentUploads int // MOD-P2-005: Limite uploads simultanés (backpressure) // RabbitMQ RabbitMQEventBus *eventbus.RabbitMQEventBus // Ajout de l'instance de l'EventBus RabbitMQURL string RabbitMQMaxRetries int RabbitMQRetryInterval time.Duration RabbitMQEnable bool // Email & Jobs EmailSender *email.SMTPEmailSender JobWorker *workers.JobWorker SMTPConfig email.SMTPConfig } // NewConfig crée une nouvelle configuration func NewConfig() (*Config, error) { // Déterminer l'environnement avec détection automatique améliorée (T0032, T0039) env := DetectEnvironment() // Charger les fichiers .env selon l'environnement (T0032) // Charge dans l'ordre: .env.{env}, .env // Les variables d'environnement système ont priorité if err := LoadEnvFiles(env); err != nil { // En cas d'erreur, continuer quand même (peut-être que les fichiers .env n'existent pas) // Les variables d'environnement système seront utilisées } // Initialiser le logger logger, err := zap.NewProduction() if err != nil { return nil, err } // SECURITY: Charger les origines CORS avec defaults sécurisés selon l'environnement (P0-SECURITY) corsOrigins := getCORSOrigins(env) // Charger la configuration du rate limiter simple rateLimitLimit := getEnvInt("RATE_LIMIT_LIMIT", 100) // 100 requêtes par défaut rateLimitWindow := getEnvInt("RATE_LIMIT_WINDOW", 60) // 60 secondes (1 minute) par défaut // Charger le niveau de log depuis les variables d'environnement (T0027) // Valeurs possibles: DEBUG, INFO, WARN, ERROR // Par défaut: INFO logLevel := getEnv("LOG_LEVEL", "INFO") // Charger le port depuis les variables d'environnement (T0031) appPort := getEnvInt("APP_PORT", 8080) // MOD-P2-005: Charger la limite d'uploads simultanés (backpressure) maxConcurrentUploads := getEnvInt("MAX_CONCURRENT_UPLOADS", 10) // 10 par défaut // Configuration depuis les variables d'environnement // SECURITY: JWT_SECRET est REQUIS - pas de valeur par défaut pour éviter les failles de sécurité jwtSecret, err := getEnvRequired("JWT_SECRET") if err != nil { return nil, err } databaseURL, err := getEnvRequired("DATABASE_URL") if err != nil { return nil, err } config := &Config{ Env: env, // Store environment for validation (P0-SECURITY) AppPort: appPort, JWTSecret: jwtSecret, JWTIssuer: getEnv("JWT_ISSUER", "veza-api"), JWTAudience: getEnv("JWT_AUDIENCE", "veza-app"), ChatJWTSecret: getEnv("CHAT_JWT_SECRET", jwtSecret), // Fallback to main JWT secret if not set RedisURL: getEnv("REDIS_URL", "redis://localhost:6379"), RedisEnable: getEnvBool("REDIS_ENABLE", true), // SECURITY: DATABASE_URL est REQUIS - contient des credentials sensibles DatabaseURL: databaseURL, UploadDir: getEnv("UPLOAD_DIR", "uploads"), StreamServerURL: getEnv("STREAM_SERVER_URL", "http://localhost:8082"), ChatServerURL: getEnv("CHAT_SERVER_URL", "http://localhost:8081"), CORSOrigins: corsOrigins, // Sentry configuration SentryDsn: getEnv("SENTRY_DSN", ""), SentryEnvironment: env, // Utiliser l'environnement détecté SentrySampleRateErrors: getEnvFloat64("SENTRY_SAMPLE_RATE_ERRORS", 1.0), SentrySampleRateTransactions: getEnvFloat64("SENTRY_SAMPLE_RATE_TRANSACTIONS", 0.1), RateLimitLimit: rateLimitLimit, RateLimitWindow: rateLimitWindow, AuthRateLimitLoginAttempts: getEnvInt("AUTH_RATE_LIMIT_LOGIN_ATTEMPTS", 5), // Default: 5 attempts AuthRateLimitLoginWindow: getEnvInt("AUTH_RATE_LIMIT_LOGIN_WINDOW", 1), // Default: 1 minute HandlerTimeout: getEnvDuration("HANDLER_TIMEOUT", 30*time.Second), // Default: 30 seconds LogLevel: logLevel, Logger: logger, DBMaxRetries: getEnvInt("DB_MAX_RETRIES", 5), // 5 tentatives par défaut DBRetryInterval: getEnvDuration("DB_RETRY_INTERVAL", 5*time.Second), // 5 secondes par défaut MaxConcurrentUploads: maxConcurrentUploads, // MOD-P2-005: Limite uploads simultanés // Configuration RabbitMQ RabbitMQURL: getEnv("RABBITMQ_URL", "amqp://guest:guest@localhost:5672/"), RabbitMQMaxRetries: getEnvInt("RABBITMQ_MAX_RETRIES", 3), // 3 tentatives par défaut RabbitMQRetryInterval: getEnvDuration("RABBITMQ_RETRY_INTERVAL", 2*time.Second), // 2 secondes par défaut RabbitMQEnable: getEnvBool("RABBITMQ_ENABLE", true), // Activé par défaut } // Initialiser le SecretsProvider (T0037) secretKeys := DefaultSecretKeys() config.SecretsProvider = NewEnvSecretsProvider(secretKeys) // SECURITY: Valider la configuration selon l'environnement (P0-SECURITY) if err := config.ValidateForEnvironment(); err != nil { logger.Error("Configuration validation failed", zap.Error(err), zap.String("env", env)) return nil, fmt.Errorf("invalid configuration: %w", err) } // Warn if CORS is strict/empty in production (MOD-P0-002) if env == EnvProduction && len(config.CORSOrigins) == 0 { logger.Warn("CORS_ALLOWED_ORIGINS is empty in production. Strict mode enabled: ALL CORS requests will be rejected.") } // Initialiser Redis if config.RedisEnable { config.RedisClient, err = initRedis(config.RedisURL, logger) if err != nil { logger.Error("Failed to initialize Redis", zap.Error(err)) return nil, err } } else { logger.Warn("Redis désactivé par configuration (REDIS_ENABLE=false)") } // Initialiser la base de données avec retry config.Database, err = initDatabaseWithRetry(config.DatabaseURL, config.DBMaxRetries, config.DBRetryInterval, config.Logger) if err != nil { logger.Error("Failed to initialize database", zap.Error(err)) return nil, err } // Initialiser RabbitMQ avec retry config.RabbitMQEventBus, err = eventbus.NewRabbitMQEventBusWithRetry(&eventbus.RabbitMQConfig{ URL: config.RabbitMQURL, MaxRetries: config.RabbitMQMaxRetries, RetryInterval: config.RabbitMQRetryInterval, Enable: config.RabbitMQEnable, }, config.Logger) if err != nil { // En mode dégradé, l'erreur n'est pas fatale au démarrage du service if _, ok := err.(*eventbus.EventBusUnavailableError); ok && !config.RabbitMQEnable { logger.Warn("RabbitMQ EventBus est indisponible mais le service démarre en mode dégradé.", zap.Error(err)) } else if _, ok := err.(*eventbus.EventBusUnavailableError); ok { // Si le service est censé être enabled et qu'il est injoignable après retries logger.Fatal("Impossible de se connecter à RabbitMQ après plusieurs tentatives. Le service ne peut pas démarrer.", zap.Error(err)) return nil, err // Retourner l'erreur fatale } else { logger.Error("Failed to initialize RabbitMQ EventBus", zap.Error(err)) return nil, err } } // Initialiser les services err = config.initServices() if err != nil { logger.Error("Failed to initialize services", zap.Error(err)) return nil, err } // Initialiser les middlewares err = config.initMiddlewares() if err != nil { logger.Error("Failed to initialize middlewares", zap.Error(err)) return nil, err } // Initialiser les métriques d'erreurs (T0020) config.ErrorMetrics = metrics.NewErrorMetrics() // Initialiser la configuration SMTP config.SMTPConfig = email.LoadSMTPConfigFromEnv() config.EmailSender = email.NewSMTPEmailSender(config.SMTPConfig, logger) // Initialiser le JobService jobService := services.NewJobService(logger) // Initialiser le JobWorker config.JobWorker = workers.NewJobWorker( config.Database.GormDB, jobService, logger, 100, // queueSize 3, // workers 3, // maxRetries config.EmailSender, // emailSender ) // Logger la configuration avec masquage des secrets (T0037) config.logConfigInitialized(logger) // Initialiser le ConfigWatcher si activé (T0040) // Le watcher peut être activé via une variable d'environnement CONFIG_WATCH=true if getEnv("CONFIG_WATCH", "false") == "true" { reloader := config.GetConfigReloader() watcher, err := NewConfigWatcher(reloader, logger) if err != nil { logger.Warn("Failed to create config watcher", zap.Error(err)) } else { config.ConfigWatcher = watcher // Surveiller les fichiers .env envFiles := []string{".env", ".env." + env} if err := watcher.Watch(envFiles); err != nil { logger.Warn("Failed to start watching config files", zap.Error(err)) } else { logger.Info("Config watcher started", zap.Strings("files", watcher.GetWatchedFiles())) } } } return config, nil } // GetConfigReloader retourne le ConfigReloader pour cette configuration (T0034) func (c *Config) GetConfigReloader() *ConfigReloader { return NewConfigReloader(c, c.Logger) } // initServices initialise tous les services func (c *Config) initServices() error { // Service de session c.SessionService = services.NewSessionService(c.Database, c.Logger) // Service d'audit c.AuditService = services.NewAuditService(c.Database, c.Logger) // Service TOTP c.TOTPService = services.NewTOTPService(c.Database, c.Logger) // Validateur d'upload uploadConfig := services.DefaultUploadConfig() // MOD-P1-002: Lire CLAMAV_REQUIRED depuis l'environnement (défaut: true pour sécurité) clamAVRequired := getEnvBool("CLAMAV_REQUIRED", true) uploadConfig.ClamAVRequired = clamAVRequired if !clamAVRequired { c.Logger.Warn("CLAMAV_REQUIRED=false - Uploads will be accepted even if ClamAV is unavailable (degraded mode). This should only be used in development or with alternative security measures.") } var err error c.UploadValidator, err = services.NewUploadValidator(uploadConfig, c.Logger) if err != nil { return err } // Service de cache c.CacheService = services.NewCacheService(c.RedisClient, c.Logger) // Service de playlist c.PlaylistService = services.NewPlaylistServiceWithDB(c.Database.GormDB, c.Logger) // Service de permissions c.PermissionService = services.NewPermissionService(c.Database.GormDB) // JWT Service c.JWTService, err = services.NewJWTService(c.JWTSecret, c.JWTIssuer, c.JWTAudience) if err != nil { return err } // User Service userRepo := repositories.NewGormUserRepository(c.Database.GormDB) c.UserService = services.NewUserServiceWithDB(userRepo, c.Database.GormDB) return nil } // initMiddlewares initialise tous les middlewares func (c *Config) initMiddlewares() error { // Rate limiter global (avec Redis) rateLimiterConfig := &middleware.RateLimiterConfig{ IPRequestsPerMinute: 100, IPBurst: 10, UserRequestsPerMinute: 1000, UserBurst: 100, RedisClient: c.RedisClient, KeyPrefix: "veza:rate_limit", } c.RateLimiter = middleware.NewRateLimiter(rateLimiterConfig) // Simple rate limiter (T0015) - sans dépendance Redis window := time.Duration(c.RateLimitWindow) * time.Second c.SimpleRateLimiter = middleware.NewSimpleRateLimiter(c.RateLimitLimit, window) // Rate limiter par endpoint endpointLimiterConfig := &middleware.EndpointLimiterConfig{ RedisClient: c.RedisClient, KeyPrefix: "veza:endpoint_limit", } endpointLimits := middleware.DefaultEndpointLimits() // Override defaults with config (PR-3) endpointLimits.LoginAttempts = c.AuthRateLimitLoginAttempts endpointLimits.LoginWindow = time.Duration(c.AuthRateLimitLoginWindow) * time.Minute c.EndpointLimiter = middleware.NewEndpointLimiter(endpointLimiterConfig, endpointLimits) // Middleware d'authentification c.AuthMiddleware = middleware.NewAuthMiddleware( c.SessionService, c.AuditService, c.PermissionService, c.JWTService, c.UserService, c.Logger, ) return nil } // NOTE: Les handlers ne sont plus initialisés dans Config pour éviter les imports cycliques. // Les handlers doivent être créés dans main.go ou dans les routes selon les besoins. // // SetupRoutes a été supprimé pour casser le cycle d'import config <-> api. // Utiliser directement api.SetupRoutes() dans cmd/modern-server/main.go // SetupMiddleware configure les middlewares globaux // DÉPRÉCIÉ : Cette méthode est conservée pour compatibilité mais ne fait plus rien // Les middlewares globaux sont maintenant configurés dans internal/api/router.go via APIRouter.Setup() // TODO: Améliorer la configuration CORS dans api/router.go pour utiliser c.CORSOrigins depuis la config func (c *Config) SetupMiddleware(router *gin.Engine) { // No-op : Les middlewares sont configurés dans api/router.go // Cette méthode existe uniquement pour compatibilité avec cmd/main.go (legacy) // qui sera désactivé dans le Chantier 1 - Étape 2 } // initRedis initialise la connexion Redis func initRedis(redisURL string, logger *zap.Logger) (*redis.Client, error) { opts, err := redis.ParseURL(redisURL) if err != nil { return nil, err } // Configurer un logger filtré pour Redis pour éviter les warnings "maint_notifications" redis.SetLogger(&filteredRedisLogger{logger: logger}) client := redis.NewClient(opts) // Test de connexion ctx := context.Background() _, err = client.Ping(ctx).Result() if err != nil { return nil, err } return client, nil } // filteredRedisLogger est un wrapper pour filtrer les logs de Redis type filteredRedisLogger struct { logger *zap.Logger } func (l *filteredRedisLogger) Printf(ctx context.Context, format string, v ...interface{}) { msg := fmt.Sprintf(format, v...) if strings.Contains(msg, "maint_notifications") { return // Ignorer ce warning spécifique en mode auto-discovery } l.logger.Debug("Redis internal", zap.String("message", msg)) } // initDatabaseWithRetry initialise la connexion à la base de données avec des tentatives de retry func initDatabaseWithRetry(databaseURL string, maxRetries int, retryInterval time.Duration, logger *zap.Logger) (*database.Database, error) { dbConfig := &database.Config{ URL: databaseURL, MaxOpenConns: 25, MaxIdleConns: 10, MaxLifetime: 5 * time.Minute, MaxIdleTime: 1 * time.Minute, MaxRetries: maxRetries, RetryInterval: retryInterval, } // Utiliser la fonction de connexion avec retry du package database return database.NewDatabaseWithRetry(dbConfig, logger) } // EnvConfig représente la configuration de base chargée depuis les variables d'environnement // Cette struct est utilisée par la fonction Load() pour charger la configuration de base type EnvConfig struct { AppEnv string AppPort int DBHost string DBPort int DBUser string DBPassword string DBName string JWTSecret string RedisURL string CORSOrigins []string // Liste des origines CORS autorisées } // Load charge et valide les variables d'environnement avec valeurs par défaut func Load() (*EnvConfig, error) { // Déterminer l'environnement (T0032) env := getEnv("APP_ENV", "development") // Charger les fichiers .env selon l'environnement (T0032) // Charge dans l'ordre: .env.{env}, .env // Les variables d'environnement système ont priorité if err := LoadEnvFiles(env); err != nil { return nil, fmt.Errorf("failed to load environment files: %w", err) } // Charger les origines CORS depuis les variables d'environnement corsOrigins := getEnvStringSlice("CORS_ALLOWED_ORIGINS", []string{"*"}) // Database, JWTSecret are required dbPassword, err := getEnvRequired("DB_PASSWORD") if err != nil { return nil, err } jwtSecret, err := getEnvRequired("JWT_SECRET") if err != nil { return nil, err } config := &EnvConfig{ AppEnv: getEnv("APP_ENV", "development"), AppPort: getEnvInt("APP_PORT", 8080), DBHost: getEnv("DB_HOST", "localhost"), DBPort: getEnvInt("DB_PORT", 5432), DBUser: getEnv("DB_USER", "veza"), DBPassword: dbPassword, DBName: getEnv("DB_NAME", "veza_db"), JWTSecret: jwtSecret, RedisURL: getEnv("REDIS_URL", "redis://localhost:6379"), CORSOrigins: corsOrigins, } return config, nil } // getEnv récupère une variable d'environnement avec une valeur par défaut // SECURITY: Removed debug fmt.Printf to avoid leaking config info in production (P0-SECURITY) func getEnv(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return strings.TrimSpace(value) } return defaultValue } // getEnvRequired récupère une variable d'environnement requise (retourne erreur si absente) func getEnvRequired(key string) (string, error) { value := os.Getenv(key) if value == "" { return "", fmt.Errorf("required environment variable %s is not set", key) } return value, nil } // getEnvInt récupère une variable d'environnement entière avec une valeur par défaut func getEnvInt(key string, defaultValue int) int { if value := os.Getenv(key); value != "" { if intValue, err := strconv.Atoi(value); err == nil { return intValue } } return defaultValue } // getEnvBool récupère une variable d'environnement booléenne avec une valeur par défaut func getEnvBool(key string, defaultValue bool) bool { if value := os.Getenv(key); value != "" { if boolValue, err := strconv.ParseBool(value); err == nil { return boolValue } } return defaultValue } // getEnvDuration récupère une variable d'environnement durée avec une valeur par défaut func getEnvDuration(key string, defaultValue time.Duration) time.Duration { if value := os.Getenv(key); value != "" { if duration, err := time.ParseDuration(value); err == nil { return duration } } return defaultValue } // getEnvFloat64 récupère une variable d'environnement float64 avec une valeur par défaut func getEnvFloat64(key string, defaultValue float64) float64 { if value := os.Getenv(key); value != "" { if floatValue, err := strconv.ParseFloat(value, 64); err == nil { return floatValue } } return defaultValue } // getEnvStringSlice récupère une variable d'environnement comme une slice de strings // Format attendu: "value1,value2,value3" (séparées par des virgules) func getEnvStringSlice(key string, defaultValue []string) []string { if value := os.Getenv(key); value != "" { // Séparer par virgule et nettoyer les espaces parts := strings.Split(value, ",") result := make([]string, 0, len(parts)) for _, part := range parts { trimmed := strings.TrimSpace(part) if trimmed != "" { result = append(result, trimmed) } } if len(result) > 0 { return result } } return defaultValue } // getCORSOrigins charge les origines CORS avec defaults sécurisés selon l'environnement (P0-SECURITY) // - development: defaults permissifs (localhost uniquement) si CORS_ALLOWED_ORIGINS non défini // - test: liste vide ou configurée explicitement // - production: CORS_ALLOWED_ORIGINS comportement: // - si défini: utiliser // - si absent/vide: liste vide (STRICT, reject all) func getCORSOrigins(env string) []string { // Si CORS_ALLOWED_ORIGINS est défini, l'utiliser if value := os.Getenv("CORS_ALLOWED_ORIGINS"); value != "" { origins := getEnvStringSlice("CORS_ALLOWED_ORIGINS", nil) if len(origins) > 0 { return origins } } // Defaults selon l'environnement switch env { case EnvProduction: // Production: defaults to empty (Strict Mode) // MOD-P0-002: "si CORS_ALLOWED_ORIGINS vide, appliquer un comportement strict par défaut (reject toutes origines)" return []string{} case EnvTest: // Test: liste vide par défaut (peut être configurée explicitement) return []string{} case EnvDevelopment, EnvStaging: // Development/Staging: defaults permissifs pour localhost return []string{"http://localhost:3000", "http://127.0.0.1:3000", "http://localhost:5173", "http://127.0.0.1:5173"} default: // Fallback: development-like return []string{"http://localhost:3000", "http://127.0.0.1:3000"} } } // ValidateForEnvironment valide la configuration selon l'environnement (P0-SECURITY) // En production: validation stricte (CORS requis, pas de wildcard, etc.) // En development: validation permissive avec warnings func (c *Config) ValidateForEnvironment() error { // D'abord, validation de base (port, secrets, URLs, etc.) if err := c.Validate(); err != nil { return err } // Validations spécifiques selon l'environnement switch c.Env { case EnvProduction: // PRODUCTION: Validation stricte // 1. MOD-P0-001: CORS_ALLOWED_ORIGINS MUST be configured in production (fail-fast) // Empty CORS origins means strict mode (reject all), which makes the service inaccessible from frontend if len(c.CORSOrigins) == 0 { return fmt.Errorf("CORS_ALLOWED_ORIGINS is required in production environment. Empty CORS origins will reject all CORS requests, making the service inaccessible from frontend. Please set CORS_ALLOWED_ORIGINS with explicit origins (e.g., CORS_ALLOWED_ORIGINS=https://app.veza.com,https://www.veza.com)") } // 2. CORS_ALLOWED_ORIGINS ne doit PAS contenir "*" (wildcard interdit en prod) for _, origin := range c.CORSOrigins { if origin == "*" { return fmt.Errorf("CORS wildcard '*' is not allowed in production environment. Please specify explicit origins in CORS_ALLOWED_ORIGINS") } } // 3. LogLevel ne doit pas être DEBUG en production if c.LogLevel == "DEBUG" { return fmt.Errorf("LOG_LEVEL=DEBUG is not allowed in production environment for security reasons") } case EnvTest: // TEST: Validation adaptée aux tests // CORS peut être vide ou configuré explicitement // Pas de validation stricte sur les secrets (peuvent être des valeurs de test) case EnvDevelopment, EnvStaging: // DEVELOPMENT/STAGING: Validation permissive avec warnings // Si CORS contient "*", logger un warning mais ne pas bloquer for _, origin := range c.CORSOrigins { if origin == "*" { c.Logger.Warn("CORS wildcard '*' detected in development environment. This is acceptable for dev but should never be used in production") break } } } return nil } // Validate valide la configuration (T0031, T0036) // Vérifie que toutes les valeurs de configuration sont valides avant le démarrage de l'application // Utilise ConfigValidator pour une validation stricte selon les règles de schéma (T0036) func (c *Config) Validate() error { validator := NewConfigValidator() // Valider le port (1-65535) avec ConfigValidator (T0036) if err := validator.ValidatePort(c.AppPort); err != nil { return fmt.Errorf("APP_PORT validation failed: %w", err) } // Valider JWT secret (minimum 32 caractères pour sécurité) avec ConfigValidator (T0036) if err := validator.ValidateSecretLength(c.JWTSecret, 32); err != nil { return fmt.Errorf("JWT_SECRET validation failed: %w", err) } // Valider DatabaseURL (requis) avec ConfigValidator (T0036) if c.DatabaseURL == "" { return errors.New("DATABASE_URL is required") } // Valider le format de DatabaseURL avec ConfigValidator (T0036) // Support postgres, postgresql, et sqlite if err := validator.ValidateURL(c.DatabaseURL, "postgres"); err != nil { if err2 := validator.ValidateURL(c.DatabaseURL, "postgresql"); err2 != nil { if err3 := validator.ValidateURL(c.DatabaseURL, "sqlite"); err3 != nil { return fmt.Errorf("DATABASE_URL validation failed: must start with postgres://, postgresql://, or sqlite://") } } } // Valider RedisURL (requis) avec ConfigValidator (T0036) if c.RedisURL == "" { return errors.New("REDIS_URL is required") } // Valider le format de RedisURL avec ConfigValidator (T0036) // Support redis et rediss (Redis avec SSL) if err := validator.ValidateURL(c.RedisURL, "redis"); err != nil { if err2 := validator.ValidateURL(c.RedisURL, "rediss"); err2 != nil { return fmt.Errorf("REDIS_URL validation failed: must start with redis:// or rediss://") } } // Valider LogLevel avec ValidateEnum (T0036) if c.LogLevel != "" { allowedLevels := []string{"DEBUG", "INFO", "WARN", "ERROR"} if err := validator.ValidateEnum(c.LogLevel, allowedLevels); err != nil { return fmt.Errorf("LOG_LEVEL validation failed: %w", err) } } // Valider RateLimitLimit et RateLimitWindow avec ValidatePositiveInt (T0036) if err := validator.ValidatePositiveInt(c.RateLimitLimit, "RATE_LIMIT_LIMIT"); err != nil { return fmt.Errorf("RATE_LIMIT_LIMIT validation failed: %w", err) } if err := validator.ValidatePositiveInt(c.RateLimitWindow, "RATE_LIMIT_WINDOW"); err != nil { return fmt.Errorf("RATE_LIMIT_WINDOW validation failed: %w", err) } return nil } // logConfigInitialized log la configuration initialisée avec masquage des secrets (T0037) // MOD-P0-002: Always mask secrets in logs, even in DEBUG mode func (c *Config) logConfigInitialized(logger *zap.Logger) { logger.Info("Configuration initialized successfully", zap.Int("app_port", c.AppPort), zap.String("jwt_secret", MaskConfigValue("JWT_SECRET", c.JWTSecret, c.SecretsProvider)), zap.String("jwt_issuer", c.JWTIssuer), zap.String("jwt_audience", c.JWTAudience), zap.String("chat_jwt_secret", MaskConfigValue("CHAT_JWT_SECRET", c.ChatJWTSecret, c.SecretsProvider)), zap.String("database_url", MaskConfigValue("DATABASE_URL", c.DatabaseURL, c.SecretsProvider)), zap.String("redis_url", MaskConfigValue("REDIS_URL", c.RedisURL, c.SecretsProvider)), zap.String("rabbitmq_url", MaskConfigValue("RABBITMQ_URL", c.RabbitMQURL, c.SecretsProvider)), zap.Strings("cors_origins", c.CORSOrigins), zap.Int("rate_limit_limit", c.RateLimitLimit), zap.Int("rate_limit_window", c.RateLimitWindow), zap.Int("auth_rate_limit_login_attempts", c.AuthRateLimitLoginAttempts), zap.Int("auth_rate_limit_login_window", c.AuthRateLimitLoginWindow), zap.Duration("handler_timeout", c.HandlerTimeout), zap.String("log_level", c.LogLevel), zap.String("sentry_dsn", MaskConfigValue("SENTRY_DSN", c.SentryDsn, c.SecretsProvider)), ) } // Close ferme toutes les connexions (T0040) func (c *Config) Close() error { var err error // Arrêter le ConfigWatcher si actif (T0040) if c.ConfigWatcher != nil { if closeErr := c.ConfigWatcher.Stop(); closeErr != nil { err = closeErr } } if c.RedisClient != nil { if closeErr := c.RedisClient.Close(); closeErr != nil { err = closeErr } } if c.Database != nil { if closeErr := c.Database.Close(); closeErr != nil { err = closeErr } } if c.RabbitMQEventBus != nil { if closeErr := c.RabbitMQEventBus.Close(); closeErr != nil { err = closeErr } } if c.Logger != nil { c.Logger.Sync() } return err }