diff --git a/VEZA_COMPLETE_MVP_TODOLIST.json b/VEZA_COMPLETE_MVP_TODOLIST.json index e239be62f..40359bb51 100644 --- a/VEZA_COMPLETE_MVP_TODOLIST.json +++ b/VEZA_COMPLETE_MVP_TODOLIST.json @@ -1864,7 +1864,18 @@ "description": "POST /api/v1/users/:id/follow, DELETE /api/v1/users/:id/follow", "owner": "backend", "estimated_hours": 3, - "status": "todo", + "status": "completed", + "completion": { + "completed_at": "2025-12-23T09:57:30Z", + "actual_hours": 1.0, + "commits": [], + "files_changed": [ + "veza-backend-api/internal/handlers/profile_handler.go", + "veza-backend-api/internal/api/router.go" + ], + "notes": "Added FollowUser and UnfollowUser handlers in ProfileHandler. Added socialService field and SetSocialService method to ProfileHandler. Initialized SocialService in setupUserRoutes and injected it into ProfileHandler. Added routes: POST /users/:id/follow and DELETE /users/:id/follow (protected). Handlers use existing SocialService.FollowUser and SocialService.UnfollowUser methods. Includes validation to prevent users from following themselves. 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 4b3cae8c1..52fe309bc 100644 --- a/veza-backend-api/internal/api/router.go +++ b/veza-backend-api/internal/api/router.go @@ -365,6 +365,9 @@ func (r *APIRouter) setupUserRoutes(router *gin.RouterGroup) { if r.config != nil && r.config.PermissionService != nil { profileHandler.SetPermissionService(r.config.PermissionService) } + // BE-API-017: Initialize SocialService for follow/unfollow functionality + socialService := services.NewSocialService(r.db, r.logger) + profileHandler.SetSocialService(socialService) users := router.Group("/users") { @@ -387,6 +390,10 @@ func (r *APIRouter) setupUserRoutes(router *gin.RouterGroup) { protected.GET("/:id/completion", profileHandler.GetProfileCompletion) + // BE-API-017: User follow/unfollow endpoints + protected.POST("/:id/follow", profileHandler.FollowUser) // Follow user endpoint + protected.DELETE("/:id/follow", profileHandler.UnfollowUser) // Unfollow 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 210810e88..7f78fed44 100644 --- a/veza-backend-api/internal/handlers/profile_handler.go +++ b/veza-backend-api/internal/handlers/profile_handler.go @@ -19,6 +19,8 @@ type ProfileHandler struct { userService *services.UserService commonHandler *CommonHandler permissionService *services.PermissionService // MOD-P1-003: Added for admin check + socialService *services.SocialService // BE-API-017: Added for follow/unfollow functionality + logger *zap.Logger // BE-API-017: Added for logging } // NewProfileHandler creates a new ProfileHandler instance @@ -26,9 +28,16 @@ func NewProfileHandler(userService *services.UserService, logger *zap.Logger) *P return &ProfileHandler{ userService: userService, commonHandler: NewCommonHandler(logger), + logger: logger, } } +// SetSocialService sets the social service for follow/unfollow functionality +// BE-API-017: Implement user follow/unfollow endpoints +func (h *ProfileHandler) SetSocialService(socialService *services.SocialService) { + h.socialService = socialService +} + // SetPermissionService définit le service de permissions (pour injection de dépendance) // MOD-P1-003: Added for admin check in ownership verification func (h *ProfileHandler) SetPermissionService(permissionService *services.PermissionService) { @@ -197,6 +206,99 @@ func (h *ProfileHandler) SearchUsers(c *gin.Context) { }) } +// FollowUser gère le suivi d'un utilisateur +// POST /api/v1/users/:id/follow +// BE-API-017: Implement user follow/unfollow endpoints +func (h *ProfileHandler) FollowUser(c *gin.Context) { + // Récupérer l'ID de l'utilisateur à suivre 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é + followerID, ok := GetUserIDUUID(c) + if !ok { + return // Erreur déjà envoyée par GetUserIDUUID + } + + // Vérifier qu'on ne peut pas se suivre soi-même + if followerID == userID { + RespondWithAppError(c, apperrors.NewValidationError("cannot follow yourself")) + return + } + + // Vérifier que l'utilisateur existe (on peut utiliser GetProfile qui vérifie l'existence) + // Pour simplifier, on laisse le service social gérer l'erreur si l'utilisateur n'existe pas + + // Vérifier que le service social est initialisé + if h.socialService == nil { + RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Social service not initialized", nil)) + return + } + + // Suivre l'utilisateur + err = h.socialService.FollowUser(followerID, userID) + if err != nil { + h.logger.Error("failed to follow user", + zap.Error(err), + zap.String("follower_id", followerID.String()), + zap.String("followed_id", userID.String())) + RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to follow user", err)) + return + } + + h.logger.Info("user followed", + zap.String("follower_id", followerID.String()), + zap.String("followed_id", userID.String())) + + RespondSuccess(c, http.StatusOK, gin.H{"message": "User followed successfully"}) +} + +// UnfollowUser gère l'arrêt du suivi d'un utilisateur +// DELETE /api/v1/users/:id/follow +// BE-API-017: Implement user follow/unfollow endpoints +func (h *ProfileHandler) UnfollowUser(c *gin.Context) { + // Récupérer l'ID de l'utilisateur à ne plus suivre 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é + followerID, 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 + } + + // Ne plus suivre l'utilisateur + err = h.socialService.UnfollowUser(followerID, userID) + if err != nil { + h.logger.Error("failed to unfollow user", + zap.Error(err), + zap.String("follower_id", followerID.String()), + zap.String("followed_id", userID.String())) + RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to unfollow user", err)) + return + } + + h.logger.Info("user unfollowed", + zap.String("follower_id", followerID.String()), + zap.String("followed_id", userID.String())) + + RespondSuccess(c, http.StatusOK, gin.H{"message": "User unfollowed 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"`