veza/veza-backend-api/internal/core/track/handler.go
senke 43309327e6 feat(v0.501): Sprint 5 -- integration, tests, and cleanup
- INT-01: Add E2E streaming tests (upload -> HLS auth)
- INT-02: Add E2E cloud tests (CRUD auth, public gear)
- INT-03: Split track/handler.go into 4 focused sub-handlers
- INT-04: Create migration squash script + MIGRATIONS.md
- INT-05: Add Trivy container image scanning CI workflow
- INT-06: Replace production console.log with structured logger
2026-02-22 18:40:07 +01:00

1463 lines
48 KiB
Go

package track
import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"veza-backend-api/internal/common"
apperrors "veza-backend-api/internal/errors"
"veza-backend-api/internal/handlers"
"veza-backend-api/internal/models"
"veza-backend-api/internal/response"
"veza-backend-api/internal/services"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// TrackHandler gère les opérations sur les tracks
type TrackHandler struct {
trackService *TrackService
trackUploadService *services.TrackUploadService
chunkService *services.TrackChunkService
likeService *services.TrackLikeService
streamService *services.StreamService
jobEnqueuer services.JobEnqueuer // Optional: for HLS transcoding via job queue
searchService *services.TrackSearchService
shareService *services.TrackShareService
versionService *services.TrackVersionService
historyService *services.TrackHistoryService
playbackAnalyticsService *services.PlaybackAnalyticsService // BE-API-019: Added for play analytics
permissionService *services.PermissionService // MOD-P1-003: Added for admin check
uploadValidator *services.UploadValidator // MOD-P1-001: Added for ClamAV scan before persistence
licenseChecker services.TrackDownloadLicenseChecker // A04: Verify paid track download rights
notificationService *services.NotificationService // Phase 2.2: Optional, for like notifications
trackRecommendationService *services.TrackRecommendationService
waveformService *services.WaveformService
}
// NewTrackHandler crée un nouveau handler de tracks
func NewTrackHandler(
trackService *TrackService,
trackUploadService *services.TrackUploadService,
chunkService *services.TrackChunkService,
likeService *services.TrackLikeService,
streamService *services.StreamService,
) *TrackHandler {
return &TrackHandler{
trackService: trackService,
trackUploadService: trackUploadService,
chunkService: chunkService,
likeService: likeService,
streamService: streamService,
}
}
// SetUploadValidator définit le validateur d'upload (pour injection de dépendance)
// MOD-P1-001: Added for ClamAV scan before persistence
func (h *TrackHandler) SetUploadValidator(uploadValidator *services.UploadValidator) {
h.uploadValidator = uploadValidator
}
// SetPermissionService définit le service de permissions (pour injection de dépendance)
// MOD-P1-003: Added for admin check in ownership verification
func (h *TrackHandler) SetPermissionService(permissionService *services.PermissionService) {
h.permissionService = permissionService
}
// SetSearchService définit le service de recherche (pour injection de dépendance)
func (h *TrackHandler) SetSearchService(searchService *services.TrackSearchService) {
h.searchService = searchService
}
// SetShareService définit le service de partage (pour injection de dépendance)
func (h *TrackHandler) SetShareService(shareService *services.TrackShareService) {
h.shareService = shareService
}
// SetVersionService définit le service de versioning (pour injection de dépendance)
func (h *TrackHandler) SetVersionService(versionService *services.TrackVersionService) {
h.versionService = versionService
}
// SetHistoryService définit le service d'historique (pour injection de dépendance)
func (h *TrackHandler) SetHistoryService(historyService *services.TrackHistoryService) {
h.historyService = historyService
}
// SetPlaybackAnalyticsService définit le service d'analytics de lecture (pour injection de dépendance)
// BE-API-019: Implement track play analytics endpoint
func (h *TrackHandler) SetPlaybackAnalyticsService(analyticsService *services.PlaybackAnalyticsService) {
h.playbackAnalyticsService = analyticsService
}
// SetLicenseChecker définit le vérificateur de licence pour les téléchargements de tracks payants (A04)
func (h *TrackHandler) SetLicenseChecker(checker services.TrackDownloadLicenseChecker) {
h.licenseChecker = checker
}
// SetJobEnqueuer définit le job enqueuer pour le transcoding HLS (optionnel)
func (h *TrackHandler) SetJobEnqueuer(enqueuer services.JobEnqueuer) {
h.jobEnqueuer = enqueuer
}
// SetNotificationService définit le service de notifications (Phase 2.2)
func (h *TrackHandler) SetNotificationService(notificationService *services.NotificationService) {
h.notificationService = notificationService
}
// SetTrackRecommendationService définit le service de recommandations
func (h *TrackHandler) SetTrackRecommendationService(svc *services.TrackRecommendationService) {
h.trackRecommendationService = svc
}
// SetWaveformService définit le service de waveform (S1-05)
func (h *TrackHandler) SetWaveformService(svc *services.WaveformService) {
h.waveformService = svc
}
// getUserID récupère l'ID utilisateur du contexte de manière sécurisée (fail-secure)
// MOD-P1-RES-003: Remplace c.MustGet() pour éviter les panics
// Retourne false si user_id est absent ou invalide (répond déjà avec 401)
func (h *TrackHandler) getUserID(c *gin.Context) (uuid.UUID, bool) {
userIDInterface, exists := c.Get("user_id")
if !exists {
// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.Unauthorized
handlers.RespondWithAppError(c, apperrors.NewUnauthorizedError("unauthorized"))
return uuid.Nil, false
}
userID, ok := userIDInterface.(uuid.UUID)
if !ok {
// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.Unauthorized
handlers.RespondWithAppError(c, apperrors.NewUnauthorizedError("unauthorized"))
return uuid.Nil, false
}
if userID == uuid.Nil {
// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.Unauthorized
handlers.RespondWithAppError(c, apperrors.NewUnauthorizedError("unauthorized"))
return uuid.Nil, false
}
return userID, true
}
// respondWithError est un helper pour migrer vers RespondWithAppError
// MOD-P1-RES-001: Helper pour standardiser les réponses d'erreur
func (h *TrackHandler) respondWithError(c *gin.Context, httpStatus int, message string) {
var errCode apperrors.ErrorCode
switch httpStatus {
case http.StatusBadRequest:
errCode = apperrors.ErrCodeValidation
case http.StatusUnauthorized:
errCode = apperrors.ErrCodeUnauthorized
case http.StatusForbidden:
errCode = apperrors.ErrCodeForbidden
case http.StatusNotFound:
errCode = apperrors.ErrCodeNotFound
case http.StatusInternalServerError:
errCode = apperrors.ErrCodeInternal
default:
errCode = apperrors.ErrCodeInternal
}
handlers.RespondWithAppError(c, apperrors.New(errCode, message))
}
// ListTracks gère la liste des tracks avec pagination, filtres et tri
// @Summary List Tracks
// @Description Get a paginated list of tracks with filters
// @Tags Track
// @Accept json
// @Produce json
// @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"
// @Param genre query string false "Filter by Genre"
// @Param format query string false "Filter by Format"
// @Param sort_by query string false "Sort field" default(created_at)
// @Param sort_order query string false "Sort order (asc/desc)" default(desc)
// @Success 200 {object} response.APIResponse{data=object{tracks=[]models.Track,pagination=object}}
// @Failure 500 {object} response.APIResponse "Internal Error"
// @Router /tracks [get]
func (h *TrackHandler) ListTracks(c *gin.Context) {
// Récupérer les paramètres de query
page := c.DefaultQuery("page", "1")
limit := c.DefaultQuery("limit", "20")
userIDStr := c.Query("user_id")
genre := c.Query("genre")
format := c.Query("format")
sortBy := c.DefaultQuery("sort_by", "created_at")
sortOrder := c.DefaultQuery("sort_order", "desc")
// Parser les paramètres — return 400 if out of bounds (no silent normalization)
var pageInt, limitInt int
if _, err := fmt.Sscanf(page, "%d", &pageInt); err != nil || pageInt < 1 {
response.BadRequest(c, "pagination: page must be >= 1 and limit must be between 1 and 100")
return
}
if _, err := fmt.Sscanf(limit, "%d", &limitInt); err != nil || limitInt < 1 || limitInt > 100 {
response.BadRequest(c, "pagination: page must be >= 1 and limit must be between 1 and 100")
return
}
// Construire les paramètres
params := TrackListParams{
Page: pageInt,
Limit: limitInt,
SortBy: sortBy,
SortOrder: sortOrder,
}
// Parser user_id si fourni
if userIDStr != "" {
if uid, err := uuid.Parse(userIDStr); err == nil {
params.UserID = &uid
}
}
// Parser genre si fourni
if genre != "" {
params.Genre = &genre
}
// Parser format si fourni
if format != "" {
params.Format = &format
}
// Appeler le service
tracks, total, err := h.trackService.ListTracks(c.Request.Context(), params)
if err != nil {
response.InternalServerError(c, "failed to list tracks")
return
}
// INT-007: Standardize pagination format
pagination := handlers.BuildPaginationData(pageInt, limitInt, total)
// Masquer l'URL de stream pour les utilisateurs non authentifiés
_, exists := c.Get("user_id")
if !exists {
for _, t := range tracks {
t.StreamManifestURL = ""
}
}
response.Success(c, gin.H{
"tracks": tracks,
"pagination": pagination,
})
}
// GetRecommendations returns personalized track recommendations (D2 autoplay)
func (h *TrackHandler) GetRecommendations(c *gin.Context) {
if h.trackRecommendationService == nil {
response.InternalServerError(c, "recommendations unavailable")
return
}
var userID uuid.UUID
if uid, exists := c.Get("user_id"); exists {
if parsed, ok := uid.(uuid.UUID); ok {
userID = parsed
}
}
limitStr := c.DefaultQuery("limit", "20")
var limit int
if _, err := fmt.Sscanf(limitStr, "%d", &limit); err != nil || limit < 1 {
limit = 20
}
if limit > 100 {
limit = 100
}
params := services.TrackRecommendationParams{
UserID: userID,
Limit: limit,
}
if seedStr := c.Query("seed_track_id"); seedStr != "" {
if sid, err := uuid.Parse(seedStr); err == nil {
params.SeedTrackID = &sid
}
}
recs, err := h.trackRecommendationService.GetRecommendations(c.Request.Context(), params)
if err != nil {
response.InternalServerError(c, "failed to get recommendations")
return
}
tracks := make([]*models.Track, 0, len(recs))
for _, r := range recs {
if r.Track != nil {
tracks = append(tracks, r.Track)
}
}
response.Success(c, gin.H{"tracks": tracks})
}
// tagSuggestionsByGenre holds static tag suggestions per genre (E4)
var tagSuggestionsByGenre = map[string][]string{
"pop": {"Pop", "Catchy", "Radio", "Mainstream", "Vocal"},
"rock": {"Rock", "Guitar", "Drums", "Alternative", "Indie"},
"electronic": {"Electronic", "Synth", "EDM", "Techno", "House", "Dubstep"},
"hip-hop": {"Hip-Hop", "Rap", "Beats", "Urban", "Trap"},
"jazz": {"Jazz", "Smooth", "Saxophone", "Blues", "Soul"},
"classical": {"Classical", "Orchestral", "Piano", "Strings"},
"ambient": {"Ambient", "Chill", "Cinematic", "Atmospheric"},
"default": {"Synthwave", "Lo-Fi", "Experimental", "Instrumental"},
}
// GetSuggestedTags returns tag suggestions based on genre and BPM (E4)
// @Summary Get Suggested Tags
// @Description Get tag suggestions for a track based on genre and optional BPM
// @Tags Track
// @Produce json
// @Param genre query string false "Genre filter"
// @Param bpm query int false "BPM hint"
// @Success 200 {object} response.APIResponse{data=object{tags=[]string}}
// @Router /tracks/suggested-tags [get]
func (h *TrackHandler) GetSuggestedTags(c *gin.Context) {
genre := strings.ToLower(strings.TrimSpace(c.DefaultQuery("genre", "")))
if genre == "" {
genre = "default"
}
tags, ok := tagSuggestionsByGenre[genre]
if !ok {
tags = tagSuggestionsByGenre["default"]
}
handlers.RespondSuccess(c, http.StatusOK, gin.H{"tags": tags})
}
// GetTrack gère la récupération d'un track par son ID
// @Summary Get Track by ID
// @Description Get detailed information about a track
// @Tags Track
// @Accept json
// @Produce json
// @Param id path string true "Track ID"
// @Success 200 {object} response.APIResponse{data=object{track=models.Track}}
// @Failure 400 {object} response.APIResponse "Invalid ID"
// @Failure 404 {object} response.APIResponse "Track not found"
// @Router /tracks/{id} [get]
func (h *TrackHandler) GetTrack(c *gin.Context) {
trackIDStr := c.Param("id")
if trackIDStr == "" {
response.BadRequest(c, "track id is required")
return
}
// MIGRATION UUID: TrackID is UUID
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
response.BadRequest(c, "invalid track id")
return
}
track, err := h.trackService.GetTrackByID(c.Request.Context(), trackID)
if err != nil {
if errors.Is(err, ErrTrackNotFound) || errors.Is(err, gorm.ErrRecordNotFound) {
response.NotFound(c, "track not found")
return
}
response.InternalServerError(c, "failed to get track")
return
}
// Masquer l'URL de stream pour les utilisateurs non authentifiés
_, exists := c.Get("user_id")
if !exists {
track.StreamManifestURL = ""
}
response.Success(c, gin.H{"track": track})
}
// UpdateTrackRequest représente la requête de mise à jour d'un track
// MOD-P1-002: Added validation tags for systematic input validation
type UpdateTrackRequest struct {
Title *string `json:"title" binding:"omitempty,min=1,max=255" validate:"omitempty,min=1,max=255"`
Artist *string `json:"artist" binding:"omitempty,max=255" validate:"omitempty,max=255"`
Album *string `json:"album" binding:"omitempty,max=255" validate:"omitempty,max=255"`
Genre *string `json:"genre" binding:"omitempty,max=100" validate:"omitempty,max=100"`
Tags []string `json:"tags"`
Year *int `json:"year" binding:"omitempty,min=1900,max=2100" validate:"omitempty,min=1900,max=2100"`
BPM *int `json:"bpm" binding:"omitempty,min=0,max=300" validate:"omitempty,min=0,max=300"`
MusicalKey *string `json:"musical_key" binding:"omitempty,max=10" validate:"omitempty,max=10"`
IsPublic *bool `json:"is_public"`
}
// UpdateTrack gère la mise à jour d'un track
// @Summary Update Track
// @Description Update track metadata
// @Tags Track
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Track ID"
// @Param track body UpdateTrackRequest true "Track Metadata"
// @Success 200 {object} response.APIResponse{data=object{track=models.Track}}
// @Failure 400 {object} response.APIResponse "Validation Error"
// @Failure 401 {object} response.APIResponse "Unauthorized"
// @Failure 403 {object} response.APIResponse "Forbidden"
// @Failure 404 {object} response.APIResponse "Track not found"
// @Router /tracks/{id} [put]
func (h *TrackHandler) UpdateTrack(c *gin.Context) {
// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
userID, ok := h.getUserID(c)
if !ok {
return // Erreur déjà envoyée par getUserID
}
trackIDStr := c.Param("id")
if trackIDStr == "" {
response.BadRequest(c, "track id is required")
return
}
// MIGRATION UUID: TrackID is UUID
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
response.BadRequest(c, "invalid track id")
return
}
// MOD-P1-002: Utiliser helper centralisé pour bind + validate
var req UpdateTrackRequest
if !common.BindAndValidateJSON(c, &req) {
return // Erreur déjà envoyée au client
}
// Convertir la requête en paramètres de service
params := UpdateTrackParams{
Title: req.Title,
Artist: req.Artist,
Album: req.Album,
Genre: req.Genre,
Tags: req.Tags,
Year: req.Year,
BPM: req.BPM,
MusicalKey: req.MusicalKey,
IsPublic: req.IsPublic,
}
// MOD-P1-003: Check if user is admin for ownership bypass
isAdmin := false
if h.permissionService != nil {
hasRole, err := h.permissionService.HasRole(c.Request.Context(), userID, "admin")
if err == nil && hasRole {
isAdmin = true
}
}
// Pass isAdmin via context
ctx := context.WithValue(c.Request.Context(), "is_admin", isAdmin)
track, err := h.trackService.UpdateTrack(ctx, trackID, userID, params)
if err != nil {
if errors.Is(err, ErrTrackNotFound) || errors.Is(err, gorm.ErrRecordNotFound) {
response.NotFound(c, "track not found")
return
}
if errors.Is(err, ErrForbidden) {
response.Forbidden(c, "forbidden")
return
}
// Erreur de validation (title empty, year negative, etc.)
if strings.Contains(err.Error(), "cannot be") {
// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.BadRequest
h.respondWithError(c, http.StatusBadRequest, err.Error())
return
}
// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.InternalServerError
h.respondWithError(c, http.StatusInternalServerError, "failed to update track")
return
}
// MOD-P1-RES-001: Utiliser RespondSuccess au lieu de response.Success
handlers.RespondSuccess(c, http.StatusOK, gin.H{"track": track})
}
// GetLyrics gère la récupération des paroles d'un track (E3)
// @Summary Get Track Lyrics
// @Description Get lyrics for a track (public)
// @Tags Track
// @Produce json
// @Param id path string true "Track ID"
// @Success 200 {object} response.APIResponse{data=object{lyrics=models.TrackLyrics}}
// @Failure 404 {object} response.APIResponse "Track not found"
// @Router /tracks/{id}/lyrics [get]
func (h *TrackHandler) GetLyrics(c *gin.Context) {
trackIDStr := c.Param("id")
if trackIDStr == "" {
h.respondWithError(c, http.StatusBadRequest, "track id is required")
return
}
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
h.respondWithError(c, http.StatusBadRequest, "invalid track id")
return
}
// Verify track exists
if _, err := h.trackService.GetTrackByID(c.Request.Context(), trackID); err != nil {
if errors.Is(err, ErrTrackNotFound) || errors.Is(err, gorm.ErrRecordNotFound) {
h.respondWithError(c, http.StatusNotFound, "track not found")
return
}
h.respondWithError(c, http.StatusInternalServerError, "failed to get track")
return
}
lyrics, err := h.trackService.GetLyrics(c.Request.Context(), trackID)
if err != nil {
h.respondWithError(c, http.StatusInternalServerError, "failed to get lyrics")
return
}
if lyrics == nil {
handlers.RespondSuccess(c, http.StatusOK, gin.H{"lyrics": nil})
return
}
handlers.RespondSuccess(c, http.StatusOK, gin.H{"lyrics": lyrics})
}
// UpdateLyricsRequest représente la requête pour créer/mettre à jour les paroles
type UpdateLyricsRequest struct {
Content string `json:"content"`
}
// UpdateLyrics gère la création/mise à jour des paroles (E3)
// @Summary Update Track Lyrics
// @Description Create or update lyrics for a track (track owner only)
// @Tags Track
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Track ID"
// @Param body body UpdateLyricsRequest true "Lyrics content"
// @Success 200 {object} response.APIResponse{data=object{lyrics=models.TrackLyrics}}
// @Failure 401 {object} response.APIResponse "Unauthorized"
// @Failure 403 {object} response.APIResponse "Forbidden"
// @Failure 404 {object} response.APIResponse "Track not found"
// @Router /tracks/{id}/lyrics [put]
func (h *TrackHandler) UpdateLyrics(c *gin.Context) {
userID, ok := h.getUserID(c)
if !ok {
return
}
trackIDStr := c.Param("id")
if trackIDStr == "" {
h.respondWithError(c, http.StatusBadRequest, "track id is required")
return
}
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
h.respondWithError(c, http.StatusBadRequest, "invalid track id")
return
}
var req UpdateLyricsRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.respondWithError(c, http.StatusBadRequest, "invalid request body")
return
}
lyrics, err := h.trackService.CreateOrUpdateLyrics(c.Request.Context(), trackID, userID, req.Content)
if err != nil {
if errors.Is(err, ErrTrackNotFound) || errors.Is(err, gorm.ErrRecordNotFound) {
h.respondWithError(c, http.StatusNotFound, "track not found")
return
}
if errors.Is(err, ErrForbidden) {
h.respondWithError(c, http.StatusForbidden, "forbidden")
return
}
h.respondWithError(c, http.StatusInternalServerError, "failed to update lyrics")
return
}
handlers.RespondSuccess(c, http.StatusOK, gin.H{"lyrics": lyrics})
}
// DeleteTrack gère la suppression d'un track
// @Summary Delete Track
// @Description Permanently delete a track
// @Tags Track
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Track ID"
// @Success 200 {object} response.APIResponse{data=object{message=string}}
// @Failure 401 {object} response.APIResponse "Unauthorized"
// @Failure 403 {object} response.APIResponse "Forbidden"
// @Failure 404 {object} response.APIResponse "Track not found"
// @Router /tracks/{id} [delete]
func (h *TrackHandler) DeleteTrack(c *gin.Context) {
// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
userID, ok := h.getUserID(c)
if !ok {
return // Erreur déjà envoyée par getUserID
}
trackIDStr := c.Param("id")
if trackIDStr == "" {
// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de c.JSON
h.respondWithError(c, http.StatusBadRequest, "track id is required")
return
}
// MIGRATION UUID: TrackID is UUID
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.BadRequest
h.respondWithError(c, http.StatusBadRequest, "invalid track id")
return
}
// MOD-P1-003: Check if user is admin for ownership bypass
isAdmin := false
if h.permissionService != nil {
hasRole, err := h.permissionService.HasRole(c.Request.Context(), userID, "admin")
if err == nil && hasRole {
isAdmin = true
}
}
// Pass isAdmin via context
ctx := context.WithValue(c.Request.Context(), "is_admin", isAdmin)
err = h.trackService.DeleteTrack(ctx, trackID, userID)
if err != nil {
if errors.Is(err, ErrTrackNotFound) || errors.Is(err, gorm.ErrRecordNotFound) {
// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.NotFound
h.respondWithError(c, http.StatusNotFound, "track not found")
return
}
if errors.Is(err, ErrForbidden) {
// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.Forbidden
h.respondWithError(c, http.StatusForbidden, "forbidden")
return
}
// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.InternalServerError
h.respondWithError(c, http.StatusInternalServerError, "failed to delete track")
return
}
// MOD-P1-RES-001: Utiliser RespondSuccess au lieu de response.Success
handlers.RespondSuccess(c, http.StatusOK, gin.H{"message": "track deleted successfully"})
}
// BatchDeleteRequest représente la requête pour supprimer plusieurs tracks
type BatchDeleteRequest struct {
TrackIDs []string `json:"track_ids" binding:"required" validate:"required,min=1,dive,uuid"`
}
// BatchDeleteTracks gère la suppression en lot de plusieurs tracks
// POST /api/v1/tracks/batch/delete
// BE-API-024: Implement track batch operations validation
// @Summary Batch Delete Tracks
// @Description Delete multiple tracks at once
// @Tags Track
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body BatchDeleteRequest true "List of Track IDs"
// @Success 200 {object} response.APIResponse{data=object{deleted=[]string,failed=object}}
// @Failure 400 {object} response.APIResponse "Validation Error"
// @Failure 500 {object} response.APIResponse "Internal Error"
// @Router /tracks/batch/delete [post]
func (h *TrackHandler) BatchDeleteTracks(c *gin.Context) {
// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
userID, ok := h.getUserID(c)
if !ok {
return // Erreur déjà envoyée par getUserID
}
// MOD-P1-002: Utiliser helper centralisé pour bind + validate
var req BatchDeleteRequest
if !common.BindAndValidateJSON(c, &req) {
return // Erreur déjà envoyée au client
}
// Convertir les IDs en UUIDs
var trackUUIDs []uuid.UUID
for _, idStr := range req.TrackIDs {
if uid, err := uuid.Parse(idStr); err == nil {
trackUUIDs = append(trackUUIDs, uid)
}
}
// MOD-P1-003: Check if user is admin for ownership bypass
isAdmin := false
if h.permissionService != nil {
hasRole, err := h.permissionService.HasRole(c.Request.Context(), userID, "admin")
if err == nil && hasRole {
isAdmin = true
}
}
// Pass isAdmin via context
ctx := context.WithValue(c.Request.Context(), "is_admin", isAdmin)
result, err := h.trackService.BatchDeleteTracks(ctx, trackUUIDs, userID)
if err != nil {
// Vérifier si c'est une erreur de taille de batch
if strings.Contains(err.Error(), "batch size exceeds maximum") {
handlers.RespondWithAppError(c, apperrors.NewValidationError(err.Error()))
return
}
handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to delete tracks", err))
return
}
// BE-API-024: Standardize batch delete response format
handlers.RespondSuccess(c, http.StatusOK, gin.H{
"deleted": result.Deleted,
"failed": result.Failed,
})
}
// BatchUpdateRequest représente la requête pour mettre à jour plusieurs tracks
type BatchUpdateRequest struct {
TrackIDs []string `json:"track_ids" binding:"required" validate:"required,min=1,dive,uuid"`
Updates map[string]interface{} `json:"updates" binding:"required" validate:"required,min=1"`
}
// BatchUpdateTracks gère la mise à jour en lot de plusieurs tracks
// POST /api/v1/tracks/batch/update
// BE-API-024: Implement track batch operations validation
func (h *TrackHandler) BatchUpdateTracks(c *gin.Context) {
// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
userID, ok := h.getUserID(c)
if !ok {
return // Erreur déjà envoyée par getUserID
}
// MOD-P1-002: Utiliser helper centralisé pour bind + validate
var req BatchUpdateRequest
if !common.BindAndValidateJSON(c, &req) {
return // Erreur déjà envoyée au client
}
// Convertir les IDs en UUIDs
var trackUUIDs []uuid.UUID
for _, idStr := range req.TrackIDs {
if uid, err := uuid.Parse(idStr); err == nil {
trackUUIDs = append(trackUUIDs, uid)
}
}
// MOD-P1-003: Check if user is admin for ownership bypass
isAdmin := false
if h.permissionService != nil {
hasRole, err := h.permissionService.HasRole(c.Request.Context(), userID, "admin")
if err == nil && hasRole {
isAdmin = true
}
}
// Pass isAdmin via context
ctx := context.WithValue(c.Request.Context(), "is_admin", isAdmin)
result, err := h.trackService.BatchUpdateTracks(ctx, trackUUIDs, userID, req.Updates)
if err != nil {
// Vérifier si c'est une erreur de validation
if strings.Contains(err.Error(), "batch size exceeds maximum") ||
strings.Contains(err.Error(), "cannot be empty") ||
strings.Contains(err.Error(), "invalid value") ||
strings.Contains(err.Error(), "exceeds maximum length") ||
strings.Contains(err.Error(), "must be between") {
// BE-API-024: Standardize batch update error response format
handlers.RespondWithAppError(c, apperrors.NewValidationError(err.Error()))
return
}
// BE-API-024: Standardize batch update error response format
handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to update tracks", err))
return
}
// BE-API-024: Standardize batch update response format
handlers.RespondSuccess(c, http.StatusOK, gin.H{
"updated": result.Updated,
"failed": result.Failed,
})
}
// LikeTrack gère l'ajout d'un like sur un track
func (h *TrackHandler) LikeTrack(c *gin.Context) {
// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
userID, ok := h.getUserID(c)
if !ok {
return // Erreur déjà envoyée par getUserID
}
trackIDStr := c.Param("id")
if trackIDStr == "" {
// MOD-P2-003: Utiliser AppError au lieu de gin.H
h.respondWithError(c, http.StatusBadRequest, "track id is required")
return
}
// MIGRATION UUID: TrackID is UUID
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
// MOD-P2-003: Utiliser AppError au lieu de gin.H
h.respondWithError(c, http.StatusBadRequest, "invalid track id")
return
}
if err := h.likeService.LikeTrack(c.Request.Context(), userID, trackID); err != nil {
// MOD-P2-003: Utiliser AppError au lieu de gin.H
if err.Error() == "track not found" {
h.respondWithError(c, http.StatusNotFound, "track not found")
return
}
h.respondWithError(c, http.StatusInternalServerError, err.Error())
return
}
// Phase 2.2: Create notification for track creator (skip if user likes own track)
if h.notificationService != nil {
track, err := h.trackService.GetTrackByID(c.Request.Context(), trackID)
if err == nil && track.UserID != userID {
link := "/tracks/" + trackID.String()
if err := h.notificationService.CreateNotification(track.UserID, "like", "New like", "Someone liked your track", link); err != nil {
// Log but don't fail the request
}
}
}
c.JSON(http.StatusOK, gin.H{"message": "track liked"})
}
// UnlikeTrack gère la suppression d'un like sur un track
func (h *TrackHandler) UnlikeTrack(c *gin.Context) {
// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
userID, ok := h.getUserID(c)
if !ok {
return // Erreur déjà envoyée par getUserID
}
trackIDStr := c.Param("id")
if trackIDStr == "" {
// MOD-P2-003: Utiliser AppError au lieu de gin.H
h.respondWithError(c, http.StatusBadRequest, "track id is required")
return
}
// MIGRATION UUID: TrackID is UUID
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
// MOD-P2-003: Utiliser AppError au lieu de gin.H
h.respondWithError(c, http.StatusBadRequest, "invalid track id")
return
}
if err := h.likeService.UnlikeTrack(c.Request.Context(), userID, trackID); err != nil {
// MOD-P2-003: Utiliser AppError au lieu de gin.H
h.respondWithError(c, http.StatusInternalServerError, err.Error())
return
}
handlers.RespondSuccess(c, http.StatusOK, gin.H{"message": "track unliked"})
}
// GetTrackLikes gère la récupération du nombre de likes d'un track
func (h *TrackHandler) GetTrackLikes(c *gin.Context) {
trackIDStr := c.Param("id")
if trackIDStr == "" {
// MOD-P2-003: Utiliser AppError au lieu de gin.H
h.respondWithError(c, http.StatusBadRequest, "track id is required")
return
}
// MIGRATION UUID: TrackID is UUID
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
// MOD-P2-003: Utiliser AppError au lieu de gin.H
h.respondWithError(c, http.StatusBadRequest, "invalid track id")
return
}
count, err := h.likeService.GetTrackLikesCount(c.Request.Context(), trackID)
if err != nil {
// MOD-P2-003: Utiliser AppError au lieu de gin.H
h.respondWithError(c, http.StatusInternalServerError, err.Error())
return
}
// Vérifier si l'utilisateur a liké ce track (optionnel)
var isLiked bool
if userIDInterface, exists := c.Get("user_id"); exists {
userID, ok := userIDInterface.(uuid.UUID)
if ok && userID != uuid.Nil {
isLiked, _ = h.likeService.IsLiked(c.Request.Context(), userID, trackID)
}
}
handlers.RespondSuccess(c, http.StatusOK, gin.H{
"count": count,
"is_liked": isLiked,
})
}
// GetUserLikedTracks gère la récupération des tracks likés par un utilisateur
// GET /api/v1/users/:id/likes
// BE-API-027: Implement user liked tracks endpoint
func (h *TrackHandler) GetUserLikedTracks(c *gin.Context) {
userIDStr := c.Param("id")
if userIDStr == "" {
handlers.RespondWithAppError(c, apperrors.NewValidationError("user id is required"))
return
}
userID, err := uuid.Parse(userIDStr)
if err != nil {
handlers.RespondWithAppError(c, apperrors.NewValidationError("invalid user id"))
return
}
// Parse pagination parameters
limit := 20 // default
if limitStr := c.Query("limit"); limitStr != "" {
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
// Limiter à un maximum raisonnable
if parsedLimit > 100 {
parsedLimit = 100
}
limit = parsedLimit
}
}
offset := 0 // default
if offsetStr := c.Query("offset"); offsetStr != "" {
if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 {
offset = parsedOffset
}
}
tracks, err := h.likeService.GetUserLikedTracks(c.Request.Context(), userID, limit, offset)
if err != nil {
handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to get user liked tracks", err))
return
}
total, err := h.likeService.GetUserLikedTracksCount(c.Request.Context(), userID)
if err != nil {
handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to get user liked tracks count", err))
return
}
// BE-API-027: Standardize response format
handlers.RespondSuccess(c, http.StatusOK, gin.H{
"tracks": tracks,
"total": total,
"limit": limit,
"offset": offset,
})
}
// SearchTracks gère la recherche avancée de tracks
func (h *TrackHandler) SearchTracks(c *gin.Context) {
if h.searchService == nil {
// MOD-P2-003: Utiliser AppError au lieu de gin.H
h.respondWithError(c, http.StatusInternalServerError, "search service not available")
return
}
// Récupérer les paramètres de query
params := services.TrackSearchParams{
Query: c.Query("q"),
TagMode: c.DefaultQuery("tag_mode", "OR"),
Page: 1,
Limit: 20,
SortBy: c.DefaultQuery("sort_by", "created_at"),
SortOrder: c.DefaultQuery("sort_order", "desc"),
}
// Parser page
if pageStr := c.Query("page"); pageStr != "" {
if page, err := strconv.Atoi(pageStr); err == nil && page > 0 {
params.Page = page
}
}
// Parser limit
if limitStr := c.Query("limit"); limitStr != "" {
if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 {
params.Limit = limit
}
}
if params.Limit > 100 {
params.Limit = 100
}
// Parser tags
if tagsStr := c.Query("tags"); tagsStr != "" {
params.Tags = strings.Split(tagsStr, ",")
for i := range params.Tags {
params.Tags[i] = strings.TrimSpace(params.Tags[i])
}
}
// Parser min_duration
if minDurationStr := c.Query("min_duration"); minDurationStr != "" {
if minDuration, err := strconv.Atoi(minDurationStr); err == nil && minDuration >= 0 {
params.MinDuration = &minDuration
}
}
// Parser max_duration
if maxDurationStr := c.Query("max_duration"); maxDurationStr != "" {
if maxDuration, err := strconv.Atoi(maxDurationStr); err == nil && maxDuration >= 0 {
params.MaxDuration = &maxDuration
}
}
// Parser min_bpm
if minBPMStr := c.Query("min_bpm"); minBPMStr != "" {
if minBPM, err := strconv.Atoi(minBPMStr); err == nil && minBPM >= 0 {
params.MinBPM = &minBPM
}
}
// Parser max_bpm
if maxBPMStr := c.Query("max_bpm"); maxBPMStr != "" {
if maxBPM, err := strconv.Atoi(maxBPMStr); err == nil && maxBPM >= 0 {
params.MaxBPM = &maxBPM
}
}
// Parser genre
if genre := c.Query("genre"); genre != "" {
params.Genre = &genre
}
// Parser format
if format := c.Query("format"); format != "" {
params.Format = &format
}
// Parser musical_key
if musicalKey := c.Query("musical_key"); musicalKey != "" {
params.MusicalKey = &musicalKey
}
// Parser min_date
if minDate := c.Query("min_date"); minDate != "" {
params.MinDate = &minDate
}
// Parser max_date
if maxDate := c.Query("max_date"); maxDate != "" {
params.MaxDate = &maxDate
}
// Effectuer la recherche avec filtres combinés
tracks, total, err := h.searchService.SearchTracks(c.Request.Context(), params)
if err != nil {
// MOD-P2-003: Utiliser AppError au lieu de gin.H
h.respondWithError(c, http.StatusInternalServerError, "failed to search tracks")
return
}
// Calculer les métadonnées de pagination
totalPages := (int(total) + params.Limit - 1) / params.Limit
if totalPages == 0 {
totalPages = 1
}
c.JSON(http.StatusOK, gin.H{
"tracks": tracks,
"pagination": gin.H{
"page": params.Page,
"limit": params.Limit,
"total": total,
"total_pages": totalPages,
},
})
}
// CreateShareRequest représente la requête pour créer un lien de partage
type CreateShareRequest struct {
Permissions string `json:"permissions" binding:"required" validate:"required,oneof=read write admin"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
}
// CreateShare crée un nouveau lien de partage pour un track
func (h *TrackHandler) CreateShare(c *gin.Context) {
// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
userID, ok := h.getUserID(c)
if !ok {
return // Erreur déjà envoyée par getUserID
}
trackIDStr := c.Param("id")
if trackIDStr == "" {
// MOD-P2-003: Utiliser AppError au lieu de gin.H
h.respondWithError(c, http.StatusBadRequest, "track id is required")
return
}
// MIGRATION UUID: TrackID is UUID
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
// MOD-P2-003: Utiliser AppError au lieu de gin.H
h.respondWithError(c, http.StatusBadRequest, "invalid track id")
return
}
if h.shareService == nil {
// MOD-P2-003: Utiliser AppError au lieu de gin.H
h.respondWithError(c, http.StatusInternalServerError, "share service not available")
return
}
// MOD-P1-002: Utiliser helper centralisé pour bind + validate
var req CreateShareRequest
if !common.BindAndValidateJSON(c, &req) {
return // Erreur déjà envoyée au client
}
share, err := h.shareService.CreateShare(c.Request.Context(), trackID, userID, req.Permissions, req.ExpiresAt)
if err != nil {
if errors.Is(err, services.ErrForbidden) {
// MOD-P2-003: Utiliser AppError au lieu de gin.H
h.respondWithError(c, http.StatusForbidden, "forbidden")
return
}
if errors.Is(err, services.ErrTrackNotFound) {
// MOD-P2-003: Utiliser AppError au lieu de gin.H
h.respondWithError(c, http.StatusNotFound, "track not found")
return
}
// MOD-P2-003: Utiliser AppError au lieu de gin.H
h.respondWithError(c, http.StatusInternalServerError, "failed to create share")
return
}
handlers.RespondSuccess(c, http.StatusOK, gin.H{"share": share})
}
// GetSharedTrack récupère un track via son token de partage
// GET /api/v1/tracks/shared/:token
// BE-API-029: Implement shared track access endpoint validation
func (h *TrackHandler) GetSharedTrack(c *gin.Context) {
token := c.Param("token")
if token == "" {
handlers.RespondWithAppError(c, apperrors.NewValidationError("share token is required"))
return
}
if h.shareService == nil {
handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "share service not available", nil))
return
}
share, err := h.shareService.ValidateShareToken(c.Request.Context(), token)
if err != nil {
if errors.Is(err, services.ErrShareNotFound) {
handlers.RespondWithAppError(c, apperrors.NewNotFoundError("share"))
return
}
if errors.Is(err, services.ErrShareExpired) {
handlers.RespondWithAppError(c, apperrors.NewForbiddenError("share link expired"))
return
}
handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to validate share token", err))
return
}
// Récupérer le track
track, err := h.trackService.GetTrackByID(c.Request.Context(), share.TrackID)
if err != nil {
if errors.Is(err, ErrTrackNotFound) || errors.Is(err, gorm.ErrRecordNotFound) {
handlers.RespondWithAppError(c, apperrors.NewNotFoundError("track"))
return
}
handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to get track", err))
return
}
// BE-API-029: Standardize response format
handlers.RespondSuccess(c, http.StatusOK, gin.H{
"track": track,
"share": share,
})
}
// RevokeShare révoque un lien de partage
// DELETE /api/v1/tracks/share/:id
// BE-API-028: Implement track share revoke endpoint validation
func (h *TrackHandler) RevokeShare(c *gin.Context) {
// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
userID, ok := h.getUserID(c)
if !ok {
return // Erreur déjà envoyée par getUserID
}
shareIDStr := c.Param("id")
if shareIDStr == "" {
handlers.RespondWithAppError(c, apperrors.NewValidationError("share id is required"))
return
}
// MIGRATION UUID: ShareID is UUID
shareID, err := uuid.Parse(shareIDStr)
if err != nil {
handlers.RespondWithAppError(c, apperrors.NewValidationError("invalid share id"))
return
}
if h.shareService == nil {
handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "share service not available", nil))
return
}
err = h.shareService.RevokeShare(c.Request.Context(), shareID, userID)
if err != nil {
if errors.Is(err, services.ErrShareNotFound) {
handlers.RespondWithAppError(c, apperrors.NewNotFoundError("share"))
return
}
if errors.Is(err, services.ErrForbidden) {
handlers.RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
return
}
handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to revoke share", err))
return
}
// BE-API-028: Standardize response format
handlers.RespondSuccess(c, http.StatusOK, gin.H{"message": "share revoked"})
}
// GetTrackStats returns track statistics (plays, likes, views, etc.)
// GET /api/v1/tracks/:id/stats
func (h *TrackHandler) GetTrackStats(c *gin.Context) {
trackIDStr := c.Param("id")
if trackIDStr == "" {
h.respondWithError(c, http.StatusBadRequest, "track id is required")
return
}
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
h.respondWithError(c, http.StatusBadRequest, "invalid track id")
return
}
stats, err := h.trackService.GetTrackStats(c.Request.Context(), trackID)
if err != nil {
if errors.Is(err, ErrTrackNotFound) {
h.respondWithError(c, http.StatusNotFound, "track not found")
return
}
h.respondWithError(c, http.StatusInternalServerError, "failed to get track stats")
return
}
// Map to response format expected by frontend
avgDuration := float64(0)
if stats.Views > 0 {
avgDuration = float64(stats.TotalPlayTime) / float64(stats.Views)
}
resp := gin.H{
"total_plays": stats.Views,
"unique_listeners": 0, // TrackService does not compute; use /analytics/tracks/:id/stats for full analytics
"average_duration": avgDuration,
"completion_rate": 0,
"views": stats.Views,
"likes": stats.Likes,
"comments": stats.Comments,
"total_play_time": stats.TotalPlayTime,
"downloads": stats.Downloads,
}
handlers.RespondSuccess(c, http.StatusOK, gin.H{"stats": resp})
}
// GetTrackHistory returns modification history for a track
// GET /api/v1/tracks/:id/history
func (h *TrackHandler) GetTrackHistory(c *gin.Context) {
if h.historyService == nil {
h.respondWithError(c, http.StatusInternalServerError, "history service not available")
return
}
trackIDStr := c.Param("id")
if trackIDStr == "" {
h.respondWithError(c, http.StatusBadRequest, "track id is required")
return
}
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
h.respondWithError(c, http.StatusBadRequest, "invalid track id")
return
}
limit := 50
offset := 0
if l := c.Query("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
limit = parsed
}
}
if o := c.Query("offset"); o != "" {
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
offset = parsed
}
}
histories, total, err := h.historyService.GetHistory(c.Request.Context(), trackID, limit, offset)
if err != nil {
if errors.Is(err, services.ErrTrackNotFound) {
h.respondWithError(c, http.StatusNotFound, "track not found")
return
}
h.respondWithError(c, http.StatusInternalServerError, "failed to get track history")
return
}
// Map models to response format
historyItems := make([]gin.H, 0, len(histories))
for _, item := range histories {
historyItems = append(historyItems, gin.H{
"id": item.ID.String(),
"track_id": item.TrackID.String(),
"user_id": item.UserID.String(),
"action": string(item.Action),
"old_value": item.OldValue,
"new_value": item.NewValue,
"created_at": item.CreatedAt,
})
}
handlers.RespondSuccess(c, http.StatusOK, gin.H{
"history": historyItems,
"total": total,
"limit": limit,
"offset": offset,
})
}
// RecordPlayRequest représente la requête pour enregistrer un événement de lecture
// BE-API-019: Implement track play analytics endpoint
type RecordPlayRequest struct {
PlayTime int `json:"play_time,omitempty" binding:"omitempty,min=0"` // seconds, optional, defaults to 0
}
// RecordPlay enregistre un événement de lecture pour un track
// POST /api/v1/tracks/:id/play
// BE-API-019: Implement track play analytics endpoint
func (h *TrackHandler) RecordPlay(c *gin.Context) {
if h.playbackAnalyticsService == nil {
h.respondWithError(c, http.StatusInternalServerError, "playback analytics service not available")
return
}
// Récupérer l'ID du track depuis l'URL
trackIDStr := c.Param("id")
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
h.respondWithError(c, http.StatusBadRequest, "invalid track id")
return
}
// Récupérer l'ID utilisateur du contexte
userID, ok := h.getUserID(c)
if !ok {
return // Erreur déjà envoyée par getUserID
}
// Parser la requête (optionnelle, peut être vide)
var req RecordPlayRequest
if c.Request.ContentLength > 0 {
if err := c.ShouldBindJSON(&req); err != nil {
// Si le body est présent mais invalide, retourner une erreur
h.respondWithError(c, http.StatusBadRequest, "invalid request body")
return
}
}
// Créer un événement d'analytics basique
// Note: Le service RecordPlayback vérifie que le track existe
playTime := req.PlayTime
if playTime < 0 {
playTime = 0
}
analytics := &models.PlaybackAnalytics{
TrackID: trackID,
UserID: userID,
PlayTime: playTime,
PauseCount: 0,
SeekCount: 0,
CompletionRate: 0, // Sera calculé par le service si track.Duration > 0
StartedAt: time.Now(),
EndedAt: nil,
}
// Enregistrer l'événement via le service
err = h.playbackAnalyticsService.RecordPlayback(c.Request.Context(), analytics)
if err != nil {
if errors.Is(err, services.ErrTrackNotFound) {
h.respondWithError(c, http.StatusNotFound, "track not found")
return
}
h.respondWithError(c, http.StatusInternalServerError, "failed to record play event")
return
}
// Retourner le succès
handlers.RespondSuccess(c, http.StatusOK, gin.H{
"message": "Play event recorded",
"id": analytics.ID.String(),
})
}
// RestoreVersion restaure une version spécifique d'un track
// POST /api/v1/tracks/:id/versions/:versionId/restore
// BE-API-014: Implement track versions restore endpoint
func (h *TrackHandler) RestoreVersion(c *gin.Context) {
if h.versionService == nil {
h.respondWithError(c, http.StatusInternalServerError, "version service not available")
return
}
// Récupérer l'ID du track depuis l'URL
trackIDStr := c.Param("id")
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
h.respondWithError(c, http.StatusBadRequest, "invalid track id")
return
}
// Récupérer l'ID de la version depuis l'URL
versionIDStr := c.Param("versionId")
versionID, err := uuid.Parse(versionIDStr)
if err != nil {
h.respondWithError(c, http.StatusBadRequest, "invalid version id")
return
}
// Récupérer l'ID utilisateur du contexte
userID, ok := h.getUserID(c)
if !ok {
return // Erreur déjà envoyée par getUserID
}
// Restaurer la version
err = h.versionService.RestoreVersion(c.Request.Context(), trackID, versionID, userID)
if err != nil {
if errors.Is(err, services.ErrTrackNotFound) {
h.respondWithError(c, http.StatusNotFound, "track not found")
return
}
if errors.Is(err, services.ErrVersionNotFound) {
h.respondWithError(c, http.StatusNotFound, "version not found")
return
}
if errors.Is(err, services.ErrForbidden) {
h.respondWithError(c, http.StatusForbidden, "forbidden: only track owner can restore versions")
return
}
h.respondWithError(c, http.StatusInternalServerError, "failed to restore version")
return
}
c.JSON(http.StatusOK, gin.H{"message": "Version restored successfully"})
}