package handlers import ( "errors" "net/http" "strconv" "time" apperrors "veza-backend-api/internal/errors" "veza-backend-api/internal/models" "veza-backend-api/internal/monitoring" "veza-backend-api/internal/services" "veza-backend-api/internal/utils" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" "gorm.io/gorm" ) // PlaylistHandler gère les opérations sur les playlists type PlaylistHandler struct { playlistService services.PlaylistServiceInterface playlistAnalyticsService *services.PlaylistAnalyticsService playlistFollowService *services.PlaylistFollowService db *gorm.DB commonHandler *CommonHandler } // NewPlaylistHandler crée un nouveau handler de playlists func NewPlaylistHandler(playlistService *services.PlaylistService, db *gorm.DB, logger *zap.Logger) *PlaylistHandler { return &PlaylistHandler{ playlistService: playlistService, db: db, commonHandler: NewCommonHandler(logger), } } // NewPlaylistHandlerWithInterface crée un nouveau handler avec l'interface service (pour les tests) func NewPlaylistHandlerWithInterface(playlistService services.PlaylistServiceInterface, db *gorm.DB, logger *zap.Logger) *PlaylistHandler { return &PlaylistHandler{ playlistService: playlistService, db: db, commonHandler: NewCommonHandler(logger), } } // SetPlaylistAnalyticsService définit le service d'analytics de playlist // T0491: Create Playlist Analytics Backend func (h *PlaylistHandler) SetPlaylistAnalyticsService(analyticsService *services.PlaylistAnalyticsService) { h.playlistAnalyticsService = analyticsService } // SetPlaylistFollowService définit le service de follow de playlist // T0498: Create Playlist Recommendations func (h *PlaylistHandler) SetPlaylistFollowService(followService *services.PlaylistFollowService) { h.playlistFollowService = followService } // CreatePlaylistRequest représente la requête pour créer une playlist // MOD-P1-001: Ajout tags validate pour validation systématique (Description manquait) type CreatePlaylistRequest struct { Title string `json:"title" binding:"required,min=1,max=200" validate:"required,min=1,max=200"` Description string `json:"description,omitempty" validate:"omitempty,max=1000"` IsPublic bool `json:"is_public"` } // UpdatePlaylistRequest représente la requête pour mettre à jour une playlist // MOD-P1-001: Ajout tags validate pour validation systématique (Description manquait) type UpdatePlaylistRequest struct { Title *string `json:"title,omitempty" binding:"omitempty,min=1,max=200" validate:"omitempty,min=1,max=200"` Description *string `json:"description,omitempty" validate:"omitempty,max=1000"` IsPublic *bool `json:"is_public,omitempty"` } // ReorderTracksRequest représente la requête pour réorganiser les tracks type ReorderTracksRequest struct { TrackIDs []uuid.UUID `json:"track_ids" binding:"required,min=1" validate:"required,min=1"` // Changed to []uuid.UUID } // CreatePlaylist gère la création d'une playlist // @Summary Create Playlist // @Description Create a new playlist // @Tags Playlist // @Accept json // @Produce json // @Security BearerAuth // @Param request body CreatePlaylistRequest true "Playlist Metadata" // @Success 201 {object} APIResponse{data=object{playlist=models.Playlist}} // @Failure 400 {object} APIResponse "Validation Error" // @Failure 401 {object} APIResponse "Unauthorized" // @Failure 500 {object} APIResponse "Internal Error" // @Router /playlists [post] func (h *PlaylistHandler) CreatePlaylist(c *gin.Context) { // MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel userID, ok := GetUserIDUUID(c) if !ok { return // Erreur déjà envoyée par GetUserIDUUID } var req CreatePlaylistRequest if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil { RespondWithAppError(c, appErr) return } // BE-SEC-009: Sanitize user inputs to prevent XSS and injection attacks req.Title = utils.SanitizeText(req.Title, 200) if req.Description != "" { req.Description = utils.SanitizeText(req.Description, 1000) } // MOD-P1-004: Ajouter timeout context pour opération DB critique ctx, cancel := WithTimeout(c.Request.Context(), 5*time.Second) defer cancel() playlist, err := h.playlistService.CreatePlaylist(ctx, userID, req.Title, req.Description, req.IsPublic) if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to create playlist", err)) return } // MOD-P2-003: Enregistrer la métrique business (depuis le handler pour éviter cycle d'import) monitoring.RecordPlaylistCreated() RespondSuccess(c, http.StatusCreated, gin.H{"playlist": playlist}) } // ImportPlaylistRequest represents JSON import payload (v0.10.4 F145) type ImportPlaylistRequest struct { Playlist struct { Title string `json:"title"` Description string `json:"description"` IsPublic bool `json:"is_public"` } `json:"playlist"` Tracks []struct { ID string `json:"id"` } `json:"tracks"` } // ImportPlaylist gère l'import d'une playlist depuis JSON (v0.10.4 F145) func (h *PlaylistHandler) ImportPlaylist(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } var req ImportPlaylistRequest if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil { RespondWithAppError(c, appErr) return } title := req.Playlist.Title if title != "" { title = utils.SanitizeText(title, 200) } else { title = "Imported Playlist" } description := req.Playlist.Description if description != "" { description = utils.SanitizeText(description, 1000) } trackIDs := make([]uuid.UUID, 0, len(req.Tracks)) for _, t := range req.Tracks { if t.ID == "" { continue } id, err := uuid.Parse(t.ID) if err != nil { continue } trackIDs = append(trackIDs, id) } ctx, cancel := WithTimeout(c.Request.Context(), 30*time.Second) defer cancel() playlist, err := h.playlistService.ImportPlaylistWithTracks(ctx, userID, title, description, req.Playlist.IsPublic, trackIDs) if err != nil { RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to import playlist", err)) return } RespondSuccess(c, http.StatusCreated, gin.H{"playlist": playlist}) } // GetFavorisPlaylist returns the current user's Favoris playlist, creating it if needed (v0.10.4 F136) func (h *PlaylistHandler) GetFavorisPlaylist(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } ctx, cancel := WithTimeout(c.Request.Context(), 5*time.Second) defer cancel() playlist, err := h.playlistService.GetOrCreateFavorisPlaylist(ctx, userID) if err != nil { RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get Favoris playlist", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"playlist": playlist}) } // GetPlaylists gère la récupération des playlists avec pagination // @Summary Get Playlists // @Description Get a paginated list of playlists // @Tags Playlist // @Accept json // @Produce json // @Security BearerAuth // @Param page query int false "Page number" default(1) // @Param limit query int false "Items per page" default(20) // @Param user_id query string false "Filter by User ID" // @Success 200 {object} APIResponse{data=object{playlists=[]models.Playlist,pagination=object}} // @Failure 500 {object} APIResponse "Internal Error" // @Router /playlists [get] func (h *PlaylistHandler) GetPlaylists(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) // Bounds checking: return 400 with clear message instead of silently normalizing if page < 1 || limit < 1 || limit > 100 { msg := "pagination: page must be >= 1 and limit must be between 1 and 100" RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, msg)) return } // Filtres optionnels var filterUserID *uuid.UUID if filterUserIDStr := c.Query("user_id"); filterUserIDStr != "" { if uid, err := uuid.Parse(filterUserIDStr); err == nil { filterUserID = &uid } } // Get current user ID var currentUserID *uuid.UUID if uidInterface, exists := c.Get("user_id"); exists { h.commonHandler.logger.Debug("GetPlaylists: user_id found in context", zap.Any("value", uidInterface)) if uid, ok := uidInterface.(uuid.UUID); ok { currentUserID = &uid } else { h.commonHandler.logger.Debug("GetPlaylists: user_id type assertion failed") } } else { h.commonHandler.logger.Debug("GetPlaylists: user_id not found in context") } // MOD-P1-004: Ajouter timeout context pour opération DB ctx, cancel := WithTimeout(c.Request.Context(), 5*time.Second) defer cancel() playlists, total, err := h.playlistService.GetPlaylists(ctx, currentUserID, filterUserID, page, limit) if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get playlists", err)) return } RespondSuccess(c, http.StatusOK, gin.H{ "playlists": playlists, "total": total, "page": page, "limit": limit, }) } // GetPlaylist gère la récupération d'une playlist // @Summary Get Playlist by ID // @Description Get detailed information about a playlist // @Tags Playlist // @Accept json // @Produce json // @Security BearerAuth // @Param id path string true "Playlist ID" // @Success 200 {object} APIResponse{data=object{playlist=models.Playlist}} // @Failure 400 {object} APIResponse "Invalid ID" // @Failure 404 {object} APIResponse "Playlist not found" // @Router /playlists/{id} [get] func (h *PlaylistHandler) GetPlaylist(c *gin.Context) { // Playlist IDs are uuid.UUID playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id")) return } var currentUserID *uuid.UUID if uidInterface, exists := c.Get("user_id"); exists { if uid, ok := uidInterface.(uuid.UUID); ok { currentUserID = &uid } } // MOD-P1-004: Ajouter timeout context pour opération DB ctx, cancel := WithTimeout(c.Request.Context(), 5*time.Second) defer cancel() playlist, err := h.playlistService.GetPlaylist(ctx, playlistID, currentUserID) if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} if errors.Is(err, services.ErrPlaylistNotFound) || errors.Is(err, services.ErrAccessDenied) { RespondWithAppError(c, apperrors.NewNotFoundError("playlist")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get playlist", err)) return } RespondSuccess(c, http.StatusOK, playlist) } // GetPlaylistByShareToken returns a playlist by its public share token (v0.10.4 F143). // No authentication required. func (h *PlaylistHandler) GetPlaylistByShareToken(c *gin.Context) { token := c.Param("token") if token == "" { RespondWithAppError(c, apperrors.NewValidationError("share token is required")) return } ctx, cancel := WithTimeout(c.Request.Context(), 5*time.Second) defer cancel() playlist, err := h.playlistService.GetPlaylistByShareToken(ctx, token) if err != nil { if errors.Is(err, services.ErrPlaylistNotFound) { RespondWithAppError(c, apperrors.NewNotFoundError("playlist")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get playlist", err)) return } RespondSuccess(c, http.StatusOK, playlist) } // UpdatePlaylist gère la mise à jour d'une playlist // @Summary Update Playlist // @Description Update playlist metadata // @Tags Playlist // @Accept json // @Produce json // @Security BearerAuth // @Param id path string true "Playlist ID" // @Param playlist body UpdatePlaylistRequest true "Playlist Metadata" // @Success 200 {object} APIResponse{data=object{playlist=models.Playlist}} // @Failure 400 {object} APIResponse "Validation Error" // @Failure 401 {object} APIResponse "Unauthorized" // @Failure 403 {object} APIResponse "Forbidden" // @Failure 404 {object} APIResponse "Playlist not found" // @Router /playlists/{id} [put] func (h *PlaylistHandler) UpdatePlaylist(c *gin.Context) { // MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel userID, ok := GetUserIDUUID(c) if !ok { return // Erreur déjà envoyée par GetUserIDUUID } // Playlist IDs are uuid.UUID playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id")) return } var req UpdatePlaylistRequest if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil { RespondWithAppError(c, appErr) return } // BE-SEC-009: Sanitize user inputs to prevent XSS and injection attacks if req.Title != nil { sanitized := utils.SanitizeText(*req.Title, 200) req.Title = &sanitized } if req.Description != nil { sanitized := utils.SanitizeText(*req.Description, 1000) req.Description = &sanitized } // MOD-P1-004: Ajouter timeout context pour opération DB ctx, cancel := WithTimeout(c.Request.Context(), 5*time.Second) defer cancel() playlist, err := h.playlistService.UpdatePlaylist(ctx, playlistID, userID, req.Title, req.Description, req.IsPublic) if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} if errors.Is(err, services.ErrPlaylistNotFound) { RespondWithAppError(c, apperrors.NewNotFoundError("playlist")) return } if errors.Is(err, services.ErrAccessDenied) { RespondWithAppError(c, apperrors.NewForbiddenError("forbidden")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to update playlist", err)) return } RespondSuccess(c, http.StatusOK, playlist) } // DeletePlaylist gère la suppression d'une playlist // @Summary Delete Playlist // @Description Permanently delete a playlist // @Tags Playlist // @Accept json // @Produce json // @Security BearerAuth // @Param id path string true "Playlist ID" // @Success 200 {object} APIResponse{data=object{message=string}} // @Failure 401 {object} APIResponse "Unauthorized" // @Failure 403 {object} APIResponse "Forbidden" // @Failure 404 {object} APIResponse "Playlist not found" // @Router /playlists/{id} [delete] func (h *PlaylistHandler) DeletePlaylist(c *gin.Context) { // MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel userID, ok := GetUserIDUUID(c) if !ok { return // Erreur déjà envoyée par GetUserIDUUID } // Playlist IDs are uuid.UUID playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id")) return } // MOD-P1-004: Ajouter timeout context pour opération DB ctx, cancel := WithTimeout(c.Request.Context(), 5*time.Second) defer cancel() if err := h.playlistService.DeletePlaylist(ctx, playlistID, userID); err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} if errors.Is(err, services.ErrPlaylistNotFound) { RespondWithAppError(c, apperrors.NewNotFoundError("playlist")) return } if errors.Is(err, services.ErrAccessDenied) { RespondWithAppError(c, apperrors.NewForbiddenError("forbidden")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to delete playlist", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"message": "playlist deleted"}) } // AddTrack gère l'ajout d'un track à une playlist // @Summary Add Track to Playlist // @Description Add a track to the playlist // @Tags Playlist // @Accept json // @Produce json // @Security BearerAuth // @Param id path string true "Playlist ID" // @Param trackId body object{track_id=string} true "Track ID (in body)" // @Success 200 {object} APIResponse{data=object{message=string}} // @Failure 400 {object} APIResponse "Track already present or invalid ID" // @Failure 404 {object} APIResponse "Playlist or Track not found" // @Router /playlists/{id}/tracks [post] func (h *PlaylistHandler) AddTrack(c *gin.Context) { // MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel userID, ok := GetUserIDUUID(c) if !ok { return // Erreur déjà envoyée par GetUserIDUUID } // Playlist IDs are uuid.UUID playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id")) return } // Track IDs are uuid.UUID trackID, err := uuid.Parse(c.Param("trackId")) // Changed to uuid.Parse if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.NewValidationError("invalid track id")) return } if err := h.playlistService.AddTrack(c.Request.Context(), playlistID, trackID, userID); err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} if errors.Is(err, services.ErrPlaylistNotFound) { RespondWithAppError(c, apperrors.NewNotFoundError("playlist")) return } if errors.Is(err, services.ErrTrackNotFound) { RespondWithAppError(c, apperrors.NewNotFoundError("track")) return } if errors.Is(err, services.ErrTrackAlreadyInPlaylist) { RespondWithAppError(c, apperrors.NewValidationError("track already in playlist")) return } if errors.Is(err, services.ErrAccessDenied) { RespondWithAppError(c, apperrors.NewForbiddenError("forbidden")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to add track to playlist", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"message": "track added to playlist"}) } // RemoveTrack gère la suppression d'un track d'une playlist // @Summary Remove Track from Playlist // @Description Remove a track from the playlist // @Tags Playlist // @Accept json // @Produce json // @Security BearerAuth // @Param id path string true "Playlist ID" // @Param trackId path string true "Track ID" // @Success 200 {object} APIResponse{data=object{message=string}} // @Failure 404 {object} APIResponse "Playlist or Track not found" // @Router /playlists/{id}/tracks/{trackId} [delete] func (h *PlaylistHandler) RemoveTrack(c *gin.Context) { // MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel userID, ok := GetUserIDUUID(c) if !ok { return // Erreur déjà envoyée par GetUserIDUUID } // Playlist IDs are uuid.UUID playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id")) return } // Track IDs are uuid.UUID trackID, err := uuid.Parse(c.Param("trackId")) // Changed to uuid.Parse if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.NewValidationError("invalid track id")) return } if err := h.playlistService.RemoveTrack(c.Request.Context(), playlistID, trackID, userID); err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} if err.Error() == "playlist not found" { RespondWithAppError(c, apperrors.NewNotFoundError("playlist")) return } if err.Error() == "track not in playlist" { RespondWithAppError(c, apperrors.NewNotFoundError("track not in playlist")) return } if err.Error() == "forbidden" { RespondWithAppError(c, apperrors.NewForbiddenError("forbidden")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to remove track from playlist", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"message": "track removed from playlist"}) } // ReorderTracks gère la réorganisation des tracks d'une playlist // @Summary Reorder Tracks // @Description Reorder tracks in the playlist // @Tags Playlist // @Accept json // @Produce json // @Security BearerAuth // @Param id path string true "Playlist ID" // @Param order body ReorderTracksRequest true "New Track Order" // @Success 200 {object} APIResponse{data=object{message=string}} // @Failure 400 {object} APIResponse "Validation Error" // @Router /playlists/{id}/tracks/reorder [put] func (h *PlaylistHandler) ReorderTracks(c *gin.Context) { // MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel userID, ok := GetUserIDUUID(c) if !ok { return // Erreur déjà envoyée par GetUserIDUUID } // Playlist IDs are uuid.UUID playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id")) return } var req ReorderTracksRequest if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil { RespondWithAppError(c, appErr) return } if err := h.playlistService.ReorderTracks(c.Request.Context(), playlistID, userID, req.TrackIDs); err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} if err.Error() == "playlist not found" { RespondWithAppError(c, apperrors.NewNotFoundError("playlist")) return } if err.Error() == "some tracks are not in the playlist" { RespondWithAppError(c, apperrors.NewValidationError("some tracks are not in the playlist")) return } if err.Error() == "forbidden" { RespondWithAppError(c, apperrors.NewForbiddenError("forbidden")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to reorder tracks", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"message": "tracks reordered"}) } // AddCollaboratorRequest représente la requête pour ajouter un collaborateur type AddCollaboratorRequest struct { UserID uuid.UUID `json:"user_id" binding:"required" validate:"required"` Permission string `json:"permission" binding:"required,oneof=read write admin" validate:"required,oneof=read write admin"` } // UpdateCollaboratorPermissionRequest représente la requête pour mettre à jour la permission d'un collaborateur type UpdateCollaboratorPermissionRequest struct { Permission string `json:"permission" binding:"required,oneof=read write admin" validate:"required,oneof=read write admin"` } // AddCollaborator gère l'ajout d'un collaborateur à une playlist // T0479: POST /api/v1/playlists/:id/collaborators func (h *PlaylistHandler) AddCollaborator(c *gin.Context) { // MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel userID, ok := GetUserIDUUID(c) if !ok { return // Erreur déjà envoyée par GetUserIDUUID } // Playlist IDs are uuid.UUID playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id")) return } var req AddCollaboratorRequest if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil { RespondWithAppError(c, appErr) return } // Convertir la permission string en PlaylistPermission var permission models.PlaylistPermission switch req.Permission { case "read": permission = models.PlaylistPermissionRead case "write": permission = models.PlaylistPermissionWrite case "admin": permission = models.PlaylistPermissionAdmin default: // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.NewValidationError("invalid permission")) return } collaborator, err := h.playlistService.AddCollaborator(c.Request.Context(), playlistID, userID, req.UserID, permission) if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} if err.Error() == "playlist not found" { RespondWithAppError(c, apperrors.NewNotFoundError("playlist")) return } if err.Error() == "user not found" { RespondWithAppError(c, apperrors.NewNotFoundError("user")) return } if err.Error() == "user is already a collaborator" { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeConflict, "user is already a collaborator")) return } if err.Error() == "cannot add playlist owner as collaborator" { RespondWithAppError(c, apperrors.NewValidationError("cannot add playlist owner as collaborator")) return } if err.Error() == "forbidden: only playlist owner can add collaborators" { RespondWithAppError(c, apperrors.NewForbiddenError("forbidden")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to add collaborator", err)) return } RespondSuccess(c, http.StatusCreated, collaborator) } // RemoveCollaborator gère la suppression d'un collaborateur d'une playlist // T0479: DELETE /api/v1/playlists/:id/collaborators/:userId func (h *PlaylistHandler) RemoveCollaborator(c *gin.Context) { // MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel userID, ok := GetUserIDUUID(c) if !ok { return // Erreur déjà envoyée par GetUserIDUUID } // Playlist IDs are uuid.UUID playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id")) return } // User IDs are UUID collaboratorUserID, err := uuid.Parse(c.Param("userId")) if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.NewValidationError("invalid user id")) return } if err := h.playlistService.RemoveCollaborator(c.Request.Context(), playlistID, userID, collaboratorUserID); err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} if err.Error() == "playlist not found" { RespondWithAppError(c, apperrors.NewNotFoundError("playlist")) return } if err.Error() == "collaborator not found" { RespondWithAppError(c, apperrors.NewNotFoundError("collaborator")) return } if err.Error() == "forbidden: only playlist owner can remove collaborators" { RespondWithAppError(c, apperrors.NewForbiddenError("forbidden")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to remove collaborator", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"message": "collaborator removed"}) } // UpdateCollaboratorPermission gère la mise à jour de la permission d'un collaborateur // T0479: PUT /api/v1/playlists/:id/collaborators/:userId func (h *PlaylistHandler) UpdateCollaboratorPermission(c *gin.Context) { // MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel userID, ok := GetUserIDUUID(c) if !ok { return // Erreur déjà envoyée par GetUserIDUUID } // Playlist IDs are uuid.UUID playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id")) return } // User IDs are UUID collaboratorUserID, err := uuid.Parse(c.Param("userId")) if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.NewValidationError("invalid user id")) return } var req UpdateCollaboratorPermissionRequest if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil { RespondWithAppError(c, appErr) return } // Convertir la permission string en PlaylistPermission var permission models.PlaylistPermission switch req.Permission { case "read": permission = models.PlaylistPermissionRead case "write": permission = models.PlaylistPermissionWrite case "admin": permission = models.PlaylistPermissionAdmin default: // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.NewValidationError("invalid permission")) return } if err := h.playlistService.UpdateCollaboratorPermission(c.Request.Context(), playlistID, userID, collaboratorUserID, permission); err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} if err.Error() == "playlist not found" { RespondWithAppError(c, apperrors.NewNotFoundError("playlist")) return } if err.Error() == "collaborator not found" { RespondWithAppError(c, apperrors.NewNotFoundError("collaborator")) return } if err.Error() == "invalid permission" { RespondWithAppError(c, apperrors.NewValidationError("invalid permission")) return } if err.Error() == "forbidden: only playlist owner can update collaborator permissions" { RespondWithAppError(c, apperrors.NewForbiddenError("forbidden")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to update collaborator permission", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"message": "collaborator permission updated"}) } // GetCollaborators gère la récupération des collaborateurs d'une playlist // T0479: GET /api/v1/playlists/:id/collaborators func (h *PlaylistHandler) GetCollaborators(c *gin.Context) { // MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel userID, ok := GetUserIDUUID(c) if !ok { return // Erreur déjà envoyée par GetUserIDUUID } // Playlist IDs are uuid.UUID playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id")) return } collaborators, err := h.playlistService.GetCollaborators(c.Request.Context(), playlistID, userID) if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} if err.Error() == "playlist not found" { RespondWithAppError(c, apperrors.NewNotFoundError("playlist")) return } if err.Error() == "forbidden: access denied" { RespondWithAppError(c, apperrors.NewForbiddenError("forbidden")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get collaborators", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"collaborators": collaborators}) } // CreateShareLink gère la création d'un lien de partage public pour une playlist // T0488: Create Playlist Public Share Link func (h *PlaylistHandler) CreateShareLink(c *gin.Context) { // MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel userID, ok := GetUserIDUUID(c) if !ok { return // Erreur déjà envoyée par GetUserIDUUID } // Playlist IDs are uuid.UUID playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id")) return } // Créer le lien de partage via le service // La vérification des permissions (owner ou admin) est faite dans PlaylistService.CreateShareLink shareLink, err := h.playlistService.CreateShareLink(c.Request.Context(), playlistID, userID, nil) if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} if err.Error() == "playlist not found" { RespondWithAppError(c, apperrors.NewNotFoundError("playlist")) return } if err.Error() == "forbidden: only owner or admin can create share links" { RespondWithAppError(c, apperrors.NewForbiddenError("forbidden")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to create share link", err)) return } RespondSuccess(c, http.StatusOK, shareLink) } // FollowPlaylist gère le follow d'une playlist // T0489: Create Playlist Follow Feature func (h *PlaylistHandler) FollowPlaylist(c *gin.Context) { // MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel userID, ok := GetUserIDUUID(c) if !ok { return // Erreur déjà envoyée par GetUserIDUUID } // Playlist IDs are uuid.UUID playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id")) return } err = h.playlistService.FollowPlaylist(c.Request.Context(), playlistID, userID) if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} if err.Error() == "playlist not found" { RespondWithAppError(c, apperrors.NewNotFoundError("playlist")) return } if err.Error() == "cannot follow own playlist" { RespondWithAppError(c, apperrors.NewValidationError("cannot follow own playlist")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to follow playlist", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"message": "playlist followed"}) } // UnfollowPlaylist gère l'unfollow d'une playlist // T0489: Create Playlist Follow Feature func (h *PlaylistHandler) UnfollowPlaylist(c *gin.Context) { // MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel userID, ok := GetUserIDUUID(c) if !ok { return // Erreur déjà envoyée par GetUserIDUUID } // Playlist IDs are uuid.UUID playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id")) return } err = h.playlistService.UnfollowPlaylist(c.Request.Context(), playlistID, userID) if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} if err.Error() == "playlist not found" { RespondWithAppError(c, apperrors.NewNotFoundError("playlist")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to unfollow playlist", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"message": "playlist unfollowed"}) } // GetPlaylistStats gère la récupération des statistiques d'une playlist // T0491: Create Playlist Analytics Backend func (h *PlaylistHandler) GetPlaylistStats(c *gin.Context) { // Playlist IDs are uuid.UUID playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id")) return } // Vérifier que la playlist existe et que l'utilisateur a accès var userID *uuid.UUID if uidInterface, exists := c.Get("user_id"); exists { if uid, ok := uidInterface.(uuid.UUID); ok { userID = &uid } } playlist, err := h.playlistService.GetPlaylist(c.Request.Context(), playlistID, userID) if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} if err.Error() == "playlist not found" { RespondWithAppError(c, apperrors.NewNotFoundError("playlist")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get playlist", err)) return } // Vérifier que l'utilisateur a accès (propriétaire, collaborateur ou playlist publique) // Use uuid.Nil for comparison if userID is nil currentUserID := uuid.Nil if userID != nil { currentUserID = *userID } if playlist.UserID != currentUserID && !playlist.IsPublic { // Vérifier si l'utilisateur est collaborateur if userID != nil { hasAccess, err := h.playlistService.CheckPermission(c.Request.Context(), playlistID, *userID, models.PlaylistPermissionRead) if err != nil || !hasAccess { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.NewForbiddenError("forbidden")) return } } else { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.NewForbiddenError("forbidden")) return } } // Récupérer les statistiques via le service d'analytics if h.playlistAnalyticsService == nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "analytics service not available")) return } stats, err := h.playlistAnalyticsService.GetPlaylistStats(c.Request.Context(), playlistID) if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} if err.Error() == "playlist not found" { RespondWithAppError(c, apperrors.NewNotFoundError("playlist")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get playlist stats", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"stats": stats}) } // DuplicatePlaylistRequest représente la requête pour dupliquer une playlist type DuplicatePlaylistRequest struct { NewTitle string `json:"new_title"` NewDescription string `json:"new_description,omitempty"` IsPublic *bool `json:"is_public,omitempty"` } // DuplicatePlaylist gère la duplication d'une playlist // T0495: Create Playlist Duplicate Feature func (h *PlaylistHandler) DuplicatePlaylist(c *gin.Context) { // Playlist IDs are uuid.UUID playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id")) return } // MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel userID, ok := GetUserIDUUID(c) if !ok { return // Erreur déjà envoyée par GetUserIDUUID } var req DuplicatePlaylistRequest if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil { RespondWithAppError(c, appErr) return } // Créer le service de duplication duplicateService := services.NewPlaylistDuplicateService(h.playlistService, h.db, nil) // Dupliquer la playlist newPlaylist, err := duplicateService.DuplicatePlaylist( c.Request.Context(), playlistID, userID, services.DuplicatePlaylistRequest{ NewTitle: req.NewTitle, NewDescription: req.NewDescription, IsPublic: req.IsPublic, }, ) if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} if err.Error() == "playlist not found" { RespondWithAppError(c, apperrors.NewNotFoundError("playlist")) return } if err.Error() == "forbidden: you don't have access to this playlist" { RespondWithAppError(c, apperrors.NewForbiddenError("forbidden")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to duplicate playlist", err)) return } RespondSuccess(c, http.StatusOK, gin.H{ "message": "playlist duplicated successfully", "playlist": newPlaylist, }) } // SearchPlaylists gère la recherche de playlists // T0496: Create Playlist Search Backend func (h *PlaylistHandler) SearchPlaylists(c *gin.Context) { // Get current user ID var currentUserID *uuid.UUID if uidInterface, exists := c.Get("user_id"); exists { if uid, ok := uidInterface.(uuid.UUID); ok { currentUserID = &uid } } // Récupérer les paramètres de recherche query := c.Query("q") userIDParam := c.Query("user_id") isPublicParam := c.Query("is_public") pageParam := c.DefaultQuery("page", "1") limitParam := c.DefaultQuery("limit", "20") // Parser les paramètres var filterUserID *uuid.UUID if userIDParam != "" { if parsed, err := uuid.Parse(userIDParam); err == nil { filterUserID = &parsed } } var filterIsPublic *bool if isPublicParam != "" { if parsed, err := strconv.ParseBool(isPublicParam); err == nil { filterIsPublic = &parsed } } page, err := strconv.Atoi(pageParam) if err != nil || page < 1 { page = 1 } limit, err := strconv.Atoi(limitParam) if err != nil || limit < 1 { limit = 20 } // Rechercher les playlists playlists, total, err := h.playlistService.SearchPlaylists(c.Request.Context(), services.SearchPlaylistsParams{ Query: query, UserID: filterUserID, IsPublic: filterIsPublic, Page: page, Limit: limit, CurrentUserID: currentUserID, }) if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to search playlists", err)) return } RespondSuccess(c, http.StatusOK, gin.H{ "playlists": playlists, "total": total, "page": page, "limit": limit, }) } // GetRecommendations gère la récupération des recommandations de playlists // T0498: Create Playlist Recommendations func (h *PlaylistHandler) GetRecommendations(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return // Erreur déjà envoyée par GetUserIDUUID } if userID == uuid.Nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.NewUnauthorizedError("unauthorized")) return } // Parser les paramètres de requête limitParam := c.DefaultQuery("limit", "20") limit, err := strconv.Atoi(limitParam) if err != nil || limit < 1 { limit = 20 } if limit > 100 { limit = 100 } minScoreParam := c.DefaultQuery("min_score", "0.1") minScore, err := strconv.ParseFloat(minScoreParam, 64) if err != nil || minScore < 0 { minScore = 0.1 } includeOwnParam := c.DefaultQuery("include_own", "false") includeOwn := includeOwnParam == "true" // Vérifier que le service de follow est disponible if h.playlistFollowService == nil { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "follow service not available")) return } // Créer le service de recommandations recommendationService := services.NewPlaylistRecommendationService( h.db, h.playlistService, h.playlistFollowService, h.commonHandler.logger, ) // Obtenir les recommandations recommendations, err := recommendationService.GetRecommendations( c.Request.Context(), services.GetRecommendationsParams{ UserID: userID, Limit: limit, MinScore: minScore, IncludeOwn: includeOwn, }, ) if err != nil { // MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"} RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get recommendations", err)) return } // Formater la réponse response := make([]gin.H, 0, len(recommendations)) for _, rec := range recommendations { response = append(response, gin.H{ "playlist": rec.Playlist, "score": rec.Score, "reason": rec.Reason, }) } RespondSuccess(c, http.StatusOK, gin.H{ "recommendations": response, "count": len(response), }) }