[BE-API-021] be-api: Implement avatar upload endpoint
- Standardized UploadAvatar handler to use RespondSuccess/RespondWithAppError - Replaced common.GetUserIDFromContext with GetUserIDUUID - Handler accepts both :userId and :id parameters - Added route: POST /users/:userId/avatar - Handler validates user authentication and ownership - Handler uses existing ImageService methods - Handler updates avatar URL in database Phase: PHASE-2 Priority: P1 Progress: 30/267 (11.2%)
This commit is contained in:
parent
a205ad44af
commit
3afc57dfbc
3 changed files with 69 additions and 34 deletions
|
|
@ -2042,7 +2042,18 @@
|
|||
"description": "POST /api/v1/users/:userId/avatar for avatar image upload",
|
||||
"owner": "backend",
|
||||
"estimated_hours": 3,
|
||||
"status": "todo",
|
||||
"status": "completed",
|
||||
"completion": {
|
||||
"completed_at": "2025-12-23T10:01:30Z",
|
||||
"actual_hours": 1.0,
|
||||
"commits": [],
|
||||
"files_changed": [
|
||||
"veza-backend-api/internal/handlers/avatar_handler.go",
|
||||
"veza-backend-api/internal/api/router.go"
|
||||
],
|
||||
"notes": "Standardized UploadAvatar handler to use RespondSuccess and RespondWithAppError. Replaced common.GetUserIDFromContext with GetUserIDUUID. Handler accepts both :userId and :id parameters. Added route: POST /users/:userId/avatar (protected). Handler validates user authentication and ownership. Handler uses existing ImageService.ProcessAvatar and ImageService.UploadToS3 methods. Handler updates avatar URL in database via UserService.UpdateAvatarURL. Handler uses standard API response format.",
|
||||
"issues_encountered": []
|
||||
},
|
||||
"files_involved": [],
|
||||
"implementation_steps": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -403,6 +403,15 @@ func (r *APIRouter) setupUserRoutes(router *gin.RouterGroup) {
|
|||
roleHandler := handlers.NewRoleHandler(roleService, r.logger)
|
||||
protected.POST("/:userId/roles", roleHandler.AssignRole) // POST /api/v1/users/:userId/roles
|
||||
protected.DELETE("/:userId/roles/:roleId", roleHandler.RevokeRole) // DELETE /api/v1/users/:userId/roles/:roleId
|
||||
|
||||
// BE-API-021: Avatar upload endpoint
|
||||
avatarUploadDir := r.config.UploadDir
|
||||
if avatarUploadDir == "" {
|
||||
avatarUploadDir = "uploads/avatars"
|
||||
}
|
||||
imageService := services.NewImageService(avatarUploadDir)
|
||||
avatarHandler := handlers.NewAvatarHandler(imageService, userService)
|
||||
protected.POST("/:userId/avatar", avatarHandler.UploadAvatar) // BE-API-021: Upload avatar endpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"net/http"
|
||||
"veza-backend-api/internal/common"
|
||||
|
||||
apperrors "veza-backend-api/internal/errors"
|
||||
"veza-backend-api/internal/services"
|
||||
)
|
||||
|
||||
|
|
@ -23,53 +25,60 @@ func NewAvatarHandler(imageService *services.ImageService, userService *services
|
|||
}
|
||||
|
||||
// UploadAvatar handles avatar upload
|
||||
// POST /api/v1/users/:userId/avatar
|
||||
// BE-API-021: Implement avatar upload endpoint
|
||||
// T0221: Validates user_id, file format/size, processes image, uploads to S3, and updates DB
|
||||
func (h *AvatarHandler) UploadAvatar(c *gin.Context) {
|
||||
userIDStr := c.Param("id")
|
||||
// Récupérer l'ID utilisateur depuis l'URL (peut être "id" ou "userId")
|
||||
userIDStr := c.Param("userId")
|
||||
if userIDStr == "" {
|
||||
userIDStr = c.Param("id")
|
||||
}
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"})
|
||||
RespondWithAppError(c, apperrors.NewValidationError("invalid user id"))
|
||||
return
|
||||
}
|
||||
|
||||
// Check that user_id corresponds to authenticated user
|
||||
authenticatedUserID, exists := common.GetUserIDFromContext(c)
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
|
||||
return
|
||||
// Vérifier que l'utilisateur est authentifié
|
||||
authenticatedUserID, ok := GetUserIDUUID(c)
|
||||
if !ok {
|
||||
return // Erreur déjà envoyée par GetUserIDUUID
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur ne peut modifier que son propre avatar
|
||||
if userID != authenticatedUserID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "cannot update other user's avatar"})
|
||||
RespondWithAppError(c, apperrors.NewForbiddenError("cannot update other user's avatar"))
|
||||
return
|
||||
}
|
||||
|
||||
// Récupérer le fichier
|
||||
fileHeader, err := c.FormFile("avatar")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no file provided"})
|
||||
RespondWithAppError(c, apperrors.NewValidationError("no file provided"))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate and process image
|
||||
// Valider et traiter l'image
|
||||
resizedImage, err := h.imageService.ProcessAvatar(fileHeader)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
RespondWithAppError(c, apperrors.NewValidationError(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Generate S3 key
|
||||
// Générer la clé S3
|
||||
s3Key := h.imageService.GenerateS3Key(userID)
|
||||
|
||||
// Upload to S3 (or local storage for now)
|
||||
// Upload vers S3 (ou stockage local pour l'instant)
|
||||
avatarURL, err := h.imageService.UploadToS3(resizedImage, s3Key)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to upload avatar"})
|
||||
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to upload avatar", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Update avatar_url in DB
|
||||
// Mettre à jour l'URL de l'avatar dans la DB
|
||||
if err := h.userService.UpdateAvatarURL(userID, avatarURL); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update avatar"})
|
||||
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to update avatar", err))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -77,46 +86,52 @@ func (h *AvatarHandler) UploadAvatar(c *gin.Context) {
|
|||
}
|
||||
|
||||
// DeleteAvatar handles avatar deletion
|
||||
// DELETE /api/v1/users/:userId/avatar
|
||||
// BE-API-022: Implement avatar delete endpoint (will be implemented in next task)
|
||||
// T0222: Validates user_id, deletes file from S3, and sets avatar_url to NULL in DB
|
||||
func (h *AvatarHandler) DeleteAvatar(c *gin.Context) {
|
||||
userIDStr := c.Param("id")
|
||||
// Récupérer l'ID utilisateur depuis l'URL (peut être "id" ou "userId")
|
||||
userIDStr := c.Param("userId")
|
||||
if userIDStr == "" {
|
||||
userIDStr = c.Param("id")
|
||||
}
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"})
|
||||
RespondWithAppError(c, apperrors.NewValidationError("invalid user id"))
|
||||
return
|
||||
}
|
||||
|
||||
// Check that user_id corresponds to authenticated user
|
||||
authenticatedUserID, exists := common.GetUserIDFromContext(c)
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
|
||||
return
|
||||
// Vérifier que l'utilisateur est authentifié
|
||||
authenticatedUserID, ok := GetUserIDUUID(c)
|
||||
if !ok {
|
||||
return // Erreur déjà envoyée par GetUserIDUUID
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur ne peut supprimer que son propre avatar
|
||||
if userID != authenticatedUserID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "cannot delete other user's avatar"})
|
||||
RespondWithAppError(c, apperrors.NewForbiddenError("cannot delete other user's avatar"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get current avatar_url from DB
|
||||
// Récupérer l'utilisateur actuel pour obtenir l'URL de l'avatar
|
||||
user, err := h.userService.GetByID(userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
||||
RespondWithAppError(c, apperrors.NewNotFoundError("user"))
|
||||
return
|
||||
}
|
||||
|
||||
// Delete file from S3 (or local storage) if exists
|
||||
// Supprimer le fichier de S3 (ou stockage local) s'il existe
|
||||
if user.Avatar != "" {
|
||||
if err := h.imageService.DeleteFromS3(user.Avatar); err != nil {
|
||||
// Log error but continue (file may already be deleted)
|
||||
// In production, you might want to use a logger here
|
||||
// Logger l'erreur mais continuer (le fichier peut déjà être supprimé)
|
||||
// En production, vous pourriez vouloir utiliser un logger ici
|
||||
_ = err
|
||||
}
|
||||
}
|
||||
|
||||
// Set avatar_url to empty string (NULL in DB)
|
||||
// Mettre l'URL de l'avatar à une chaîne vide (NULL dans la DB)
|
||||
if err := h.userService.UpdateAvatarURL(userID, ""); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete avatar"})
|
||||
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to delete avatar", err))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue