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.).
725 lines
25 KiB
Go
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
|
|
}
|