2025-12-03 19:29:37 +00:00
package handlers
import (
"net/http"
2025-12-23 09:42:26 +00:00
"strconv"
2025-12-03 19:29:37 +00:00
"time"
2025-12-06 16:34:18 +00:00
apperrors "veza-backend-api/internal/errors"
2025-12-03 19:29:37 +00:00
"veza-backend-api/internal/services"
"veza-backend-api/internal/types"
2025-12-24 11:15:25 +00:00
"veza-backend-api/internal/utils"
2025-12-13 02:34:34 +00:00
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
2025-12-03 19:29:37 +00:00
)
// ProfileHandler handles profile-related operations
type ProfileHandler struct {
chore: consolidate CI, E2E, backend and frontend updates
- CI: workflows updates (cd, ci), remove playwright.yml
- E2E: global-setup, auth/playlists/profile specs
- Remove playwright-report and test-results artifacts from tracking
- Backend: auth, handlers, services, workers, migrations
- Frontend: components, features, vite config
- Add e2e-results.json to gitignore
- Docs: REMEDIATION_PROGRESS, audit archive
- Rust: chat-server, stream-server updates
2026-02-17 15:43:21 +00:00
userService * services . UserService
commonHandler * CommonHandler
2026-03-05 22:03:43 +00:00
permissionService * services . PermissionService // MOD-P1-003: Added for admin check
socialService * services . SocialService // BE-API-017: Added for follow/unfollow functionality
chore: consolidate CI, E2E, backend and frontend updates
- CI: workflows updates (cd, ci), remove playwright.yml
- E2E: global-setup, auth/playlists/profile specs
- Remove playwright-report and test-results artifacts from tracking
- Backend: auth, handlers, services, workers, migrations
- Frontend: components, features, vite config
- Add e2e-results.json to gitignore
- Docs: REMEDIATION_PROGRESS, audit archive
- Rust: chat-server, stream-server updates
2026-02-17 15:43:21 +00:00
notificationService * services . NotificationService // Phase 2.2: Optional, for follow notifications
2026-03-05 22:03:43 +00:00
logger * zap . Logger // BE-API-017: Added for logging
2025-12-03 19:29:37 +00:00
}
// NewProfileHandler creates a new ProfileHandler instance
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
func NewProfileHandler ( userService * services . UserService , logger * zap . Logger ) * ProfileHandler {
return & ProfileHandler {
userService : userService ,
commonHandler : NewCommonHandler ( logger ) ,
2025-12-24 10:26:32 +00:00
logger : logger ,
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
}
2025-12-03 19:29:37 +00:00
}
2025-12-24 10:26:32 +00:00
// 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
}
chore: consolidate CI, E2E, backend and frontend updates
- CI: workflows updates (cd, ci), remove playwright.yml
- E2E: global-setup, auth/playlists/profile specs
- Remove playwright-report and test-results artifacts from tracking
- Backend: auth, handlers, services, workers, migrations
- Frontend: components, features, vite config
- Add e2e-results.json to gitignore
- Docs: REMEDIATION_PROGRESS, audit archive
- Rust: chat-server, stream-server updates
2026-02-17 15:43:21 +00:00
// SetNotificationService sets the notification service for follow notifications (Phase 2.2)
func ( h * ProfileHandler ) SetNotificationService ( notificationService * services . NotificationService ) {
h . notificationService = notificationService
}
2025-12-13 02:34:34 +00:00
// 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
}
2025-12-03 19:29:37 +00:00
// GetProfile retrieves a public user profile by ID
2025-12-06 16:34:18 +00:00
// @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]
2025-12-03 19:29:37 +00:00
func ( h * ProfileHandler ) GetProfile ( c * gin . Context ) {
userIDStr := c . Param ( "id" )
userID , err := uuid . Parse ( userIDStr )
if err != nil {
2025-12-06 16:34:18 +00:00
RespondWithAppError ( c , apperrors . New ( apperrors . ErrCodeValidation , "invalid user id" ) )
2025-12-03 19:29:37 +00:00
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
2026-03-12 04:40:53 +00:00
profile , err := h . userService . GetProfile ( c . Request . Context ( ) , userID , requesterID )
2025-12-03 19:29:37 +00:00
if err != nil {
2025-12-06 16:34:18 +00:00
RespondWithAppError ( c , apperrors . New ( apperrors . ErrCodeNotFound , "user not found" ) )
2025-12-03 19:29:37 +00:00
return
}
2025-12-23 00:42:53 +00:00
RespondSuccess ( c , http . StatusOK , profile )
2025-12-03 19:29:37 +00:00
}
// GetProfileByUsername retrieves a public profile by username
2025-12-06 16:34:18 +00:00
// @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]
2025-12-03 19:29:37 +00:00
func ( h * ProfileHandler ) GetProfileByUsername ( c * gin . Context ) {
username := c . Param ( "username" )
2025-12-24 11:15:25 +00:00
// BE-SEC-009: Sanitize username parameter to prevent injection
username = utils . SanitizeUsername ( username )
2025-12-03 19:29:37 +00:00
if username == "" {
2025-12-06 16:34:18 +00:00
RespondWithAppError ( c , apperrors . New ( apperrors . ErrCodeValidation , "username required" ) )
2025-12-03 19:29:37 +00:00
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
2026-03-12 04:40:53 +00:00
profile , err := h . userService . GetProfileByUsername ( c . Request . Context ( ) , username , requesterID )
2025-12-03 19:29:37 +00:00
if err != nil {
2025-12-06 16:34:18 +00:00
RespondWithAppError ( c , apperrors . New ( apperrors . ErrCodeNotFound , "user not found" ) )
2025-12-03 19:29:37 +00:00
return
}
2025-12-23 00:42:53 +00:00
RespondSuccess ( c , http . StatusOK , profile )
2025-12-03 19:29:37 +00:00
}
// GetProfileCompletion retrieves the profile completion status
2025-12-24 10:37:51 +00:00
// GET /api/v1/users/:id/completion
// BE-API-023: Implement user completion endpoint validation
2025-12-03 19:29:37 +00:00
// T0220: Returns percentage and missing fields
2025-12-06 16:34:18 +00:00
// @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]
2025-12-03 19:29:37 +00:00
func ( h * ProfileHandler ) GetProfileCompletion ( c * gin . Context ) {
userIDStr := c . Param ( "id" )
userID , err := uuid . Parse ( userIDStr )
if err != nil {
2025-12-24 10:37:51 +00:00
RespondWithAppError ( c , apperrors . NewValidationError ( "invalid user id" ) )
2025-12-03 19:29:37 +00:00
return
}
// Get authenticated user ID
2025-12-24 10:37:51 +00:00
authenticatedUserID , ok := GetUserIDUUID ( c )
if ! ok {
return // Erreur déjà envoyée par GetUserIDUUID
2025-12-03 19:29:37 +00:00
}
// Verify that user_id corresponds to authenticated user
if userID != authenticatedUserID {
2025-12-06 16:34:18 +00:00
RespondWithAppError ( c , apperrors . NewForbiddenError ( "cannot access other user's profile completion" ) )
2025-12-03 19:29:37 +00:00
return
}
// Calculate profile completion
2026-03-12 04:40:53 +00:00
completion , err := h . userService . CalculateProfileCompletion ( c . Request . Context ( ) , userID )
2025-12-03 19:29:37 +00:00
if err != nil {
2025-12-24 10:37:51 +00:00
if err . Error ( ) == "user not found" {
RespondWithAppError ( c , apperrors . NewNotFoundError ( "user" ) )
return
}
RespondWithAppError ( c , apperrors . Wrap ( apperrors . ErrCodeInternal , "failed to calculate profile completion" , err ) )
2025-12-03 19:29:37 +00:00
return
}
2025-12-24 10:37:51 +00:00
// 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
}
}
2025-12-06 16:34:18 +00:00
RespondSuccess ( c , http . StatusOK , completion )
2025-12-03 19:29:37 +00:00
}
[BE-API-040] api: Implement user list endpoint
- Added ListUsers method to UserService with pagination and filtering
- Added ListUsers handler to ProfileHandler
- Registered GET /api/v1/users endpoint in router
- Supports filtering by role, is_active, is_verified, and search
- Supports sorting by created_at, username, email, last_login_at
- Includes pagination metadata (page, limit, total, total_pages, has_next, has_prev)
Phase: PHASE-2
Priority: P1
Progress: 5/267 (1.9%)
2025-12-24 10:59:56 +00:00
// 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)
2026-01-07 18:39:21 +00:00
// @Success 200 {object} handlers.APIResponse{data=object{users=array,pagination=object}}
[BE-API-040] api: Implement user list endpoint
- Added ListUsers method to UserService with pagination and filtering
- Added ListUsers handler to ProfileHandler
- Registered GET /api/v1/users endpoint in router
- Supports filtering by role, is_active, is_verified, and search
- Supports sorting by created_at, username, email, last_login_at
- Includes pagination metadata (page, limit, total, total_pages, has_next, has_prev)
Phase: PHASE-2
Priority: P1
Progress: 5/267 (1.9%)
2025-12-24 10:59:56 +00:00
// @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" )
Phase 2 stabilisation: code mort, Modal→Dialog, feature flags, tests, router split, Rust legacy
Bloc A - Code mort:
- Suppression Studio (components, views, features)
- Suppression gamification + services mock (projectService, storageService, gamificationService)
- Mise à jour Sidebar, Navbar, locales
Bloc B - Frontend:
- Suppression modal.tsx deprecated, Modal.stories (doublon Dialog)
- Feature flags: PLAYLIST_SEARCH, PLAYLIST_RECOMMENDATIONS, ROLE_MANAGEMENT = true
- Suppression 19 tests orphelins, retrait exclusions vitest.config
Bloc C - Backend:
- Extraction routes_auth.go depuis router.go
Bloc D - Rust:
- Suppression security_legacy.rs (code mort, patterns déjà dans security/)
2026-02-14 16:23:32 +00:00
// Parser les paramètres de pagination — return 400 if out of bounds (no silent normalization)
[BE-API-040] api: Implement user list endpoint
- Added ListUsers method to UserService with pagination and filtering
- Added ListUsers handler to ProfileHandler
- Registered GET /api/v1/users endpoint in router
- Supports filtering by role, is_active, is_verified, and search
- Supports sorting by created_at, username, email, last_login_at
- Includes pagination metadata (page, limit, total, total_pages, has_next, has_prev)
Phase: PHASE-2
Priority: P1
Progress: 5/267 (1.9%)
2025-12-24 10:59:56 +00:00
page , err := strconv . Atoi ( pageParam )
if err != nil || page < 1 {
Phase 2 stabilisation: code mort, Modal→Dialog, feature flags, tests, router split, Rust legacy
Bloc A - Code mort:
- Suppression Studio (components, views, features)
- Suppression gamification + services mock (projectService, storageService, gamificationService)
- Mise à jour Sidebar, Navbar, locales
Bloc B - Frontend:
- Suppression modal.tsx deprecated, Modal.stories (doublon Dialog)
- Feature flags: PLAYLIST_SEARCH, PLAYLIST_RECOMMENDATIONS, ROLE_MANAGEMENT = true
- Suppression 19 tests orphelins, retrait exclusions vitest.config
Bloc C - Backend:
- Extraction routes_auth.go depuis router.go
Bloc D - Rust:
- Suppression security_legacy.rs (code mort, patterns déjà dans security/)
2026-02-14 16:23:32 +00:00
RespondWithAppError ( c , apperrors . New ( apperrors . ErrCodeValidation , "pagination: page must be >= 1 and limit must be between 1 and 100" ) )
return
[BE-API-040] api: Implement user list endpoint
- Added ListUsers method to UserService with pagination and filtering
- Added ListUsers handler to ProfileHandler
- Registered GET /api/v1/users endpoint in router
- Supports filtering by role, is_active, is_verified, and search
- Supports sorting by created_at, username, email, last_login_at
- Includes pagination metadata (page, limit, total, total_pages, has_next, has_prev)
Phase: PHASE-2
Priority: P1
Progress: 5/267 (1.9%)
2025-12-24 10:59:56 +00:00
}
limit , err := strconv . Atoi ( limitParam )
Phase 2 stabilisation: code mort, Modal→Dialog, feature flags, tests, router split, Rust legacy
Bloc A - Code mort:
- Suppression Studio (components, views, features)
- Suppression gamification + services mock (projectService, storageService, gamificationService)
- Mise à jour Sidebar, Navbar, locales
Bloc B - Frontend:
- Suppression modal.tsx deprecated, Modal.stories (doublon Dialog)
- Feature flags: PLAYLIST_SEARCH, PLAYLIST_RECOMMENDATIONS, ROLE_MANAGEMENT = true
- Suppression 19 tests orphelins, retrait exclusions vitest.config
Bloc C - Backend:
- Extraction routes_auth.go depuis router.go
Bloc D - Rust:
- Suppression security_legacy.rs (code mort, patterns déjà dans security/)
2026-02-14 16:23:32 +00:00
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
2026-02-12 21:44:03 +00:00
}
[BE-API-040] api: Implement user list endpoint
- Added ListUsers method to UserService with pagination and filtering
- Added ListUsers handler to ProfileHandler
- Registered GET /api/v1/users endpoint in router
- Supports filtering by role, is_active, is_verified, and search
- Supports sorting by created_at, username, email, last_login_at
- Includes pagination metadata (page, limit, total, total_pages, has_next, has_prev)
Phase: PHASE-2
Priority: P1
Progress: 5/267 (1.9%)
2025-12-24 10:59:56 +00:00
// Récupérer les paramètres de filtrage
2025-12-24 11:15:25 +00:00
// BE-SEC-009: Sanitize search query to prevent injection
searchQuery := utils . SanitizeText ( c . Query ( "search" ) , 100 )
[BE-API-040] api: Implement user list endpoint
- Added ListUsers method to UserService with pagination and filtering
- Added ListUsers handler to ProfileHandler
- Registered GET /api/v1/users endpoint in router
- Supports filtering by role, is_active, is_verified, and search
- Supports sorting by created_at, username, email, last_login_at
- Includes pagination metadata (page, limit, total, total_pages, has_next, has_prev)
Phase: PHASE-2
Priority: P1
Progress: 5/267 (1.9%)
2025-12-24 10:59:56 +00:00
params := services . ListUsersParams {
Page : page ,
Limit : limit ,
2025-12-24 11:15:25 +00:00
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 ) ,
[BE-API-040] api: Implement user list endpoint
- Added ListUsers method to UserService with pagination and filtering
- Added ListUsers handler to ProfileHandler
- Registered GET /api/v1/users endpoint in router
- Supports filtering by role, is_active, is_verified, and search
- Supports sorting by created_at, username, email, last_login_at
- Includes pagination metadata (page, limit, total, total_pages, has_next, has_prev)
Phase: PHASE-2
Priority: P1
Progress: 5/267 (1.9%)
2025-12-24 10:59:56 +00:00
}
// 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
}
2025-12-25 14:14:26 +00:00
// INT-007: Standardize pagination format
pagination := BuildPaginationData ( page , limit , total )
[BE-API-040] api: Implement user list endpoint
- Added ListUsers method to UserService with pagination and filtering
- Added ListUsers handler to ProfileHandler
- Registered GET /api/v1/users endpoint in router
- Supports filtering by role, is_active, is_verified, and search
- Supports sorting by created_at, username, email, last_login_at
- Includes pagination metadata (page, limit, total, total_pages, has_next, has_prev)
Phase: PHASE-2
Priority: P1
Progress: 5/267 (1.9%)
2025-12-24 10:59:56 +00:00
RespondSuccess ( c , http . StatusOK , gin . H {
2025-12-25 14:14:26 +00:00
"users" : users ,
"pagination" : pagination ,
[BE-API-040] api: Implement user list endpoint
- Added ListUsers method to UserService with pagination and filtering
- Added ListUsers handler to ProfileHandler
- Registered GET /api/v1/users endpoint in router
- Supports filtering by role, is_active, is_verified, and search
- Supports sorting by created_at, username, email, last_login_at
- Includes pagination metadata (page, limit, total, total_pages, has_next, has_prev)
Phase: PHASE-2
Priority: P1
Progress: 5/267 (1.9%)
2025-12-24 10:59:56 +00:00
} )
}
2025-12-23 09:42:26 +00:00
// 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" )
Phase 2 stabilisation: code mort, Modal→Dialog, feature flags, tests, router split, Rust legacy
Bloc A - Code mort:
- Suppression Studio (components, views, features)
- Suppression gamification + services mock (projectService, storageService, gamificationService)
- Mise à jour Sidebar, Navbar, locales
Bloc B - Frontend:
- Suppression modal.tsx deprecated, Modal.stories (doublon Dialog)
- Feature flags: PLAYLIST_SEARCH, PLAYLIST_RECOMMENDATIONS, ROLE_MANAGEMENT = true
- Suppression 19 tests orphelins, retrait exclusions vitest.config
Bloc C - Backend:
- Extraction routes_auth.go depuis router.go
Bloc D - Rust:
- Suppression security_legacy.rs (code mort, patterns déjà dans security/)
2026-02-14 16:23:32 +00:00
// Bounds checking: return 400 instead of silently normalizing (DoS prevention)
2025-12-23 09:42:26 +00:00
page , err := strconv . Atoi ( pageParam )
if err != nil || page < 1 {
Phase 2 stabilisation: code mort, Modal→Dialog, feature flags, tests, router split, Rust legacy
Bloc A - Code mort:
- Suppression Studio (components, views, features)
- Suppression gamification + services mock (projectService, storageService, gamificationService)
- Mise à jour Sidebar, Navbar, locales
Bloc B - Frontend:
- Suppression modal.tsx deprecated, Modal.stories (doublon Dialog)
- Feature flags: PLAYLIST_SEARCH, PLAYLIST_RECOMMENDATIONS, ROLE_MANAGEMENT = true
- Suppression 19 tests orphelins, retrait exclusions vitest.config
Bloc C - Backend:
- Extraction routes_auth.go depuis router.go
Bloc D - Rust:
- Suppression security_legacy.rs (code mort, patterns déjà dans security/)
2026-02-14 16:23:32 +00:00
RespondWithAppError ( c , apperrors . New ( apperrors . ErrCodeValidation , "pagination: page must be >= 1 and limit must be between 1 and 100" ) )
return
2025-12-23 09:42:26 +00:00
}
limit , err := strconv . Atoi ( limitParam )
Phase 2 stabilisation: code mort, Modal→Dialog, feature flags, tests, router split, Rust legacy
Bloc A - Code mort:
- Suppression Studio (components, views, features)
- Suppression gamification + services mock (projectService, storageService, gamificationService)
- Mise à jour Sidebar, Navbar, locales
Bloc B - Frontend:
- Suppression modal.tsx deprecated, Modal.stories (doublon Dialog)
- Feature flags: PLAYLIST_SEARCH, PLAYLIST_RECOMMENDATIONS, ROLE_MANAGEMENT = true
- Suppression 19 tests orphelins, retrait exclusions vitest.config
Bloc C - Backend:
- Extraction routes_auth.go depuis router.go
Bloc D - Rust:
- Suppression security_legacy.rs (code mort, patterns déjà dans security/)
2026-02-14 16:23:32 +00:00
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
2025-12-23 09:42:26 +00:00
}
// 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
}
2025-12-25 14:14:26 +00:00
// INT-007: Standardize pagination format
pagination := BuildPaginationData ( page , limit , total )
2025-12-23 09:42:26 +00:00
RespondSuccess ( c , http . StatusOK , gin . H {
2025-12-25 14:14:26 +00:00
"users" : users ,
"pagination" : pagination ,
2025-12-23 09:42:26 +00:00
} )
}
2025-12-24 10:26:32 +00:00
// 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
2026-03-06 18:13:16 +00:00
err = h . socialService . FollowUser ( c . Request . Context ( ) , followerID , userID )
2025-12-24 10:26:32 +00:00
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 ( ) ) )
chore: consolidate CI, E2E, backend and frontend updates
- CI: workflows updates (cd, ci), remove playwright.yml
- E2E: global-setup, auth/playlists/profile specs
- Remove playwright-report and test-results artifacts from tracking
- Backend: auth, handlers, services, workers, migrations
- Frontend: components, features, vite config
- Add e2e-results.json to gitignore
- Docs: REMEDIATION_PROGRESS, audit archive
- Rust: chat-server, stream-server updates
2026-02-17 15:43:21 +00:00
// 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 ( ) ) )
}
}
2025-12-24 10:26:32 +00:00
RespondSuccess ( c , http . StatusOK , gin . H { "message" : "User followed successfully" } )
}
2026-03-09 18:36:33 +00:00
// 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 } )
}
2025-12-24 10:26:32 +00:00
// 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
2026-03-06 18:13:16 +00:00
err = h . socialService . UnfollowUser ( c . Request . Context ( ) , followerID , userID )
2025-12-24 10:26:32 +00:00
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" } )
}
2025-12-24 10:28:49 +00:00
// 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" } )
}
2025-12-03 19:29:37 +00:00
// UpdateProfileRequest represents the request body for updating a user profile
type UpdateProfileRequest struct {
2026-01-04 00:41:51 +00:00
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" `
2026-02-20 14:10:02 +00:00
BannerURL string ` json:"banner_url" binding:"omitempty,max=2048" `
IsPublic * bool ` json:"is_public" binding:"omitempty" `
2025-12-03 19:29:37 +00:00
}
// UpdateProfile updates a user profile
2025-12-06 16:34:18 +00:00
// @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]
2025-12-03 19:29:37 +00:00
func ( h * ProfileHandler ) UpdateProfile ( c * gin . Context ) {
userIDStr := c . Param ( "id" )
userID , err := uuid . Parse ( userIDStr )
if err != nil {
2025-12-06 16:34:18 +00:00
RespondWithAppError ( c , apperrors . New ( apperrors . ErrCodeValidation , "invalid user id" ) )
2025-12-03 19:29:37 +00:00
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 {
2025-12-06 16:34:18 +00:00
RespondWithAppError ( c , apperrors . NewUnauthorizedError ( "user not authenticated" ) )
2025-12-03 19:29:37 +00:00
return
}
} else {
2025-12-06 16:34:18 +00:00
RespondWithAppError ( c , apperrors . NewUnauthorizedError ( "user not authenticated" ) )
2025-12-03 19:29:37 +00:00
return
}
2025-12-13 02:34:34 +00:00
// 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 {
2025-12-06 16:34:18 +00:00
RespondWithAppError ( c , apperrors . NewForbiddenError ( "cannot update other user's profile" ) )
2025-12-03 19:29:37 +00:00
return
}
var req UpdateProfileRequest
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
if appErr := h . commonHandler . BindAndValidateJSON ( c , & req ) ; appErr != nil {
RespondWithAppError ( c , appErr )
2025-12-03 19:29:37 +00:00
return
}
2025-12-24 11:15:25 +00:00
// 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 )
}
2025-12-03 19:29:37 +00:00
// Validate username if provided
if req . Username != "" {
// Validate username format (alphanumeric + underscore, 3-30 chars)
if ! isValidUsername ( req . Username ) {
2025-12-06 16:34:18 +00:00
RespondWithAppError ( c , apperrors . New ( apperrors . ErrCodeValidation , "username must be 3-30 characters, alphanumeric and underscore only" ) )
2025-12-03 19:29:37 +00:00
return
}
// Validate username uniqueness if modified
2026-03-12 04:40:53 +00:00
if err := h . userService . ValidateUsername ( c . Request . Context ( ) , userID , req . Username ) ; err != nil {
2025-12-06 16:34:18 +00:00
RespondWithAppError ( c , apperrors . New ( apperrors . ErrCodeValidation , err . Error ( ) ) )
2025-12-03 19:29:37 +00:00
return
}
// Check if username can be modified (once per month)
2026-03-12 04:40:53 +00:00
canChange , err := h . userService . CanChangeUsername ( c . Request . Context ( ) , userID )
2025-12-03 19:29:37 +00:00
if err != nil {
2025-12-06 16:34:18 +00:00
RespondWithAppError ( c , apperrors . New ( apperrors . ErrCodeInternal , "failed to check username change eligibility" ) )
2025-12-03 19:29:37 +00:00
return
}
if ! canChange {
2025-12-06 16:34:18 +00:00
RespondWithAppError ( c , apperrors . New ( apperrors . ErrCodeValidation , "username can only be changed once per month" ) )
2025-12-03 19:29:37 +00:00
return
}
}
// Validate birthdate if provided
if req . Birthdate != "" {
birthdate , err := time . Parse ( "2006-01-02" , req . Birthdate )
if err != nil {
2025-12-06 16:34:18 +00:00
RespondWithAppError ( c , apperrors . New ( apperrors . ErrCodeValidation , "invalid birthdate format, expected YYYY-MM-DD" ) )
2025-12-03 19:29:37 +00:00
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 {
2025-12-06 16:34:18 +00:00
RespondWithAppError ( c , apperrors . New ( apperrors . ErrCodeValidation , "user must be at least 13 years old" ) )
2025-12-03 19:29:37 +00:00
return
}
}
// Convert UpdateProfileRequest to types.UpdateProfileRequest
serviceReq := types . UpdateProfileRequest {
2026-01-04 00:41:51 +00:00
FirstName : & req . FirstName ,
LastName : & req . LastName ,
Username : & req . Username ,
Bio : & req . Bio ,
Location : & req . Location ,
Gender : & req . Gender ,
SocialLinks : req . SocialLinks ,
2025-12-03 19:29:37 +00:00
}
2026-02-20 13:56:25 +00:00
if req . BannerURL != "" {
serviceReq . BannerURL = & req . BannerURL
}
2025-12-03 19:29:37 +00:00
if req . Birthdate != "" {
birthdate , _ := time . Parse ( "2006-01-02" , req . Birthdate )
birthdateStr := birthdate . Format ( "2006-01-02" )
serviceReq . BirthDate = & birthdateStr
}
2026-02-20 14:10:02 +00:00
if req . IsPublic != nil {
serviceReq . IsPublic = req . IsPublic
}
2025-12-03 19:29:37 +00:00
// Update profile using the new UpdateProfile method
2026-03-12 04:40:53 +00:00
profile , err := h . userService . UpdateProfile ( c . Request . Context ( ) , userID , serviceReq )
2025-12-03 19:29:37 +00:00
if err != nil {
2025-12-06 16:34:18 +00:00
RespondWithAppError ( c , apperrors . New ( apperrors . ErrCodeInternal , "failed to update profile" ) )
2025-12-03 19:29:37 +00:00
return
}
2025-12-23 00:42:53 +00:00
RespondSuccess ( c , http . StatusOK , profile )
2025-12-03 19:29:37 +00:00
}
// 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
2025-12-06 16:21:59 +00:00
}
2025-12-24 14:03:21 +00:00
// 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"
2026-01-07 18:39:21 +00:00
// @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"
2025-12-24 14:03:21 +00:00
// @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" } )
}