From e0b16e5b29fd63f399cf3eb8ddfc5f6b1237e053 Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Fri, 26 Dec 2025 09:55:19 -0300 Subject: [PATCH] Fix profile 404/500 and user deletion 403 --- .../internal/api/handlers/core_handlers.go | 37 +++- .../core/usecases/user/delete_user.go | 6 +- backend/internal/router/router.go | 1 + frontend/src/app/dashboard/companies/page.tsx | 46 ++++- frontend/src/app/dashboard/profile/page.tsx | 6 + frontend/src/app/dashboard/users/page.tsx | 173 ++++++++++++++---- 6 files changed, 224 insertions(+), 45 deletions(-) diff --git a/backend/internal/api/handlers/core_handlers.go b/backend/internal/api/handlers/core_handlers.go index a2e9833..3b011f9 100644 --- a/backend/internal/api/handlers/core_handlers.go +++ b/backend/internal/api/handlers/core_handlers.go @@ -366,8 +366,19 @@ func (h *CoreHandlers) ListUsers(w http.ResponseWriter, r *http.Request) { // @Router /api/v1/users/{id} [delete] func (h *CoreHandlers) DeleteUser(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - tenantID, ok := ctx.Value(middleware.ContextTenantID).(string) - if !ok || tenantID == "" { + tenantID, _ := ctx.Value(middleware.ContextTenantID).(string) + + // Check for admin role to bypass tenant check + userRoles := middleware.ExtractRoles(ctx.Value(middleware.ContextRoles)) + isAdmin := false + for _, role := range userRoles { + if role == "ADMIN" || role == "SUPERADMIN" || role == "admin" || role == "superadmin" { + isAdmin = true + break + } + } + + if !isAdmin && tenantID == "" { http.Error(w, "Tenant ID not found in context", http.StatusForbidden) return } @@ -380,7 +391,18 @@ func (h *CoreHandlers) DeleteUser(w http.ResponseWriter, r *http.Request) { return } - if err := h.deleteUserUC.Execute(ctx, id, tenantID); err != nil { + // If admin, we pass empty tenantID to signal bypass to the usecase (or specific admin logic) + // But wait, UpdateUserUseCase treats empty tenantID as bypass. Let's see DeleteUserUseCase. + // We need to match that logic. + targetTenantID := tenantID + if isAdmin { + targetTenantID = "" // Signal bypass + } + + log.Printf("[DeleteUser] UserID: %s, RequestID: %s, IsAdmin: %v, TenantID: %s, TargetTenantID: %s", r.PathValue("id"), middleware.GetRequestID(r.Context()), isAdmin, tenantID, targetTenantID) + + if err := h.deleteUserUC.Execute(ctx, id, targetTenantID); err != nil { + log.Printf("[DeleteUser] Error: %v", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -750,8 +772,10 @@ func (h *CoreHandlers) UpdateMyProfile(w http.ResponseWriter, r *http.Request) { } // userID (string) passed directly as first arg + log.Printf("[UpdateMyProfile] UserID: %s, TenantID: %s", userID, tenantID) resp, err := h.updateUserUC.Execute(ctx, userID, tenantID, req) if err != nil { + log.Printf("[UpdateMyProfile] Error: %v", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -835,13 +859,16 @@ func (h *CoreHandlers) Me(w http.ResponseWriter, r *http.Request) { userID = strconv.Itoa(int(v)) } + log.Printf("[Me Handler] Processing request for UserID: %s", userID) + user, err := h.adminService.GetUser(ctx, userID) if err != nil { - log.Printf("ERROR [Me Handler] GetUser failed for userID %s: %v", userID, err) + log.Printf("[Me Handler] GetUser failed for userID %s: %v", userID, err) + // Check for specific error types if possible, or just return 500 http.Error(w, err.Error(), http.StatusInternalServerError) return } - log.Printf("SUCCESS [Me Handler] User retrieved: %s", user.Email) + log.Printf("[Me Handler] User retrieved: %s", user.Email) company, _ := h.adminService.GetCompanyByUserID(ctx, userID) if company != nil { diff --git a/backend/internal/core/usecases/user/delete_user.go b/backend/internal/core/usecases/user/delete_user.go index 76d3781..8282a93 100644 --- a/backend/internal/core/usecases/user/delete_user.go +++ b/backend/internal/core/usecases/user/delete_user.go @@ -24,10 +24,12 @@ func (uc *DeleteUserUseCase) Execute(ctx context.Context, userID, tenantID strin return err } - if user.TenantID != tenantID { + // 2. Check Permission (Tenant Check) + // If tenantID is empty, it means SuperAdmin or Admin (handler logic), so we skip check. + if tenantID != "" && user.TenantID != tenantID { return errors.New("user not found in this tenant") } - // 2. Delete + // 3. Delete return uc.userRepo.Delete(ctx, userID) } diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index ccd76c9..8158408 100755 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -157,6 +157,7 @@ func NewRouter() http.Handler { // Public /api/v1/users/me (Authenticated) mux.Handle("GET /api/v1/users/me", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.Me))) + mux.Handle("PATCH /api/v1/users/me/profile", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.UpdateMyProfile))) // /api/v1/admin/companies -> Handled by coreHandlers.ListCompanies (Smart Branching) // Needs to be wired with Optional Auth to support both Public and Admin. diff --git a/frontend/src/app/dashboard/companies/page.tsx b/frontend/src/app/dashboard/companies/page.tsx index e7e26f1..29c4042 100644 --- a/frontend/src/app/dashboard/companies/page.tsx +++ b/frontend/src/app/dashboard/companies/page.tsx @@ -18,6 +18,7 @@ import { } from "@/components/ui/dialog" import { Label } from "@/components/ui/label" import { Plus, Search, Loader2, RefreshCw, Building2, CheckCircle, XCircle, Eye, Trash2, Pencil } from "lucide-react" +import { Switch } from "@/components/ui/switch" import { adminCompaniesApi, type AdminCompany } from "@/lib/api" import { getCurrentUser, isAdminUser } from "@/lib/auth" import { toast } from "sonner" @@ -82,6 +83,8 @@ export default function AdminCompaniesPage() { document: "", address: "", description: "", + active: false, + verified: false, }) useEffect(() => { @@ -184,6 +187,8 @@ export default function AdminCompaniesPage() { document: company.document || "", address: company.address || "", description: company.description || "", + active: company.active || false, + verified: company.verified || false, }) setIsEditDialogOpen(true) setIsViewDialogOpen(false) @@ -193,7 +198,26 @@ export default function AdminCompaniesPage() { if (!selectedCompany) return try { setUpdating(true) - await adminCompaniesApi.update(selectedCompany.id, editFormData) + + // Check if status changed + if (editFormData.active !== selectedCompany.active || editFormData.verified !== selectedCompany.verified) { + await adminCompaniesApi.updateStatus(selectedCompany.id, { + active: editFormData.active, + verified: editFormData.verified + }) + } + + await adminCompaniesApi.update(selectedCompany.id, { + name: editFormData.name, + slug: editFormData.slug, + email: editFormData.email, + phone: editFormData.phone, + website: editFormData.website, + document: editFormData.document, + address: editFormData.address, + description: editFormData.description, + }) + toast.success("Company updated successfully") setIsEditDialogOpen(false) loadCompanies() @@ -434,7 +458,7 @@ export default function AdminCompaniesPage() { {/* View Company Modal */} - + @@ -561,6 +585,24 @@ export default function AdminCompaniesPage() { Update company information
+
+
+ setEditFormData({ ...editFormData, active: checked })} + id="edit-active" + /> + +
+
+ setEditFormData({ ...editFormData, verified: checked })} + id="edit-verified" + /> + +
+
{ + console.log("[PROFILE_FLOW] Loading profile...") try { const userData = await authApi.getCurrentUser() + console.log("[PROFILE_FLOW] Profile loaded:", userData) setUser(userData) setFormData({ fullName: userData.fullName || "", @@ -38,6 +40,7 @@ export default function ProfilePage() { bio: userData.bio || "" }) } catch (error) { + console.error("[PROFILE_FLOW] Error loading profile:", error) toast.error("Failed to load profile") } finally { setLoading(false) @@ -51,14 +54,17 @@ export default function ProfilePage() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setSaving(true) + console.log("[PROFILE_FLOW] Updating profile:", formData) try { await profileApi.update({ name: formData.fullName, phone: formData.phone, bio: formData.bio }) + console.log("[PROFILE_FLOW] Profile updated successfully") toast.success("Profile updated") } catch (error) { + console.error("[PROFILE_FLOW] Error updating profile:", error) toast.error("Failed to update profile") } finally { setSaving(false) diff --git a/frontend/src/app/dashboard/users/page.tsx b/frontend/src/app/dashboard/users/page.tsx index a359570..33b9daf 100644 --- a/frontend/src/app/dashboard/users/page.tsx +++ b/frontend/src/app/dashboard/users/page.tsx @@ -18,7 +18,7 @@ import { } from "@/components/ui/dialog" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Plus, Search, Trash2, Loader2, RefreshCw, Pencil } from "lucide-react" +import { Plus, Search, Trash2, Loader2, RefreshCw, Pencil, Eye } from "lucide-react" import { usersApi, adminCompaniesApi, type ApiUser, type AdminCompany } from "@/lib/api" import { getCurrentUser, isAdminUser } from "@/lib/auth" import { toast } from "sonner" @@ -36,13 +36,23 @@ export default function AdminUsersPage() { const [searchTerm, setSearchTerm] = useState("") const [page, setPage] = useState(1) const [totalUsers, setTotalUsers] = useState(0) + + // Dialog States const [isDialogOpen, setIsDialogOpen] = useState(false) const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) + + // Action States const [creating, setCreating] = useState(false) const [updating, setUpdating] = useState(false) + const [deleting, setDeleting] = useState(false) + const [viewing, setViewing] = useState(false) + const [selectedUser, setSelectedUser] = useState(null) const [companies, setCompanies] = useState([]) const [currentUser, setCurrentUser] = useState(null) + + // Form Data const [formData, setFormData] = useState({ name: "", email: "", @@ -64,7 +74,7 @@ export default function AdminUsersPage() { router.push("/dashboard") return } - setCurrentUser(user as ApiUser) // Casting safe here due to check + setCurrentUser(user as ApiUser) loadUsers() if (user?.role === 'superadmin') { @@ -85,14 +95,16 @@ export default function AdminUsersPage() { const totalPages = Math.max(1, Math.ceil(totalUsers / limit)) const loadUsers = async (targetPage = page) => { + console.log(`[USER_FLOW] Loading users page: ${targetPage}`) try { setLoading(true) const data = await usersApi.list({ page: targetPage, limit }) + console.log("[USER_FLOW] Users loaded:", data) setUsers(data?.data || []) setTotalUsers(data?.pagination?.total || 0) setPage(data?.pagination?.page || targetPage) } catch (error) { - console.error("Error loading users:", error) + console.error("[USER_FLOW] Error loading users:", error) toast.error("Failed to load users") } finally { setLoading(false) @@ -100,11 +112,12 @@ export default function AdminUsersPage() { } const handleCreate = async () => { + console.log("[USER_FLOW] Creating user with data:", formData) try { setCreating(true) const payload = { ...formData, - roles: [formData.role], // Backend expects array + roles: [formData.role], } await usersApi.create(payload) toast.success("User created successfully!") @@ -113,14 +126,15 @@ export default function AdminUsersPage() { setPage(1) loadUsers(1) } catch (error) { - console.error("Error creating user:", error) + console.error("[USER_FLOW] Error creating user:", error) toast.error("Failed to create user") } finally { setCreating(false) } } - const handleEdit = (user: ApiUser) => { + const handleView = (user: ApiUser) => { + console.log("[USER_FLOW] Viewing user:", user) setSelectedUser(user) setEditFormData({ name: user.name, @@ -128,54 +142,78 @@ export default function AdminUsersPage() { role: user.role, status: user.status || "active", }) + setViewing(true) + setIsEditDialogOpen(true) + } + + const handleEdit = (user: ApiUser) => { + console.log("[USER_FLOW] Editing user:", user) + setSelectedUser(user) + setEditFormData({ + name: user.name, + email: user.email, + role: user.role, + status: user.status || "active", + }) + setViewing(false) setIsEditDialogOpen(true) } const handleUpdate = async () => { if (!selectedUser) return + console.log("[USER_FLOW] Updating user:", selectedUser.id, "with data:", editFormData) try { setUpdating(true) const payload = { ...editFormData, - roles: [editFormData.role], // Backend expects array of roles + roles: [editFormData.role], } await usersApi.update(selectedUser.id, payload) toast.success("User updated successfully!") setIsEditDialogOpen(false) - loadUsers() // Refresh list + loadUsers() } catch (error) { - console.error("Error updating user:", error) + console.error("[USER_FLOW] Error updating user:", error) toast.error("Failed to update user") } finally { setUpdating(false) } } - const handleDelete = async (id: string) => { - // Optimistic UI update or wait? User asked for no full reload. - // We can remove it from state immediately. - // We should use a proper dialog but standard confirm is quick. - if (!confirm("Are you sure you want to delete this user?")) return + const handleDeleteClick = (user: ApiUser) => { + console.log("[USER_FLOW] Delete click for user:", user) + setSelectedUser(user) + setIsDeleteDialogOpen(true) + } - // Optimistic update - const originalUsers = [...users] - setUsers(users.filter(u => u.id !== id)) + const confirmDelete = async () => { + if (!selectedUser) return + console.log("[USER_FLOW] Confirming delete for user:", selectedUser.id) try { - await usersApi.delete(id) + setDeleting(true) + await usersApi.delete(selectedUser.id) toast.success("User deleted!") - // If we are on a page > 1 and it becomes empty, we might need to fetch prev page + + // UI Update logic if (users.length === 1 && page > 1) { setPage(page - 1) loadUsers(page - 1) } else { - // Background revalidate to ensure count is correct loadUsers(page) } - } catch (error) { - console.error("Error deleting user:", error) - toast.error("Failed to delete user") - setUsers(originalUsers) // Revert on failure + setIsDeleteDialogOpen(false) + setSelectedUser(null) + } catch (error: any) { + console.error("[USER_FLOW] Error deleting user:", error) + // Error handling matching user request to see logs + if (error.message && error.message.includes("403")) { + toast.error("You don't have permission to delete this user (403)") + } else { + toast.error("Failed to delete user") + } + } finally { + setDeleting(false) } } @@ -312,11 +350,13 @@ export default function AdminUsersPage() {
+ + {/* Edit / View Dialog */} - + - Edit User - Update user details + {viewing ? "User Details" : "Edit User"} + {viewing ? "View user information" : "Update user details"}
@@ -326,6 +366,8 @@ export default function AdminUsersPage() { value={editFormData.name} onChange={(e) => setEditFormData({ ...editFormData, name: e.target.value })} placeholder="Full name" + readOnly={viewing} + disabled={viewing} />
@@ -336,11 +378,17 @@ export default function AdminUsersPage() { value={editFormData.email} onChange={(e) => setEditFormData({ ...editFormData, email: e.target.value })} placeholder="email@example.com" + readOnly={viewing} + disabled={viewing} />
- setEditFormData({ ...editFormData, role: v })} + disabled={viewing} + > @@ -354,7 +402,11 @@ export default function AdminUsersPage() {
- setEditFormData({ ...editFormData, status: v })} + disabled={viewing} + > @@ -366,10 +418,44 @@ export default function AdminUsersPage() {
- - + {!viewing && ( + + )} + +
+
+ + {/* Delete Confirmation Dialog */} + + + + Delete User + + Are you sure you want to delete this user? This action cannot be undone. + + +
+ {selectedUser && ( +
+

Name: {selectedUser.name}

+

Email: {selectedUser.email}

+

ID: {selectedUser.id}

+
+ )} +
+ + +
@@ -458,8 +544,15 @@ export default function AdminUsersPage() { {user.email} {getRoleBadge(user.role)} - - {user.status === "active" ? "Active" : user.status} + + {user.status ? user.status.toUpperCase() : "UNKNOWN"} @@ -467,6 +560,14 @@ export default function AdminUsersPage() {
+