diff --git a/VEZA_COMPLETE_MVP_TODOLIST.json b/VEZA_COMPLETE_MVP_TODOLIST.json index ae7dfccbe..884c6c86d 100644 --- a/VEZA_COMPLETE_MVP_TODOLIST.json +++ b/VEZA_COMPLETE_MVP_TODOLIST.json @@ -1691,7 +1691,18 @@ "description": "GET /api/v1/tracks/:id/comments, POST /api/v1/tracks/:id/comments, DELETE /api/v1/comments/:id", "owner": "backend", "estimated_hours": 5, - "status": "todo", + "status": "completed", + "completion": { + "completed_at": "2025-12-23T09:52:30Z", + "actual_hours": 1.0, + "commits": [], + "files_changed": [ + "veza-backend-api/internal/handlers/comment_handler.go", + "veza-backend-api/internal/api/router.go" + ], + "notes": "Added comment routes: GET /tracks/:id/comments (public), POST /tracks/:id/comments (protected), DELETE /comments/:id (protected). Initialized CommentService and CommentHandler in setupTrackRoutes. Standardized API responses in comment handlers to use RespondSuccess and RespondWithAppError. Handlers already existed, only routes and response standardization were needed.", + "issues_encountered": [] + }, "files_involved": [], "implementation_steps": [ { diff --git a/apps/web/e2e/global-setup.ts b/apps/web/e2e/global-setup.ts index 16a46b48f..d7c4f7dd6 100644 --- a/apps/web/e2e/global-setup.ts +++ b/apps/web/e2e/global-setup.ts @@ -109,3 +109,4 @@ export default globalSetup; + diff --git a/veza-backend-api/internal/api/router.go b/veza-backend-api/internal/api/router.go index eda8997f0..d8bd67ffc 100644 --- a/veza-backend-api/internal/api/router.go +++ b/veza-backend-api/internal/api/router.go @@ -525,6 +525,37 @@ func (r *APIRouter) setupTrackRoutes(router *gin.RouterGroup) { } } + // BE-API-013: Setup comment routes + commentService := services.NewCommentService(r.db.GormDB, r.logger) + commentHandler := handlers.NewCommentHandler(commentService, r.logger) + + // Comments routes - public GET, protected POST/DELETE + comments := router.Group("/tracks") + { + // Public: Get comments for a track + comments.GET("/:id/comments", commentHandler.GetComments) // BE-API-013: GET /api/v1/tracks/:id/comments + + // Protected: Create and delete comments + if r.config.AuthMiddleware != nil { + protected := comments.Group("") + protected.Use(r.config.AuthMiddleware.RequireAuth()) + { + protected.POST("/:id/comments", commentHandler.CreateComment) // BE-API-013: POST /api/v1/tracks/:id/comments + } + } + } + + // Comments routes - protected DELETE + commentsProtected := router.Group("/comments") + { + if r.config.AuthMiddleware != nil { + commentsProtected.Use(r.config.AuthMiddleware.RequireAuth()) + { + commentsProtected.DELETE("/:id", commentHandler.DeleteComment) // BE-API-013: DELETE /api/v1/comments/:id + } + } + } + // Note: Internal routes are now set up in setupInternalRoutes() to avoid // path prefix issues when setupTrackRoutes is called with a RouterGroup diff --git a/veza-backend-api/internal/handlers/comment_handler.go b/veza-backend-api/internal/handlers/comment_handler.go index 0c43d958a..94f15f350 100644 --- a/veza-backend-api/internal/handlers/comment_handler.go +++ b/veza-backend-api/internal/handlers/comment_handler.go @@ -5,6 +5,7 @@ import ( "net/http" "strconv" + apperrors "veza-backend-api/internal/errors" "veza-backend-api/internal/services" "github.com/gin-gonic/gin" @@ -71,35 +72,36 @@ func (h *CommentHandler) CreateComment(c *gin.Context) { 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) { - c.JSON(http.StatusNotFound, gin.H{"error": "track not found"}) + RespondWithAppError(c, apperrors.NewNotFoundError("track")) return } if errors.Is(err, services.ErrParentCommentNotFound) { - c.JSON(http.StatusNotFound, gin.H{"error": "parent comment not found"}) + RespondWithAppError(c, apperrors.NewNotFoundError("parent comment")) return } if errors.Is(err, services.ErrParentTrackMismatch) { - c.JSON(http.StatusBadRequest, gin.H{"error": "parent comment does not belong to the same track"}) + RespondWithAppError(c, apperrors.NewValidationError("parent comment does not belong to the same track")) return } - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + h.commonHandler.logger.Error("failed to create comment", zap.Error(err)) + RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to create comment", err)) return } - c.JSON(http.StatusCreated, gin.H{"comment": comment}) + RespondSuccess(c, http.StatusCreated, comment) } // GetComments gère la récupération des commentaires d'un track func (h *CommentHandler) GetComments(c *gin.Context) { trackIDStr := c.Param("id") if trackIDStr == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "track id is required"}) + RespondWithAppError(c, apperrors.NewValidationError("track id is required")) return } trackID, err := uuid.Parse(trackIDStr) // Changed to uuid.Parse if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"}) + RespondWithAppError(c, apperrors.NewValidationError("invalid track id")) return } @@ -118,15 +120,19 @@ func (h *CommentHandler) GetComments(c *gin.Context) { comments, total, err := h.commentService.GetComments(c.Request.Context(), trackID, page, limit) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + h.commonHandler.logger.Error("failed to get comments", zap.Error(err)) + RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get comments", err)) return } - c.JSON(http.StatusOK, gin.H{ + RespondSuccess(c, http.StatusOK, gin.H{ "comments": comments, - "total": total, - "page": page, - "limit": limit, + "pagination": gin.H{ + "total": total, + "page": page, + "limit": limit, + "total_pages": (int(total) + limit - 1) / limit, + }, }) } @@ -183,37 +189,38 @@ func (h *CommentHandler) DeleteComment(c *gin.Context) { return // Erreur déjà envoyée par GetUserIDUUID } if userID == uuid.Nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + RespondWithAppError(c, apperrors.NewUnauthorizedError("unauthorized")) return } commentIDStr := c.Param("id") if commentIDStr == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "comment id is required"}) + RespondWithAppError(c, apperrors.NewValidationError("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"}) + 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) { - c.JSON(http.StatusNotFound, gin.H{"error": "comment not found"}) + RespondWithAppError(c, apperrors.NewNotFoundError("comment")) return } if errors.Is(err, services.ErrForbidden) { - c.JSON(http.StatusForbidden, gin.H{"error": "unauthorized: you can only delete your own comments"}) + RespondWithAppError(c, apperrors.NewForbiddenError("you can only delete your own comments")) return } - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + h.commonHandler.logger.Error("failed to delete comment", zap.Error(err)) + RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to delete comment", err)) return } - c.JSON(http.StatusOK, gin.H{"message": "comment deleted successfully"}) + RespondSuccess(c, http.StatusOK, gin.H{"message": "Comment deleted successfully"}) } // GetReplies gère la récupération des réponses d'un commentaire