- Updated system_metrics.go to use RespondSuccess() helper
- Updated bitrate_handler.go success responses to use wrapped format
- Updated frontend_log_handler.go to use RespondSuccess() helper
- Updated csrf.go to use RespondSuccess() and RespondWithError() helpers
- Updated audit.go: all 30+ error and success responses now use wrapped format helpers
- Updated comment_handler.go error responses to use RespondWithError()
- Updated system_metrics_test.go to expect wrapped format {success, data}
- All handlers now consistently use wrapped format helpers
- Build and tests pass successfully
- Action 1.3.2.1 complete - backend handlers standardized to wrapped format
366 lines
13 KiB
Go
366 lines
13 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
apperrors "veza-backend-api/internal/errors"
|
|
"veza-backend-api/internal/models"
|
|
"veza-backend-api/internal/services"
|
|
"veza-backend-api/internal/utils"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// CommentServiceInterface defines the interface for comment operations
|
|
// This allows for easier testing with mocks
|
|
type CommentServiceInterface interface {
|
|
CreateComment(ctx context.Context, trackID uuid.UUID, userID uuid.UUID, content string, timestamp float64, parentID *uuid.UUID) (*models.TrackComment, error)
|
|
GetComments(ctx context.Context, trackID uuid.UUID, page, limit int) ([]models.TrackComment, int64, error)
|
|
UpdateComment(ctx context.Context, commentID uuid.UUID, userID uuid.UUID, content string) (*models.TrackComment, error)
|
|
DeleteComment(ctx context.Context, commentID uuid.UUID, userID uuid.UUID, isAdmin bool) error
|
|
GetReplies(ctx context.Context, parentID uuid.UUID, page, limit int) ([]models.TrackComment, int64, error)
|
|
}
|
|
|
|
// CommentHandler gère les opérations sur les commentaires de tracks
|
|
type CommentHandler struct {
|
|
commentService CommentServiceInterface
|
|
commonHandler *CommonHandler
|
|
}
|
|
|
|
// NewCommentHandler crée un nouveau handler de commentaires
|
|
func NewCommentHandler(commentService *services.CommentService, logger *zap.Logger) *CommentHandler {
|
|
return &CommentHandler{
|
|
commentService: commentService,
|
|
commonHandler: NewCommonHandler(logger),
|
|
}
|
|
}
|
|
|
|
// NewCommentHandlerWithInterface crée un nouveau handler avec une interface (pour les tests)
|
|
func NewCommentHandlerWithInterface(commentService CommentServiceInterface, logger *zap.Logger) *CommentHandler {
|
|
return &CommentHandler{
|
|
commentService: commentService,
|
|
commonHandler: NewCommonHandler(logger),
|
|
}
|
|
}
|
|
|
|
// CreateCommentRequest représente la requête pour créer un commentaire
|
|
// MOD-P1-001: Ajout tags validate pour validation systématique
|
|
type CreateCommentRequest struct {
|
|
Content string `json:"content" binding:"required,min=1,max=5000" validate:"required,min=1,max=5000"`
|
|
ParentID *uuid.UUID `json:"parent_id,omitempty" validate:"omitempty"` // Changed to *uuid.UUID
|
|
}
|
|
|
|
// UpdateCommentRequest représente la requête pour mettre à jour un commentaire
|
|
// MOD-P1-001: Ajout tags validate pour validation systématique
|
|
type UpdateCommentRequest struct {
|
|
Content string `json:"content" binding:"required,min=1,max=5000" validate:"required,min=1,max=5000"`
|
|
}
|
|
|
|
// CreateComment gère la création d'un commentaire sur un track
|
|
// @Summary Create comment
|
|
// @Description Create a new comment on a track. Can be a top-level comment or a reply to another comment (using parent_id).
|
|
// @Tags Comment
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param id path string true "Track ID (UUID)"
|
|
// @Param comment body handlers.CreateCommentRequest true "Comment data"
|
|
// @Success 201 {object} handlers.APIResponse{data=object{comment=object}}
|
|
// @Failure 400 {object} handlers.APIResponse "Validation error"
|
|
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
|
|
// @Failure 404 {object} handlers.APIResponse "Track not found"
|
|
// @Failure 500 {object} handlers.APIResponse "Internal server error"
|
|
// @Router /tracks/{id}/comments [post]
|
|
func (h *CommentHandler) CreateComment(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return // Erreur déjà envoyée par GetUserIDUUID
|
|
}
|
|
if userID == uuid.Nil {
|
|
// Action 1.3.2.1: Use wrapped format helper for errors
|
|
RespondWithError(c, int(apperrors.ErrCodeUnauthorized), "unauthorized")
|
|
return
|
|
}
|
|
|
|
trackIDStr := c.Param("id")
|
|
if trackIDStr == "" {
|
|
// Action 1.3.2.1: Use wrapped format helper for errors
|
|
RespondWithError(c, int(apperrors.ErrCodeValidation), "track id is required")
|
|
return
|
|
}
|
|
|
|
trackID, err := uuid.Parse(trackIDStr) // Changed to uuid.Parse
|
|
if err != nil {
|
|
// Action 1.3.2.1: Use wrapped format helper for errors
|
|
RespondWithError(c, int(apperrors.ErrCodeValidation), "invalid track id")
|
|
return
|
|
}
|
|
|
|
var req CreateCommentRequest
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
RespondWithAppError(c, appErr)
|
|
return
|
|
}
|
|
|
|
// BE-SEC-009: Sanitize user inputs to prevent XSS and injection attacks
|
|
req.Content = utils.SanitizeText(req.Content, 5000)
|
|
|
|
comment, err := h.commentService.CreateComment(c.Request.Context(), trackID, userID, req.Content, 0.0, req.ParentID) // req.ParentID is already *uuid.UUID
|
|
if err != nil {
|
|
if errors.Is(err, services.ErrTrackNotFound) {
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("track"))
|
|
return
|
|
}
|
|
if errors.Is(err, services.ErrParentCommentNotFound) {
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("parent comment"))
|
|
return
|
|
}
|
|
if errors.Is(err, services.ErrParentTrackMismatch) {
|
|
RespondWithAppError(c, apperrors.NewValidationError("parent comment does not belong to the same track"))
|
|
return
|
|
}
|
|
h.commonHandler.logger.Error("failed to create comment", zap.Error(err))
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to create comment", err))
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusCreated, comment)
|
|
}
|
|
|
|
// GetComments gère la récupération des commentaires d'un track
|
|
// @Summary Get track comments
|
|
// @Description Get paginated list of comments for a track
|
|
// @Tags Comment
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "Track ID"
|
|
// @Param page query int false "Page number" default(1)
|
|
// @Param limit query int false "Items per page" default(20)
|
|
// @Success 200 {object} handlers.APIResponse{data=object{comments=array,pagination=object}}
|
|
// @Failure 400 {object} handlers.APIResponse "Validation error"
|
|
// @Failure 404 {object} handlers.APIResponse "Track not found"
|
|
// @Failure 500 {object} handlers.APIResponse "Internal server error"
|
|
// @Router /tracks/{id}/comments [get]
|
|
func (h *CommentHandler) GetComments(c *gin.Context) {
|
|
trackIDStr := c.Param("id")
|
|
if trackIDStr == "" {
|
|
RespondWithAppError(c, apperrors.NewValidationError("track id is required"))
|
|
return
|
|
}
|
|
|
|
trackID, err := uuid.Parse(trackIDStr) // Changed to uuid.Parse
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid track id"))
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
comments, total, err := h.commentService.GetComments(c.Request.Context(), trackID, page, limit)
|
|
if err != nil {
|
|
h.commonHandler.logger.Error("failed to get comments", zap.Error(err))
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get comments", err))
|
|
return
|
|
}
|
|
|
|
// INT-007: Standardize pagination format
|
|
pagination := BuildPaginationData(page, limit, total)
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
|
"comments": comments,
|
|
"pagination": pagination,
|
|
})
|
|
}
|
|
|
|
// UpdateComment gère la mise à jour d'un commentaire
|
|
// @Summary Update comment
|
|
// @Description Update a comment (only by owner)
|
|
// @Tags Comment
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param id path string true "Comment ID (UUID)"
|
|
// @Param comment body handlers.UpdateCommentRequest true "Updated comment content"
|
|
// @Success 200 {object} handlers.APIResponse{data=object{comment=object}}
|
|
// @Failure 400 {object} handlers.APIResponse "Validation error"
|
|
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
|
|
// @Failure 403 {object} handlers.APIResponse "Forbidden - can only edit own comments"
|
|
// @Failure 404 {object} handlers.APIResponse "Comment not found"
|
|
// @Failure 500 {object} handlers.APIResponse "Internal server error"
|
|
// @Router /comments/{id} [put]
|
|
func (h *CommentHandler) UpdateComment(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return // Erreur déjà envoyée par GetUserIDUUID
|
|
}
|
|
if userID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
return
|
|
}
|
|
|
|
commentIDStr := c.Param("id")
|
|
if commentIDStr == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "comment id is required"})
|
|
return
|
|
}
|
|
|
|
commentID, err := uuid.Parse(commentIDStr) // Changed to uuid.Parse
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid comment id"})
|
|
return
|
|
}
|
|
|
|
var req UpdateCommentRequest
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
RespondWithAppError(c, appErr)
|
|
return
|
|
}
|
|
|
|
// BE-SEC-009: Sanitize user inputs to prevent XSS and injection attacks
|
|
req.Content = utils.SanitizeText(req.Content, 5000)
|
|
|
|
comment, err := h.commentService.UpdateComment(c.Request.Context(), commentID, userID, req.Content)
|
|
if err != nil {
|
|
if errors.Is(err, services.ErrCommentNotFound) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "comment not found"})
|
|
return
|
|
}
|
|
if errors.Is(err, services.ErrForbidden) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "unauthorized: you can only edit your own comments"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"comment": comment})
|
|
}
|
|
|
|
// DeleteComment gère la suppression d'un commentaire
|
|
// @Summary Delete comment
|
|
// @Description Delete a comment (only by owner or admin)
|
|
// @Tags Comment
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param id path string true "Track ID"
|
|
// @Param comment_id path string true "Comment ID"
|
|
// @Success 200 {object} handlers.APIResponse{data=object{message=string}}
|
|
// @Failure 400 {object} handlers.APIResponse "Validation error"
|
|
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
|
|
// @Failure 403 {object} handlers.APIResponse "Forbidden - not comment owner"
|
|
// @Failure 404 {object} handlers.APIResponse "Comment not found"
|
|
// @Router /tracks/{id}/comments/{comment_id} [delete]
|
|
func (h *CommentHandler) DeleteComment(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return // Erreur déjà envoyée par GetUserIDUUID
|
|
}
|
|
if userID == uuid.Nil {
|
|
RespondWithAppError(c, apperrors.NewUnauthorizedError("unauthorized"))
|
|
return
|
|
}
|
|
|
|
commentIDStr := c.Param("id")
|
|
if commentIDStr == "" {
|
|
RespondWithAppError(c, apperrors.NewValidationError("comment id is required"))
|
|
return
|
|
}
|
|
|
|
commentID, err := uuid.Parse(commentIDStr) // Changed to uuid.Parse
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid comment id"))
|
|
return
|
|
}
|
|
|
|
err = h.commentService.DeleteComment(c.Request.Context(), commentID, userID, false) // Added false for isAdmin
|
|
if err != nil {
|
|
if errors.Is(err, services.ErrCommentNotFound) {
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("comment"))
|
|
return
|
|
}
|
|
if errors.Is(err, services.ErrForbidden) {
|
|
RespondWithAppError(c, apperrors.NewForbiddenError("you can only delete your own comments"))
|
|
return
|
|
}
|
|
h.commonHandler.logger.Error("failed to delete comment", zap.Error(err))
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to delete comment", err))
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": "Comment deleted successfully"})
|
|
}
|
|
|
|
// GetReplies gère la récupération des réponses d'un commentaire
|
|
// @Summary Get comment replies
|
|
// @Description Get paginated list of replies to a comment
|
|
// @Tags Comment
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "Parent Comment ID (UUID)"
|
|
// @Param page query int false "Page number" default(1) minimum(1)
|
|
// @Param limit query int false "Items per page" default(20) minimum(1) maximum(100)
|
|
// @Success 200 {object} handlers.APIResponse{data=object{replies=array,pagination=object}}
|
|
// @Failure 400 {object} handlers.APIResponse "Validation error"
|
|
// @Failure 404 {object} handlers.APIResponse "Parent comment not found"
|
|
// @Failure 500 {object} handlers.APIResponse "Internal server error"
|
|
// @Router /comments/{id}/replies [get]
|
|
func (h *CommentHandler) GetReplies(c *gin.Context) {
|
|
parentIDStr := c.Param("id")
|
|
if parentIDStr == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "parent comment id is required"})
|
|
return
|
|
}
|
|
|
|
parentID, err := uuid.Parse(parentIDStr) // Changed to uuid.Parse
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid parent comment id"})
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
replies, total, err := h.commentService.GetReplies(c.Request.Context(), parentID, page, limit)
|
|
if err != nil {
|
|
if errors.Is(err, services.ErrParentCommentNotFound) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "parent comment not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"replies": replies,
|
|
"total": total,
|
|
"page": page,
|
|
"limit": limit,
|
|
})
|
|
}
|