veza/veza-backend-api/internal/config/config.go
okinrev b7955a680c P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.

Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.

Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).

Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.

Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 11:14:38 +01:00

725 lines
25 KiB
Go

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/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
// 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
ChatJWTSecret string // Secret pour les tokens WebSocket Chat
RedisURL string
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
LogLevel string // Niveau de log (T0027)
DBMaxRetries int
DBRetryInterval time.Duration
// 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)
// 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 := getEnvRequired("JWT_SECRET")
config := &Config{
Env: env, // Store environment for validation (P0-SECURITY)
AppPort: appPort,
JWTSecret: jwtSecret,
ChatJWTSecret: getEnv("CHAT_JWT_SECRET", jwtSecret), // Fallback to main JWT secret if not set
RedisURL: getEnv("REDIS_URL", "redis://localhost:6379"),
// SECURITY: DATABASE_URL est REQUIS - contient des credentials sensibles
DatabaseURL: getEnvRequired("DATABASE_URL"),
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,
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
// 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)
}
// Initialiser Redis
config.RedisClient, err = initRedis(config.RedisURL)
if err != nil {
logger.Error("Failed to initialize Redis", zap.Error(err))
return nil, err
}
// 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()
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)
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()
c.EndpointLimiter = middleware.NewEndpointLimiter(endpointLimiterConfig, endpointLimits)
// Middleware d'authentification
c.AuthMiddleware = middleware.NewAuthMiddleware(
c.SessionService,
c.AuditService,
c.PermissionService,
c.Logger,
c.JWTSecret,
)
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) (*redis.Client, error) {
opts, err := redis.ParseURL(redisURL)
if err != nil {
return nil, err
}
client := redis.NewClient(opts)
// Test de connexion
ctx := context.Background()
_, err = client.Ping(ctx).Result()
if err != nil {
return nil, err
}
return client, nil
}
// 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)
}
// initDatabase initialise la connexion à la base de données
// NOTE: Cette fonction est maintenant dépréciée et remplacée par initDatabaseWithRetry
func initDatabase(databaseURL string) (*database.Database, error) {
// Configuration de la base de données
dbConfig := &database.Config{
URL: databaseURL,
MaxOpenConns: 25,
MaxIdleConns: 10,
MaxLifetime: 5 * time.Minute,
MaxIdleTime: 1 * time.Minute,
}
return database.NewDatabase(dbConfig)
}
// 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{"*"})
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: getEnvRequired("DB_PASSWORD"),
DBName: getEnv("DB_NAME", "veza_db"),
JWTSecret: getEnvRequired("JWT_SECRET"),
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 (panique si absente)
func getEnvRequired(key string) string {
value := os.Getenv(key)
if value == "" {
panic(fmt.Sprintf("Required environment variable %s is not set", key))
}
return value
}
// 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 REQUIS, pas de wildcard
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: pas de default, doit être défini explicitement
// La validation ValidateForEnvironment() vérifiera que c'est non vide
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. CORS_ALLOWED_ORIGINS doit être défini et non vide
if len(c.CORSOrigins) == 0 {
return fmt.Errorf("CORS_ALLOWED_ORIGINS is required in production environment and must not be empty")
}
// 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)
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("database_url", MaskConfigValue("DATABASE_URL", c.DatabaseURL, c.SecretsProvider)),
zap.String("redis_url", MaskConfigValue("REDIS_URL", c.RedisURL, c.SecretsProvider)),
zap.Strings("cors_origins", c.CORSOrigins),
zap.Int("rate_limit_limit", c.RateLimitLimit),
zap.Int("rate_limit_window", c.RateLimitWindow),
zap.String("log_level", c.LogLevel),
)
}
// 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
}