808 lines
29 KiB
Go
808 lines
29 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/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
|
|
}
|