From 73967ca52bd7e0edc50b9f2fc64342d07bfe7948 Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Fri, 26 Dec 2025 00:51:54 -0300 Subject: [PATCH] fix(users): allow superadmin to list all users without tenant restriction - Modified ListUsers handler to check for admin/superadmin role - Superadmins can now list all users across tenants - Added ListUsers method to AdminService - Added Status field to dto.User Fixes 403 error when superadmin tries to access /api/v1/users --- .../internal/api/handlers/core_handlers.go | 49 +++++++++++++++++-- backend/internal/dto/auth.go | 1 + backend/internal/services/admin_service.go | 38 ++++++++++++++ 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/backend/internal/api/handlers/core_handlers.go b/backend/internal/api/handlers/core_handlers.go index 94c3ffb..633b8b6 100644 --- a/backend/internal/api/handlers/core_handlers.go +++ b/backend/internal/api/handlers/core_handlers.go @@ -279,15 +279,58 @@ func (h *CoreHandlers) CreateUser(w http.ResponseWriter, r *http.Request) { // @Router /api/v1/users [get] func (h *CoreHandlers) ListUsers(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + + // Check if user is admin/superadmin (they can list all users) + roles := middleware.ExtractRoles(ctx.Value(middleware.ContextRoles)) + isAdmin := false + for _, role := range roles { + if role == "ADMIN" || role == "SUPERADMIN" || role == "admin" || role == "superadmin" { + isAdmin = true + break + } + } + + page, _ := strconv.Atoi(r.URL.Query().Get("page")) + if page < 1 { + page = 1 + } + limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) + if limit < 1 { + limit = 10 + } + if limit > 100 { + limit = 100 + } + + if isAdmin { + // Admin view: List all users using AdminService + users, total, err := h.adminService.ListUsers(ctx, page, limit) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + response := map[string]interface{}{ + "data": users, + "pagination": map[string]interface{}{ + "page": page, + "limit": limit, + "total": total, + }, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + // Non-admin: require tenant ID tenantID, ok := ctx.Value(middleware.ContextTenantID).(string) if !ok || tenantID == "" { http.Error(w, "Tenant ID not found in context", http.StatusForbidden) return } - page, _ := strconv.Atoi(r.URL.Query().Get("page")) - limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) - users, err := h.listUsersUC.Execute(ctx, tenantID, page, limit) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/backend/internal/dto/auth.go b/backend/internal/dto/auth.go index 37f8f79..b9dbba7 100755 --- a/backend/internal/dto/auth.go +++ b/backend/internal/dto/auth.go @@ -53,6 +53,7 @@ type User struct { Name string `json:"name"` Email string `json:"email"` Role string `json:"role"` + Status string `json:"status"` CreatedAt time.Time `json:"createdAt"` CompanyID *string `json:"companyId,omitempty"` } diff --git a/backend/internal/services/admin_service.go b/backend/internal/services/admin_service.go index 3aa56ea..ef5dd17 100644 --- a/backend/internal/services/admin_service.go +++ b/backend/internal/services/admin_service.go @@ -87,6 +87,44 @@ func (s *AdminService) ListCompanies(ctx context.Context, verified *bool, page, return companies, total, nil } +// ListUsers returns all users with pagination (for admin view) +func (s *AdminService) ListUsers(ctx context.Context, page, limit int) ([]dto.User, int, error) { + offset := (page - 1) * limit + + // Count Total + var total int + if err := s.DB.QueryRowContext(ctx, `SELECT COUNT(*) FROM users`).Scan(&total); err != nil { + return nil, 0, err + } + + // Fetch Data + query := ` + SELECT id, COALESCE(name, full_name, identifier, ''), email, role, COALESCE(status, 'active'), created_at + FROM users + ORDER BY created_at DESC + LIMIT $1 OFFSET $2 + ` + + rows, err := s.DB.QueryContext(ctx, query, limit, offset) + if err != nil { + return nil, 0, err + } + defer rows.Close() + + users := []dto.User{} + for rows.Next() { + var u dto.User + var roleStr string + if err := rows.Scan(&u.ID, &u.Name, &u.Email, &roleStr, &u.Status, &u.CreatedAt); err != nil { + return nil, 0, err + } + u.Role = roleStr + users = append(users, u) + } + + return users, total, nil +} + func (s *AdminService) UpdateCompanyStatus(ctx context.Context, id string, active *bool, verified *bool) (*models.Company, error) { company, err := s.getCompanyByID(ctx, id) if err != nil {