diff --git a/VEZA_COMPLETE_MVP_TODOLIST.json b/VEZA_COMPLETE_MVP_TODOLIST.json index e2afdee49..8cf81e3f4 100644 --- a/VEZA_COMPLETE_MVP_TODOLIST.json +++ b/VEZA_COMPLETE_MVP_TODOLIST.json @@ -1908,7 +1908,19 @@ "description": "POST /api/v1/users/:id/block, DELETE /api/v1/users/:id/block", "owner": "backend", "estimated_hours": 3, - "status": "todo", + "status": "completed", + "completion": { + "completed_at": "2025-12-23T09:58:30Z", + "actual_hours": 1.0, + "commits": [], + "files_changed": [ + "veza-backend-api/internal/services/social_service.go", + "veza-backend-api/internal/handlers/profile_handler.go", + "veza-backend-api/internal/api/router.go" + ], + "notes": "Added BlockUser and UnblockUser methods to SocialService. Added BlockUser and UnblockUser handlers in ProfileHandler. Added routes: POST /users/:id/block and DELETE /users/:id/block (protected). Handlers use existing SocialService methods. Includes validation to prevent users from blocking themselves. Added IsBlocked helper method to check block status. Handlers use standard API response format (RespondSuccess, RespondWithAppError).", + "issues_encountered": [] + }, "files_involved": [], "implementation_steps": [ { diff --git a/veza-backend-api/internal/api/router.go b/veza-backend-api/internal/api/router.go index 52fe309bc..4bcfd3d3d 100644 --- a/veza-backend-api/internal/api/router.go +++ b/veza-backend-api/internal/api/router.go @@ -394,6 +394,10 @@ func (r *APIRouter) setupUserRoutes(router *gin.RouterGroup) { protected.POST("/:id/follow", profileHandler.FollowUser) // Follow user endpoint protected.DELETE("/:id/follow", profileHandler.UnfollowUser) // Unfollow user endpoint + // BE-API-018: User block/unblock endpoints + protected.POST("/:id/block", profileHandler.BlockUser) // Block user endpoint + protected.DELETE("/:id/block", profileHandler.UnblockUser) // Unblock user endpoint + // BE-API-007: User role assignment routes roleService := services.NewRoleService(r.db.GormDB) roleHandler := handlers.NewRoleHandler(roleService, r.logger) diff --git a/veza-backend-api/internal/handlers/profile_handler.go b/veza-backend-api/internal/handlers/profile_handler.go index 7f78fed44..f376021d4 100644 --- a/veza-backend-api/internal/handlers/profile_handler.go +++ b/veza-backend-api/internal/handlers/profile_handler.go @@ -299,6 +299,100 @@ func (h *ProfileHandler) UnfollowUser(c *gin.Context) { RespondSuccess(c, http.StatusOK, gin.H{"message": "User unfollowed successfully"}) } +// BlockUser gère le blocage d'un utilisateur +// POST /api/v1/users/:id/block +// BE-API-018: Implement user block/unblock endpoints +func (h *ProfileHandler) BlockUser(c *gin.Context) { + // Récupérer l'ID de l'utilisateur à bloquer depuis l'URL + userIDStr := c.Param("id") + userID, err := uuid.Parse(userIDStr) + if err != nil { + RespondWithAppError(c, apperrors.NewValidationError("invalid user id")) + return + } + + // Récupérer l'ID de l'utilisateur authentifié + blockerID, ok := GetUserIDUUID(c) + if !ok { + return // Erreur déjà envoyée par GetUserIDUUID + } + + // Vérifier qu'on ne peut pas se bloquer soi-même + if blockerID == userID { + RespondWithAppError(c, apperrors.NewValidationError("cannot block yourself")) + return + } + + // Vérifier que le service social est initialisé + if h.socialService == nil { + RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Social service not initialized", nil)) + return + } + + // Bloquer l'utilisateur + err = h.socialService.BlockUser(blockerID, userID) + if err != nil { + if err.Error() == "cannot block yourself" { + RespondWithAppError(c, apperrors.NewValidationError("cannot block yourself")) + return + } + h.logger.Error("failed to block user", + zap.Error(err), + zap.String("blocker_id", blockerID.String()), + zap.String("blocked_id", userID.String())) + RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to block user", err)) + return + } + + h.logger.Info("user blocked", + zap.String("blocker_id", blockerID.String()), + zap.String("blocked_id", userID.String())) + + RespondSuccess(c, http.StatusOK, gin.H{"message": "User blocked successfully"}) +} + +// UnblockUser gère le déblocage d'un utilisateur +// DELETE /api/v1/users/:id/block +// BE-API-018: Implement user block/unblock endpoints +func (h *ProfileHandler) UnblockUser(c *gin.Context) { + // Récupérer l'ID de l'utilisateur à débloquer depuis l'URL + userIDStr := c.Param("id") + userID, err := uuid.Parse(userIDStr) + if err != nil { + RespondWithAppError(c, apperrors.NewValidationError("invalid user id")) + return + } + + // Récupérer l'ID de l'utilisateur authentifié + blockerID, ok := GetUserIDUUID(c) + if !ok { + return // Erreur déjà envoyée par GetUserIDUUID + } + + // Vérifier que le service social est initialisé + if h.socialService == nil { + RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Social service not initialized", nil)) + return + } + + // Débloquer l'utilisateur + err = h.socialService.UnblockUser(blockerID, userID) + if err != nil { + h.logger.Error("failed to unblock user", + zap.Error(err), + zap.String("blocker_id", blockerID.String()), + zap.String("blocked_id", userID.String())) + RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to unblock user", err)) + return + } + + h.logger.Info("user unblocked", + zap.String("blocker_id", blockerID.String()), + zap.String("blocked_id", userID.String())) + + RespondSuccess(c, http.StatusOK, gin.H{"message": "User unblocked successfully"}) +} + // UpdateProfileRequest represents the request body for updating a user profile type UpdateProfileRequest struct { FirstName string `json:"first_name" binding:"omitempty,max=100" validate:"omitempty,max=100"` diff --git a/veza-backend-api/internal/services/social_service.go b/veza-backend-api/internal/services/social_service.go index dc0e98352..9859b1b60 100644 --- a/veza-backend-api/internal/services/social_service.go +++ b/veza-backend-api/internal/services/social_service.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "github.com/google/uuid" "veza-backend-api/internal/database" @@ -242,3 +243,76 @@ func (ss *SocialService) IsTrackLiked(userID, trackID uuid.UUID) (bool, error) { return exists, nil } + +// BlockUser creates a block relationship between users +// BE-API-018: Implement user block/unblock endpoints +func (ss *SocialService) BlockUser(blockerID, blockedID uuid.UUID) error { + ctx := context.Background() + + // Vérifier qu'on ne peut pas se bloquer soi-même + if blockerID == blockedID { + return fmt.Errorf("cannot block yourself") + } + + _, err := ss.db.ExecContext(ctx, ` + INSERT INTO user_blocks (blocker_id, blocked_id) + VALUES ($1, $2) + ON CONFLICT (blocker_id, blocked_id) DO NOTHING + `, blockerID, blockedID) + + if err != nil { + return fmt.Errorf("failed to block user: %w", err) + } + + ss.logger.Info("User blocked", + zap.String("blocker_id", blockerID.String()), + zap.String("blocked_id", blockedID.String()), + ) + + return nil +} + +// UnblockUser removes a block relationship between users +// BE-API-018: Implement user block/unblock endpoints +func (ss *SocialService) UnblockUser(blockerID, blockedID uuid.UUID) error { + ctx := context.Background() + + _, err := ss.db.ExecContext(ctx, ` + DELETE FROM user_blocks + WHERE blocker_id = $1 AND blocked_id = $2 + `, blockerID, blockedID) + + if err != nil { + return fmt.Errorf("failed to unblock user: %w", err) + } + + ss.logger.Info("User unblocked", + zap.String("blocker_id", blockerID.String()), + zap.String("blocked_id", blockedID.String()), + ) + + return nil +} + +// IsBlocked checks if a user is blocked by another user +// BE-API-018: Helper method to check block status +func (ss *SocialService) IsBlocked(blockerID, blockedID uuid.UUID) (bool, error) { + ctx := context.Background() + + var exists bool + err := ss.db.QueryRowContext(ctx, ` + SELECT EXISTS( + SELECT 1 FROM user_blocks + WHERE blocker_id = $1 AND blocked_id = $2 + ) + `, blockerID, blockedID).Scan(&exists) + + if err != nil { + if err == sql.ErrNoRows { + return false, nil + } + return false, fmt.Errorf("failed to check block status: %w", err) + } + + return exists, nil +}