[BE-API-002] api: Implement playlist collaborators endpoints

- Added routes in router.go: POST, GET, PUT, DELETE /playlists/:id/collaborators
- Applied RequireOwnershipOrAdmin middleware to POST, PUT, DELETE routes
- GET route accessible to collaborators (service layer checks permissions)
- Fixed UpdateCollaboratorPermission handler to use RespondWithAppError
- All handlers already existed in playlist_handler.go
- All endpoints properly authenticated and ownership checks enforced

Phase: PHASE-1
Priority: P0
Progress: 5/267 (1.9%)
This commit is contained in:
senke 2025-12-23 01:41:43 +01:00
parent 8d65f85541
commit ed8949ee76
7 changed files with 31 additions and 11 deletions

View file

@ -686,7 +686,18 @@
"description": "Frontend calls POST /playlists/:id/collaborators, DELETE /playlists/:id/collaborators/:userId, PUT /playlists/:id/collaborators/:userId, GET /playlists/:id/collaborators but these endpoints don't exist.",
"owner": "backend",
"estimated_hours": 6,
"status": "todo",
"status": "completed",
"completion": {
"completed_at": "2025-12-23T00:41:32Z",
"actual_hours": 1.0,
"commits": [],
"files_changed": [
"veza-backend-api/internal/api/router.go",
"veza-backend-api/internal/handlers/playlist_handler.go"
],
"notes": "All collaborator handlers already existed in playlist_handler.go. Added routes in router.go: POST /playlists/:id/collaborators, GET /playlists/:id/collaborators, PUT /playlists/:id/collaborators/:userId, DELETE /playlists/:id/collaborators/:userId. Applied RequireOwnershipOrAdmin middleware to POST, PUT, DELETE routes. GET route accessible to collaborators (service layer checks permissions). Fixed UpdateCollaboratorPermission handler to use RespondWithAppError. All endpoints properly authenticated and ownership checks enforced.",
"issues_encountered": []
},
"files_involved": [
{
"path": "veza-backend-api/internal/api/router.go",

View file

@ -563,6 +563,14 @@ func (r *APIRouter) setupPlaylistRoutes(router *gin.RouterGroup) {
playlists.POST("/:id/tracks", playlistHandler.AddTrack)
playlists.DELETE("/:id/tracks/:track_id", playlistHandler.RemoveTrack)
playlists.PUT("/:id/tracks/reorder", playlistHandler.ReorderTracks)
// Playlist Collaborators
// BE-API-002: Add collaborator routes with ownership checks
// POST and DELETE require ownership (enforced by service layer, but middleware adds extra security)
playlists.POST("/:id/collaborators", r.config.AuthMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), playlistHandler.AddCollaborator)
playlists.GET("/:id/collaborators", playlistHandler.GetCollaborators) // GET accessible to collaborators (service checks permissions)
playlists.PUT("/:id/collaborators/:userId", r.config.AuthMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), playlistHandler.UpdateCollaboratorPermission)
playlists.DELETE("/:id/collaborators/:userId", r.config.AuthMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), playlistHandler.RemoveCollaborator)
}
}
}
@ -729,16 +737,16 @@ func (r *APIRouter) setupCoreProtectedRoutes(v1 *gin.RouterGroup) {
// Services nécessaires
sessionService := services.NewSessionService(r.db, r.logger)
// CSRF Middleware (si Redis est disponible)
var csrfMiddleware *middleware.CSRFMiddleware
if r.config.RedisClient != nil {
csrfMiddleware = middleware.NewCSRFMiddleware(r.config.RedisClient, r.logger)
csrfHandler := handlers.NewCSRFHandler(csrfMiddleware, r.logger)
// Route CSRF token (doit être accessible sans vérification CSRF)
protected.GET("/csrf-token", csrfHandler.GetCSRFToken())
// Appliquer le middleware CSRF à toutes les routes protégées (sauf /csrf-token qui est déjà définie)
protected.Use(csrfMiddleware.Middleware())
} else {

View file

@ -429,4 +429,3 @@ func TestDeleteTrack_UserCanDeleteOwnTrack(t *testing.T) {
err := db.First(&track, "id = ?", trackID).Error
assert.Error(t, err) // Should not find the track
}

View file

@ -635,7 +635,8 @@ func (h *PlaylistHandler) UpdateCollaboratorPermission(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"})
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
return
}

View file

@ -116,7 +116,7 @@ func createTestUserForProfile(t *testing.T, db *gorm.DB, userID uuid.UUID, usern
// Create user first
err := db.Create(user).Error
require.NoError(t, err)
// Then create admin role and assign it if admin
if isAdmin {
// Create admin role and assign it to user
@ -128,7 +128,7 @@ func createTestUserForProfile(t *testing.T, db *gorm.DB, userID uuid.UUID, usern
}
err = db.FirstOrCreate(adminRole, models.Role{Name: "admin"}).Error
require.NoError(t, err)
userRole := &models.UserRole{
UserID: userID,
RoleID: adminRole.ID,

View file

@ -5,8 +5,9 @@ import (
"veza-backend-api/internal/services"
"github.com/gin-gonic/gin"
apperrors "veza-backend-api/internal/errors"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
@ -248,4 +249,3 @@ func (h *TwoFactorHandler) GetTwoFactorStatus(c *gin.Context) {
RespondSuccess(c, http.StatusOK, gin.H{"enabled": enabled})
}

View file

@ -6,9 +6,10 @@ import (
"database/sql"
"encoding/base32"
"fmt"
"github.com/google/uuid"
mathrand "math/rand"
"github.com/google/uuid"
"veza-backend-api/internal/database"
"veza-backend-api/internal/models"