[BE-API-017] be-api: Implement user follow/unfollow endpoints

- Added FollowUser and UnfollowUser handlers in ProfileHandler
- Added socialService field and SetSocialService method
- Initialized SocialService in setupUserRoutes
- Added routes: POST /users/:id/follow and DELETE /users/:id/follow
- Handlers use existing SocialService methods
- Includes validation to prevent users from following themselves
- Handlers use standard API response format

Phase: PHASE-2
Priority: P2
Progress: 26/267 (9.7%)
This commit is contained in:
senke 2025-12-24 11:26:32 +01:00
parent a20266299c
commit 9c49bce93e
3 changed files with 121 additions and 1 deletions

View file

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

View file

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

View file

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