veza/veza-backend-api/internal/api/routes_core.go

469 lines
18 KiB
Go
Raw Normal View History

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)
}
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)
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, 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("/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", 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"))
}
}
// 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("/: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())
// 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))
}
}
}