package handlers import ( "net/http" "strconv" "time" apperrors "veza-backend-api/internal/errors" "veza-backend-api/internal/services" "veza-backend-api/internal/types" "veza-backend-api/internal/utils" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" ) // ProfileHandler handles profile-related operations 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 notificationService *services.NotificationService // Phase 2.2: Optional, for follow notifications logger *zap.Logger // BE-API-017: Added for logging } // NewProfileHandler creates a new ProfileHandler instance func NewProfileHandler(userService *services.UserService, logger *zap.Logger) *ProfileHandler { 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 } // SetNotificationService sets the notification service for follow notifications (Phase 2.2) func (h *ProfileHandler) SetNotificationService(notificationService *services.NotificationService) { h.notificationService = notificationService } // 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) { h.permissionService = permissionService } // GetProfile retrieves a public user profile by ID // @Summary Get Profile by ID // @Description Get public profile information for a user // @Tags User // @Accept json // @Produce json // @Param id path string true "User ID" // @Success 200 {object} handlers.APIResponse{data=object{profile=object}} // @Failure 400 {object} handlers.APIResponse "Invalid ID" // @Failure 404 {object} handlers.APIResponse "User not found" // @Router /users/{id} [get] func (h *ProfileHandler) GetProfile(c *gin.Context) { userIDStr := c.Param("id") userID, err := uuid.Parse(userIDStr) if err != nil { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "invalid user id")) return } // Get the requesting user ID if authenticated (optional) var requesterID *uuid.UUID if reqID, exists := c.Get("user_id"); exists { if reqUUID, ok := reqID.(uuid.UUID); ok { requesterID = &reqUUID } } // Get user profile with privacy check profile, err := h.userService.GetProfile(c.Request.Context(), userID, requesterID) if err != nil { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeNotFound, "user not found")) return } RespondSuccess(c, http.StatusOK, profile) } // GetProfileByUsername retrieves a public profile by username // @Summary Get Profile by Username // @Description Get public profile information for a user by username // @Tags User // @Accept json // @Produce json // @Param username path string true "Username" // @Success 200 {object} handlers.APIResponse{data=object{profile=object}} // @Failure 400 {object} handlers.APIResponse "Missing username" // @Failure 404 {object} handlers.APIResponse "User not found" // @Router /users/by-username/{username} [get] func (h *ProfileHandler) GetProfileByUsername(c *gin.Context) { username := c.Param("username") // BE-SEC-009: Sanitize username parameter to prevent injection username = utils.SanitizeUsername(username) if username == "" { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "username required")) return } // Get the requesting user ID if authenticated (optional) var requesterID *uuid.UUID if reqID, exists := c.Get("user_id"); exists { if reqUUID, ok := reqID.(uuid.UUID); ok { requesterID = &reqUUID } } // Get profile with privacy check profile, err := h.userService.GetProfileByUsername(c.Request.Context(), username, requesterID) if err != nil { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeNotFound, "user not found")) return } RespondSuccess(c, http.StatusOK, profile) } // GetProfileCompletion retrieves the profile completion status // GET /api/v1/users/:id/completion // BE-API-023: Implement user completion endpoint validation // T0220: Returns percentage and missing fields // @Summary Get Profile Completion // @Description Get profile completion percentage and missing fields // @Tags User // @Accept json // @Produce json // @Param id path string true "User ID" // @Success 200 {object} handlers.APIResponse{data=object} // @Failure 400 {object} handlers.APIResponse "Invalid ID" // @Failure 401 {object} handlers.APIResponse "Unauthorized" // @Failure 403 {object} handlers.APIResponse "Forbidden" // @Router /users/{id}/completion [get] func (h *ProfileHandler) GetProfileCompletion(c *gin.Context) { userIDStr := c.Param("id") userID, err := uuid.Parse(userIDStr) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid user id")) return } // Get authenticated user ID authenticatedUserID, ok := GetUserIDUUID(c) if !ok { return // Erreur déjà envoyée par GetUserIDUUID } // Verify that user_id corresponds to authenticated user if userID != authenticatedUserID { RespondWithAppError(c, apperrors.NewForbiddenError("cannot access other user's profile completion")) return } // Calculate profile completion completion, err := h.userService.CalculateProfileCompletion(c.Request.Context(), userID) if err != nil { if err.Error() == "user not found" { RespondWithAppError(c, apperrors.NewNotFoundError("user")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to calculate profile completion", err)) return } // Verify that percentage is between 0 and 100 if completion.Percentage < 0 || completion.Percentage > 100 { h.logger.Warn("Invalid completion percentage calculated", zap.Int("percentage", completion.Percentage), zap.String("user_id", userID.String())) // Clamp to valid range if completion.Percentage < 0 { completion.Percentage = 0 } else if completion.Percentage > 100 { completion.Percentage = 100 } } RespondSuccess(c, http.StatusOK, completion) } // ListUsers gère la liste des utilisateurs avec pagination et filtrage // BE-API-040: Implement user list endpoint // @Summary List Users // @Description Get a paginated list of users with optional filtering // @Tags User // @Accept json // @Produce json // @Param page query int false "Page number" default(1) // @Param limit query int false "Items per page" default(20) // @Param role query string false "Filter by role" // @Param is_active query bool false "Filter by active status" // @Param is_verified query bool false "Filter by verified status" // @Param search query string false "Search by username, email, first_name, last_name" // @Param sort_by query string false "Sort field (created_at, username, email, last_login_at)" default(created_at) // @Param sort_order query string false "Sort order (asc, desc)" default(desc) // @Success 200 {object} handlers.APIResponse{data=object{users=array,pagination=object}} // @Failure 500 {object} handlers.APIResponse "Internal Error" // @Router /users [get] func (h *ProfileHandler) ListUsers(c *gin.Context) { // Récupérer les paramètres de pagination pageParam := c.DefaultQuery("page", "1") limitParam := c.DefaultQuery("limit", "20") // Parser les paramètres de pagination — return 400 if out of bounds (no silent normalization) page, err := strconv.Atoi(pageParam) if err != nil || page < 1 { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "pagination: page must be >= 1 and limit must be between 1 and 100")) return } limit, err := strconv.Atoi(limitParam) if err != nil || limit < 1 || limit > 100 { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "pagination: page must be >= 1 and limit must be between 1 and 100")) return } // Récupérer les paramètres de filtrage // BE-SEC-009: Sanitize search query to prevent injection searchQuery := utils.SanitizeText(c.Query("search"), 100) params := services.ListUsersParams{ Page: page, Limit: limit, Role: utils.SanitizeText(c.Query("role"), 50), Search: searchQuery, SortBy: utils.SanitizeText(c.DefaultQuery("sort_by", "created_at"), 50), SortOrder: utils.SanitizeText(c.DefaultQuery("sort_order", "desc"), 10), } // Parser is_active si fourni if isActiveStr := c.Query("is_active"); isActiveStr != "" { if isActive, err := strconv.ParseBool(isActiveStr); err == nil { params.IsActive = &isActive } } // Parser is_verified si fourni if isVerifiedStr := c.Query("is_verified"); isVerifiedStr != "" { if isVerified, err := strconv.ParseBool(isVerifiedStr); err == nil { params.IsVerified = &isVerified } } // Lister les utilisateurs users, total, err := h.userService.ListUsers(c.Request.Context(), params) if err != nil { RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to list users", err)) return } // INT-007: Standardize pagination format pagination := BuildPaginationData(page, limit, total) RespondSuccess(c, http.StatusOK, gin.H{ "users": users, "pagination": pagination, }) } // SearchUsers gère la recherche d'utilisateurs // BE-API-008: Implement user search endpoint func (h *ProfileHandler) SearchUsers(c *gin.Context) { // Récupérer les paramètres de recherche query := c.Query("q") pageParam := c.DefaultQuery("page", "1") limitParam := c.DefaultQuery("limit", "20") // Bounds checking: return 400 instead of silently normalizing (DoS prevention) page, err := strconv.Atoi(pageParam) if err != nil || page < 1 { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "pagination: page must be >= 1 and limit must be between 1 and 100")) return } limit, err := strconv.Atoi(limitParam) if err != nil || limit < 1 || limit > 100 { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "pagination: page must be >= 1 and limit must be between 1 and 100")) return } // Rechercher les utilisateurs users, total, err := h.userService.SearchUsers(c.Request.Context(), services.SearchUsersParams{ Query: query, Page: page, Limit: limit, }) if err != nil { RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to search users", err)) return } // INT-007: Standardize pagination format pagination := BuildPaginationData(page, limit, total) RespondSuccess(c, http.StatusOK, gin.H{ "users": users, "pagination": pagination, }) } // 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(c.Request.Context(), 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())) // Phase 2.2: Create notification for the followed user if h.notificationService != nil { link := "/users/" + followerID.String() if err := h.notificationService.CreateNotification(userID, "follow", "New follower", "Someone started following you", link); err != nil { h.logger.Warn("failed to create follow notification", zap.Error(err), zap.String("followed_id", userID.String())) } } RespondSuccess(c, http.StatusOK, gin.H{"message": "User followed successfully"}) } // GetFollowSuggestions returns users to follow (v0.10.0 F211) // GET /api/v1/users/suggestions?limit=10 func (h *ProfileHandler) GetFollowSuggestions(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } if h.socialService == nil { RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Social service not initialized", nil)) return } limit := 10 if l := c.Query("limit"); l != "" { if n, err := strconv.Atoi(l); err == nil && n > 0 && n <= 20 { limit = n } } suggestions, err := h.socialService.GetFollowSuggestions(c.Request.Context(), userID, limit) if err != nil { h.logger.Error("failed to get follow suggestions", zap.Error(err), zap.String("user_id", userID.String())) RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get suggestions", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"suggestions": suggestions}) } // 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(c.Request.Context(), 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"}) } // 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"` LastName string `json:"last_name" binding:"omitempty,max=100" validate:"omitempty,max=100"` Username string `json:"username" binding:"omitempty,min=3,max=30" validate:"omitempty,min=3,max=30,username"` Bio string `json:"bio" binding:"omitempty,max=500" validate:"omitempty,max=500"` Location string `json:"location" binding:"omitempty,max=100" validate:"omitempty,max=100"` Birthdate string `json:"birthdate" binding:"omitempty,datetime=2006-01-02" validate:"omitempty,datetime=2006-01-02"` Gender string `json:"gender" binding:"omitempty,oneof=Male Female Other 'Prefer not to say'" validate:"omitempty,oneof=Male Female Other 'Prefer not to say'"` SocialLinks map[string]interface{} `json:"social_links" binding:"omitempty"` BannerURL string `json:"banner_url" binding:"omitempty,max=2048"` IsPublic *bool `json:"is_public" binding:"omitempty"` } // UpdateProfile updates a user profile // @Summary Update Profile // @Description Update user profile details // @Tags User // @Accept json // @Produce json // @Security BearerAuth // @Param id path string true "User ID" // @Param profile body UpdateProfileRequest true "Profile Data" // @Success 200 {object} handlers.APIResponse{data=object{profile=object}} // @Failure 400 {object} handlers.APIResponse "Validation Error" // @Failure 401 {object} handlers.APIResponse "Unauthorized" // @Failure 403 {object} handlers.APIResponse "Forbidden" // @Router /users/{id} [put] func (h *ProfileHandler) UpdateProfile(c *gin.Context) { userIDStr := c.Param("id") userID, err := uuid.Parse(userIDStr) if err != nil { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "invalid user id")) return } // Get authenticated user ID var authenticatedUserID uuid.UUID if reqID, exists := c.Get("user_id"); exists { if reqUUID, ok := reqID.(uuid.UUID); ok { authenticatedUserID = reqUUID } else { RespondWithAppError(c, apperrors.NewUnauthorizedError("user not authenticated")) return } } else { RespondWithAppError(c, apperrors.NewUnauthorizedError("user not authenticated")) return } // MOD-P1-003: Verify that user_id corresponds to authenticated user or user is admin isAdmin := false if h.permissionService != nil { hasRole, err := h.permissionService.HasRole(c.Request.Context(), authenticatedUserID, "admin") if err == nil && hasRole { isAdmin = true } } if userID != authenticatedUserID && !isAdmin { RespondWithAppError(c, apperrors.NewForbiddenError("cannot update other user's profile")) return } var req UpdateProfileRequest if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil { RespondWithAppError(c, appErr) return } // BE-SEC-009: Sanitize user inputs to prevent XSS and injection attacks if req.Username != "" { req.Username = utils.SanitizeUsername(req.Username) } if req.Bio != "" { req.Bio = utils.SanitizeText(req.Bio, 500) } if req.FirstName != "" { req.FirstName = utils.SanitizeText(req.FirstName, 100) } if req.LastName != "" { req.LastName = utils.SanitizeText(req.LastName, 100) } // Validate username if provided if req.Username != "" { // Validate username format (alphanumeric + underscore, 3-30 chars) if !isValidUsername(req.Username) { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "username must be 3-30 characters, alphanumeric and underscore only")) return } // Validate username uniqueness if modified if err := h.userService.ValidateUsername(c.Request.Context(), userID, req.Username); err != nil { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, err.Error())) return } // Check if username can be modified (once per month) canChange, err := h.userService.CanChangeUsername(c.Request.Context(), userID) if err != nil { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "failed to check username change eligibility")) return } if !canChange { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "username can only be changed once per month")) return } } // Validate birthdate if provided if req.Birthdate != "" { birthdate, err := time.Parse("2006-01-02", req.Birthdate) if err != nil { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "invalid birthdate format, expected YYYY-MM-DD")) return } // Check if user is at least 13 years old age := time.Since(birthdate) minAge := 13 * 365 * 24 * time.Hour // 13 years if age < minAge { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "user must be at least 13 years old")) return } } // Convert UpdateProfileRequest to types.UpdateProfileRequest serviceReq := types.UpdateProfileRequest{ FirstName: &req.FirstName, LastName: &req.LastName, Username: &req.Username, Bio: &req.Bio, Location: &req.Location, Gender: &req.Gender, SocialLinks: req.SocialLinks, } if req.BannerURL != "" { serviceReq.BannerURL = &req.BannerURL } if req.Birthdate != "" { birthdate, _ := time.Parse("2006-01-02", req.Birthdate) birthdateStr := birthdate.Format("2006-01-02") serviceReq.BirthDate = &birthdateStr } if req.IsPublic != nil { serviceReq.IsPublic = req.IsPublic } // Update profile using the new UpdateProfile method profile, err := h.userService.UpdateProfile(c.Request.Context(), userID, serviceReq) if err != nil { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "failed to update profile")) return } RespondSuccess(c, http.StatusOK, profile) } // isValidUsername validates username format (alphanumeric + underscore, 3-30 chars) func isValidUsername(username string) bool { if len(username) < 3 || len(username) > 30 { return false } for _, char := range username { if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '_') { return false } } return true } // DeleteUser gère la suppression d'un utilisateur (soft delete) // BE-API-041: DELETE /api/v1/users/:id with soft delete support // @Summary Delete user // @Description Soft delete a user (only user owner or admin can delete) // @Tags User // @Accept json // @Produce json // @Security BearerAuth // @Param id path string true "User ID" // @Success 200 {object} handlers.APIResponse "User deleted successfully" // @Failure 400 {object} handlers.APIResponse "Invalid ID" // @Failure 401 {object} handlers.APIResponse "Unauthorized" // @Failure 403 {object} handlers.APIResponse "Forbidden - Not user owner or admin" // @Failure 404 {object} handlers.APIResponse "User not found" // @Router /users/{id} [delete] func (h *ProfileHandler) DeleteUser(c *gin.Context) { userIDStr := c.Param("id") userID, err := uuid.Parse(userIDStr) if err != nil { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "invalid user id")) return } // Get the requesting user ID requesterID, ok := GetUserIDUUID(c) if !ok { return // Erreur déjà envoyée par GetUserIDUUID } // Check if requester is the user owner or admin if requesterID != userID { // Check if requester is admin if h.permissionService != nil { isAdmin, err := h.permissionService.HasRole(c.Request.Context(), requesterID, "admin") if err != nil || !isAdmin { RespondWithAppError(c, apperrors.NewForbiddenError("only user owner or admin can delete user")) return } } else { RespondWithAppError(c, apperrors.NewForbiddenError("only user owner or admin can delete user")) return } } // Delete user (soft delete) if err := h.userService.DeleteUser(c.Request.Context(), userID); err != nil { if err.Error() == "user not found" { RespondWithAppError(c, apperrors.NewNotFoundError("user")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to delete user", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"message": "user deleted successfully"}) }