package api import ( "net/http" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/redis/go-redis/v9" "go.uber.org/zap" "golang.org/x/crypto/bcrypt" 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) socialService := services.NewSocialService(r.db, r.logger) userService.SetSocialService(socialService) // v0.10.0 F187: profiles get is_following, counts profileHandler := handlers.NewProfileHandler(userService, r.logger) if r.config != nil && r.config.PermissionService != nil { profileHandler.SetPermissionService(r.config.PermissionService) } profileHandler.SetSocialService(socialService) if r.notificationService == nil { r.notificationService = services.NewNotificationService(r.db, r.logger) if r.pushService != nil { r.notificationService.SetPushService(r.pushService) } } profileHandler.SetNotificationService(r.notificationService) users := router.Group("/users") { users.GET("", profileHandler.ListUsers) // v0.10.0 F211: /suggestions must be before /:id to avoid "suggestions" matching as id if r.config.AuthMiddleware != nil { users.GET("/suggestions", r.config.AuthMiddleware.RequireAuth(), profileHandler.GetFollowSuggestions) } users.GET("/:id", profileHandler.GetProfile) users.GET("/by-username/:username", profileHandler.GetProfileByUsername) users.GET("/search", profileHandler.SearchUsers) // v0.10.3 F203: User reposts (public - profile shows reposted tracks) uploadDir := "uploads/tracks" var redisClient *redis.Client streamServerURL, streamAPIKey := "", "" if r.config != nil { if r.config.UploadDir != "" { uploadDir = r.config.UploadDir } redisClient = r.config.RedisClient streamServerURL = r.config.StreamServerURL streamAPIKey = r.config.StreamServerInternalAPIKey } repostService := services.NewTrackRepostService(r.db.GormDB) trackServiceForReposts := trackcore.NewTrackServiceWithDB(r.db, r.logger, uploadDir) if r.config != nil && r.config.CacheService != nil { trackServiceForReposts.SetCacheService(r.config.CacheService) } trackHandlerForReposts := trackcore.NewTrackHandler( trackServiceForReposts, services.NewTrackUploadService(r.db.GormDB, r.logger), services.NewTrackChunkService(uploadDir+"/chunks", redisClient, r.logger), services.NewTrackLikeService(r.db.GormDB, r.logger), services.NewStreamServiceWithAPIKey(streamServerURL, streamAPIKey, r.logger), ) trackHandlerForReposts.SetRepostService(repostService) users.GET("/:id/reposts", trackHandlerForReposts.GetUserRepostedTracks) 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) // v0.801: UI preferences (theme, contrast, etc.) protected.GET("/me/preferences", settingsHandler.GetPreferences) protected.PUT("/me/preferences", settingsHandler.UpdatePreferences) 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) presenceService := services.NewPresenceService(r.db.GormDB, r.logger) presenceHandler := handlers.NewPresenceHandler(presenceService, r.logger) protected.PUT("/me/presence", presenceHandler.UpdatePresence) protected.GET("/:id/presence", presenceHandler.GetPresence) 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.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 } chunksDir := uploadDir + "/chunks" chunkService := services.NewTrackChunkService(chunksDir, redisClient, r.logger) trackHandlerForLikes := trackcore.NewTrackHandler( trackService, trackUploadService, chunkService, likeService, streamService, ) protected.GET("/:id/likes", trackHandlerForLikes.GetUserLikedTracks) dataExportService := services.NewDataExportService(r.db.GormDB, r.logger) gdprExportService := services.NewGDPRExportService( r.db.GormDB, dataExportService, r.config.S3StorageService, r.notificationService, r.logger, ) emailSvc := services.NewEmailService(r.db, r.logger) gdprExportService.SetEmailService(emailSvc) var gdprRedis *redis.Client var gdprS3 *services.S3StorageService if r.config != nil { gdprRedis = r.config.RedisClient gdprS3 = r.config.S3StorageService } gdprExportHandler := handlers.NewGDPRExportHandler( r.db.GormDB, gdprExportService, gdprS3, gdprRedis, r.logger, ) // PUT /me/password: Change password protected.PUT("/me/password", func(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": gin.H{"code": "UNAUTHORIZED", "message": "User ID not found"}}) return } userUUID, ok := userID.(uuid.UUID) if !ok { c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{"code": "INVALID_USER_ID", "message": "Invalid user ID"}}) return } var req struct { CurrentPassword string `json:"current_password" binding:"required"` NewPassword string `json:"new_password" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{"code": "INVALID_REQUEST", "message": "Current password and new password are required"}}) return } if len(req.NewPassword) < 12 { c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{"code": "WEAK_PASSWORD", "message": "Password must be at least 12 characters long"}}) return } var currentHash string if err := r.db.GormDB.Raw("SELECT password_hash FROM users WHERE id = ?", userUUID).Scan(¤tHash).Error; err != nil { r.logger.Error("Failed to get current password hash", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{"code": "INTERNAL_ERROR", "message": "Failed to verify password"}}) return } if err := bcrypt.CompareHashAndPassword([]byte(currentHash), []byte(req.CurrentPassword)); err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": gin.H{"code": "WRONG_PASSWORD", "message": "Current password is incorrect"}}) return } newHash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost) if err != nil { r.logger.Error("Failed to hash new password", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{"code": "INTERNAL_ERROR", "message": "Failed to update password"}}) return } if err := r.db.GormDB.Exec("UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", string(newHash), userUUID).Error; err != nil { r.logger.Error("Failed to update password", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{"code": "INTERNAL_ERROR", "message": "Failed to update password"}}) return } c.JSON(http.StatusOK, gin.H{"message": "Password changed successfully"}) }) // GET /me/export: sync JSON fallback (v0.10.8 - prefer POST for async ZIP) protected.GET("/me/export", gdprExportHandler.ExportJSON) // POST /me/export: async GDPR export (v0.10.8 F065) protected.POST("/me/export", gdprExportHandler.RequestExport) // GET /me/exports: list exports (v0.10.8) protected.GET("/me/exports", gdprExportHandler.ListExports) // GET /me/exports/:id/download: redirect to S3 presigned URL (must be before :id) protected.GET("/me/exports/:id/download", gdprExportHandler.DownloadExport) // GET /me/exports/:id: export status protected.GET("/me/exports/:id", gdprExportHandler.GetExport) // DELETE /me: Account deletion (v0.803 SEC2-05) protected.DELETE("/me", handlers.DeleteAccountHandler( r.db.GormDB, r.config.SessionService, r.config.AuditService, r.config.S3StorageService, r.logger, )) // POST /me/privacy/opt-out: CCPA Do Not Sell (v0.803 SEC2-06) protected.POST("/me/privacy/opt-out", handlers.PrivacyOptOut(r.db.GormDB)) // POST /me/upgrade-creator: self-service creator role (v1.0.6) // Requires is_verified=true; one-way flip from 'user' to 'creator'. var upgradeAudit *services.AuditService if r.config != nil { upgradeAudit = r.config.AuditService } protected.POST("/me/upgrade-creator", handlers.UpgradeToCreator(r.db.GormDB, upgradeAudit, r.logger)) } } } // 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) protected.POST("", roleHandler.CreateRole) protected.PUT("/:id", roleHandler.UpdateRole) protected.DELETE("/:id", roleHandler.DeleteRole) } } } }