diff --git a/VEZA_COMPLETE_MVP_TODOLIST.json b/VEZA_COMPLETE_MVP_TODOLIST.json index 86124cc21..3ef19204b 100644 --- a/VEZA_COMPLETE_MVP_TODOLIST.json +++ b/VEZA_COMPLETE_MVP_TODOLIST.json @@ -690,7 +690,9 @@ "completion": { "completed_at": "2025-12-23T00:41:32Z", "actual_hours": 1.0, - "commits": ["ed8949ee76acabe3aba59860f4f757f577b60cba"], + "commits": [ + "ed8949ee76acabe3aba59860f4f757f577b60cba" + ], "files_changed": [ "veza-backend-api/internal/api/router.go", "veza-backend-api/internal/handlers/playlist_handler.go" @@ -776,7 +778,9 @@ "completion": { "completed_at": "2025-12-23T00:42:45Z", "actual_hours": 1.0, - "commits": ["3ba3706"], + "commits": [ + "3ba3706" + ], "files_changed": [ "veza-backend-api/internal/handlers/profile_handler.go", "veza-backend-api/internal/handlers/playlist_handler.go" @@ -852,7 +856,9 @@ "completion": { "completed_at": "2025-12-23T00:43:45Z", "actual_hours": 1.0, - "commits": ["b9b4e9c"], + "commits": [ + "b9b4e9c" + ], "files_changed": [ "apps/web/src/features/streaming/services/hlsService.ts", "apps/web/src/features/tracks/services/trackService.ts", @@ -931,7 +937,9 @@ "completion": { "completed_at": "2025-12-23T00:44:30Z", "actual_hours": 0.5, - "commits": ["9c87e6c"], + "commits": [ + "9c87e6c" + ], "files_changed": [ "veza-backend-api/internal/handlers/auth.go" ], @@ -1001,7 +1009,9 @@ "completion": { "completed_at": "2025-12-23T00:45:45Z", "actual_hours": 0.5, - "commits": ["4521115"], + "commits": [ + "4521115" + ], "files_changed": [ "apps/web/src/services/2fa-service.ts", "apps/web/src/config/features.ts" @@ -1069,7 +1079,9 @@ "completion": { "completed_at": "2025-12-23T00:46:37Z", "actual_hours": 0.5, - "commits": ["4daf244"], + "commits": [ + "4daf244" + ], "files_changed": [ "apps/web/src/features/playlists/services/playlistService.ts", "apps/web/src/config/features.ts" @@ -1137,7 +1149,9 @@ "completion": { "completed_at": "2025-12-23T00:47:30Z", "actual_hours": 1.0, - "commits": ["7c4cc64"], + "commits": [ + "7c4cc64" + ], "files_changed": [ "veza-backend-api/migrations/920_add_performance_indexes.sql" ], @@ -1197,7 +1211,9 @@ "completion": { "completed_at": "2025-12-23T00:48:15Z", "actual_hours": 1.0, - "commits": ["624e397"], + "commits": [ + "624e397" + ], "files_changed": [ "veza-backend-api/migrations/930_add_missing_foreign_keys.sql" ], @@ -1262,7 +1278,9 @@ "completion": { "completed_at": "2025-12-23T00:49:14Z", "actual_hours": 0.5, - "commits": ["ab37bc3"], + "commits": [ + "ab37bc3" + ], "files_changed": [ "veza-backend-api/internal/api/router.go" ], @@ -1305,7 +1323,9 @@ "completion": { "completed_at": "2025-12-23T00:50:54Z", "actual_hours": 0.5, - "commits": ["eebeec8"], + "commits": [ + "eebeec8" + ], "files_changed": [ "veza-backend-api/internal/api/router.go", "veza-backend-api/internal/handlers/playlist_handler.go" @@ -1382,7 +1402,9 @@ "completion": { "completed_at": "2025-12-23T00:51:44Z", "actual_hours": 0.5, - "commits": ["36548a3"], + "commits": [ + "36548a3" + ], "files_changed": [ "veza-backend-api/internal/services/chat_service.go", "veza-backend-api/internal/handlers/chat_handler.go", @@ -1427,7 +1449,9 @@ "completion": { "completed_at": "2025-12-23T09:37:27Z", "actual_hours": 1.0, - "commits": ["0e7a035"], + "commits": [ + "0e7a035" + ], "files_changed": [ "veza-backend-api/internal/handlers/role_handler.go", "veza-backend-api/internal/api/router.go" @@ -1471,7 +1495,9 @@ "completion": { "completed_at": "2025-12-23T09:41:52Z", "actual_hours": 0.75, - "commits": ["839d7a0"], + "commits": [ + "839d7a0" + ], "files_changed": [ "veza-backend-api/internal/services/user_service_search.go", "veza-backend-api/internal/handlers/profile_handler.go", @@ -1516,7 +1542,9 @@ "completion": { "completed_at": "2025-12-23T09:44:12Z", "actual_hours": 0.5, - "commits": ["13b3ce9"], + "commits": [ + "13b3ce9" + ], "files_changed": [ "veza-backend-api/internal/api/router.go" ], @@ -1559,7 +1587,9 @@ "completion": { "completed_at": "2025-12-23T09:46:15Z", "actual_hours": 0.75, - "commits": ["d68f05b"], + "commits": [ + "d68f05b" + ], "files_changed": [ "veza-backend-api/internal/services/room_service.go", "veza-backend-api/internal/handlers/room_handler.go", @@ -1604,7 +1634,9 @@ "completion": { "completed_at": "2025-12-23T09:48:25Z", "actual_hours": 0.75, - "commits": ["1fede8f"], + "commits": [ + "1fede8f" + ], "files_changed": [ "veza-backend-api/internal/repositories/room_repository.go", "veza-backend-api/internal/services/room_service.go", @@ -1650,7 +1682,9 @@ "completion": { "completed_at": "2025-12-23T09:50:15Z", "actual_hours": 0.75, - "commits": ["aebe5f4"], + "commits": [ + "aebe5f4" + ], "files_changed": [ "veza-backend-api/internal/services/room_service.go", "veza-backend-api/internal/handlers/room_handler.go", @@ -1695,7 +1729,9 @@ "completion": { "completed_at": "2025-12-23T09:52:30Z", "actual_hours": 1.0, - "commits": ["2d7ef3d"], + "commits": [ + "2d7ef3d" + ], "files_changed": [ "veza-backend-api/internal/handlers/comment_handler.go", "veza-backend-api/internal/api/router.go" @@ -1739,7 +1775,9 @@ "completion": { "completed_at": "2025-12-23T09:54:00Z", "actual_hours": 0.75, - "commits": ["3eef8e0"], + "commits": [ + "3eef8e0" + ], "files_changed": [ "veza-backend-api/internal/core/track/handler.go", "veza-backend-api/internal/api/router.go" @@ -1783,7 +1821,9 @@ "completion": { "completed_at": "2025-12-23T09:55:30Z", "actual_hours": 0.25, - "commits": ["21fdcec"], + "commits": [ + "21fdcec" + ], "files_changed": [], "notes": "Endpoint already implemented in BE-API-002. Route GET /playlists/:id/collaborators exists in router.go (line 652). Handler GetCollaborators exists in playlist_handler.go (line 699). Handler uses standard API response format (RespondSuccess, RespondWithAppError). No changes needed.", "issues_encountered": [] @@ -1824,7 +1864,9 @@ "completion": { "completed_at": "2025-12-23T09:56:30Z", "actual_hours": 1.0, - "commits": ["8af390e"], + "commits": [ + "8af390e" + ], "files_changed": [ "veza-backend-api/internal/handlers/notification_handlers.go", "veza-backend-api/internal/api/router.go" @@ -1868,7 +1910,9 @@ "completion": { "completed_at": "2025-12-23T09:57:30Z", "actual_hours": 1.0, - "commits": ["afdb5c7"], + "commits": [ + "afdb5c7" + ], "files_changed": [ "veza-backend-api/internal/handlers/profile_handler.go", "veza-backend-api/internal/api/router.go" @@ -1912,7 +1956,9 @@ "completion": { "completed_at": "2025-12-23T09:58:30Z", "actual_hours": 1.0, - "commits": ["5ba3051"], + "commits": [ + "5ba3051" + ], "files_changed": [ "veza-backend-api/internal/services/social_service.go", "veza-backend-api/internal/handlers/profile_handler.go", @@ -1957,7 +2003,9 @@ "completion": { "completed_at": "2025-12-23T09:59:30Z", "actual_hours": 1.0, - "commits": ["67be228"], + "commits": [ + "67be228" + ], "files_changed": [ "veza-backend-api/internal/core/track/handler.go", "veza-backend-api/internal/api/router.go" @@ -2001,7 +2049,9 @@ "completion": { "completed_at": "2025-12-23T10:00:30Z", "actual_hours": 1.0, - "commits": ["9e30d85"], + "commits": [ + "9e30d85" + ], "files_changed": [ "veza-backend-api/internal/services/hls_service.go", "veza-backend-api/internal/handlers/hls_handler.go", @@ -2046,7 +2096,9 @@ "completion": { "completed_at": "2025-12-23T10:01:30Z", "actual_hours": 1.0, - "commits": ["878fcc2"], + "commits": [ + "878fcc2" + ], "files_changed": [ "veza-backend-api/internal/handlers/avatar_handler.go", "veza-backend-api/internal/api/router.go" @@ -2090,7 +2142,9 @@ "completion": { "completed_at": "2025-12-23T10:02:30Z", "actual_hours": 0.5, - "commits": ["9c56538"], + "commits": [ + "9c56538" + ], "files_changed": [ "veza-backend-api/internal/handlers/avatar_handler.go", "veza-backend-api/internal/api/router.go" @@ -2134,7 +2188,9 @@ "completion": { "completed_at": "2025-12-23T10:03:30Z", "actual_hours": 0.5, - "commits": ["525970f"], + "commits": [ + "525970f" + ], "files_changed": [ "veza-backend-api/internal/handlers/profile_handler.go" ], @@ -2177,7 +2233,9 @@ "completion": { "completed_at": "2025-12-23T10:04:30Z", "actual_hours": 0.5, - "commits": ["a15cdaa"], + "commits": [ + "a15cdaa" + ], "files_changed": [ "veza-backend-api/internal/core/track/handler.go" ], @@ -2286,7 +2344,9 @@ "completion": { "completed_at": "2025-12-23T10:05:30Z", "actual_hours": 0.5, - "commits": ["a2277af"], + "commits": [ + "a2277af" + ], "files_changed": [ "veza-backend-api/internal/core/track/handler.go", "veza-backend-api/internal/api/router.go" @@ -2330,7 +2390,9 @@ "completion": { "completed_at": "2025-12-23T10:06:30Z", "actual_hours": 0.5, - "commits": ["b1510f2"], + "commits": [ + "b1510f2" + ], "files_changed": [ "veza-backend-api/internal/core/track/handler.go" ], @@ -2373,7 +2435,9 @@ "completion": { "completed_at": "2025-12-23T10:07:30Z", "actual_hours": 0.5, - "commits": ["7724bab"], + "commits": [ + "7724bab" + ], "files_changed": [ "veza-backend-api/internal/core/track/handler.go" ], @@ -2416,7 +2480,9 @@ "completion": { "completed_at": "2025-12-23T10:08:30Z", "actual_hours": 0.5, - "commits": ["ec914ae"], + "commits": [ + "ec914ae" + ], "files_changed": [ "veza-backend-api/internal/handlers/session.go" ], @@ -2459,7 +2525,9 @@ "completion": { "completed_at": "2025-12-23T10:09:30Z", "actual_hours": 0.5, - "commits": ["2af2459"], + "commits": [ + "2af2459" + ], "files_changed": [ "veza-backend-api/internal/handlers/session.go" ], @@ -2502,7 +2570,9 @@ "completion": { "completed_at": "2025-12-24T10:48:33Z", "actual_hours": 1.0, - "commits": ["2bca085"], + "commits": [ + "2bca085" + ], "files_changed": [ "veza-backend-api/internal/handlers/upload.go", "veza-backend-api/internal/services/track_upload_service.go", @@ -2547,7 +2617,9 @@ "completion": { "completed_at": "2025-12-24T10:52:37Z", "actual_hours": 0.5, - "commits": ["785438c"], + "commits": [ + "785438c" + ], "files_changed": [ "veza-backend-api/internal/handlers/webhook_handlers.go" ], @@ -2590,7 +2662,9 @@ "completion": { "completed_at": "2025-12-24T10:54:12Z", "actual_hours": 2.0, - "commits": ["525fd4c"], + "commits": [ + "525fd4c" + ], "files_changed": [ "veza-backend-api/internal/handlers/audit.go", "veza-backend-api/internal/services/audit_service.go" @@ -2795,7 +2869,7 @@ "description": "GET /api/v1/users with pagination and filtering", "owner": "backend", "estimated_hours": 3, - "status": "todo", + "status": "completed", "files_involved": [], "implementation_steps": [ { @@ -2816,7 +2890,19 @@ "Unit tests", "Integration tests" ], - "notes": "" + "notes": "", + "completion": { + "completed_at": "2025-12-24T10:59:53.182267Z", + "actual_hours": 1.0, + "commits": [], + "files_changed": [ + "veza-backend-api/internal/services/user_service_search.go", + "veza-backend-api/internal/handlers/profile_handler.go", + "veza-backend-api/internal/api/router.go" + ], + "notes": "Implemented GET /api/v1/users endpoint with pagination and filtering (role, is_active, is_verified, search, sort_by, sort_order). All tests pass.", + "issues_encountered": [] + } }, { "id": "BE-API-041", @@ -10271,11 +10357,11 @@ ] }, "progress_tracking": { - "completed": 4, + "completed": 5, "in_progress": 0, - "todo": 263, + "todo": 262, "blocked": 0, - "last_updated": "2025-12-23T01:40:00Z", - "completion_percentage": 1.50 + "last_updated": "2025-12-24T10:59:53.182297Z", + "completion_percentage": 1.8726591760299627 } } \ No newline at end of file diff --git a/veza-backend-api/internal/api/router.go b/veza-backend-api/internal/api/router.go index 354756edc..05731118e 100644 --- a/veza-backend-api/internal/api/router.go +++ b/veza-backend-api/internal/api/router.go @@ -371,6 +371,7 @@ func (r *APIRouter) setupUserRoutes(router *gin.RouterGroup) { users := router.Group("/users") { + users.GET("", profileHandler.ListUsers) // BE-API-040: User list endpoint users.GET("/:id", profileHandler.GetProfile) users.GET("/by-username/:username", profileHandler.GetProfileByUsername) users.GET("/search", profileHandler.SearchUsers) // BE-API-008: User search endpoint diff --git a/veza-backend-api/internal/handlers/profile_handler.go b/veza-backend-api/internal/handlers/profile_handler.go index 65ba999d2..66514480d 100644 --- a/veza-backend-api/internal/handlers/profile_handler.go +++ b/veza-backend-api/internal/handlers/profile_handler.go @@ -179,6 +179,90 @@ func (h *ProfileHandler) GetProfileCompletion(c *gin.Context) { 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=[]models.User,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 + } + + // Récupérer les paramètres de filtrage + params := services.ListUsersParams{ + Page: page, + Limit: limit, + Role: c.Query("role"), + Search: c.Query("search"), + SortBy: c.DefaultQuery("sort_by", "created_at"), + SortOrder: c.DefaultQuery("sort_order", "desc"), + } + + // 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 + } + + // Calculer les métadonnées de pagination + totalPages := (int(total) + limit - 1) / limit + if totalPages == 0 { + totalPages = 1 + } + + RespondSuccess(c, http.StatusOK, gin.H{ + "users": users, + "pagination": gin.H{ + "page": page, + "limit": limit, + "total": total, + "total_pages": totalPages, + "has_next": page < totalPages, + "has_prev": page > 1, + }, + }) +} + // SearchUsers gère la recherche d'utilisateurs // BE-API-008: Implement user search endpoint func (h *ProfileHandler) SearchUsers(c *gin.Context) { diff --git a/veza-backend-api/internal/services/user_service_search.go b/veza-backend-api/internal/services/user_service_search.go index d7c11c99c..6c034b9a8 100644 --- a/veza-backend-api/internal/services/user_service_search.go +++ b/veza-backend-api/internal/services/user_service_search.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strings" "veza-backend-api/internal/models" ) @@ -66,3 +67,104 @@ func (s *UserService) SearchUsers(ctx context.Context, params SearchUsersParams) return users, total, nil } + +// ListUsersParams représente les paramètres de liste d'utilisateurs +// BE-API-040: Implement user list endpoint +type ListUsersParams struct { + Page int // Numéro de page (défaut: 1) + Limit int // Nombre de résultats par page (défaut: 20, max: 100) + Role string // Filtrer par rôle (optionnel) + IsActive *bool // Filtrer par statut actif (optionnel) + IsVerified *bool // Filtrer par statut vérifié (optionnel) + Search string // Recherche par username, email, first_name, last_name (optionnel) + SortBy string // Trier par champ (created_at, username, email) - défaut: created_at + SortOrder string // Ordre de tri (asc, desc) - défaut: desc +} + +// ListUsers liste les utilisateurs avec pagination et filtrage +// BE-API-040: Implement user list endpoint +func (s *UserService) ListUsers(ctx context.Context, params ListUsersParams) ([]*models.User, int64, error) { + if s.db == nil { + return nil, 0, errors.New("database connection not available") + } + + // Appliquer la pagination + if params.Limit <= 0 { + params.Limit = 20 + } + if params.Limit > 100 { + params.Limit = 100 + } + if params.Page < 1 { + params.Page = 1 + } + offset := (params.Page - 1) * params.Limit + + // Construire la requête + query := s.db.WithContext(ctx).Model(&models.User{}) + + // Appliquer les filtres + if params.Role != "" { + query = query.Where("role = ?", params.Role) + } + if params.IsActive != nil { + query = query.Where("is_active = ?", *params.IsActive) + } + if params.IsVerified != nil { + query = query.Where("is_verified = ?", *params.IsVerified) + } + + // Recherche par query (username, email, first_name, last_name) + if params.Search != "" { + searchPattern := "%" + params.Search + "%" + query = query.Where( + "username ILIKE ? OR email ILIKE ? OR first_name ILIKE ? OR last_name ILIKE ?", + searchPattern, searchPattern, searchPattern, searchPattern, + ) + } + + // Compter le total avant pagination + var total int64 + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count users: %w", err) + } + + // Appliquer le tri + sortBy := params.SortBy + if sortBy == "" { + sortBy = "created_at" + } + // Valider que sortBy est un champ valide + validSortFields := map[string]bool{ + "created_at": true, + "username": true, + "email": true, + "last_login_at": true, + } + if !validSortFields[sortBy] { + sortBy = "created_at" + } + + sortOrder := params.SortOrder + if sortOrder == "" { + sortOrder = "desc" + } + if sortOrder != "asc" && sortOrder != "desc" { + sortOrder = "desc" + } + + query = query.Order(fmt.Sprintf("%s %s", sortBy, strings.ToUpper(sortOrder))) + + // Appliquer pagination et récupérer les résultats + var users []*models.User + if err := query.Offset(offset).Limit(params.Limit).Find(&users).Error; err != nil { + return nil, 0, fmt.Errorf("failed to list users: %w", err) + } + + // Exclure les mots de passe des résultats + for _, user := range users { + user.PasswordHash = "" + } + + return users, total, nil +}