diff --git a/veza-backend-api/internal/api/router.go b/veza-backend-api/internal/api/router.go index 133580432..15ed3d54b 100644 --- a/veza-backend-api/internal/api/router.go +++ b/veza-backend-api/internal/api/router.go @@ -3,42 +3,25 @@ package api import ( "context" "fmt" - "net/http" // MOD-P2-006: Pour pprof "os" "strconv" "strings" "time" "github.com/gin-gonic/gin" - "github.com/google/uuid" - "github.com/redis/go-redis/v9" "go.uber.org/zap" "veza-backend-api/internal/config" "veza-backend-api/internal/database" - "veza-backend-api/internal/handlers" // Single handlers import + "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" - // swaggerFiles "github.com/swaggo/files" // Uncommented - // ginSwagger "github.com/swaggo/gin-swagger" // Uncommented - - // Add missing imports. swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" - eduApi "veza-backend-api/internal/api/education" authcore "veza-backend-api/internal/core/auth" - eduCore "veza-backend-api/internal/core/education" - "veza-backend-api/internal/core/marketplace" - socialcore "veza-backend-api/internal/core/social" - trackcore "veza-backend-api/internal/core/track" - "veza-backend-api/internal/services" - "veza-backend-api/internal/workers" - // swaggerFiles "github.com/swaggo/files" - // ginSwagger "github.com/swaggo/gin-swagger" ) // APIRouter gère la configuration des routes de l'API @@ -326,483 +309,6 @@ func (r *APIRouter) Setup(router *gin.Engine) error { return nil } -// Méthodes de configuration des routes par module -// setupMarketplaceRoutes configure les routes de la marketplace -func (r *APIRouter) setupMarketplaceRoutes(router *gin.RouterGroup) { - uploadDir := r.config.UploadDir - if uploadDir == "" { - uploadDir = "uploads/tracks" - } - - // Storage service (reused from tracks logic) - storageService := services.NewTrackStorageService(uploadDir, false, r.logger) - - // Marketplace service - marketService := marketplace.NewService(r.db.GormDB, r.logger, storageService) - marketHandler := handlers.NewMarketplaceHandler(marketService, r.logger) - - group := router.Group("/marketplace") - // Public routes - group.GET("/products", marketHandler.ListProducts) - - // Protected routes - if r.config.AuthMiddleware != nil { - protected := group.Group("") - protected.Use(r.config.AuthMiddleware.RequireAuth()) - // BE-SEC-004: Apply CSRF protection to all state-changing endpoints - r.applyCSRFProtection(protected) - - // GO-012: Create product requires creator/premium/admin role - createGroup := protected.Group("") - createGroup.Use(r.config.AuthMiddleware.RequireContentCreatorRole()) - createGroup.POST("/products", marketHandler.CreateProduct) - - // BE-API-037: Update product endpoint (requires ownership) - // Resolver: Load product from DB to get its seller_id - productOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { - productIDStr := c.Param("id") - productID, err := uuid.Parse(productIDStr) - if err != nil { - return uuid.Nil, err - } - // Load product to get seller ID - product, err := marketService.GetProduct(c.Request.Context(), productID) - if err != nil { - return uuid.Nil, err - } - return product.SellerID, nil - } - protected.PUT("/products/:id", r.config.AuthMiddleware.RequireOwnershipOrAdmin("product", productOwnerResolver), marketHandler.UpdateProduct) - - // BE-API-038: List orders endpoint - protected.GET("/orders", marketHandler.ListOrders) - // BE-API-039: Get order details endpoint - protected.GET("/orders/:id", marketHandler.GetOrder) - protected.POST("/orders", marketHandler.CreateOrder) - protected.GET("/download/:product_id", marketHandler.GetDownloadURL) - - // Wishlist routes - marketplaceExtHandler := handlers.NewMarketplaceExtHandler(marketService, r.logger) - protected.GET("/wishlist", marketplaceExtHandler.GetWishlist) - protected.POST("/wishlist", marketplaceExtHandler.AddToWishlist) - protected.DELETE("/wishlist/:productId", marketplaceExtHandler.RemoveFromWishlist) - } - - // Commerce routes (cart) - commerce := router.Group("/commerce") - if r.config.AuthMiddleware != nil { - cartProtected := commerce.Group("") - cartProtected.Use(r.config.AuthMiddleware.RequireAuth()) - r.applyCSRFProtection(cartProtected) - - marketplaceExtHandler := handlers.NewMarketplaceExtHandler(marketService, r.logger) - cartProtected.GET("/cart", marketplaceExtHandler.GetCart) - cartProtected.POST("/cart/items", marketplaceExtHandler.AddToCart) - cartProtected.DELETE("/cart/items/:id", marketplaceExtHandler.RemoveFromCart) - cartProtected.POST("/cart/checkout", marketplaceExtHandler.Checkout) - } -} - -// setupValidateRoutes configures the validation endpoint -// Action 5.2.1.1: Create /api/v1/validate endpoint -func (r *APIRouter) setupValidateRoutes(router *gin.RouterGroup) { - validateHandler := handlers.NewValidateHandler(r.logger) - // Public endpoint - no auth required for validation - router.POST("/validate", validateHandler.Validate) -} - -// setupInternalRoutes configure les routes internal (legacy and modern) -// These routes must be on the root router, not under /api/v1 -func (r *APIRouter) setupInternalRoutes(router *gin.Engine) { - // Create track handler for internal routes - uploadDir := r.config.UploadDir - if uploadDir == "" { - uploadDir = "uploads/tracks" - } - chunksDir := uploadDir + "/chunks" - - trackService := trackcore.NewTrackService(r.db.GormDB, r.logger, uploadDir) - // BE-SVC-001: Set cache service for TrackService - 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, - ) - - // Stream callback auth: validates X-Internal-API-Key from Stream Server (A01, A04) - expectedKey := "" - if r.config != nil { - expectedKey = r.config.StreamServerInternalAPIKey - } - streamCallbackAuth := middleware.StreamCallbackAuth(expectedKey, r.logger) - - // Deprecated /internal routes (legacy, on root router) - internalDeprecated := router.Group("/internal") - internalDeprecated.Use(middleware.DeprecationWarning(r.logger)) - internalDeprecated.Use(streamCallbackAuth) - { - internalDeprecated.POST("/tracks/:id/stream-ready", trackHandler.HandleStreamCallback) - } - - // New /api/v1/internal routes (modern, on root router) - v1Internal := router.Group("/api/v1/internal") - v1Internal.Use(streamCallbackAuth) - { - v1Internal.POST("/tracks/:id/stream-ready", trackHandler.HandleStreamCallback) - } -} - -// setupUserRoutes configure les routes utilisateur -func (r *APIRouter) setupUserRoutes(router *gin.RouterGroup) { - userRepo := repositories.NewGormUserRepository(r.db.GormDB) - userService := services.NewUserServiceWithDB(userRepo, r.db.GormDB) - profileHandler := handlers.NewProfileHandler(userService, r.logger) - // MOD-P1-003: Set permission service for admin check in ownership verification - if r.config != nil && r.config.PermissionService != nil { - profileHandler.SetPermissionService(r.config.PermissionService) - } - // BE-API-017: Initialize SocialService for follow/unfollow functionality - socialService := services.NewSocialService(r.db, r.logger) - profileHandler.SetSocialService(socialService) - - users := router.Group("/users") - { - users.GET("", profileHandler.ListUsers) // BE-API-040: User list endpoint - users.GET("/:id", profileHandler.GetProfile) - users.GET("/by-username/:username", profileHandler.GetProfileByUsername) - users.GET("/search", profileHandler.SearchUsers) // BE-API-008: User search endpoint - - // Protected routes - if r.config.AuthMiddleware != nil { - protected := users.Group("") - protected.Use(r.config.AuthMiddleware.RequireAuth()) - // BE-SEC-004: Apply CSRF protection to all state-changing endpoints - r.applyCSRFProtection(protected) - - // T0231/T0232: User settings endpoints (uses authenticated user, no :id) - settingsHandler := handlers.NewSettingsHandler(userService, r.logger) - protected.GET("/settings", settingsHandler.GetSettings) // GET /api/v1/users/settings - protected.PUT("/settings", settingsHandler.UpdateSettings) // PUT /api/v1/users/settings - - // MOD-P0-003: Apply ownership middleware for PUT /users/:id - // Resolver: For users, the :id param is directly the user_id - userOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { - userIDStr := c.Param("id") - return uuid.Parse(userIDStr) - } - protected.PUT("/:id", r.config.AuthMiddleware.RequireOwnershipOrAdmin("user", userOwnerResolver), profileHandler.UpdateProfile) - // BE-API-041: Delete user endpoint (soft delete) - protected.DELETE("/:id", r.config.AuthMiddleware.RequireOwnershipOrAdmin("user", userOwnerResolver), profileHandler.DeleteUser) - - protected.GET("/:id/completion", profileHandler.GetProfileCompletion) - - // BE-API-017: User follow/unfollow endpoints - protected.POST("/:id/follow", profileHandler.FollowUser) // Follow user endpoint - protected.DELETE("/:id/follow", profileHandler.UnfollowUser) // Unfollow user endpoint - - // BE-API-018: User block/unblock endpoints - protected.POST("/:id/block", profileHandler.BlockUser) // Block user endpoint - protected.DELETE("/:id/block", profileHandler.UnblockUser) // Unblock user endpoint - - // BE-API-007: User role assignment routes - roleService := services.NewRoleService(r.db.GormDB) - roleHandler := handlers.NewRoleHandler(roleService, r.logger) - protected.POST("/:id/roles", roleHandler.AssignRole) // POST /api/v1/users/:id/roles - protected.DELETE("/:id/roles/:roleId", roleHandler.RevokeRole) // DELETE /api/v1/users/:id/roles/:roleId - - // BE-API-021: Avatar upload endpoint - // BE-API-022: Avatar delete endpoint - avatarUploadDir := r.config.UploadDir - if avatarUploadDir == "" { - avatarUploadDir = "uploads/avatars" - } - imageService := services.NewImageService(avatarUploadDir) - avatarHandler := handlers.NewAvatarHandler(imageService, userService) - protected.POST("/:id/avatar", avatarHandler.UploadAvatar) // BE-API-021: Upload avatar endpoint - protected.DELETE("/:id/avatar", avatarHandler.DeleteAvatar) // BE-API-022: Delete avatar endpoint - - // BE-API-027: User liked tracks endpoint - // Initialize TrackLikeService and minimal TrackHandler for user liked tracks - uploadDir := r.config.UploadDir - if uploadDir == "" { - uploadDir = "uploads/tracks" - } - likeService := services.NewTrackLikeService(r.db.GormDB, r.logger) - trackService := trackcore.NewTrackService(r.db.GormDB, r.logger, uploadDir) - // BE-SVC-001: Set cache service for TrackService - 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 - } - chunksDir := uploadDir + "/chunks" - chunkService := services.NewTrackChunkService(chunksDir, redisClient, r.logger) - streamService := services.NewStreamServiceWithAPIKey(r.config.StreamServerURL, r.config.StreamServerInternalAPIKey, r.logger) - trackHandlerForLikes := trackcore.NewTrackHandler( - trackService, - trackUploadService, - chunkService, - likeService, - streamService, - ) - protected.GET("/:id/likes", trackHandlerForLikes.GetUserLikedTracks) // BE-API-027: Get user liked tracks endpoint - - // BE-SVC-022: Data export endpoint for GDPR compliance - dataExportService := services.NewDataExportService(r.db.GormDB, r.logger) - exportHandler := func(c *gin.Context) { - userID, exists := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, gin.H{"error": "User ID not found"}) - return - } - - userUUID, ok := userID.(uuid.UUID) - if !ok { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) - return - } - - // Export user data as JSON file - jsonData, err := dataExportService.ExportUserDataAsJSON(c.Request.Context(), userUUID) - if err != nil { - r.logger.Error("Failed to export user data", zap.Error(err), zap.String("user_id", userUUID.String())) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to export user data"}) - return - } - - // Set headers for file download - filename := "veza-data-export-" + time.Now().Format("2006-01-02T15-04-05") + ".json" - c.Header("Content-Type", "application/json") - c.Header("Content-Disposition", `attachment; filename="`+filename+`"`) - c.Header("Content-Length", strconv.Itoa(len(jsonData))) - - // Send JSON file - c.Data(http.StatusOK, "application/json", jsonData) - } - protected.GET("/me/export", exportHandler) // BE-SVC-022: Export user data endpoint - } - } -} - -// setupRoleRoutes configure les routes de gestion des rôles -// BE-API-007: Implement roles management endpoints -func (r *APIRouter) setupRoleRoutes(router *gin.RouterGroup) { - roleService := services.NewRoleService(r.db.GormDB) - roleHandler := handlers.NewRoleHandler(roleService, r.logger) - - // Roles routes - roles := router.Group("/roles") - { - // Protected routes - if r.config.AuthMiddleware != nil { - protected := roles.Group("") - protected.Use(r.config.AuthMiddleware.RequireAuth()) - // BE-SEC-004: Apply CSRF protection to all state-changing endpoints - r.applyCSRFProtection(protected) - { - protected.GET("", roleHandler.GetRoles) // GET /api/v1/roles - protected.GET("/:id", roleHandler.GetRole) // GET /api/v1/roles/:id - } - } - } -} - -// setupTrackRoutes configure les routes de gestion des tracks -func (r *APIRouter) setupTrackRoutes(router *gin.RouterGroup) { - uploadDir := r.config.UploadDir - if uploadDir == "" { - uploadDir = "uploads/tracks" - } - chunksDir := uploadDir + "/chunks" - - trackService := trackcore.NewTrackService(r.db.GormDB, r.logger, uploadDir) - // BE-SVC-001: Set cache service for TrackService - 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, - ) - // MOD-P1-003: Set permission service for admin check in ownership verification - if r.config != nil && r.config.PermissionService != nil { - trackHandler.SetPermissionService(r.config.PermissionService) - } - // MOD-P1-001: Set upload validator for ClamAV scan before persistence - // CORRECTION: Charger la config depuis l'environnement pour respecter ENABLE_CLAMAV - uploadConfig := getUploadConfigWithEnv() - fmt.Printf("🔧 [ROUTER] Configuration upload chargée - ClamAVEnabled=%v, ClamAVRequired=%v\n", - uploadConfig.ClamAVEnabled, uploadConfig.ClamAVRequired) - 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) - } - trackHandler.SetUploadValidator(uploadValidator) - - // BE-API-009: Initialize TrackSearchService for track search functionality - trackSearchService := services.NewTrackSearchService(r.db.GormDB) - trackHandler.SetSearchService(trackSearchService) - - // BE-API-014: Initialize TrackVersionService for track version restore functionality - trackVersionService := services.NewTrackVersionService(r.db.GormDB, r.logger, uploadDir) - trackHandler.SetVersionService(trackVersionService) - - // BE-API-019: Initialize PlaybackAnalyticsService for track play analytics - playbackAnalyticsService := services.NewPlaybackAnalyticsService(r.db.GormDB, r.logger) - trackHandler.SetPlaybackAnalyticsService(playbackAnalyticsService) - - tracks := router.Group("/tracks") - { - // Public routes - tracks.GET("", trackHandler.ListTracks) - tracks.GET("/search", trackHandler.SearchTracks) // BE-API-009: Track search endpoint - tracks.GET("/:id", trackHandler.GetTrack) - tracks.GET("/:id/stats", trackHandler.GetTrackStats) - tracks.GET("/:id/history", trackHandler.GetTrackHistory) - tracks.GET("/:id/download", trackHandler.DownloadTrack) - tracks.GET("/shared/:token", trackHandler.GetSharedTrack) - - // Protected routes - if r.config.AuthMiddleware != nil { - protected := tracks.Group("") - protected.Use(r.config.AuthMiddleware.RequireAuth()) - // BE-SEC-004: Apply CSRF protection to all state-changing endpoints - r.applyCSRFProtection(protected) - - // GO-012: Upload track requires creator/premium/admin role - uploadGroup := protected.Group("") - uploadGroup.Use(r.config.AuthMiddleware.RequireContentCreatorRole()) - uploadGroup.POST("", trackHandler.UploadTrack) - - // MOD-P0-003: Apply ownership middleware for PUT/DELETE /tracks/:id - // Resolver: Load track from DB to get its user_id - trackOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { - trackIDStr := c.Param("id") - trackID, err := uuid.Parse(trackIDStr) - if err != nil { - return uuid.Nil, err - } - // Load track to get owner ID - track, err := trackService.GetTrackByID(c.Request.Context(), trackID) - if err != nil { - return uuid.Nil, err - } - return track.UserID, nil - } - protected.PUT("/:id", r.config.AuthMiddleware.RequireOwnershipOrAdmin("track", trackOwnerResolver), trackHandler.UpdateTrack) - protected.DELETE("/:id", r.config.AuthMiddleware.RequireOwnershipOrAdmin("track", trackOwnerResolver), trackHandler.DeleteTrack) - - // Upload - protected.GET("/:id/status", trackHandler.GetUploadStatus) - protected.POST("/initiate", trackHandler.InitiateChunkedUpload) - protected.POST("/chunk", trackHandler.UploadChunk) - protected.POST("/complete", trackHandler.CompleteChunkedUpload) - protected.GET("/quota/:id", trackHandler.GetUploadQuota) - protected.GET("/resume/:uploadId", trackHandler.ResumeUpload) - - // Batch operations - protected.POST("/batch/delete", trackHandler.BatchDeleteTracks) - protected.POST("/batch/update", trackHandler.BatchUpdateTracks) - - // Social - protected.POST("/:id/like", trackHandler.LikeTrack) - protected.DELETE("/:id/like", trackHandler.UnlikeTrack) - protected.GET("/:id/likes", trackHandler.GetTrackLikes) - - // Sharing - protected.POST("/:id/share", trackHandler.CreateShare) - protected.DELETE("/share/:id", trackHandler.RevokeShare) - - // Versions - protected.POST("/:id/versions/:versionId/restore", trackHandler.RestoreVersion) // BE-API-014: Restore track version endpoint - - // Analytics - protected.POST("/:id/play", trackHandler.RecordPlay) // BE-API-019: Record track play event endpoint - - // HLS Streaming - // BE-API-020: HLS stream info and status endpoints - hlsOutputDir := r.config.UploadDir - if hlsOutputDir == "" { - hlsOutputDir = "uploads/tracks" - } - hlsService := services.NewHLSService(r.db.GormDB, hlsOutputDir, r.logger) - hlsHandler := handlers.NewHLSHandler(hlsService) - tracks.GET("/:id/hls/info", hlsHandler.GetStreamInfo) // BE-API-020: Get HLS stream info - tracks.GET("/:id/hls/status", hlsHandler.GetStreamStatus) // BE-API-020: Get HLS stream status - } - } - - // BE-API-013: Setup comment routes - commentService := services.NewCommentService(r.db.GormDB, r.logger) - commentHandler := handlers.NewCommentHandler(commentService, r.logger) - - // Comments routes - public GET, protected POST/DELETE - comments := router.Group("/tracks") - { - // Public: Get comments for a track - comments.GET("/:id/comments", commentHandler.GetComments) // BE-API-013: GET /api/v1/tracks/:id/comments - - // Protected: Create and delete comments - if r.config.AuthMiddleware != nil { - protected := comments.Group("") - protected.Use(r.config.AuthMiddleware.RequireAuth()) - // BE-SEC-004: Apply CSRF protection to all state-changing endpoints - r.applyCSRFProtection(protected) - { - protected.POST("/:id/comments", commentHandler.CreateComment) // BE-API-013: POST /api/v1/tracks/:id/comments - } - } - } - - // Comments routes - protected DELETE - commentsProtected := router.Group("/comments") - { - if r.config.AuthMiddleware != nil { - commentsProtected.Use(r.config.AuthMiddleware.RequireAuth()) - // BE-SEC-004: Apply CSRF protection to all state-changing endpoints - r.applyCSRFProtection(commentsProtected) - { - commentsProtected.DELETE("/:id", commentHandler.DeleteComment) // BE-API-013: DELETE /api/v1/comments/:id - } - } - } - - // Note: Internal routes are now set up in setupInternalRoutes() to avoid - // path prefix issues when setupTrackRoutes is called with a RouterGroup - // BE-API-027: User liked tracks route moved to setupUserRoutes -} - // setupChatRoutes configure les routes de chat func (r *APIRouter) setupChatRoutes(router *gin.RouterGroup) { // BE-API-006: Use NewChatServiceWithDB to enable stats functionality @@ -824,576 +330,3 @@ func (r *APIRouter) setupChatRoutes(router *gin.RouterGroup) { } } -// setupPlaylistRoutes configure les routes pour les playlists -func (r *APIRouter) setupPlaylistRoutes(router *gin.RouterGroup) { - playlistRepo := repositories.NewPlaylistRepository(r.db.GormDB) - playlistTrackRepo := repositories.NewPlaylistTrackRepository(r.db.GormDB) - playlistCollaboratorRepo := repositories.NewPlaylistCollaboratorRepository(r.db.GormDB) - userRepo := repositories.NewGormUserRepository(r.db.GormDB) - - playlistService := services.NewPlaylistService( - playlistRepo, - playlistTrackRepo, - playlistCollaboratorRepo, - userRepo, - r.logger, - ) - - // BE-API-004: Initialize PlaylistShareService for share link functionality - playlistShareService := services.NewPlaylistShareService(r.db.GormDB) - playlistService.SetPlaylistShareService(playlistShareService) - - // BE-API-005: Initialize PlaylistFollowService for recommendations functionality - playlistFollowService := services.NewPlaylistFollowService(r.db.GormDB, r.logger) - playlistService.SetPlaylistFollowService(playlistFollowService) - - playlistHandler := handlers.NewPlaylistHandler(playlistService, r.db.GormDB, r.logger) - // BE-API-005: Inject PlaylistFollowService into handler for recommendations - playlistHandler.SetPlaylistFollowService(playlistFollowService) - - // Protected routes for playlists - playlists := router.Group("/playlists") - if r.config.AuthMiddleware != nil { - playlists.Use(r.config.AuthMiddleware.RequireAuth()) - // BE-SEC-004: Apply CSRF protection to all state-changing endpoints - r.applyCSRFProtection(playlists) - { - playlists.GET("", playlistHandler.GetPlaylists) - playlists.POST("", playlistHandler.CreatePlaylist) - playlists.GET("/search", playlistHandler.SearchPlaylists) // BE-API-003: Playlist search endpoint - playlists.GET("/recommendations", playlistHandler.GetRecommendations) // BE-API-005: Playlist recommendations endpoint - playlists.GET("/:id", playlistHandler.GetPlaylist) - - // BE-SEC-003: Apply ownership middleware for PUT/DELETE /playlists/:id - // Resolver: Load playlist from DB to get its user_id - playlistOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { - playlistIDStr := c.Param("id") - playlistID, err := uuid.Parse(playlistIDStr) - if err != nil { - return uuid.Nil, err - } - // Load playlist to get owner ID - playlist, err := playlistRepo.GetByID(c.Request.Context(), playlistID) - if err != nil { - return uuid.Nil, err - } - return playlist.UserID, nil - } - playlists.PUT("/:id", r.config.AuthMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), playlistHandler.UpdatePlaylist) - playlists.DELETE("/:id", r.config.AuthMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), playlistHandler.DeletePlaylist) - - // Playlist Tracks - playlists.POST("/:id/tracks", playlistHandler.AddTrack) - playlists.DELETE("/:id/tracks/:track_id", playlistHandler.RemoveTrack) - playlists.PUT("/:id/tracks/reorder", playlistHandler.ReorderTracks) - - // Playlist Collaborators - // BE-API-002: Add collaborator routes with ownership checks - // POST and DELETE require ownership (enforced by service layer, but middleware adds extra security) - playlists.POST("/:id/collaborators", r.config.AuthMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), playlistHandler.AddCollaborator) - playlists.GET("/:id/collaborators", playlistHandler.GetCollaborators) // GET accessible to collaborators (service checks permissions) - playlists.PUT("/:id/collaborators/:userId", r.config.AuthMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), playlistHandler.UpdateCollaboratorPermission) - playlists.DELETE("/:id/collaborators/:userId", r.config.AuthMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), playlistHandler.RemoveCollaborator) - - // BE-API-004: Playlist share link endpoint - playlists.POST("/:id/share", r.config.AuthMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), playlistHandler.CreateShareLink) - - // Playlist Export - exportHandler := handlers.NewPlaylistExportHandler(playlistService) - playlists.GET("/:id/export/json", exportHandler.ExportPlaylistJSON) - playlists.GET("/:id/export/csv", exportHandler.ExportPlaylistCSV) - - // Playlist Duplicate - playlists.POST("/:id/duplicate", playlistHandler.DuplicatePlaylist) - } - } -} - -// setupWebhookRoutes configure les routes pour les webhooks -func (r *APIRouter) setupWebhookRoutes(router *gin.RouterGroup) { - webhookService := services.NewWebhookService(r.db.GormDB, r.logger, r.config.JWTSecret) - - webhookWorker := workers.NewWebhookWorker( - r.db.GormDB, - webhookService, - r.logger, - 100, // Queue size - 5, // Workers - 3, // Max retries - ) - - // Start worker in background - go webhookWorker.Start(context.Background()) - - webhookHandler := handlers.NewWebhookHandler(webhookService, webhookWorker, r.logger) - - webhooks := router.Group("/webhooks") - if r.config.AuthMiddleware != nil { - webhooks.Use(r.config.AuthMiddleware.RequireAuth()) - // BE-SEC-004: Apply CSRF protection to all state-changing endpoints - r.applyCSRFProtection(webhooks) - } - { - webhooks.POST("", webhookHandler.RegisterWebhook()) - webhooks.GET("", webhookHandler.ListWebhooks()) - webhooks.DELETE("/:id", webhookHandler.DeleteWebhook()) - webhooks.GET("/stats", webhookHandler.GetWebhookStats()) - webhooks.POST("/:id/test", webhookHandler.TestWebhook()) - // BE-SEC-012: Regenerate API key endpoint - webhooks.POST("/:id/regenerate-key", webhookHandler.RegenerateAPIKey()) - } -} - -// setupAnalyticsRoutes configure les routes pour les analytics -// BE-API-035: Implement analytics events endpoint -func (r *APIRouter) setupAnalyticsRoutes(router *gin.RouterGroup) { - if r.db == nil || r.db.GormDB == nil { - return - } - - // Initialize analytics service - analyticsService := services.NewAnalyticsService(r.db.GormDB, r.logger) - analyticsHandler := handlers.NewAnalyticsHandler(analyticsService, r.logger) - - // Set JobWorker if available - if r.config != nil && r.config.JobWorker != nil { - analyticsHandler.SetJobWorker(r.config.JobWorker) - } - - analytics := router.Group("/analytics") - if r.config != nil && r.config.AuthMiddleware != nil { - analytics.Use(r.config.AuthMiddleware.RequireAuth()) - // BE-SEC-004: Apply CSRF protection to all state-changing endpoints - r.applyCSRFProtection(analytics) - } - { - // BE-API-037: GET /analytics - Aggregated analytics endpoint - analytics.GET("", analyticsHandler.GetAnalytics) - // BE-API-035: Analytics events endpoint - analytics.POST("/events", analyticsHandler.RecordEvent) - // BE-API-036: Track analytics dashboard endpoint - analytics.GET("/tracks/:id", analyticsHandler.GetTrackAnalyticsDashboard) - } -} - -// setupCorePublicRoutes configure les routes publiques core (health, metrics, upload info) -func (r *APIRouter) setupCorePublicRoutes(router *gin.Engine) { - // Health check handlers - 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 - } - // BE-SVC-016: Pass additional services for detailed health checks - 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 - } - - // Deprecated Public Core Routes - apply deprecation middleware only to specific routes - // Use a wrapper function to apply middleware to individual routes - deprecationMW := middleware.DeprecationWarning(r.logger) - - // INT-021: Add health check monitoring middleware - healthMonitoringMW := middleware.HealthCheckMonitoring(r.logger, r.monitoringService) - - // Wrap handlers with deprecation middleware for legacy routes only - 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) - - // New /api/v1 Public Core Routes - v1Public := router.Group("/api/v1") - { - v1Public.GET("/health", healthCheckHandler) - v1Public.GET("/healthz", livenessHandler) - v1Public.GET("/readyz", readinessHandler) - - // Status endpoint (comprehensive health check) - 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 - } - // Get build info from environment or defaults - 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) - - // Upload info endpoints (public, already in /api/v1) - // MOD-P1-001-REFINEMENT: Permettre démarrage même si ClamAV down - if r.db != nil && r.db.GormDB != nil { - // CORRECTION: Charger la config depuis l'environnement pour respecter ENABLE_CLAMAV - 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)) - // Créer un validateur minimal pour permettre les endpoints info - 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()) - } - - // Frontend logging endpoint (public - frontend needs to send logs) - 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 { - v1Public.POST("/logs/frontend", 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 - } - - // Route CSRF token (doit être accessible sans authentification pour le login) - // Doit être enregistrée AVANT le groupe protected pour ne pas hériter ses middlewares - 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()) - } - - // Middleware d'authentification pour routes protégées - protected := v1.Group("/") - if r.config.AuthMiddleware != nil { - protected.Use(r.config.AuthMiddleware.RequireAuth()) - } - - // Services nécessaires - sessionService := services.NewSessionService(r.db, r.logger) - - // CSRF Middleware (si Redis est disponible) - if r.config.RedisClient != nil { - // Appliquer le middleware CSRF à toutes les routes protégées - protected.Use(csrfMiddleware.Middleware()) - - // INT-AUTH-001: Log explicit activation - r.logger.Info("CSRF protection enabled for core protected routes", - zap.String("environment", r.config.Env), - ) - } else { - // INT-AUTH-001: In production, fail-fast if Redis unavailable - if r.config.Env == config.EnvProduction { - r.logger.Fatal("CSRF protection requires Redis but Redis is unavailable in production. This is a security requirement.") - panic("CSRF protection requires Redis in production environment. Redis is unavailable.") - } - // In non-production, log warning and continue without CSRF - r.logger.Warn("Redis not available - CSRF protection disabled (non-production environment)", - zap.String("environment", r.config.Env), - ) - } - // CORRECTION: Charger la config depuis l'environnement pour respecter ENABLE_CLAMAV - uploadConfig := getUploadConfigWithEnv() - // MOD-P1-001-REFINEMENT: Permettre démarrage même si ClamAV down - 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)) - // Créer un validateur minimal pour permettre les routes d'upload (qui rejetteront) - uploadConfig.ClamAVEnabled = false - uploadValidator, _ = services.NewUploadValidator(uploadConfig, r.logger) - } - auditService := services.NewAuditService(r.db, r.logger) - trackUploadService := services.NewTrackUploadService(r.db.GormDB, r.logger) - - // Handlers - sessionHandler := handlers.NewSessionHandler(sessionService, auditService, r.logger) - uploadHandler := handlers.NewUploadHandler(uploadValidator, auditService, trackUploadService, r.logger, r.config.MaxConcurrentUploads) - auditHandler := handlers.NewAuditHandler(auditService, r.logger) - - // Routes de session - sessions := protected.Group("/sessions") - { - sessions.POST("/logout", sessionHandler.Logout()) - sessions.POST("/logout-all", sessionHandler.LogoutAll()) - // ISSUE-007: Ajouter route sans trailing slash pour éviter redirection 301 - sessions.GET("", sessionHandler.GetSessions()) - sessions.GET("/", sessionHandler.GetSessions()) // Garder aussi avec slash pour compatibilité - sessions.DELETE("/:session_id", sessionHandler.RevokeSession()) - sessions.GET("/stats", sessionHandler.GetSessionStats()) - sessions.POST("/refresh", sessionHandler.RefreshSession()) - } - - // Routes d'upload avec rate limiting spécifique - 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()) - } - - // Routes d'audit - 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()) - } - - // Action 2.1.1.2: Dashboard aggregation endpoint - // Create track service wrapper for dashboard handler (to avoid import cycle) - uploadDir := r.config.UploadDir - if uploadDir == "" { - uploadDir = "uploads/tracks" - } - trackServiceForDashboard := trackcore.NewTrackService(r.db.GormDB, r.logger, uploadDir) - if r.config.CacheService != nil { - trackServiceForDashboard.SetCacheService(r.config.CacheService) - } - - // Create wrapper function that adapts trackcore.TrackListParams to handlers.TrackListParamsForDashboard - 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()) - - // Routes de conversations (chat rooms) - roomRepo := repositories.NewRoomRepository(r.db.GormDB) - messageRepo := repositories.NewChatMessageRepository(r.db.GormDB) // New - roomService := services.NewRoomService(roomRepo, messageRepo, r.logger) // Updated constructor - 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) // BE-API-012: Update conversation endpoint - conversations.DELETE("/:id", roomHandler.DeleteRoom) // BE-API-010: Delete conversation endpoint - conversations.POST("/:id/members", roomHandler.AddMember) - conversations.POST("/:id/participants", roomHandler.AddParticipant) // BE-API-011: Add participant endpoint - conversations.DELETE("/:id/participants/:userId", roomHandler.RemoveParticipant) // BE-API-011: Remove participant endpoint - conversations.GET("/:id/history", roomHandler.GetRoomHistory) - } - - // BE-API-016: Setup notification routes - notificationService := services.NewNotificationService(r.db, r.logger) - handlers.NewNotificationHandlers(notificationService) - notifications := protected.Group("/notifications") - { - notifications.GET("", handlers.NotificationHandlersInstance.GetNotifications) // BE-API-016: Get notifications endpoint - notifications.GET("/unread-count", handlers.NotificationHandlersInstance.GetUnreadCount) // BE-API-016: Get unread count endpoint - notifications.POST("/:id/read", handlers.NotificationHandlersInstance.MarkAsRead) // BE-API-016: Mark notification as read endpoint - notifications.POST("/read-all", handlers.NotificationHandlersInstance.MarkAllAsRead) // BE-API-016: Mark all notifications as read endpoint - notifications.DELETE("/:id", handlers.NotificationHandlersInstance.DeleteNotification) // BE-API-016: Delete notification endpoint - notifications.DELETE("", handlers.NotificationHandlersInstance.DeleteAllNotifications) // BE-API-016: Delete all notifications endpoint - } - - // Routes administrateur (avec authentification + permissions admin) - admin := v1.Group("/admin") - { - if r.config.AuthMiddleware != nil { - admin.Use(r.config.AuthMiddleware.RequireAuth()) - admin.Use(r.config.AuthMiddleware.RequireAdmin()) - } - - // Audit logs (disponibles) - admin.GET("/audit/logs", auditHandler.SearchLogs()) - admin.GET("/audit/stats", auditHandler.GetStats()) - admin.GET("/audit/suspicious", auditHandler.DetectSuspiciousActivity()) - - // MOD-P2-006: Profiling pprof (protégé par auth admin) - admin.Any("/debug/pprof/*path", gin.WrapH(http.DefaultServeMux)) - - // BE-SEC-007: Unlock account locked by failed login attempts (admin only) - if r.authService != nil { - admin.POST("/auth/unlock-account", handlers.UnlockAccount(r.authService, r.logger)) - } - } -} - -// setupSocialRoutes configure les routes sociales -func (r *APIRouter) setupSocialRoutes(router *gin.RouterGroup) { - socialService := socialcore.NewService(r.db.GormDB, r.logger) - socialHandler := handlers.NewSocialHandler(socialService, r.logger) - groupHandler := handlers.NewGroupHandler(socialService, r.logger) - - social := router.Group("/social") - { - // Public routes - social.GET("/feed", socialHandler.GetFeed) - social.GET("/posts/user/:user_id", socialHandler.GetPostsByUser) - social.GET("/groups", groupHandler.ListGroups) - social.GET("/groups/:id", groupHandler.GetGroup) - - // Protected routes - if r.config.AuthMiddleware != nil { - protected := social.Group("") - protected.Use(r.config.AuthMiddleware.RequireAuth()) - // BE-SEC-004: Apply CSRF protection to all state-changing endpoints - r.applyCSRFProtection(protected) - - protected.POST("/posts", socialHandler.CreatePost) - protected.POST("/like", socialHandler.ToggleLike) - protected.POST("/comments", socialHandler.AddComment) - - // Group routes - protected.POST("/groups", groupHandler.CreateGroup) - protected.POST("/groups/:id/join", groupHandler.JoinGroup) - protected.DELETE("/groups/:id/leave", groupHandler.LeaveGroup) - } - } -} - -// setupEducationRoutes configure les routes d'éducation (cours, tutoriels) -func (r *APIRouter) setupEducationRoutes(router *gin.RouterGroup) { - if r.config == nil || r.config.AuthMiddleware == nil { - return - } - courseManager := eduCore.NewCourseManager(r.logger) - tutorialManager := eduCore.NewTutorialManager(r.logger) - eduHandler := eduApi.NewHandler(courseManager, tutorialManager, r.logger) - eduApi.SetupRoutes(router, eduHandler, r.config.JWTSecret, r.config.AuthMiddleware) -} - -// setupGearRoutes configure les routes d'inventaire gear/équipement -func (r *APIRouter) setupGearRoutes(router *gin.RouterGroup) { - if r.config == nil || r.config.AuthMiddleware == nil { - return - } - gearRepo := repositories.NewGearRepository(r.db.GormDB) - gearService := services.NewGearService(gearRepo, r.logger) - gearHandler := handlers.NewGearHandler(gearService, r.logger) - - inventory := router.Group("/inventory") - inventory.Use(r.config.AuthMiddleware.RequireAuth()) - r.applyCSRFProtection(inventory) - { - inventory.GET("/gear", gearHandler.ListGear) - inventory.POST("/gear", gearHandler.CreateGear) - inventory.GET("/gear/:id", gearHandler.GetGear) - inventory.PUT("/gear/:id", gearHandler.UpdateGear) - inventory.DELETE("/gear/:id", gearHandler.DeleteGear) - } -} - -// setupLiveRoutes configure les routes live streams -func (r *APIRouter) setupLiveRoutes(router *gin.RouterGroup) { - liveStreamRepo := repositories.NewLiveStreamRepository(r.db.GormDB) - liveStreamService := services.NewLiveStreamService(liveStreamRepo) - liveStreamHandler := handlers.NewLiveStreamHandler(liveStreamService, r.logger) - - live := router.Group("/live") - { - // Public: list and get streams - live.GET("/streams", liveStreamHandler.ListLiveStreams) - live.GET("/streams/:id", liveStreamHandler.GetLiveStream) - - // Protected: create stream - if r.config != nil && r.config.AuthMiddleware != nil { - protected := live.Group("") - protected.Use(r.config.AuthMiddleware.RequireAuth()) - r.applyCSRFProtection(protected) - protected.POST("/streams", liveStreamHandler.CreateLiveStream) - } - } -} diff --git a/veza-backend-api/internal/api/routes_analytics.go b/veza-backend-api/internal/api/routes_analytics.go new file mode 100644 index 000000000..6cebd4c31 --- /dev/null +++ b/veza-backend-api/internal/api/routes_analytics.go @@ -0,0 +1,33 @@ +package api + +import ( + "github.com/gin-gonic/gin" + + "veza-backend-api/internal/handlers" + "veza-backend-api/internal/services" +) + +// setupAnalyticsRoutes configure les routes pour les analytics +func (r *APIRouter) setupAnalyticsRoutes(router *gin.RouterGroup) { + if r.db == nil || r.db.GormDB == nil { + return + } + + analyticsService := services.NewAnalyticsService(r.db.GormDB, r.logger) + analyticsHandler := handlers.NewAnalyticsHandler(analyticsService, r.logger) + + if r.config != nil && r.config.JobWorker != nil { + analyticsHandler.SetJobWorker(r.config.JobWorker) + } + + analytics := router.Group("/analytics") + if r.config != nil && r.config.AuthMiddleware != nil { + analytics.Use(r.config.AuthMiddleware.RequireAuth()) + r.applyCSRFProtection(analytics) + } + { + analytics.GET("", analyticsHandler.GetAnalytics) + analytics.POST("/events", analyticsHandler.RecordEvent) + analytics.GET("/tracks/:id", analyticsHandler.GetTrackAnalyticsDashboard) + } +} diff --git a/veza-backend-api/internal/api/routes_core.go b/veza-backend-api/internal/api/routes_core.go new file mode 100644 index 000000000..629cf9502 --- /dev/null +++ b/veza-backend-api/internal/api/routes_core.go @@ -0,0 +1,368 @@ +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 +func (r *APIRouter) setupValidateRoutes(router *gin.RouterGroup) { + validateHandler := handlers.NewValidateHandler(r.logger) + router.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.NewTrackService(r.db.GormDB, 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, + ) + + 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) + } + + v1Internal := router.Group("/api/v1/internal") + v1Internal.Use(streamCallbackAuth) + { + v1Internal.POST("/tracks/:id/stream-ready", trackHandler.HandleStreamCallback) + } +} + +// 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 { + 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 { + v1Public.POST("/logs/frontend", 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 { + if r.config.Env == config.EnvProduction { + r.logger.Fatal("CSRF protection requires Redis but Redis is unavailable in production. This is a security requirement.") + panic("CSRF protection requires Redis in production environment. Redis is unavailable.") + } + 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.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.NewTrackService(r.db.GormDB, 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) + } + + notificationService := services.NewNotificationService(r.db, r.logger) + handlers.NewNotificationHandlers(notificationService) + notifications := protected.Group("/notifications") + { + notifications.GET("", handlers.NotificationHandlersInstance.GetNotifications) + notifications.GET("/unread-count", handlers.NotificationHandlersInstance.GetUnreadCount) + 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()) + + admin.Any("/debug/pprof/*path", gin.WrapH(http.DefaultServeMux)) + + if r.authService != nil { + admin.POST("/auth/unlock-account", handlers.UnlockAccount(r.authService, r.logger)) + } + } +} diff --git a/veza-backend-api/internal/api/routes_education.go b/veza-backend-api/internal/api/routes_education.go new file mode 100644 index 000000000..1a3ac9028 --- /dev/null +++ b/veza-backend-api/internal/api/routes_education.go @@ -0,0 +1,19 @@ +package api + +import ( + "github.com/gin-gonic/gin" + + eduApi "veza-backend-api/internal/api/education" + eduCore "veza-backend-api/internal/core/education" +) + +// setupEducationRoutes configure les routes d'éducation (cours, tutoriels) +func (r *APIRouter) setupEducationRoutes(router *gin.RouterGroup) { + if r.config == nil || r.config.AuthMiddleware == nil { + return + } + courseManager := eduCore.NewCourseManager(r.logger) + tutorialManager := eduCore.NewTutorialManager(r.logger) + eduHandler := eduApi.NewHandler(courseManager, tutorialManager, r.logger) + eduApi.SetupRoutes(router, eduHandler, r.config.JWTSecret, r.config.AuthMiddleware) +} diff --git a/veza-backend-api/internal/api/routes_gear.go b/veza-backend-api/internal/api/routes_gear.go new file mode 100644 index 000000000..0e5f8b883 --- /dev/null +++ b/veza-backend-api/internal/api/routes_gear.go @@ -0,0 +1,30 @@ +package api + +import ( + "github.com/gin-gonic/gin" + + "veza-backend-api/internal/handlers" + "veza-backend-api/internal/repositories" + "veza-backend-api/internal/services" +) + +// setupGearRoutes configure les routes d'inventaire gear/équipement +func (r *APIRouter) setupGearRoutes(router *gin.RouterGroup) { + if r.config == nil || r.config.AuthMiddleware == nil { + return + } + gearRepo := repositories.NewGearRepository(r.db.GormDB) + gearService := services.NewGearService(gearRepo, r.logger) + gearHandler := handlers.NewGearHandler(gearService, r.logger) + + inventory := router.Group("/inventory") + inventory.Use(r.config.AuthMiddleware.RequireAuth()) + r.applyCSRFProtection(inventory) + { + inventory.GET("/gear", gearHandler.ListGear) + inventory.POST("/gear", gearHandler.CreateGear) + inventory.GET("/gear/:id", gearHandler.GetGear) + inventory.PUT("/gear/:id", gearHandler.UpdateGear) + inventory.DELETE("/gear/:id", gearHandler.DeleteGear) + } +} diff --git a/veza-backend-api/internal/api/routes_live.go b/veza-backend-api/internal/api/routes_live.go new file mode 100644 index 000000000..d0013731c --- /dev/null +++ b/veza-backend-api/internal/api/routes_live.go @@ -0,0 +1,29 @@ +package api + +import ( + "github.com/gin-gonic/gin" + + "veza-backend-api/internal/handlers" + "veza-backend-api/internal/repositories" + "veza-backend-api/internal/services" +) + +// setupLiveRoutes configure les routes live streams +func (r *APIRouter) setupLiveRoutes(router *gin.RouterGroup) { + liveStreamRepo := repositories.NewLiveStreamRepository(r.db.GormDB) + liveStreamService := services.NewLiveStreamService(liveStreamRepo) + liveStreamHandler := handlers.NewLiveStreamHandler(liveStreamService, r.logger) + + live := router.Group("/live") + { + live.GET("/streams", liveStreamHandler.ListLiveStreams) + live.GET("/streams/:id", liveStreamHandler.GetLiveStream) + + if r.config != nil && r.config.AuthMiddleware != nil { + protected := live.Group("") + protected.Use(r.config.AuthMiddleware.RequireAuth()) + r.applyCSRFProtection(protected) + protected.POST("/streams", liveStreamHandler.CreateLiveStream) + } + } +} diff --git a/veza-backend-api/internal/api/routes_marketplace.go b/veza-backend-api/internal/api/routes_marketplace.go new file mode 100644 index 000000000..4c9a8c54c --- /dev/null +++ b/veza-backend-api/internal/api/routes_marketplace.go @@ -0,0 +1,72 @@ +package api + +import ( + "github.com/gin-gonic/gin" + "github.com/google/uuid" + + "veza-backend-api/internal/core/marketplace" + "veza-backend-api/internal/handlers" + "veza-backend-api/internal/services" +) + +// setupMarketplaceRoutes configure les routes de la marketplace +func (r *APIRouter) setupMarketplaceRoutes(router *gin.RouterGroup) { + uploadDir := r.config.UploadDir + if uploadDir == "" { + uploadDir = "uploads/tracks" + } + + storageService := services.NewTrackStorageService(uploadDir, false, r.logger) + marketService := marketplace.NewService(r.db.GormDB, r.logger, storageService) + marketHandler := handlers.NewMarketplaceHandler(marketService, r.logger) + + group := router.Group("/marketplace") + group.GET("/products", marketHandler.ListProducts) + + if r.config.AuthMiddleware != nil { + protected := group.Group("") + protected.Use(r.config.AuthMiddleware.RequireAuth()) + r.applyCSRFProtection(protected) + + createGroup := protected.Group("") + createGroup.Use(r.config.AuthMiddleware.RequireContentCreatorRole()) + createGroup.POST("/products", marketHandler.CreateProduct) + + productOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { + productIDStr := c.Param("id") + productID, err := uuid.Parse(productIDStr) + if err != nil { + return uuid.Nil, err + } + product, err := marketService.GetProduct(c.Request.Context(), productID) + if err != nil { + return uuid.Nil, err + } + return product.SellerID, nil + } + protected.PUT("/products/:id", r.config.AuthMiddleware.RequireOwnershipOrAdmin("product", productOwnerResolver), marketHandler.UpdateProduct) + + protected.GET("/orders", marketHandler.ListOrders) + protected.GET("/orders/:id", marketHandler.GetOrder) + protected.POST("/orders", marketHandler.CreateOrder) + protected.GET("/download/:product_id", marketHandler.GetDownloadURL) + + marketplaceExtHandler := handlers.NewMarketplaceExtHandler(marketService, r.logger) + protected.GET("/wishlist", marketplaceExtHandler.GetWishlist) + protected.POST("/wishlist", marketplaceExtHandler.AddToWishlist) + protected.DELETE("/wishlist/:productId", marketplaceExtHandler.RemoveFromWishlist) + } + + commerce := router.Group("/commerce") + if r.config.AuthMiddleware != nil { + cartProtected := commerce.Group("") + cartProtected.Use(r.config.AuthMiddleware.RequireAuth()) + r.applyCSRFProtection(cartProtected) + + marketplaceExtHandler := handlers.NewMarketplaceExtHandler(marketService, r.logger) + cartProtected.GET("/cart", marketplaceExtHandler.GetCart) + cartProtected.POST("/cart/items", marketplaceExtHandler.AddToCart) + cartProtected.DELETE("/cart/items/:id", marketplaceExtHandler.RemoveFromCart) + cartProtected.POST("/cart/checkout", marketplaceExtHandler.Checkout) + } +} diff --git a/veza-backend-api/internal/api/routes_playlists.go b/veza-backend-api/internal/api/routes_playlists.go new file mode 100644 index 000000000..1e0a39838 --- /dev/null +++ b/veza-backend-api/internal/api/routes_playlists.go @@ -0,0 +1,80 @@ +package api + +import ( + "github.com/gin-gonic/gin" + "github.com/google/uuid" + + "veza-backend-api/internal/handlers" + "veza-backend-api/internal/repositories" + "veza-backend-api/internal/services" +) + +// setupPlaylistRoutes configure les routes pour les playlists +func (r *APIRouter) setupPlaylistRoutes(router *gin.RouterGroup) { + playlistRepo := repositories.NewPlaylistRepository(r.db.GormDB) + playlistTrackRepo := repositories.NewPlaylistTrackRepository(r.db.GormDB) + playlistCollaboratorRepo := repositories.NewPlaylistCollaboratorRepository(r.db.GormDB) + userRepo := repositories.NewGormUserRepository(r.db.GormDB) + + playlistService := services.NewPlaylistService( + playlistRepo, + playlistTrackRepo, + playlistCollaboratorRepo, + userRepo, + r.logger, + ) + + playlistShareService := services.NewPlaylistShareService(r.db.GormDB) + playlistService.SetPlaylistShareService(playlistShareService) + + playlistFollowService := services.NewPlaylistFollowService(r.db.GormDB, r.logger) + playlistService.SetPlaylistFollowService(playlistFollowService) + + playlistHandler := handlers.NewPlaylistHandler(playlistService, r.db.GormDB, r.logger) + playlistHandler.SetPlaylistFollowService(playlistFollowService) + + playlists := router.Group("/playlists") + if r.config.AuthMiddleware != nil { + playlists.Use(r.config.AuthMiddleware.RequireAuth()) + r.applyCSRFProtection(playlists) + { + playlists.GET("", playlistHandler.GetPlaylists) + playlists.POST("", playlistHandler.CreatePlaylist) + playlists.GET("/search", playlistHandler.SearchPlaylists) + playlists.GET("/recommendations", playlistHandler.GetRecommendations) + playlists.GET("/:id", playlistHandler.GetPlaylist) + + playlistOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { + playlistIDStr := c.Param("id") + playlistID, err := uuid.Parse(playlistIDStr) + if err != nil { + return uuid.Nil, err + } + playlist, err := playlistRepo.GetByID(c.Request.Context(), playlistID) + if err != nil { + return uuid.Nil, err + } + return playlist.UserID, nil + } + playlists.PUT("/:id", r.config.AuthMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), playlistHandler.UpdatePlaylist) + playlists.DELETE("/:id", r.config.AuthMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), playlistHandler.DeletePlaylist) + + playlists.POST("/:id/tracks", playlistHandler.AddTrack) + playlists.DELETE("/:id/tracks/:track_id", playlistHandler.RemoveTrack) + playlists.PUT("/:id/tracks/reorder", playlistHandler.ReorderTracks) + + playlists.POST("/:id/collaborators", r.config.AuthMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), playlistHandler.AddCollaborator) + playlists.GET("/:id/collaborators", playlistHandler.GetCollaborators) + playlists.PUT("/:id/collaborators/:userId", r.config.AuthMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), playlistHandler.UpdateCollaboratorPermission) + playlists.DELETE("/:id/collaborators/:userId", r.config.AuthMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), playlistHandler.RemoveCollaborator) + + playlists.POST("/:id/share", r.config.AuthMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), playlistHandler.CreateShareLink) + + exportHandler := handlers.NewPlaylistExportHandler(playlistService) + playlists.GET("/:id/export/json", exportHandler.ExportPlaylistJSON) + playlists.GET("/:id/export/csv", exportHandler.ExportPlaylistCSV) + + playlists.POST("/:id/duplicate", playlistHandler.DuplicatePlaylist) + } + } +} diff --git a/veza-backend-api/internal/api/routes_social.go b/veza-backend-api/internal/api/routes_social.go new file mode 100644 index 000000000..b4cb42d97 --- /dev/null +++ b/veza-backend-api/internal/api/routes_social.go @@ -0,0 +1,37 @@ +package api + +import ( + "github.com/gin-gonic/gin" + + socialcore "veza-backend-api/internal/core/social" + "veza-backend-api/internal/handlers" +) + +// setupSocialRoutes configure les routes sociales +func (r *APIRouter) setupSocialRoutes(router *gin.RouterGroup) { + socialService := socialcore.NewService(r.db.GormDB, r.logger) + socialHandler := handlers.NewSocialHandler(socialService, r.logger) + groupHandler := handlers.NewGroupHandler(socialService, r.logger) + + social := router.Group("/social") + { + social.GET("/feed", socialHandler.GetFeed) + social.GET("/posts/user/:user_id", socialHandler.GetPostsByUser) + social.GET("/groups", groupHandler.ListGroups) + social.GET("/groups/:id", groupHandler.GetGroup) + + if r.config.AuthMiddleware != nil { + protected := social.Group("") + protected.Use(r.config.AuthMiddleware.RequireAuth()) + r.applyCSRFProtection(protected) + + protected.POST("/posts", socialHandler.CreatePost) + protected.POST("/like", socialHandler.ToggleLike) + protected.POST("/comments", socialHandler.AddComment) + + protected.POST("/groups", groupHandler.CreateGroup) + protected.POST("/groups/:id/join", groupHandler.JoinGroup) + protected.DELETE("/groups/:id/leave", groupHandler.LeaveGroup) + } + } +} diff --git a/veza-backend-api/internal/api/routes_tracks.go b/veza-backend-api/internal/api/routes_tracks.go new file mode 100644 index 000000000..c138ab68d --- /dev/null +++ b/veza-backend-api/internal/api/routes_tracks.go @@ -0,0 +1,156 @@ +package api + +import ( + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/redis/go-redis/v9" + "go.uber.org/zap" + + trackcore "veza-backend-api/internal/core/track" + "veza-backend-api/internal/handlers" + "veza-backend-api/internal/services" +) + +// setupTrackRoutes configure les routes de gestion des tracks +func (r *APIRouter) setupTrackRoutes(router *gin.RouterGroup) { + uploadDir := r.config.UploadDir + if uploadDir == "" { + uploadDir = "uploads/tracks" + } + chunksDir := uploadDir + "/chunks" + + trackService := trackcore.NewTrackService(r.db.GormDB, 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, + ) + if r.config != nil && r.config.PermissionService != nil { + trackHandler.SetPermissionService(r.config.PermissionService) + } + 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) + } + trackHandler.SetUploadValidator(uploadValidator) + + trackSearchService := services.NewTrackSearchService(r.db.GormDB) + trackHandler.SetSearchService(trackSearchService) + + trackVersionService := services.NewTrackVersionService(r.db.GormDB, r.logger, uploadDir) + trackHandler.SetVersionService(trackVersionService) + + playbackAnalyticsService := services.NewPlaybackAnalyticsService(r.db.GormDB, r.logger) + trackHandler.SetPlaybackAnalyticsService(playbackAnalyticsService) + + tracks := router.Group("/tracks") + { + tracks.GET("", trackHandler.ListTracks) + tracks.GET("/search", trackHandler.SearchTracks) + tracks.GET("/:id", trackHandler.GetTrack) + tracks.GET("/:id/stats", trackHandler.GetTrackStats) + tracks.GET("/:id/history", trackHandler.GetTrackHistory) + tracks.GET("/:id/download", trackHandler.DownloadTrack) + tracks.GET("/shared/:token", trackHandler.GetSharedTrack) + + if r.config.AuthMiddleware != nil { + protected := tracks.Group("") + protected.Use(r.config.AuthMiddleware.RequireAuth()) + r.applyCSRFProtection(protected) + + uploadGroup := protected.Group("") + uploadGroup.Use(r.config.AuthMiddleware.RequireContentCreatorRole()) + uploadGroup.POST("", trackHandler.UploadTrack) + + trackOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { + trackIDStr := c.Param("id") + trackID, err := uuid.Parse(trackIDStr) + if err != nil { + return uuid.Nil, err + } + track, err := trackService.GetTrackByID(c.Request.Context(), trackID) + if err != nil { + return uuid.Nil, err + } + return track.UserID, nil + } + protected.PUT("/:id", r.config.AuthMiddleware.RequireOwnershipOrAdmin("track", trackOwnerResolver), trackHandler.UpdateTrack) + protected.DELETE("/:id", r.config.AuthMiddleware.RequireOwnershipOrAdmin("track", trackOwnerResolver), trackHandler.DeleteTrack) + + protected.GET("/:id/status", trackHandler.GetUploadStatus) + protected.POST("/initiate", trackHandler.InitiateChunkedUpload) + protected.POST("/chunk", trackHandler.UploadChunk) + protected.POST("/complete", trackHandler.CompleteChunkedUpload) + protected.GET("/quota/:id", trackHandler.GetUploadQuota) + protected.GET("/resume/:uploadId", trackHandler.ResumeUpload) + + protected.POST("/batch/delete", trackHandler.BatchDeleteTracks) + protected.POST("/batch/update", trackHandler.BatchUpdateTracks) + + protected.POST("/:id/like", trackHandler.LikeTrack) + protected.DELETE("/:id/like", trackHandler.UnlikeTrack) + protected.GET("/:id/likes", trackHandler.GetTrackLikes) + + protected.POST("/:id/share", trackHandler.CreateShare) + protected.DELETE("/share/:id", trackHandler.RevokeShare) + + protected.POST("/:id/versions/:versionId/restore", trackHandler.RestoreVersion) + + protected.POST("/:id/play", trackHandler.RecordPlay) + + hlsOutputDir := r.config.UploadDir + if hlsOutputDir == "" { + hlsOutputDir = "uploads/tracks" + } + hlsService := services.NewHLSService(r.db.GormDB, hlsOutputDir, r.logger) + hlsHandler := handlers.NewHLSHandler(hlsService) + tracks.GET("/:id/hls/info", hlsHandler.GetStreamInfo) + tracks.GET("/:id/hls/status", hlsHandler.GetStreamStatus) + } + } + + commentService := services.NewCommentService(r.db.GormDB, r.logger) + commentHandler := handlers.NewCommentHandler(commentService, r.logger) + + comments := router.Group("/tracks") + { + comments.GET("/:id/comments", commentHandler.GetComments) + + if r.config.AuthMiddleware != nil { + protected := comments.Group("") + protected.Use(r.config.AuthMiddleware.RequireAuth()) + r.applyCSRFProtection(protected) + { + protected.POST("/:id/comments", commentHandler.CreateComment) + } + } + } + + commentsProtected := router.Group("/comments") + { + if r.config.AuthMiddleware != nil { + commentsProtected.Use(r.config.AuthMiddleware.RequireAuth()) + r.applyCSRFProtection(commentsProtected) + { + commentsProtected.DELETE("/:id", commentHandler.DeleteComment) + } + } + } +} diff --git a/veza-backend-api/internal/api/routes_users.go b/veza-backend-api/internal/api/routes_users.go new file mode 100644 index 000000000..f7283a9d0 --- /dev/null +++ b/veza-backend-api/internal/api/routes_users.go @@ -0,0 +1,151 @@ +package api + +import ( + "net/http" + "strconv" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/redis/go-redis/v9" + "go.uber.org/zap" + + trackcore "veza-backend-api/internal/core/track" + "veza-backend-api/internal/handlers" + "veza-backend-api/internal/repositories" + "veza-backend-api/internal/services" +) + +// setupUserRoutes configure les routes utilisateur +func (r *APIRouter) setupUserRoutes(router *gin.RouterGroup) { + userRepo := repositories.NewGormUserRepository(r.db.GormDB) + userService := services.NewUserServiceWithDB(userRepo, r.db.GormDB) + profileHandler := handlers.NewProfileHandler(userService, r.logger) + if r.config != nil && r.config.PermissionService != nil { + profileHandler.SetPermissionService(r.config.PermissionService) + } + socialService := services.NewSocialService(r.db, r.logger) + profileHandler.SetSocialService(socialService) + + users := router.Group("/users") + { + users.GET("", profileHandler.ListUsers) + users.GET("/:id", profileHandler.GetProfile) + users.GET("/by-username/:username", profileHandler.GetProfileByUsername) + users.GET("/search", profileHandler.SearchUsers) + + if r.config.AuthMiddleware != nil { + protected := users.Group("") + protected.Use(r.config.AuthMiddleware.RequireAuth()) + r.applyCSRFProtection(protected) + + settingsHandler := handlers.NewSettingsHandler(userService, r.logger) + protected.GET("/settings", settingsHandler.GetSettings) + protected.PUT("/settings", settingsHandler.UpdateSettings) + + userOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { + userIDStr := c.Param("id") + return uuid.Parse(userIDStr) + } + protected.PUT("/:id", r.config.AuthMiddleware.RequireOwnershipOrAdmin("user", userOwnerResolver), profileHandler.UpdateProfile) + protected.DELETE("/:id", r.config.AuthMiddleware.RequireOwnershipOrAdmin("user", userOwnerResolver), profileHandler.DeleteUser) + + protected.GET("/:id/completion", profileHandler.GetProfileCompletion) + + protected.POST("/:id/follow", profileHandler.FollowUser) + protected.DELETE("/:id/follow", profileHandler.UnfollowUser) + + protected.POST("/:id/block", profileHandler.BlockUser) + protected.DELETE("/:id/block", profileHandler.UnblockUser) + + roleService := services.NewRoleService(r.db.GormDB) + roleHandler := handlers.NewRoleHandler(roleService, r.logger) + protected.POST("/:id/roles", roleHandler.AssignRole) + protected.DELETE("/:id/roles/:roleId", roleHandler.RevokeRole) + + avatarUploadDir := r.config.UploadDir + if avatarUploadDir == "" { + avatarUploadDir = "uploads/avatars" + } + imageService := services.NewImageService(avatarUploadDir) + avatarHandler := handlers.NewAvatarHandler(imageService, userService) + protected.POST("/:id/avatar", avatarHandler.UploadAvatar) + protected.DELETE("/:id/avatar", avatarHandler.DeleteAvatar) + + uploadDir := r.config.UploadDir + if uploadDir == "" { + uploadDir = "uploads/tracks" + } + likeService := services.NewTrackLikeService(r.db.GormDB, r.logger) + trackService := trackcore.NewTrackService(r.db.GormDB, 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 + } + chunksDir := uploadDir + "/chunks" + chunkService := services.NewTrackChunkService(chunksDir, redisClient, r.logger) + streamService := services.NewStreamServiceWithAPIKey(r.config.StreamServerURL, r.config.StreamServerInternalAPIKey, r.logger) + trackHandlerForLikes := trackcore.NewTrackHandler( + trackService, + trackUploadService, + chunkService, + likeService, + streamService, + ) + protected.GET("/:id/likes", trackHandlerForLikes.GetUserLikedTracks) + + dataExportService := services.NewDataExportService(r.db.GormDB, r.logger) + exportHandler := func(c *gin.Context) { + userID, exists := c.Get("user_id") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "User ID not found"}) + return + } + + userUUID, ok := userID.(uuid.UUID) + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) + return + } + + jsonData, err := dataExportService.ExportUserDataAsJSON(c.Request.Context(), userUUID) + if err != nil { + r.logger.Error("Failed to export user data", zap.Error(err), zap.String("user_id", userUUID.String())) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to export user data"}) + return + } + + filename := "veza-data-export-" + time.Now().Format("2006-01-02T15-04-05") + ".json" + c.Header("Content-Type", "application/json") + c.Header("Content-Disposition", `attachment; filename="`+filename+`"`) + c.Header("Content-Length", strconv.Itoa(len(jsonData))) + + c.Data(http.StatusOK, "application/json", jsonData) + } + protected.GET("/me/export", exportHandler) + } + } +} + +// setupRoleRoutes configure les routes de gestion des rôles +func (r *APIRouter) setupRoleRoutes(router *gin.RouterGroup) { + roleService := services.NewRoleService(r.db.GormDB) + roleHandler := handlers.NewRoleHandler(roleService, r.logger) + + roles := router.Group("/roles") + { + if r.config.AuthMiddleware != nil { + protected := roles.Group("") + protected.Use(r.config.AuthMiddleware.RequireAuth()) + r.applyCSRFProtection(protected) + { + protected.GET("", roleHandler.GetRoles) + protected.GET("/:id", roleHandler.GetRole) + } + } + } +} diff --git a/veza-backend-api/internal/api/routes_webhooks.go b/veza-backend-api/internal/api/routes_webhooks.go new file mode 100644 index 000000000..56e6a0cdb --- /dev/null +++ b/veza-backend-api/internal/api/routes_webhooks.go @@ -0,0 +1,43 @@ +package api + +import ( + "context" + + "github.com/gin-gonic/gin" + + "veza-backend-api/internal/handlers" + "veza-backend-api/internal/services" + "veza-backend-api/internal/workers" +) + +// setupWebhookRoutes configure les routes pour les webhooks +func (r *APIRouter) setupWebhookRoutes(router *gin.RouterGroup) { + webhookService := services.NewWebhookService(r.db.GormDB, r.logger, r.config.JWTSecret) + + webhookWorker := workers.NewWebhookWorker( + r.db.GormDB, + webhookService, + r.logger, + 100, + 5, + 3, + ) + + go webhookWorker.Start(context.Background()) + + webhookHandler := handlers.NewWebhookHandler(webhookService, webhookWorker, r.logger) + + webhooks := router.Group("/webhooks") + if r.config.AuthMiddleware != nil { + webhooks.Use(r.config.AuthMiddleware.RequireAuth()) + r.applyCSRFProtection(webhooks) + } + { + webhooks.POST("", webhookHandler.RegisterWebhook()) + webhooks.GET("", webhookHandler.ListWebhooks()) + webhooks.DELETE("/:id", webhookHandler.DeleteWebhook()) + webhooks.GET("/stats", webhookHandler.GetWebhookStats()) + webhooks.POST("/:id/test", webhookHandler.TestWebhook()) + webhooks.POST("/:id/regenerate-key", webhookHandler.RegenerateAPIKey()) + } +}