2025-12-03 19:29:37 +00:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
|
|
import (
|
2025-12-06 16:21:59 +00:00
|
|
|
"errors"
|
2025-12-03 19:29:37 +00:00
|
|
|
"net/http"
|
|
|
|
|
"strconv"
|
2025-12-16 16:23:49 +00:00
|
|
|
"time"
|
2025-12-03 19:29:37 +00:00
|
|
|
|
2025-12-16 16:23:49 +00:00
|
|
|
apperrors "veza-backend-api/internal/errors"
|
2025-12-03 19:29:37 +00:00
|
|
|
"veza-backend-api/internal/models"
|
2025-12-16 18:34:08 +00:00
|
|
|
"veza-backend-api/internal/monitoring"
|
2025-12-03 19:29:37 +00:00
|
|
|
"veza-backend-api/internal/services"
|
2025-12-24 11:15:25 +00:00
|
|
|
"veza-backend-api/internal/utils"
|
2025-12-03 19:29:37 +00:00
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
"github.com/google/uuid"
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
"go.uber.org/zap"
|
|
|
|
|
"gorm.io/gorm"
|
2025-12-03 19:29:37 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// PlaylistHandler gère les opérations sur les playlists
|
|
|
|
|
type PlaylistHandler struct {
|
2025-12-29 18:23:23 +00:00
|
|
|
playlistService services.PlaylistServiceInterface
|
2025-12-03 19:29:37 +00:00
|
|
|
playlistAnalyticsService *services.PlaylistAnalyticsService
|
|
|
|
|
playlistFollowService *services.PlaylistFollowService
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
db *gorm.DB
|
|
|
|
|
commonHandler *CommonHandler
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewPlaylistHandler crée un nouveau handler de playlists
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
func NewPlaylistHandler(playlistService *services.PlaylistService, db *gorm.DB, logger *zap.Logger) *PlaylistHandler {
|
|
|
|
|
return &PlaylistHandler{
|
|
|
|
|
playlistService: playlistService,
|
|
|
|
|
db: db,
|
|
|
|
|
commonHandler: NewCommonHandler(logger),
|
|
|
|
|
}
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-29 18:23:23 +00:00
|
|
|
// 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),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 19:29:37 +00:00
|
|
|
// 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
|
2025-12-16 18:34:08 +00:00
|
|
|
// MOD-P1-001: Ajout tags validate pour validation systématique (Description manquait)
|
2025-12-03 19:29:37 +00:00
|
|
|
type CreatePlaylistRequest struct {
|
2025-12-06 16:21:59 +00:00
|
|
|
Title string `json:"title" binding:"required,min=1,max=200" validate:"required,min=1,max=200"`
|
2025-12-16 18:34:08 +00:00
|
|
|
Description string `json:"description,omitempty" validate:"omitempty,max=1000"`
|
2025-12-03 19:29:37 +00:00
|
|
|
IsPublic bool `json:"is_public"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UpdatePlaylistRequest représente la requête pour mettre à jour une playlist
|
2025-12-16 18:34:08 +00:00
|
|
|
// MOD-P1-001: Ajout tags validate pour validation systématique (Description manquait)
|
2025-12-03 19:29:37 +00:00
|
|
|
type UpdatePlaylistRequest struct {
|
2025-12-06 16:21:59 +00:00
|
|
|
Title *string `json:"title,omitempty" binding:"omitempty,min=1,max=200" validate:"omitempty,min=1,max=200"`
|
2025-12-16 18:34:08 +00:00
|
|
|
Description *string `json:"description,omitempty" validate:"omitempty,max=1000"`
|
2025-12-03 19:29:37 +00:00
|
|
|
IsPublic *bool `json:"is_public,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ReorderTracksRequest représente la requête pour réorganiser les tracks
|
|
|
|
|
type ReorderTracksRequest struct {
|
2025-12-06 16:21:59 +00:00
|
|
|
TrackIDs []uuid.UUID `json:"track_ids" binding:"required,min=1" validate:"required,min=1"` // Changed to []uuid.UUID
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CreatePlaylist gère la création d'une playlist
|
2025-12-06 16:34:18 +00:00
|
|
|
// @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]
|
2025-12-03 19:29:37 +00:00
|
|
|
func (h *PlaylistHandler) CreatePlaylist(c *gin.Context) {
|
2025-12-16 16:23:49 +00:00
|
|
|
// 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
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var req CreatePlaylistRequest
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
|
|
|
RespondWithAppError(c, appErr)
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 11:15:25 +00:00
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-16 16:23:49 +00:00
|
|
|
// 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)
|
2025-12-03 19:29:37 +00:00
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to create playlist", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-16 18:34:08 +00:00
|
|
|
// MOD-P2-003: Enregistrer la métrique business (depuis le handler pour éviter cycle d'import)
|
|
|
|
|
monitoring.RecordPlaylistCreated()
|
|
|
|
|
|
Phase 2 stabilisation: code mort, Modal→Dialog, feature flags, tests, router split, Rust legacy
Bloc A - Code mort:
- Suppression Studio (components, views, features)
- Suppression gamification + services mock (projectService, storageService, gamificationService)
- Mise à jour Sidebar, Navbar, locales
Bloc B - Frontend:
- Suppression modal.tsx deprecated, Modal.stories (doublon Dialog)
- Feature flags: PLAYLIST_SEARCH, PLAYLIST_RECOMMENDATIONS, ROLE_MANAGEMENT = true
- Suppression 19 tests orphelins, retrait exclusions vitest.config
Bloc C - Backend:
- Extraction routes_auth.go depuis router.go
Bloc D - Rust:
- Suppression security_legacy.rs (code mort, patterns déjà dans security/)
2026-02-14 16:23:32 +00:00
|
|
|
RespondSuccess(c, http.StatusCreated, gin.H{"playlist": playlist})
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-09 15:49:05 +00:00
|
|
|
// 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})
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 19:29:37 +00:00
|
|
|
// GetPlaylists gère la récupération des playlists avec pagination
|
2025-12-06 16:34:18 +00:00
|
|
|
// @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]
|
2025-12-03 19:29:37 +00:00
|
|
|
func (h *PlaylistHandler) GetPlaylists(c *gin.Context) {
|
|
|
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
|
|
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
|
|
|
|
|
Phase 2 stabilisation: code mort, Modal→Dialog, feature flags, tests, router split, Rust legacy
Bloc A - Code mort:
- Suppression Studio (components, views, features)
- Suppression gamification + services mock (projectService, storageService, gamificationService)
- Mise à jour Sidebar, Navbar, locales
Bloc B - Frontend:
- Suppression modal.tsx deprecated, Modal.stories (doublon Dialog)
- Feature flags: PLAYLIST_SEARCH, PLAYLIST_RECOMMENDATIONS, ROLE_MANAGEMENT = true
- Suppression 19 tests orphelins, retrait exclusions vitest.config
Bloc C - Backend:
- Extraction routes_auth.go depuis router.go
Bloc D - Rust:
- Suppression security_legacy.rs (code mort, patterns déjà dans security/)
2026-02-14 16:23:32 +00:00
|
|
|
// 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
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 {
|
2026-02-22 16:44:38 +00:00
|
|
|
h.commonHandler.logger.Debug("GetPlaylists: user_id found in context", zap.Any("value", uidInterface))
|
2025-12-03 19:29:37 +00:00
|
|
|
if uid, ok := uidInterface.(uuid.UUID); ok {
|
|
|
|
|
currentUserID = &uid
|
2025-12-21 23:55:51 +00:00
|
|
|
} else {
|
2026-02-22 16:44:38 +00:00
|
|
|
h.commonHandler.logger.Debug("GetPlaylists: user_id type assertion failed")
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
2025-12-21 23:55:51 +00:00
|
|
|
} else {
|
2026-02-22 16:44:38 +00:00
|
|
|
h.commonHandler.logger.Debug("GetPlaylists: user_id not found in context")
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-16 16:23:49 +00:00
|
|
|
// 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)
|
2025-12-03 19:29:37 +00:00
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get playlists", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
2025-12-03 19:29:37 +00:00
|
|
|
"playlists": playlists,
|
|
|
|
|
"total": total,
|
|
|
|
|
"page": page,
|
|
|
|
|
"limit": limit,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetPlaylist gère la récupération d'une playlist
|
2025-12-06 16:34:18 +00:00
|
|
|
// @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]
|
2025-12-03 19:29:37 +00:00
|
|
|
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 {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var currentUserID *uuid.UUID
|
|
|
|
|
if uidInterface, exists := c.Get("user_id"); exists {
|
|
|
|
|
if uid, ok := uidInterface.(uuid.UUID); ok {
|
|
|
|
|
currentUserID = &uid
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-16 16:23:49 +00:00
|
|
|
// 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)
|
2025-12-03 19:29:37 +00:00
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
2025-12-06 16:21:59 +00:00
|
|
|
if errors.Is(err, services.ErrPlaylistNotFound) || errors.Is(err, services.ErrAccessDenied) {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get playlist", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-23 00:42:53 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, playlist)
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-09 15:49:05 +00:00
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 19:29:37 +00:00
|
|
|
// UpdatePlaylist gère la mise à jour d'une playlist
|
2025-12-06 16:34:18 +00:00
|
|
|
// @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]
|
2025-12-03 19:29:37 +00:00
|
|
|
func (h *PlaylistHandler) UpdatePlaylist(c *gin.Context) {
|
2025-12-16 16:23:49 +00:00
|
|
|
// 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
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
|
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var req UpdatePlaylistRequest
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
|
|
|
RespondWithAppError(c, appErr)
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 11:15:25 +00:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-16 16:23:49 +00:00
|
|
|
// 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)
|
2025-12-03 19:29:37 +00:00
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
2025-12-06 16:21:59 +00:00
|
|
|
if errors.Is(err, services.ErrPlaylistNotFound) {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-06 16:21:59 +00:00
|
|
|
if errors.Is(err, services.ErrAccessDenied) {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to update playlist", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-23 00:42:53 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, playlist)
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DeletePlaylist gère la suppression d'une playlist
|
2025-12-06 16:34:18 +00:00
|
|
|
// @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]
|
2025-12-03 19:29:37 +00:00
|
|
|
func (h *PlaylistHandler) DeletePlaylist(c *gin.Context) {
|
2025-12-16 16:23:49 +00:00
|
|
|
// 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
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
|
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-16 16:23:49 +00:00
|
|
|
// 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"}
|
2025-12-06 16:21:59 +00:00
|
|
|
if errors.Is(err, services.ErrPlaylistNotFound) {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-06 16:21:59 +00:00
|
|
|
if errors.Is(err, services.ErrAccessDenied) {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to delete playlist", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": "playlist deleted"})
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AddTrack gère l'ajout d'un track à une playlist
|
2025-12-06 16:34:18 +00:00
|
|
|
// @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]
|
2025-12-03 19:29:37 +00:00
|
|
|
func (h *PlaylistHandler) AddTrack(c *gin.Context) {
|
2025-12-16 16:23:49 +00:00
|
|
|
// 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
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
|
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Track IDs are uuid.UUID
|
|
|
|
|
trackID, err := uuid.Parse(c.Param("trackId")) // Changed to uuid.Parse
|
|
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid track id"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := h.playlistService.AddTrack(c.Request.Context(), playlistID, trackID, userID); err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
2025-12-06 16:21:59 +00:00
|
|
|
if errors.Is(err, services.ErrPlaylistNotFound) {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-06 16:21:59 +00:00
|
|
|
if errors.Is(err, services.ErrTrackNotFound) {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("track"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-06 16:21:59 +00:00
|
|
|
if errors.Is(err, services.ErrTrackAlreadyInPlaylist) {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("track already in playlist"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-06 16:21:59 +00:00
|
|
|
if errors.Is(err, services.ErrAccessDenied) {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to add track to playlist", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": "track added to playlist"})
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RemoveTrack gère la suppression d'un track d'une playlist
|
2025-12-06 16:34:18 +00:00
|
|
|
// @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]
|
2025-12-03 19:29:37 +00:00
|
|
|
func (h *PlaylistHandler) RemoveTrack(c *gin.Context) {
|
2025-12-16 16:23:49 +00:00
|
|
|
// 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
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
|
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Track IDs are uuid.UUID
|
|
|
|
|
trackID, err := uuid.Parse(c.Param("trackId")) // Changed to uuid.Parse
|
|
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid track id"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := h.playlistService.RemoveTrack(c.Request.Context(), playlistID, trackID, userID); err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
2025-12-03 19:29:37 +00:00
|
|
|
if err.Error() == "playlist not found" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err.Error() == "track not in playlist" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("track not in playlist"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err.Error() == "forbidden" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to remove track from playlist", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": "track removed from playlist"})
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ReorderTracks gère la réorganisation des tracks d'une playlist
|
2025-12-06 16:34:18 +00:00
|
|
|
// @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]
|
2025-12-03 19:29:37 +00:00
|
|
|
func (h *PlaylistHandler) ReorderTracks(c *gin.Context) {
|
2025-12-16 16:23:49 +00:00
|
|
|
// 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
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
|
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var req ReorderTracksRequest
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
|
|
|
RespondWithAppError(c, appErr)
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := h.playlistService.ReorderTracks(c.Request.Context(), playlistID, userID, req.TrackIDs); err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
2025-12-03 19:29:37 +00:00
|
|
|
if err.Error() == "playlist not found" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err.Error() == "some tracks are not in the playlist" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("some tracks are not in the playlist"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err.Error() == "forbidden" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to reorder tracks", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": "tracks reordered"})
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AddCollaboratorRequest représente la requête pour ajouter un collaborateur
|
|
|
|
|
type AddCollaboratorRequest struct {
|
2025-12-06 16:21:59 +00:00
|
|
|
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"`
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UpdateCollaboratorPermissionRequest représente la requête pour mettre à jour la permission d'un collaborateur
|
|
|
|
|
type UpdateCollaboratorPermissionRequest struct {
|
2025-12-06 16:21:59 +00:00
|
|
|
Permission string `json:"permission" binding:"required,oneof=read write admin" validate:"required,oneof=read write admin"`
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AddCollaborator gère l'ajout d'un collaborateur à une playlist
|
|
|
|
|
// T0479: POST /api/v1/playlists/:id/collaborators
|
|
|
|
|
func (h *PlaylistHandler) AddCollaborator(c *gin.Context) {
|
2025-12-16 16:23:49 +00:00
|
|
|
// 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
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
|
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var req AddCollaboratorRequest
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
|
|
|
RespondWithAppError(c, appErr)
|
2025-12-03 19:29:37 +00:00
|
|
|
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:
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid permission"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
collaborator, err := h.playlistService.AddCollaborator(c.Request.Context(), playlistID, userID, req.UserID, permission)
|
|
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
2025-12-03 19:29:37 +00:00
|
|
|
if err.Error() == "playlist not found" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err.Error() == "user not found" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("user"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err.Error() == "user is already a collaborator" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeConflict, "user is already a collaborator"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err.Error() == "cannot add playlist owner as collaborator" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("cannot add playlist owner as collaborator"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err.Error() == "forbidden: only playlist owner can add collaborators" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to add collaborator", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-23 00:42:53 +00:00
|
|
|
RespondSuccess(c, http.StatusCreated, collaborator)
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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) {
|
2025-12-16 16:23:49 +00:00
|
|
|
// 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
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
|
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
|
|
|
if err != nil {
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// User IDs are UUID
|
|
|
|
|
collaboratorUserID, err := uuid.Parse(c.Param("userId"))
|
|
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid user id"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := h.playlistService.RemoveCollaborator(c.Request.Context(), playlistID, userID, collaboratorUserID); err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
2025-12-03 19:29:37 +00:00
|
|
|
if err.Error() == "playlist not found" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err.Error() == "collaborator not found" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("collaborator"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err.Error() == "forbidden: only playlist owner can remove collaborators" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to remove collaborator", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": "collaborator removed"})
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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) {
|
2025-12-16 16:23:49 +00:00
|
|
|
// 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
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
|
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
|
|
|
if err != nil {
|
2025-12-23 00:41:43 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// User IDs are UUID
|
|
|
|
|
collaboratorUserID, err := uuid.Parse(c.Param("userId"))
|
|
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid user id"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var req UpdateCollaboratorPermissionRequest
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
|
|
|
RespondWithAppError(c, appErr)
|
2025-12-03 19:29:37 +00:00
|
|
|
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:
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid permission"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := h.playlistService.UpdateCollaboratorPermission(c.Request.Context(), playlistID, userID, collaboratorUserID, permission); err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
2025-12-03 19:29:37 +00:00
|
|
|
if err.Error() == "playlist not found" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err.Error() == "collaborator not found" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("collaborator"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err.Error() == "invalid permission" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid permission"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err.Error() == "forbidden: only playlist owner can update collaborator permissions" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to update collaborator permission", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": "collaborator permission updated"})
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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) {
|
2025-12-16 16:23:49 +00:00
|
|
|
// 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
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
|
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
collaborators, err := h.playlistService.GetCollaborators(c.Request.Context(), playlistID, userID)
|
|
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
2025-12-03 19:29:37 +00:00
|
|
|
if err.Error() == "playlist not found" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err.Error() == "forbidden: access denied" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get collaborators", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"collaborators": collaborators})
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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) {
|
2025-12-16 16:23:49 +00:00
|
|
|
// 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
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
|
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
|
2025-12-03 19:29:37 +00:00
|
|
|
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 {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
2025-12-03 19:29:37 +00:00
|
|
|
if err.Error() == "playlist not found" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err.Error() == "forbidden: only owner or admin can create share links" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to create share link", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-23 00:51:00 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, shareLink)
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FollowPlaylist gère le follow d'une playlist
|
|
|
|
|
// T0489: Create Playlist Follow Feature
|
|
|
|
|
func (h *PlaylistHandler) FollowPlaylist(c *gin.Context) {
|
2025-12-16 16:23:49 +00:00
|
|
|
// 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
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
|
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = h.playlistService.FollowPlaylist(c.Request.Context(), playlistID, userID)
|
|
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
2025-12-03 19:29:37 +00:00
|
|
|
if err.Error() == "playlist not found" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err.Error() == "cannot follow own playlist" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("cannot follow own playlist"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to follow playlist", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": "playlist followed"})
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UnfollowPlaylist gère l'unfollow d'une playlist
|
|
|
|
|
// T0489: Create Playlist Follow Feature
|
|
|
|
|
func (h *PlaylistHandler) UnfollowPlaylist(c *gin.Context) {
|
2025-12-16 16:23:49 +00:00
|
|
|
// 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
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
|
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = h.playlistService.UnfollowPlaylist(c.Request.Context(), playlistID, userID)
|
|
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
2025-12-03 19:29:37 +00:00
|
|
|
if err.Error() == "playlist not found" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to unfollow playlist", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": "playlist unfollowed"})
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
|
2025-12-03 19:29:37 +00:00
|
|
|
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 {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
2025-12-03 19:29:37 +00:00
|
|
|
if err.Error() == "playlist not found" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get playlist", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
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 {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Récupérer les statistiques via le service d'analytics
|
|
|
|
|
if h.playlistAnalyticsService == nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "analytics service not available"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stats, err := h.playlistAnalyticsService.GetPlaylistStats(c.Request.Context(), playlistID)
|
|
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
2025-12-03 19:29:37 +00:00
|
|
|
if err.Error() == "playlist not found" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get playlist stats", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"stats": stats})
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-16 16:23:49 +00:00
|
|
|
// 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
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var req DuplicatePlaylistRequest
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
|
|
|
RespondWithAppError(c, appErr)
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Créer le service de duplication
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
duplicateService := services.NewPlaylistDuplicateService(h.playlistService, h.db, nil)
|
2025-12-03 19:29:37 +00:00
|
|
|
|
|
|
|
|
// 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 {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
2025-12-03 19:29:37 +00:00
|
|
|
if err.Error() == "playlist not found" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err.Error() == "forbidden: you don't have access to this playlist" {
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 16:23:49 +00:00
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to duplicate playlist", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
2025-12-03 19:29:37 +00:00
|
|
|
"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 {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to search playlists", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
2025-12-03 19:29:37 +00:00
|
|
|
"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) {
|
2025-12-16 16:23:49 +00:00
|
|
|
userID, ok := GetUserIDUUID(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return // Erreur déjà envoyée par GetUserIDUUID
|
|
|
|
|
}
|
2025-12-03 19:29:37 +00:00
|
|
|
if userID == uuid.Nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.NewUnauthorizedError("unauthorized"))
|
2025-12-03 19:29:37 +00:00
|
|
|
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"
|
|
|
|
|
|
2025-12-24 13:41:33 +00:00
|
|
|
// Vérifier que le service de follow est disponible
|
|
|
|
|
if h.playlistFollowService == nil {
|
|
|
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "follow service not available"))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 19:29:37 +00:00
|
|
|
// Créer le service de recommandations
|
|
|
|
|
recommendationService := services.NewPlaylistRecommendationService(
|
2025-12-24 13:41:33 +00:00
|
|
|
h.db,
|
2025-12-03 19:29:37 +00:00
|
|
|
h.playlistService,
|
|
|
|
|
h.playlistFollowService,
|
2025-12-24 13:41:33 +00:00
|
|
|
h.commonHandler.logger,
|
2025-12-03 19:29:37 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Obtenir les recommandations
|
|
|
|
|
recommendations, err := recommendationService.GetRecommendations(
|
|
|
|
|
c.Request.Context(),
|
|
|
|
|
services.GetRecommendationsParams{
|
|
|
|
|
UserID: userID,
|
|
|
|
|
Limit: limit,
|
|
|
|
|
MinScore: minScore,
|
|
|
|
|
IncludeOwn: includeOwn,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
2025-12-16 16:23:49 +00:00
|
|
|
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
|
|
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get recommendations", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
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,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
2025-12-03 19:29:37 +00:00
|
|
|
"recommendations": response,
|
|
|
|
|
"count": len(response),
|
|
|
|
|
})
|
2025-12-06 16:21:59 +00:00
|
|
|
}
|