package api import ( "context" "net/http" "os" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" "go.uber.org/zap" trackcore "veza-backend-api/internal/core/track" "veza-backend-api/internal/config" "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) } 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) streamService := services.NewStreamServiceWithAPIKey(r.config.StreamServerURL, r.config.StreamServerInternalAPIKey, r.logger) trackHandler := trackcore.NewTrackHandler( trackService, trackUploadService, chunkService, likeService, streamService, ) streamEventsHandler := handlers.NewStreamEventsHandler(r.logger) liveStreamRepo := repositories.NewLiveStreamRepository(r.db.GormDB) liveStreamService := services.NewLiveStreamService(liveStreamRepo) streamEventsHandler.SetLiveStreamService(liveStreamService) expectedKey := "" if r.config != nil { expectedKey = r.config.StreamServerInternalAPIKey } streamCallbackAuth := middleware.StreamCallbackAuth(expectedKey, r.logger) internalDeprecated := router.Group("/internal") internalDeprecated.Use(middleware.DeprecationWarning(r.logger)) internalDeprecated.Use(streamCallbackAuth) { internalDeprecated.POST("/tracks/:id/stream-ready", trackHandler.HandleStreamCallback) internalDeprecated.POST("/stream-events", streamEventsHandler.HandleStreamEvent) } 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 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, ) healthCheckHandler = healthHandler.Check livenessHandler = healthHandler.Liveness readinessHandler = healthHandler.Readiness } else { healthCheckHandler = handlers.SimpleHealthCheck livenessHandler = handlers.SimpleHealthCheck readinessHandler = handlers.SimpleHealthCheck } deprecationMW := middleware.DeprecationWarning(r.logger) healthMonitoringMW := middleware.HealthCheckMonitoring(r.logger, r.monitoringService) router.GET("/health", deprecationMW, healthMonitoringMW, healthCheckHandler) router.GET("/healthz", deprecationMW, healthMonitoringMW, livenessHandler) router.GET("/readyz", deprecationMW, healthMonitoringMW, readinessHandler) router.GET("/metrics", deprecationMW, handlers.PrometheusMetrics()) if r.config != nil && r.config.ErrorMetrics != nil { router.GET("/metrics/aggregated", deprecationMW, handlers.AggregatedMetrics(r.config.ErrorMetrics)) } router.GET("/system/metrics", deprecationMW, handlers.SystemMetrics) v1Public := router.Group("/api/v1") { v1Public.GET("/health", healthCheckHandler) 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", handlers.PrometheusMetrics()) if r.config != nil && r.config.ErrorMetrics != nil { v1Public.GET("/metrics/aggregated", handlers.AggregatedMetrics(r.config.ErrorMetrics)) } v1Public.GET("/system/metrics", 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")) } } } } // 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()) } 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("/:id", roomHandler.GetRoom) conversations.PUT("/:id", roomHandler.UpdateRoom) conversations.DELETE("/:id", roomHandler.DeleteRoom) conversations.POST("/:id/members", roomHandler.AddMember) conversations.POST("/:id/participants", roomHandler.AddParticipant) conversations.DELETE("/:id/participants/:userId", roomHandler.RemoveParticipant) 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.GET("/audit/logs", auditHandler.SearchLogs()) admin.GET("/audit/stats", auditHandler.GetStats()) admin.GET("/audit/suspicious", auditHandler.DetectSuspiciousActivity()) // 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)) } } }