440 lines
17 KiB
Go
440 lines
17 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
|
|
"veza-backend-api/internal/config"
|
|
"veza-backend-api/internal/database"
|
|
"veza-backend-api/internal/handlers"
|
|
"veza-backend-api/internal/middleware"
|
|
"veza-backend-api/internal/repositories"
|
|
"veza-backend-api/internal/services"
|
|
chatws "veza-backend-api/internal/websocket/chat"
|
|
|
|
swaggerFiles "github.com/swaggo/files"
|
|
ginSwagger "github.com/swaggo/gin-swagger"
|
|
|
|
authcore "veza-backend-api/internal/core/auth"
|
|
)
|
|
|
|
// APIRouter gère la configuration des routes de l'API
|
|
type APIRouter struct {
|
|
db *database.Database
|
|
config *config.Config
|
|
engine *gin.Engine
|
|
logger *zap.Logger
|
|
versionManager *VersionManager // BE-SVC-019: API versioning manager
|
|
monitoringService *services.MonitoringAlertingService // INT-021: API monitoring and alerting
|
|
authService *authcore.AuthService // Set by setupAuthRoutes for admin unlock
|
|
notificationService *services.NotificationService // Shared for N1.2 Web Push
|
|
pushService *services.PushService // N1 Web Push
|
|
}
|
|
|
|
// NewAPIRouter crée une nouvelle instance de APIRouter
|
|
func NewAPIRouter(db *database.Database, cfg *config.Config) *APIRouter {
|
|
logger := zap.L()
|
|
return &APIRouter{
|
|
db: db,
|
|
config: cfg,
|
|
logger: logger,
|
|
versionManager: NewVersionManager(logger), // BE-SVC-019: Initialize version manager
|
|
}
|
|
}
|
|
|
|
// applyCSRFProtection applique le middleware CSRF à un groupe de routes protégées
|
|
// BE-SEC-004: Ensure all POST/PUT/DELETE endpoints validate CSRF tokens
|
|
// INT-AUTH-001: Fail-fast in production if Redis unavailable (CSRF requires Redis)
|
|
func (r *APIRouter) applyCSRFProtection(protectedGroup *gin.RouterGroup) {
|
|
if r.config == nil {
|
|
if r.logger != nil {
|
|
r.logger.Error("CSRF protection cannot be applied: config is nil")
|
|
}
|
|
// In production, fail-fast (but we can't check env if config is nil)
|
|
// This should not happen in normal operation, but we log it
|
|
return
|
|
}
|
|
|
|
if r.config.RedisClient == nil {
|
|
// In non-production, log warning and continue without CSRF
|
|
// Production case is handled by early validation in Setup() (audit 1.4)
|
|
if r.logger != nil {
|
|
r.logger.Warn("Redis not available - CSRF protection disabled (non-production environment)")
|
|
}
|
|
return
|
|
}
|
|
|
|
// CSRF protection active
|
|
if r.logger != nil {
|
|
r.logger.Info("CSRF protection enabled",
|
|
zap.String("environment", r.config.Env),
|
|
)
|
|
}
|
|
csrfMiddleware := middleware.NewCSRFMiddleware(r.config.RedisClient, r.logger)
|
|
// MVP: Désactiver CSRF en développement
|
|
csrfMiddleware.SetEnvironment(r.config.Env)
|
|
protectedGroup.Use(csrfMiddleware.Middleware())
|
|
}
|
|
|
|
// getUploadConfigWithEnv charge la configuration d'upload depuis l'environnement
|
|
// Cette fonction garantit que ENABLE_CLAMAV et CLAMAV_REQUIRED sont correctement appliqués
|
|
func getUploadConfigWithEnv() *services.UploadConfig {
|
|
uploadConfig := services.DefaultUploadConfig()
|
|
|
|
// Lire ENABLE_CLAMAV depuis l'environnement (défaut: true pour sécurité en production)
|
|
envValue := os.Getenv("ENABLE_CLAMAV")
|
|
zap.L().Debug("ENABLE_CLAMAV from env", zap.String("value", envValue))
|
|
clamAVEnabled := getEnvBool("ENABLE_CLAMAV", true)
|
|
zap.L().Debug("ENABLE_CLAMAV parsed", zap.Bool("value", clamAVEnabled))
|
|
uploadConfig.ClamAVEnabled = clamAVEnabled
|
|
|
|
// Lire CLAMAV_REQUIRED depuis l'environnement (défaut: true pour sécurité)
|
|
clamAVRequired := getEnvBool("CLAMAV_REQUIRED", true)
|
|
uploadConfig.ClamAVRequired = clamAVRequired
|
|
|
|
zap.L().Info("Upload config final",
|
|
zap.Bool("clamav_enabled", uploadConfig.ClamAVEnabled),
|
|
zap.Bool("clamav_required", uploadConfig.ClamAVRequired))
|
|
|
|
return uploadConfig
|
|
}
|
|
|
|
// getEnvBool récupère une variable d'environnement booléenne avec une valeur par défaut
|
|
func getEnvBool(key string, defaultValue bool) bool {
|
|
value := os.Getenv(key)
|
|
if value == "" {
|
|
zap.L().Debug("Env var undefined, using default", zap.String("key", key), zap.Bool("default", defaultValue))
|
|
return defaultValue
|
|
}
|
|
// Nettoyer la valeur (trim spaces)
|
|
value = strings.TrimSpace(value)
|
|
zap.L().Debug("Env var trimmed", zap.String("key", key), zap.String("value", value))
|
|
if boolValue, err := strconv.ParseBool(value); err == nil {
|
|
zap.L().Debug("Env var parsed", zap.String("key", key), zap.Bool("value", boolValue))
|
|
return boolValue
|
|
}
|
|
zap.L().Warn("Env var parse error, using default", zap.String("key", key), zap.String("value", value), zap.Bool("default", defaultValue))
|
|
return defaultValue
|
|
}
|
|
|
|
// Setup configure toutes les routes de l'API
|
|
func (r *APIRouter) Setup(router *gin.Engine) error {
|
|
r.engine = router
|
|
|
|
// Audit 1.4 P0: Graceful error if Redis down in production (no panic/Fatal)
|
|
if r.config != nil && r.config.Env == config.EnvProduction && r.config.RedisClient == nil {
|
|
return fmt.Errorf("CSRF protection requires Redis in production. Redis is unavailable")
|
|
}
|
|
|
|
// INT-021: Initialize monitoring and alerting service
|
|
// Initialize monitoring service if Prometheus URL is configured
|
|
prometheusURL := os.Getenv("PROMETHEUS_URL")
|
|
if prometheusURL != "" {
|
|
monitoringConfig := services.MonitoringConfig{
|
|
PrometheusURL: prometheusURL,
|
|
Logger: r.logger,
|
|
}
|
|
monitoringService, err := services.NewMonitoringAlertingService(monitoringConfig)
|
|
if err != nil {
|
|
r.logger.Warn("Failed to initialize monitoring service", zap.Error(err))
|
|
} else {
|
|
r.monitoringService = monitoringService
|
|
// Add default alert rules
|
|
for _, rule := range services.GetDefaultAlertRules() {
|
|
monitoringService.AddAlertRule(rule)
|
|
}
|
|
// Start monitoring in background
|
|
go func() {
|
|
ctx := context.Background()
|
|
if err := monitoringService.StartMonitoring(ctx, 30*time.Second); err != nil {
|
|
r.logger.Error("Monitoring service stopped", zap.Error(err))
|
|
}
|
|
}()
|
|
r.logger.Info("Monitoring and alerting service initialized", zap.String("prometheus_url", prometheusURL))
|
|
}
|
|
} else {
|
|
r.logger.Info("Monitoring service disabled (PROMETHEUS_URL not configured)")
|
|
}
|
|
|
|
// P1.1: CORS middleware MUST be first to ensure headers are always present
|
|
// Even if subsequent middlewares reject the request (panic, timeout, error),
|
|
// the CORS headers will be set, preventing intermittent CORS errors
|
|
// SECURITY: CORS configuration - use config.CORSOrigins strictly (P0-SECURITY)
|
|
// No fallback to CORSDefault() to avoid wildcard in production
|
|
// MOD-P0-001: Apply CORS middleware even if CORSOrigins is empty (strict mode - reject all origins)
|
|
// The middleware itself handles empty list correctly (rejects all origins)
|
|
if r.config != nil {
|
|
// INT-018: Validate CORS configuration before applying middleware
|
|
// In production, this will fail startup if CORS is misconfigured
|
|
if err := middleware.ValidateCORSConfiguration(r.config.CORSOrigins, r.config.Env, r.logger); err != nil {
|
|
// In production, fail startup if CORS is misconfigured
|
|
if r.config.Env == "production" {
|
|
r.logger.Fatal("CORS configuration validation failed - startup aborted", zap.Error(err))
|
|
} else {
|
|
// In development/staging, log error but continue
|
|
r.logger.Error("CORS configuration validation failed", zap.Error(err))
|
|
}
|
|
}
|
|
|
|
router.Use(middleware.CORS(r.config.CORSOrigins))
|
|
if len(r.config.CORSOrigins) == 0 {
|
|
r.logger.Warn("CORS origins not configured - strict mode enabled: ALL CORS requests will be rejected.")
|
|
}
|
|
} else {
|
|
// Fallback: if config is nil, apply CORS with empty list (strict mode)
|
|
router.Use(middleware.CORS([]string{}))
|
|
r.logger.Warn("Config is nil - CORS middleware applied in strict mode (reject all origins).")
|
|
}
|
|
|
|
// Middlewares globaux (after CORS)
|
|
router.Use(middleware.MaintenanceGin()) // v0.803 ADM1-03: Maintenance mode (503 except /health, /admin)
|
|
router.Use(middleware.RequestLogger(r.logger)) // Utilisation du structured logger
|
|
router.Use(middleware.Metrics()) // Prometheus Metrics
|
|
router.Use(middleware.SentryRecover(r.logger)) // Sentry error tracking
|
|
router.Use(middleware.SecurityHeaders()) // MOD-P2-005: Security headers (HSTS, CSP, etc.)
|
|
router.Use(middleware.CCPA()) // v0.803 SEC2-06: CCPA Do Not Sell (Sec-GPC)
|
|
|
|
// v0.803 SEC2-03: HTTP audit middleware for auto-logging POST/PUT/DELETE
|
|
if r.config != nil && r.config.AuditService != nil {
|
|
router.Use(middleware.AuditMiddleware(r.config.AuditService, r.logger))
|
|
}
|
|
|
|
// INT-021: Add API monitoring middleware to track failures and trigger alerts
|
|
router.Use(middleware.APIMonitoringMiddleware(r.logger, r.monitoringService))
|
|
|
|
// MOD-P1-005: Determine if stack traces should be included in logs
|
|
// Stack traces only in dev/DEBUG mode (not in production)
|
|
// Include if: APP_ENV=development OR LOG_LEVEL=DEBUG
|
|
// MOD-P1-005: Determine if stack traces should be included in logs
|
|
// Stack traces only in dev/DEBUG mode (not in production)
|
|
includeStackTrace := r.config.Env == config.EnvDevelopment || r.config.LogLevel == "DEBUG"
|
|
router.Use(middleware.ErrorHandler(r.logger, r.config.ErrorMetrics, includeStackTrace))
|
|
router.Use(middleware.Recovery(r.logger, includeStackTrace))
|
|
router.Use(middleware.RequestID())
|
|
// Global Timeout middleware (PR-6)
|
|
// MOD-P0-003: Removed duplicate timeout middleware registration
|
|
router.Use(middleware.Timeout(r.config.HandlerTimeout))
|
|
|
|
// v0.803 SEC1-04: DDoS rate limiting (1000 req/s global, 100 req/s per-IP)
|
|
if r.config != nil && r.config.RedisClient != nil {
|
|
router.Use(middleware.DDoSRateLimitMiddleware(r.config.RedisClient))
|
|
}
|
|
|
|
// Rate limiting via config.RateLimiter si disponible, sinon utiliser SimpleRateLimiter
|
|
// Toujours actif (A04) — limites assouplies en dev via config
|
|
if r.config != nil {
|
|
if r.config.RateLimiter != nil {
|
|
router.Use(r.config.RateLimiter.RateLimitMiddleware())
|
|
} else if r.config.SimpleRateLimiter != nil {
|
|
router.Use(r.config.SimpleRateLimiter.Middleware())
|
|
}
|
|
}
|
|
|
|
// Swagger Documentation — disabled in production (A05)
|
|
if r.config == nil || (r.config.Env != config.EnvProduction && r.config.Env != "prod") {
|
|
swaggerHandler := func(c *gin.Context) {
|
|
if c.Param("any") == "/doc.json" {
|
|
if _, err := os.Stat("./docs/swagger.json"); err == nil {
|
|
c.File("./docs/swagger.json")
|
|
return
|
|
}
|
|
}
|
|
ginSwagger.WrapHandler(swaggerFiles.Handler)(c)
|
|
}
|
|
router.GET("/swagger/*any", swaggerHandler)
|
|
router.GET("/docs", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
|
router.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
|
}
|
|
|
|
// BE-SVC-019: API versioning endpoint (before version middleware)
|
|
router.GET("/api/versions", VersionInfoHandler(r.versionManager))
|
|
|
|
// BE-SVC-019: Apply version middleware to API routes
|
|
router.Use(VersionMiddleware(r.versionManager))
|
|
|
|
// P1.6: Health endpoint for Docker/K8s healthchecks
|
|
// Must be before other routes to avoid middleware overhead
|
|
// DUPLICATE REMOVED to fix panic.
|
|
|
|
// Routes core publiques (health, metrics, upload info)
|
|
r.setupCorePublicRoutes(router)
|
|
|
|
// Setup internal routes (both legacy and modern) before v1 group
|
|
// These need to be on the root router, not under /api/v1
|
|
r.setupInternalRoutes(router)
|
|
|
|
// Groupe API v1 (nouveau frontend React)
|
|
v1 := router.Group("/api/v1")
|
|
{
|
|
// Auth routes first so r.authService is set for admin unlock in setupCoreProtectedRoutes
|
|
if err := r.setupAuthRoutes(v1); err != nil {
|
|
return err
|
|
}
|
|
// Routes core protégées (sessions, uploads, audit, admin, conversations)
|
|
r.setupCoreProtectedRoutes(v1)
|
|
|
|
// Action 5.2.1.1: Validation endpoint for pre-validation
|
|
r.setupValidateRoutes(v1)
|
|
|
|
// Réactivation des routes User et Track pour Phase 1
|
|
r.setupUserRoutes(v1)
|
|
r.setupTrackRoutes(v1)
|
|
|
|
// BE-API-007: Roles management routes
|
|
r.setupRoleRoutes(v1)
|
|
|
|
// Réactivation des routes Chat pour Phase 4
|
|
r.setupChatRoutes(v1)
|
|
|
|
// v0.502: Chat WebSocket endpoint (replaces Rust chat server)
|
|
r.setupChatWebSocket(v1)
|
|
|
|
// Réactivation des routes Playlists pour Phase 5
|
|
r.setupPlaylistRoutes(v1)
|
|
// Réactivation des routes Webhooks
|
|
r.setupWebhookRoutes(v1)
|
|
|
|
// Marketplace Routes (v1.2.0)
|
|
r.setupMarketplaceRoutes(v1)
|
|
|
|
// BE-API-035: Analytics routes
|
|
r.setupAnalyticsRoutes(v1)
|
|
|
|
// Social Routes
|
|
r.setupSocialRoutes(v1)
|
|
|
|
// Feed Routes (v0.10.0 F210: tracks from followed users)
|
|
r.setupFeedRoutes(v1)
|
|
|
|
// Discover Routes (v0.10.1 F351-F355: tags, genres, browse)
|
|
r.setupDiscoverRoutes(v1)
|
|
|
|
// Inventory / Gear Routes
|
|
r.setupGearRoutes(v1)
|
|
|
|
// Queue Routes
|
|
r.setupQueueRoutes(v1)
|
|
|
|
// Developer Portal (API Keys)
|
|
r.setupDeveloperRoutes(v1)
|
|
|
|
// Live Streams Routes
|
|
r.setupLiveRoutes(v1)
|
|
|
|
// Cloud Storage Routes (v0.501 C1)
|
|
r.setupCloudRoutes(v1)
|
|
|
|
// Tag suggestions (v0.802 FM1-03)
|
|
r.setupTagRoutes(v1)
|
|
|
|
// Unified search GET /search (tracks, users, playlists)
|
|
r.setupSearchRoutes(v1)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setupChatWebSocket configures the chat WebSocket endpoint (v0.502)
|
|
func (r *APIRouter) setupChatWebSocket(router *gin.RouterGroup) {
|
|
chatService := services.NewChatServiceWithDB(r.config.ChatJWTSecret, r.db.GormDB, r.logger)
|
|
|
|
presenceService := chatws.NewChatPresenceService(r.config.RedisClient, r.logger)
|
|
hub := chatws.NewHub(r.logger, presenceService)
|
|
go hub.Run()
|
|
|
|
msgRepo := repositories.NewChatMessageRepository(r.db.GormDB)
|
|
readRepo := repositories.NewReadReceiptRepository(r.db.GormDB)
|
|
deliveredRepo := repositories.NewDeliveredStatusRepository(r.db.GormDB)
|
|
reactionRepo := repositories.NewReactionRepository(r.db.GormDB)
|
|
userRepo := repositories.NewGormUserRepository(r.db.GormDB)
|
|
|
|
pubsub := services.NewChatPubSubService(r.config.RedisClient, r.logger)
|
|
permissions := chatws.NewPermissionService(r.db.GormDB, r.logger)
|
|
rateLimiter := chatws.NewRateLimiter(r.config.RedisClient, r.logger)
|
|
|
|
msgHandler := chatws.NewMessageHandler(
|
|
hub, msgRepo, readRepo, deliveredRepo, reactionRepo, userRepo,
|
|
pubsub, permissions, rateLimiter, r.logger,
|
|
)
|
|
|
|
wsHandler := handlers.NewChatWebSocketHandler(chatService, hub, msgHandler, r.logger)
|
|
|
|
router.GET("/ws", wsHandler.HandleWebSocket)
|
|
|
|
// v0.10.5 F551: Inject chat hub into notification service for real-time delivery
|
|
if r.notificationService != nil {
|
|
r.notificationService.SetWSNotifier(&chatHubNotifierAdapter{hub: hub})
|
|
}
|
|
|
|
r.logger.Info("Chat WebSocket endpoint registered at /api/v1/ws")
|
|
}
|
|
|
|
// chatHubNotifierAdapter adapts chatws.Hub to services.NotificationWSNotifier (F551)
|
|
type chatHubNotifierAdapter struct {
|
|
hub *chatws.Hub
|
|
}
|
|
|
|
func (a *chatHubNotifierAdapter) NotifyUser(userID uuid.UUID, payload []byte) {
|
|
a.hub.SendToUser(userID, payload)
|
|
}
|
|
|
|
// setupChatRoutes configure les routes de chat
|
|
func (r *APIRouter) setupChatRoutes(router *gin.RouterGroup) {
|
|
// BE-API-006: Use NewChatServiceWithDB to enable stats functionality
|
|
chatService := services.NewChatServiceWithDB(r.config.ChatJWTSecret, r.db.GormDB, r.logger)
|
|
userRepo := repositories.NewGormUserRepository(r.db.GormDB)
|
|
userService := services.NewUserServiceWithDB(userRepo, r.db.GormDB)
|
|
|
|
chatHandler := handlers.NewChatHandler(chatService, userService, r.logger)
|
|
|
|
// v0.9.6: Chat reactions REST + message search
|
|
reactionRepo := repositories.NewReactionRepository(r.db.GormDB)
|
|
msgRepo := repositories.NewChatMessageRepository(r.db.GormDB)
|
|
permissions := chatws.NewPermissionService(r.db.GormDB, r.logger)
|
|
chatReactionHandler := handlers.NewChatReactionHandler(reactionRepo, msgRepo, permissions, r.logger)
|
|
chatSearchHandler := handlers.NewChatSearchHandler(msgRepo, permissions, r.logger)
|
|
|
|
chat := router.Group("/chat")
|
|
{
|
|
if r.config.AuthMiddleware != nil {
|
|
chat.Use(r.config.AuthMiddleware.RequireAuth())
|
|
// BE-SEC-004: Apply CSRF protection to all state-changing endpoints
|
|
r.applyCSRFProtection(chat)
|
|
chat.POST("/token", chatHandler.GetToken)
|
|
chat.GET("/stats", chatHandler.GetStats) // BE-API-006: Chat stats endpoint
|
|
|
|
// v0.9.6: Chat rooms REST (reactions, search)
|
|
// v0.9.7: Chat room attachments (file upload)
|
|
rooms := chat.Group("/rooms")
|
|
{
|
|
rooms.POST("/:roomId/messages/:messageId/reactions", chatReactionHandler.AddReaction)
|
|
rooms.DELETE("/:roomId/messages/:messageId/reactions", chatReactionHandler.RemoveReaction)
|
|
rooms.GET("/:roomId/messages/search", chatSearchHandler.SearchMessages)
|
|
// v0.9.7: Chat attachment upload (mp3, wav, ogg, jpg, png, pdf, max 50MB)
|
|
if r.config.S3StorageService != nil {
|
|
uploadConfig := getUploadConfigWithEnv()
|
|
chatUploadValidator, err := services.NewUploadValidator(uploadConfig, r.logger)
|
|
if err != nil {
|
|
r.logger.Warn("Chat upload validator unavailable - attachments disabled", zap.Error(err))
|
|
} else {
|
|
chatAttachmentHandler := handlers.NewChatAttachmentHandler(
|
|
r.config.S3StorageService,
|
|
permissions,
|
|
chatUploadValidator,
|
|
r.logger,
|
|
)
|
|
rooms.POST("/:roomId/attachments", chatAttachmentHandler.UploadChatAttachment)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|