veza/veza-backend-api/internal/handlers/profile_handler.go
senke 2d664f9177 fix(security): add SSRF protection, real track access validation, and pagination bounds
- Add IsURLSafe() function to webhook service blocking private IPs,
  localhost, and cloud metadata endpoints (SSRF protection)
- Implement real validate_track_access() in stream server querying DB
  for track visibility, ownership, and purchase status
- Remove dangerous JWT fallback user in chat server that allowed
  deleted users to maintain access with forged credentials
- Add upper limit (100) on pagination in profile, track, and room handlers
- Fix Dockerfile.production healthcheck path to /api/v1/health

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 22:44:03 +01:00

712 lines
24 KiB
Go

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
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
}
// 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(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(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(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
page, err := strconv.Atoi(pageParam)
if err != nil || page < 1 {
page = 1
}
limit, err := strconv.Atoi(limitParam)
if err != nil || limit < 1 {
limit = 20
}
if limit > 100 {
limit = 100
}
// 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")
// Parser les paramètres
page, err := strconv.Atoi(pageParam)
if err != nil || page < 1 {
page = 1
}
limit, err := strconv.Atoi(limitParam)
if err != nil || limit < 1 {
limit = 20
}
// 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(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"})
}
// 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"`
}
// 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(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(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.Birthdate != "" {
birthdate, _ := time.Parse("2006-01-02", req.Birthdate)
birthdateStr := birthdate.Format("2006-01-02")
serviceReq.BirthDate = &birthdateStr
}
// Update profile using the new UpdateProfile method
profile, err := h.userService.UpdateProfile(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"})
}