package api import ( "context" "net/http" "os" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" "go.uber.org/zap" "veza-backend-api/internal/config" trackcore "veza-backend-api/internal/core/track" elasticsearch "veza-backend-api/internal/elasticsearch" "veza-backend-api/internal/handlers" "veza-backend-api/internal/middleware" "veza-backend-api/internal/models" "veza-backend-api/internal/repositories" "veza-backend-api/internal/services" ) // setupValidateRoutes configures the validation endpoint (A01: rate limited) func (r *APIRouter) setupValidateRoutes(router *gin.RouterGroup) { validateHandler := handlers.NewValidateHandler(r.logger) validateGroup := router.Group("/") if r.config != nil && r.config.EndpointLimiter != nil { validateGroup.Use(r.config.EndpointLimiter.ValidateRateLimit()) } validateGroup.POST("validate", validateHandler.Validate) } // setupInternalRoutes configure les routes internal (legacy and modern) func (r *APIRouter) setupInternalRoutes(router *gin.Engine) { uploadDir := r.config.UploadDir if uploadDir == "" { uploadDir = "uploads/tracks" } chunksDir := uploadDir + "/chunks" trackService := trackcore.NewTrackServiceWithDB(r.db, r.logger, uploadDir) if r.config.CacheService != nil { trackService.SetCacheService(r.config.CacheService) } streamService := services.NewStreamServiceWithAPIKey(r.config.StreamServerURL, r.config.StreamServerInternalAPIKey, r.logger) trackService.SetStreamService(streamService) // INT-02: Enable HLS pipeline for regular uploads trackUploadService := services.NewTrackUploadService(r.db.GormDB, r.logger) var redisClient *redis.Client if r.config != nil { redisClient = r.config.RedisClient } chunkService := services.NewTrackChunkService(chunksDir, redisClient, r.logger) likeService := services.NewTrackLikeService(r.db.GormDB, r.logger) trackHandler := trackcore.NewTrackHandler( trackService, trackUploadService, chunkService, likeService, streamService, ) streamEventsHandler := handlers.NewStreamEventsHandler(r.logger) liveStreamRepo := repositories.NewLiveStreamRepository(r.db.GormDB) roomRepo := repositories.NewRoomRepository(r.db.GormDB) liveStreamService := services.NewLiveStreamService(liveStreamRepo, roomRepo) streamEventsHandler.SetLiveStreamService(liveStreamService) expectedKey := "" if r.config != nil { expectedKey = r.config.StreamServerInternalAPIKey } streamCallbackAuth := middleware.StreamCallbackAuth(expectedKey, r.logger) // v0.941: Removed deprecated /internal/* routes; use /api/v1/internal/* only v1Internal := router.Group("/api/v1/internal") v1Internal.Use(streamCallbackAuth) { v1Internal.POST("/tracks/:id/stream-ready", trackHandler.HandleStreamCallback) v1Internal.POST("/stream-events", streamEventsHandler.HandleStreamEvent) } } // setupCorePublicRoutes configure les routes publiques core (health, metrics, upload info) func (r *APIRouter) setupCorePublicRoutes(router *gin.Engine) { var healthCheckHandler gin.HandlerFunc var livenessHandler gin.HandlerFunc var readinessHandler gin.HandlerFunc var deepHealthHandler gin.HandlerFunc if r.db != nil && r.db.GormDB != nil { var redisClient interface{} if r.config != nil { redisClient = r.config.RedisClient } var rabbitMQEventBus interface{} if r.config != nil { rabbitMQEventBus = r.config.RabbitMQEventBus } var env string if r.config != nil { env = r.config.Env } var s3Service interface{} var jobWorker interface{} var emailSender interface{} if r.config != nil { s3Service = r.config.S3StorageService jobWorker = r.config.JobWorker emailSender = r.config.EmailSender } healthHandler := handlers.NewHealthHandlerWithServices( r.db.GormDB, r.logger, redisClient, rabbitMQEventBus, env, s3Service, jobWorker, emailSender, ) if r.config != nil { healthHandler.SetDeepHealthConfig(&handlers.DeepHealthConfig{ JWTSecretSet: len(r.config.JWTSecret) >= 32, StripeConnectEnabled: r.config.StripeConnectEnabled, PlatformFeeRate: r.config.PlatformFeeRate, TransferRetryEnabled: r.config.TransferRetryEnabled, }) } healthCheckHandler = healthHandler.Check livenessHandler = healthHandler.Liveness readinessHandler = healthHandler.Readiness deepHealthHandler = healthHandler.DeepHealth } else { healthCheckHandler = handlers.SimpleHealthCheck livenessHandler = handlers.SimpleHealthCheck readinessHandler = handlers.SimpleHealthCheck deepHealthHandler = func(c *gin.Context) { c.JSON(http.StatusServiceUnavailable, gin.H{ "success": true, "data": gin.H{"status": "unhealthy", "message": "Database not configured"}, }) } } deprecationMW := middleware.DeprecationWarning(r.logger) healthMonitoringMW := middleware.HealthCheckMonitoring(r.logger, r.monitoringService) metricsProtection := middleware.MetricsProtection(r.logger) router.GET("/health", deprecationMW, healthMonitoringMW, healthCheckHandler) router.GET("/health/deep", deprecationMW, healthMonitoringMW, deepHealthHandler) router.GET("/healthz", deprecationMW, healthMonitoringMW, livenessHandler) router.GET("/readyz", deprecationMW, healthMonitoringMW, readinessHandler) router.GET("/metrics", deprecationMW, metricsProtection, handlers.PrometheusMetrics()) if r.config != nil && r.config.ErrorMetrics != nil { router.GET("/metrics/aggregated", deprecationMW, metricsProtection, handlers.AggregatedMetrics(r.config.ErrorMetrics)) } router.GET("/system/metrics", deprecationMW, metricsProtection, handlers.SystemMetrics) v1Public := router.Group("/api/v1") { v1Public.GET("/health", healthCheckHandler) v1Public.GET("/health/deep", deepHealthHandler) v1Public.GET("/healthz", livenessHandler) v1Public.GET("/readyz", readinessHandler) if r.db != nil && r.db.GormDB != nil { var redisClient interface{} if r.config != nil { redisClient = r.config.RedisClient } chatServerURL := "" streamServerURL := "" if r.config != nil { chatServerURL = r.config.ChatServerURL streamServerURL = r.config.StreamServerURL } getEnv := func(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue } version := getEnv("APP_VERSION", "v1.0.0") gitCommit := getEnv("GIT_COMMIT", "unknown") buildTime := getEnv("BUILD_TIME", "") environment := "" if r.config != nil { environment = r.config.Env } statusHandler := handlers.NewStatusHandler( r.db.GormDB, r.logger, redisClient, chatServerURL, streamServerURL, version, gitCommit, buildTime, environment, ) v1Public.GET("/status", statusHandler.GetStatus) } v1Public.GET("/metrics", metricsProtection, handlers.PrometheusMetrics()) if r.config != nil && r.config.ErrorMetrics != nil { v1Public.GET("/metrics/aggregated", metricsProtection, handlers.AggregatedMetrics(r.config.ErrorMetrics)) } v1Public.GET("/system/metrics", metricsProtection, handlers.SystemMetrics) if r.db != nil && r.db.GormDB != nil && r.config != nil { uploadConfig := getUploadConfigWithEnv() uploadValidator, err := services.NewUploadValidator(uploadConfig, r.logger) if err != nil { r.logger.Warn("Upload validator created with ClamAV unavailable - uploads will be rejected", zap.Error(err)) uploadConfig.ClamAVEnabled = false uploadValidator, _ = services.NewUploadValidator(uploadConfig, r.logger) } auditService := services.NewAuditService(r.db, r.logger) trackUploadService := services.NewTrackUploadService(r.db.GormDB, r.logger) uploadHandler := handlers.NewUploadHandler(uploadValidator, auditService, trackUploadService, r.logger, r.config.MaxConcurrentUploads) v1Public.GET("/upload/limits", uploadHandler.GetUploadLimits()) v1Public.GET("/upload/validate-type", uploadHandler.ValidateFileType()) } if r.config != nil { frontendLogHandler, err := handlers.NewFrontendLogHandler(r.config, r.logger) if err != nil { r.logger.Warn("Failed to create frontend log handler, frontend logs will not be stored", zap.Error(err)) } else { logsRateLimit := middleware.FrontendLogRateLimit(r.config.RedisClient) v1Public.POST("/logs/frontend", logsRateLimit, frontendLogHandler.ReceiveLog) r.logger.Info("Frontend logging endpoint enabled", zap.String("endpoint", "/api/v1/logs/frontend")) } } // v0.803 ADM1-04: Active announcements (public) if r.db != nil && r.db.GormDB != nil { announcementSvc := services.NewAnnouncementService(r.db.GormDB, r.logger) announcementHandler := handlers.NewAnnouncementHandler(announcementSvc) v1Public.GET("/announcements/active", announcementHandler.GetActive) } } } // setupCoreProtectedRoutes configure les routes protégées core (sessions, uploads, audit, admin, conversations) func (r *APIRouter) setupCoreProtectedRoutes(v1 *gin.RouterGroup) { if r.db == nil || r.db.GormDB == nil || r.config == nil { return } csrfMiddleware := middleware.NewCSRFMiddleware(r.config.RedisClient, r.logger) csrfMiddleware.SetEnvironment(r.config.Env) csrfHandler := handlers.NewCSRFHandler(csrfMiddleware, r.logger) if r.config.AuthMiddleware != nil { v1.GET("/csrf-token", r.config.AuthMiddleware.OptionalAuth(), csrfHandler.GetCSRFToken()) } else { v1.GET("/csrf-token", csrfHandler.GetCSRFToken()) } protected := v1.Group("/") if r.config.AuthMiddleware != nil { protected.Use(r.config.AuthMiddleware.RequireAuth()) } sessionService := services.NewSessionService(r.db, r.logger) if r.config.RedisClient != nil { protected.Use(csrfMiddleware.Middleware()) r.logger.Info("CSRF protection enabled for core protected routes", zap.String("environment", r.config.Env), ) } else { r.logger.Warn("Redis not available - CSRF protection disabled (non-production environment)", zap.String("environment", r.config.Env), ) } uploadConfig := getUploadConfigWithEnv() uploadValidator, err := services.NewUploadValidator(uploadConfig, r.logger) if err != nil { r.logger.Warn("Upload validator created with ClamAV unavailable - uploads will be rejected", zap.Error(err)) uploadConfig.ClamAVEnabled = false uploadValidator, _ = services.NewUploadValidator(uploadConfig, r.logger) } auditService := services.NewAuditService(r.db, r.logger) trackUploadService := services.NewTrackUploadService(r.db.GormDB, r.logger) sessionHandler := handlers.NewSessionHandler(sessionService, auditService, r.logger) uploadHandler := handlers.NewUploadHandler(uploadValidator, auditService, trackUploadService, r.logger, r.config.MaxConcurrentUploads) auditHandler := handlers.NewAuditHandler(auditService, r.logger) sessions := protected.Group("/sessions") { sessions.POST("/logout", sessionHandler.Logout()) sessions.POST("/logout-all", sessionHandler.LogoutAll()) sessions.POST("/logout-others", sessionHandler.LogoutOthers()) sessions.GET("", sessionHandler.GetSessions()) sessions.GET("/", sessionHandler.GetSessions()) sessions.DELETE("/:session_id", sessionHandler.RevokeSession()) sessions.GET("/stats", sessionHandler.GetSessionStats()) sessions.POST("/refresh", sessionHandler.RefreshSession()) } uploads := protected.Group("/uploads") { if r.config.RedisClient != nil { uploads.Use(middleware.UploadRateLimit(r.config.RedisClient)) } uploads.POST("/", uploadHandler.UploadFile()) uploads.POST("/batch", uploadHandler.BatchUpload()) uploads.GET("/:id/status", uploadHandler.GetUploadStatus()) uploads.GET("/:id/progress", uploadHandler.UploadProgress()) uploads.DELETE("/:id", uploadHandler.DeleteUpload()) uploads.GET("/stats", uploadHandler.GetUploadStats()) } // v0.803 ADM1: User report endpoint (authenticated users can report content) reportServiceForUser := services.NewReportService(r.db.GormDB, r.logger) reportHandlerForUser := handlers.NewReportHandler(reportServiceForUser) protected.POST("/reports", reportHandlerForUser.CreateReport) // v0.971: Client-visible feature flags (e.g. WEBRTC_CALLS for CallButton) featureFlagSvc := services.NewFeatureFlagService(r.db.GormDB, r.logger) featureFlagHandler := handlers.NewFeatureFlagHandler(featureFlagSvc) protected.GET("/feature-flags", featureFlagHandler.List) audit := protected.Group("/audit") { audit.GET("/logs", auditHandler.SearchLogs()) audit.GET("/stats", auditHandler.GetStats()) audit.GET("/activity", auditHandler.GetUserActivity()) audit.GET("/suspicious", auditHandler.DetectSuspiciousActivity()) audit.GET("/ip/:ip", auditHandler.GetIPActivity()) audit.GET("/logs/:id", auditHandler.GetAuditLog()) audit.POST("/cleanup", auditHandler.CleanupOldLogs()) } uploadDir := r.config.UploadDir if uploadDir == "" { uploadDir = "uploads/tracks" } trackServiceForDashboard := trackcore.NewTrackServiceWithDB(r.db, r.logger, uploadDir) if r.config.CacheService != nil { trackServiceForDashboard.SetCacheService(r.config.CacheService) } trackListFunc := func(ctx context.Context, params handlers.TrackListParamsForDashboard) ([]*models.Track, int64, error) { return trackServiceForDashboard.ListTracks(ctx, trackcore.TrackListParams{ Page: params.Page, Limit: params.Limit, UserID: params.UserID, Genre: params.Genre, Format: params.Format, SortBy: params.SortBy, SortOrder: params.SortOrder, }) } dashboardHandler := handlers.NewDashboardHandler(auditService, trackListFunc, r.logger) protected.GET("/dashboard", dashboardHandler.GetDashboard()) roomRepo := repositories.NewRoomRepository(r.db.GormDB) messageRepo := repositories.NewChatMessageRepository(r.db.GormDB) roomService := services.NewRoomService(roomRepo, messageRepo, r.logger) roomHandler := handlers.NewRoomHandler(roomService, r.logger) conversations := protected.Group("/conversations") { conversations.GET("", roomHandler.GetUserRooms) conversations.POST("", roomHandler.CreateRoom) conversations.GET("/join/:token", roomHandler.JoinByToken) conversations.GET("/:id", roomHandler.GetRoom) conversations.PUT("/:id", roomHandler.UpdateRoom) conversations.DELETE("/:id", roomHandler.DeleteRoom) conversations.POST("/:id/leave", roomHandler.LeaveRoom) // v0.9.7 self-leave conversations.GET("/:id/members", roomHandler.GetMembers) conversations.POST("/:id/members", roomHandler.AddMember) conversations.DELETE("/:id/members/:userId", roomHandler.KickMember) conversations.PATCH("/:id/members/:userId", roomHandler.UpdateMemberRole) // v0.9.7 conversations.POST("/:id/participants", roomHandler.AddParticipant) conversations.DELETE("/:id/participants/:userId", roomHandler.RemoveParticipant) conversations.POST("/:id/invitations", roomHandler.CreateInvitation) conversations.GET("/:id/history", roomHandler.GetRoomHistory) } r.notificationService = services.NewNotificationService(r.db, r.logger) vapidPublic := os.Getenv("VAPID_PUBLIC_KEY") vapidPrivate := os.Getenv("VAPID_PRIVATE_KEY") r.pushService = services.NewPushService(r.db.GormDB, r.logger, vapidPublic, vapidPrivate) r.notificationService.SetPushService(r.pushService) handlers.NewNotificationHandlers(r.notificationService, r.pushService) notifications := protected.Group("/notifications") { notifications.GET("", handlers.NotificationHandlersInstance.GetNotifications) notifications.GET("/unread-count", handlers.NotificationHandlersInstance.GetUnreadCount) notifications.GET("/preferences", handlers.NotificationHandlersInstance.GetPreferences) notifications.PUT("/preferences", handlers.NotificationHandlersInstance.UpdatePreferences) notifications.POST("/push/subscribe", handlers.NotificationHandlersInstance.SubscribePush) notifications.POST("/:id/read", handlers.NotificationHandlersInstance.MarkAsRead) notifications.POST("/read-all", handlers.NotificationHandlersInstance.MarkAllAsRead) notifications.DELETE("/:id", handlers.NotificationHandlersInstance.DeleteNotification) notifications.DELETE("", handlers.NotificationHandlersInstance.DeleteAllNotifications) } admin := v1.Group("/admin") { if r.config.AuthMiddleware != nil { admin.Use(r.config.AuthMiddleware.RequireAuth()) admin.Use(r.config.AuthMiddleware.RequireAdmin()) admin.Use(r.config.AuthMiddleware.RequireMFA()) // SFIX-001: MFA obligatoire pour admin } admin.GET("/audit/logs", auditHandler.SearchLogs()) admin.GET("/audit/stats", auditHandler.GetStats()) admin.GET("/audit/suspicious", auditHandler.DetectSuspiciousActivity()) // v0.803 ADM1: Moderation queue reportService := services.NewReportService(r.db.GormDB, r.logger) reportHandler := handlers.NewReportHandler(reportService) admin.GET("/reports", reportHandler.ListReports) admin.POST("/reports/:id/resolve", reportHandler.ResolveReport) // v0.803 ADM1-03: Maintenance mode toggle admin.PUT("/maintenance", func(c *gin.Context) { var req struct { Enabled bool `json:"enabled"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "enabled is required"}) return } middleware.SetMaintenanceMode(req.Enabled) c.JSON(http.StatusOK, gin.H{"maintenance_mode": req.Enabled}) }) admin.GET("/maintenance", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"maintenance_mode": middleware.MaintenanceModeEnabled()}) }) // v0.803 ADM1-04: Announcements CRUD announcementSvc := services.NewAnnouncementService(r.db.GormDB, r.logger) announcementHandler := handlers.NewAnnouncementHandler(announcementSvc) admin.GET("/announcements", announcementHandler.List) admin.POST("/announcements", announcementHandler.Create) admin.DELETE("/announcements/:id", announcementHandler.Delete) // v0.803 ADM1-05: Feature flags CRUD featureFlagSvc := services.NewFeatureFlagService(r.db.GormDB, r.logger) featureFlagHandler := handlers.NewFeatureFlagHandler(featureFlagSvc) admin.GET("/feature-flags", featureFlagHandler.List) admin.PUT("/feature-flags/:name", featureFlagHandler.Toggle) // v0.701: Admin Transfer Dashboard var adminTransferHandler *handlers.AdminTransferHandler if r.config != nil && r.config.StripeConnectEnabled && r.config.StripeConnectSecretKey != "" { stripeConnectSvc := services.NewStripeConnectService(r.db.GormDB, r.config.StripeConnectSecretKey, r.logger) adminTransferHandler = handlers.NewAdminTransferHandler(r.db.GormDB, stripeConnectSvc, r.config.PlatformFeeRate, r.logger) } else { feeRate := 0.10 if r.config != nil { feeRate = r.config.PlatformFeeRate } adminTransferHandler = handlers.NewAdminTransferHandler(r.db.GormDB, nil, feeRate, r.logger) } admin.GET("/transfers", adminTransferHandler.GetTransfers) admin.POST("/transfers/:id/retry", adminTransferHandler.RetryTransfer) // P1.5: pprof endpoints disabled in production to avoid leaking sensitive runtime info if r.config != nil && r.config.Env != config.EnvProduction && r.config.Env != "prod" { admin.Any("/debug/pprof/*path", gin.WrapH(http.DefaultServeMux)) r.logger.Info("pprof endpoints enabled at /api/v1/admin/debug/pprof/") } if r.authService != nil { admin.POST("/auth/unlock-account", handlers.UnlockAccount(r.authService, r.logger)) } // v0.10.2 F361: Elasticsearch reindex (admin only) admin.POST("/search/reindex", func(c *gin.Context) { esCfg := elasticsearch.LoadConfig() if !esCfg.Enabled { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Elasticsearch not configured"}) return } esClient, err := elasticsearch.NewClient(esCfg, r.logger) if err != nil { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Elasticsearch unavailable", "detail": err.Error()}) return } idx := elasticsearch.NewIndexer(esClient, r.db.GormDB, r.logger) if err := idx.ReindexAll(c.Request.Context()); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Reindex failed", "detail": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "ok", "message": "Reindex completed"}) }) } }