[BE-API-018] be-api: Implement user block/unblock endpoints

- 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
- 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

Phase: PHASE-2
Priority: P2
Progress: 27/267 (10.1%)
This commit is contained in:
senke 2025-12-24 11:28:49 +01:00
parent d869aa987a
commit 78862c6ee1
4 changed files with 185 additions and 1 deletions

View file

@ -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": [
{

View file

@ -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)

View file

@ -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"`

View file

@ -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
}