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.).
937 lines
No EOL
28 KiB
Go
937 lines
No EOL
28 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"veza-backend-api/internal/models"
|
|
"veza-backend-api/internal/services"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// PlaylistHandler gère les opérations sur les playlists
|
|
type PlaylistHandler struct {
|
|
playlistService *services.PlaylistService
|
|
playlistAnalyticsService *services.PlaylistAnalyticsService
|
|
playlistFollowService *services.PlaylistFollowService
|
|
db *gorm.DB
|
|
commonHandler *CommonHandler
|
|
}
|
|
|
|
// NewPlaylistHandler crée un nouveau handler de playlists
|
|
func NewPlaylistHandler(playlistService *services.PlaylistService, db *gorm.DB, logger *zap.Logger) *PlaylistHandler {
|
|
return &PlaylistHandler{
|
|
playlistService: playlistService,
|
|
db: db,
|
|
commonHandler: NewCommonHandler(logger),
|
|
}
|
|
}
|
|
|
|
// 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
|
|
type CreatePlaylistRequest struct {
|
|
Title string `json:"title" binding:"required,min=1,max=200"`
|
|
Description string `json:"description,omitempty"`
|
|
IsPublic bool `json:"is_public"`
|
|
}
|
|
|
|
// UpdatePlaylistRequest représente la requête pour mettre à jour une playlist
|
|
type UpdatePlaylistRequest struct {
|
|
Title *string `json:"title,omitempty" binding:"omitempty,min=1,max=200"`
|
|
Description *string `json:"description,omitempty"`
|
|
IsPublic *bool `json:"is_public,omitempty"`
|
|
}
|
|
|
|
// ReorderTracksRequest représente la requête pour réorganiser les tracks
|
|
type ReorderTracksRequest struct {
|
|
TrackIDs []uuid.UUID `json:"track_ids" binding:"required,min=1"` // Changed to []uuid.UUID
|
|
}
|
|
|
|
// CreatePlaylist gère la création d'une playlist
|
|
// GO-013: Utilise validator centralisé pour validation améliorée
|
|
func (h *PlaylistHandler) CreatePlaylist(c *gin.Context) {
|
|
userID := c.MustGet("user_id").(uuid.UUID)
|
|
if userID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
return
|
|
}
|
|
|
|
var req CreatePlaylistRequest
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
RespondWithAppError(c, appErr)
|
|
return
|
|
}
|
|
|
|
playlist, err := h.playlistService.CreatePlaylist(c.Request.Context(), userID, req.Title, req.Description, req.IsPublic)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{"playlist": playlist})
|
|
}
|
|
|
|
// GetPlaylists gère la récupération des playlists avec pagination
|
|
func (h *PlaylistHandler) GetPlaylists(c *gin.Context) {
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
|
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if limit < 1 {
|
|
limit = 20
|
|
}
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
|
|
// 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 {
|
|
if uid, ok := uidInterface.(uuid.UUID); ok {
|
|
currentUserID = &uid
|
|
}
|
|
}
|
|
|
|
playlists, total, err := h.playlistService.GetPlaylists(c.Request.Context(), currentUserID, filterUserID, page, limit)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"playlists": playlists,
|
|
"total": total,
|
|
"page": page,
|
|
"limit": limit,
|
|
})
|
|
}
|
|
|
|
// GetPlaylist gère la récupération d'une playlist
|
|
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 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid playlist id"})
|
|
return
|
|
}
|
|
|
|
var currentUserID *uuid.UUID
|
|
if uidInterface, exists := c.Get("user_id"); exists {
|
|
if uid, ok := uidInterface.(uuid.UUID); ok {
|
|
currentUserID = &uid
|
|
}
|
|
}
|
|
|
|
playlist, err := h.playlistService.GetPlaylist(c.Request.Context(), playlistID, currentUserID)
|
|
if err != nil {
|
|
if err.Error() == "playlist not found" {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"playlist": playlist})
|
|
}
|
|
|
|
// UpdatePlaylist gère la mise à jour d'une playlist
|
|
func (h *PlaylistHandler) UpdatePlaylist(c *gin.Context) {
|
|
userID := c.MustGet("user_id").(uuid.UUID)
|
|
if userID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
return
|
|
}
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid playlist id"})
|
|
return
|
|
}
|
|
|
|
var req UpdatePlaylistRequest
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
RespondWithAppError(c, appErr)
|
|
return
|
|
}
|
|
|
|
playlist, err := h.playlistService.UpdatePlaylist(c.Request.Context(), playlistID, userID, req.Title, req.Description, req.IsPublic)
|
|
if err != nil {
|
|
if err.Error() == "playlist not found" {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"})
|
|
return
|
|
}
|
|
if err.Error() == "forbidden" {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"playlist": playlist})
|
|
}
|
|
|
|
// DeletePlaylist gère la suppression d'une playlist
|
|
func (h *PlaylistHandler) DeletePlaylist(c *gin.Context) {
|
|
userID := c.MustGet("user_id").(uuid.UUID)
|
|
if userID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
return
|
|
}
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid playlist id"})
|
|
return
|
|
}
|
|
|
|
if err := h.playlistService.DeletePlaylist(c.Request.Context(), playlistID, userID); err != nil {
|
|
if err.Error() == "playlist not found" {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"})
|
|
return
|
|
}
|
|
if err.Error() == "forbidden" {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "playlist deleted"})
|
|
}
|
|
|
|
// AddTrack gère l'ajout d'un track à une playlist
|
|
func (h *PlaylistHandler) AddTrack(c *gin.Context) {
|
|
userID := c.MustGet("user_id").(uuid.UUID)
|
|
if userID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
return
|
|
}
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid playlist id"})
|
|
return
|
|
}
|
|
|
|
// Track IDs are uuid.UUID
|
|
trackID, err := uuid.Parse(c.Param("trackId")) // Changed to uuid.Parse
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
|
|
return
|
|
}
|
|
|
|
if err := h.playlistService.AddTrack(c.Request.Context(), playlistID, trackID, userID); err != nil {
|
|
if err.Error() == "playlist not found" {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"})
|
|
return
|
|
}
|
|
if err.Error() == "track not found" {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "track not found"})
|
|
return
|
|
}
|
|
if err.Error() == "track already in playlist" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "track already in playlist"})
|
|
return
|
|
}
|
|
if err.Error() == "forbidden" {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "track added to playlist"})
|
|
}
|
|
|
|
// RemoveTrack gère la suppression d'un track d'une playlist
|
|
func (h *PlaylistHandler) RemoveTrack(c *gin.Context) {
|
|
userID := c.MustGet("user_id").(uuid.UUID)
|
|
if userID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
return
|
|
}
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid playlist id"})
|
|
return
|
|
}
|
|
|
|
// Track IDs are uuid.UUID
|
|
trackID, err := uuid.Parse(c.Param("trackId")) // Changed to uuid.Parse
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
|
|
return
|
|
}
|
|
|
|
if err := h.playlistService.RemoveTrack(c.Request.Context(), playlistID, trackID, userID); err != nil {
|
|
if err.Error() == "playlist not found" {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"})
|
|
return
|
|
}
|
|
if err.Error() == "track not in playlist" {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "track not in playlist"})
|
|
return
|
|
}
|
|
if err.Error() == "forbidden" {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "track removed from playlist"})
|
|
}
|
|
|
|
// ReorderTracks gère la réorganisation des tracks d'une playlist
|
|
func (h *PlaylistHandler) ReorderTracks(c *gin.Context) {
|
|
userID := c.MustGet("user_id").(uuid.UUID)
|
|
if userID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
return
|
|
}
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid playlist id"})
|
|
return
|
|
}
|
|
|
|
var req ReorderTracksRequest
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
RespondWithAppError(c, appErr)
|
|
return
|
|
}
|
|
|
|
if err := h.playlistService.ReorderTracks(c.Request.Context(), playlistID, userID, req.TrackIDs); err != nil {
|
|
if err.Error() == "playlist not found" {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"})
|
|
return
|
|
}
|
|
if err.Error() == "some tracks are not in the playlist" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "some tracks are not in the playlist"})
|
|
return
|
|
}
|
|
if err.Error() == "forbidden" {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "tracks reordered"})
|
|
}
|
|
|
|
// AddCollaboratorRequest représente la requête pour ajouter un collaborateur
|
|
type AddCollaboratorRequest struct {
|
|
UserID uuid.UUID `json:"user_id" binding:"required"`
|
|
Permission string `json:"permission" binding:"required,oneof=read write admin"`
|
|
}
|
|
|
|
// UpdateCollaboratorPermissionRequest représente la requête pour mettre à jour la permission d'un collaborateur
|
|
type UpdateCollaboratorPermissionRequest struct {
|
|
Permission string `json:"permission" binding:"required,oneof=read write admin"`
|
|
}
|
|
|
|
// AddCollaborator gère l'ajout d'un collaborateur à une playlist
|
|
// T0479: POST /api/v1/playlists/:id/collaborators
|
|
func (h *PlaylistHandler) AddCollaborator(c *gin.Context) {
|
|
userID := c.MustGet("user_id").(uuid.UUID)
|
|
if userID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
return
|
|
}
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid playlist id"})
|
|
return
|
|
}
|
|
|
|
var req AddCollaboratorRequest
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
RespondWithAppError(c, appErr)
|
|
return
|
|
}
|
|
|
|
// Convertir la permission string en PlaylistPermission
|
|
var permission models.PlaylistPermission
|
|
switch req.Permission {
|
|
case "read":
|
|
permission = models.PlaylistPermissionRead
|
|
case "write":
|
|
permission = models.PlaylistPermissionWrite
|
|
case "admin":
|
|
permission = models.PlaylistPermissionAdmin
|
|
default:
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid permission"})
|
|
return
|
|
}
|
|
|
|
collaborator, err := h.playlistService.AddCollaborator(c.Request.Context(), playlistID, userID, req.UserID, permission)
|
|
if err != nil {
|
|
if err.Error() == "playlist not found" {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"})
|
|
return
|
|
}
|
|
if err.Error() == "user not found" {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
|
return
|
|
}
|
|
if err.Error() == "user is already a collaborator" {
|
|
c.JSON(http.StatusConflict, gin.H{"error": "user is already a collaborator"})
|
|
return
|
|
}
|
|
if err.Error() == "cannot add playlist owner as collaborator" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "cannot add playlist owner as collaborator"})
|
|
return
|
|
}
|
|
if err.Error() == "forbidden: only playlist owner can add collaborators" {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{"collaborator": collaborator})
|
|
}
|
|
|
|
// RemoveCollaborator gère la suppression d'un collaborateur d'une playlist
|
|
// T0479: DELETE /api/v1/playlists/:id/collaborators/:userId
|
|
func (h *PlaylistHandler) RemoveCollaborator(c *gin.Context) {
|
|
userID := c.MustGet("user_id").(uuid.UUID)
|
|
if userID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
return
|
|
}
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid playlist id"})
|
|
return
|
|
}
|
|
|
|
// User IDs are UUID
|
|
collaboratorUserID, err := uuid.Parse(c.Param("userId"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"})
|
|
return
|
|
}
|
|
|
|
if err := h.playlistService.RemoveCollaborator(c.Request.Context(), playlistID, userID, collaboratorUserID); err != nil {
|
|
if err.Error() == "playlist not found" {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"})
|
|
return
|
|
}
|
|
if err.Error() == "collaborator not found" {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "collaborator not found"})
|
|
return
|
|
}
|
|
if err.Error() == "forbidden: only playlist owner can remove collaborators" {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "collaborator removed"})
|
|
}
|
|
|
|
// UpdateCollaboratorPermission gère la mise à jour de la permission d'un collaborateur
|
|
// T0479: PUT /api/v1/playlists/:id/collaborators/:userId
|
|
func (h *PlaylistHandler) UpdateCollaboratorPermission(c *gin.Context) {
|
|
userID := c.MustGet("user_id").(uuid.UUID)
|
|
if userID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
return
|
|
}
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid playlist id"})
|
|
return
|
|
}
|
|
|
|
// User IDs are UUID
|
|
collaboratorUserID, err := uuid.Parse(c.Param("userId"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"})
|
|
return
|
|
}
|
|
|
|
var req UpdateCollaboratorPermissionRequest
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
RespondWithAppError(c, appErr)
|
|
return
|
|
}
|
|
|
|
// Convertir la permission string en PlaylistPermission
|
|
var permission models.PlaylistPermission
|
|
switch req.Permission {
|
|
case "read":
|
|
permission = models.PlaylistPermissionRead
|
|
case "write":
|
|
permission = models.PlaylistPermissionWrite
|
|
case "admin":
|
|
permission = models.PlaylistPermissionAdmin
|
|
default:
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid permission"})
|
|
return
|
|
}
|
|
|
|
if err := h.playlistService.UpdateCollaboratorPermission(c.Request.Context(), playlistID, userID, collaboratorUserID, permission); err != nil {
|
|
if err.Error() == "playlist not found" {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"})
|
|
return
|
|
}
|
|
if err.Error() == "collaborator not found" {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "collaborator not found"})
|
|
return
|
|
}
|
|
if err.Error() == "invalid permission" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid permission"})
|
|
return
|
|
}
|
|
if err.Error() == "forbidden: only playlist owner can update collaborator permissions" {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "collaborator permission updated"})
|
|
}
|
|
|
|
// GetCollaborators gère la récupération des collaborateurs d'une playlist
|
|
// T0479: GET /api/v1/playlists/:id/collaborators
|
|
func (h *PlaylistHandler) GetCollaborators(c *gin.Context) {
|
|
userID := c.MustGet("user_id").(uuid.UUID)
|
|
if userID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
return
|
|
}
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid playlist id"})
|
|
return
|
|
}
|
|
|
|
collaborators, err := h.playlistService.GetCollaborators(c.Request.Context(), playlistID, userID)
|
|
if err != nil {
|
|
if err.Error() == "playlist not found" {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"})
|
|
return
|
|
}
|
|
if err.Error() == "forbidden: access denied" {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"collaborators": collaborators})
|
|
}
|
|
|
|
// CreateShareLink gère la création d'un lien de partage public pour une playlist
|
|
// T0488: Create Playlist Public Share Link
|
|
func (h *PlaylistHandler) CreateShareLink(c *gin.Context) {
|
|
userID := c.MustGet("user_id").(uuid.UUID)
|
|
if userID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
return
|
|
}
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid playlist id"})
|
|
return
|
|
}
|
|
|
|
// Créer le lien de partage via le service
|
|
// La vérification des permissions (owner ou admin) est faite dans PlaylistService.CreateShareLink
|
|
shareLink, err := h.playlistService.CreateShareLink(c.Request.Context(), playlistID, userID, nil)
|
|
if err != nil {
|
|
if err.Error() == "playlist not found" {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"})
|
|
return
|
|
}
|
|
if err.Error() == "forbidden: only owner or admin can create share links" {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"share_link": shareLink})
|
|
}
|
|
|
|
// FollowPlaylist gère le follow d'une playlist
|
|
// T0489: Create Playlist Follow Feature
|
|
func (h *PlaylistHandler) FollowPlaylist(c *gin.Context) {
|
|
userID := c.MustGet("user_id").(uuid.UUID)
|
|
if userID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
return
|
|
}
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid playlist id"})
|
|
return
|
|
}
|
|
|
|
err = h.playlistService.FollowPlaylist(c.Request.Context(), playlistID, userID)
|
|
if err != nil {
|
|
if err.Error() == "playlist not found" {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"})
|
|
return
|
|
}
|
|
if err.Error() == "cannot follow own playlist" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "cannot follow own playlist"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "playlist followed"})
|
|
}
|
|
|
|
// UnfollowPlaylist gère l'unfollow d'une playlist
|
|
// T0489: Create Playlist Follow Feature
|
|
func (h *PlaylistHandler) UnfollowPlaylist(c *gin.Context) {
|
|
userID := c.MustGet("user_id").(uuid.UUID)
|
|
if userID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
return
|
|
}
|
|
|
|
// Playlist IDs are uuid.UUID
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid playlist id"})
|
|
return
|
|
}
|
|
|
|
err = h.playlistService.UnfollowPlaylist(c.Request.Context(), playlistID, userID)
|
|
if err != nil {
|
|
if err.Error() == "playlist not found" {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "playlist unfollowed"})
|
|
}
|
|
|
|
// GetPlaylistStats gère la récupération des statistiques d'une playlist
|
|
// T0491: Create Playlist Analytics Backend
|
|
func (h *PlaylistHandler) GetPlaylistStats(c *gin.Context) {
|
|
// Playlist IDs are uuid.UUID
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid playlist id"})
|
|
return
|
|
}
|
|
|
|
// Vérifier que la playlist existe et que l'utilisateur a accès
|
|
var userID *uuid.UUID
|
|
if uidInterface, exists := c.Get("user_id"); exists {
|
|
if uid, ok := uidInterface.(uuid.UUID); ok {
|
|
userID = &uid
|
|
}
|
|
}
|
|
|
|
playlist, err := h.playlistService.GetPlaylist(c.Request.Context(), playlistID, userID)
|
|
if err != nil {
|
|
if err.Error() == "playlist not found" {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
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 {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
|
return
|
|
}
|
|
} else {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
|
return
|
|
}
|
|
}
|
|
|
|
// Récupérer les statistiques via le service d'analytics
|
|
if h.playlistAnalyticsService == nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "analytics service not available"})
|
|
return
|
|
}
|
|
|
|
stats, err := h.playlistAnalyticsService.GetPlaylistStats(c.Request.Context(), playlistID)
|
|
if err != nil {
|
|
if err.Error() == "playlist not found" {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"stats": stats})
|
|
}
|
|
|
|
// DuplicatePlaylistRequest représente la requête pour dupliquer une playlist
|
|
type DuplicatePlaylistRequest struct {
|
|
NewTitle string `json:"new_title"`
|
|
NewDescription string `json:"new_description,omitempty"`
|
|
IsPublic *bool `json:"is_public,omitempty"`
|
|
}
|
|
|
|
// DuplicatePlaylist gère la duplication d'une playlist
|
|
// T0495: Create Playlist Duplicate Feature
|
|
func (h *PlaylistHandler) DuplicatePlaylist(c *gin.Context) {
|
|
// Playlist IDs are uuid.UUID
|
|
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid playlist id"})
|
|
return
|
|
}
|
|
|
|
userID := c.MustGet("user_id").(uuid.UUID)
|
|
if userID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
return
|
|
}
|
|
|
|
var req DuplicatePlaylistRequest
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
RespondWithAppError(c, appErr)
|
|
return
|
|
}
|
|
|
|
// Créer le service de duplication
|
|
duplicateService := services.NewPlaylistDuplicateService(h.playlistService, h.db, nil)
|
|
|
|
// Dupliquer la playlist
|
|
newPlaylist, err := duplicateService.DuplicatePlaylist(
|
|
c.Request.Context(),
|
|
playlistID,
|
|
userID,
|
|
services.DuplicatePlaylistRequest{
|
|
NewTitle: req.NewTitle,
|
|
NewDescription: req.NewDescription,
|
|
IsPublic: req.IsPublic,
|
|
},
|
|
)
|
|
if err != nil {
|
|
if err.Error() == "playlist not found" {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"})
|
|
return
|
|
}
|
|
if err.Error() == "forbidden: you don't have access to this playlist" {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "playlist duplicated successfully",
|
|
"playlist": newPlaylist,
|
|
})
|
|
}
|
|
|
|
// SearchPlaylists gère la recherche de playlists
|
|
// T0496: Create Playlist Search Backend
|
|
func (h *PlaylistHandler) SearchPlaylists(c *gin.Context) {
|
|
// Get current user ID
|
|
var currentUserID *uuid.UUID
|
|
if uidInterface, exists := c.Get("user_id"); exists {
|
|
if uid, ok := uidInterface.(uuid.UUID); ok {
|
|
currentUserID = &uid
|
|
}
|
|
}
|
|
|
|
// Récupérer les paramètres de recherche
|
|
query := c.Query("q")
|
|
userIDParam := c.Query("user_id")
|
|
isPublicParam := c.Query("is_public")
|
|
pageParam := c.DefaultQuery("page", "1")
|
|
limitParam := c.DefaultQuery("limit", "20")
|
|
|
|
// Parser les paramètres
|
|
var filterUserID *uuid.UUID
|
|
if userIDParam != "" {
|
|
if parsed, err := uuid.Parse(userIDParam); err == nil {
|
|
filterUserID = &parsed
|
|
}
|
|
}
|
|
|
|
var filterIsPublic *bool
|
|
if isPublicParam != "" {
|
|
if parsed, err := strconv.ParseBool(isPublicParam); err == nil {
|
|
filterIsPublic = &parsed
|
|
}
|
|
}
|
|
|
|
page, err := strconv.Atoi(pageParam)
|
|
if err != nil || page < 1 {
|
|
page = 1
|
|
}
|
|
|
|
limit, err := strconv.Atoi(limitParam)
|
|
if err != nil || limit < 1 {
|
|
limit = 20
|
|
}
|
|
|
|
// Rechercher les playlists
|
|
playlists, total, err := h.playlistService.SearchPlaylists(c.Request.Context(), services.SearchPlaylistsParams{
|
|
Query: query,
|
|
UserID: filterUserID,
|
|
IsPublic: filterIsPublic,
|
|
Page: page,
|
|
Limit: limit,
|
|
CurrentUserID: currentUserID,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"playlists": playlists,
|
|
"total": total,
|
|
"page": page,
|
|
"limit": limit,
|
|
})
|
|
}
|
|
|
|
// GetRecommendations gère la récupération des recommandations de playlists
|
|
// T0498: Create Playlist Recommendations
|
|
func (h *PlaylistHandler) GetRecommendations(c *gin.Context) {
|
|
userID := c.MustGet("user_id").(uuid.UUID)
|
|
if userID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
return
|
|
}
|
|
|
|
// Parser les paramètres de requête
|
|
limitParam := c.DefaultQuery("limit", "20")
|
|
limit, err := strconv.Atoi(limitParam)
|
|
if err != nil || limit < 1 {
|
|
limit = 20
|
|
}
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
|
|
minScoreParam := c.DefaultQuery("min_score", "0.1")
|
|
minScore, err := strconv.ParseFloat(minScoreParam, 64)
|
|
if err != nil || minScore < 0 {
|
|
minScore = 0.1
|
|
}
|
|
|
|
includeOwnParam := c.DefaultQuery("include_own", "false")
|
|
includeOwn := includeOwnParam == "true"
|
|
|
|
// Créer le service de recommandations
|
|
recommendationService := services.NewPlaylistRecommendationService(
|
|
nil, // Le service utilisera les services injectés via les interfaces
|
|
h.playlistService,
|
|
h.playlistFollowService,
|
|
nil, // logger
|
|
)
|
|
|
|
// Obtenir les recommandations
|
|
recommendations, err := recommendationService.GetRecommendations(
|
|
c.Request.Context(),
|
|
services.GetRecommendationsParams{
|
|
UserID: userID,
|
|
Limit: limit,
|
|
MinScore: minScore,
|
|
IncludeOwn: includeOwn,
|
|
},
|
|
)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
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,
|
|
})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"recommendations": response,
|
|
"count": len(response),
|
|
})
|
|
} |