Fix profile 404/500 and user deletion 403
This commit is contained in:
parent
f51a8dd99c
commit
e0b16e5b29
6 changed files with 224 additions and 45 deletions
|
|
@ -366,8 +366,19 @@ func (h *CoreHandlers) ListUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
// @Router /api/v1/users/{id} [delete]
|
// @Router /api/v1/users/{id} [delete]
|
||||||
func (h *CoreHandlers) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
func (h *CoreHandlers) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
tenantID, ok := ctx.Value(middleware.ContextTenantID).(string)
|
tenantID, _ := ctx.Value(middleware.ContextTenantID).(string)
|
||||||
if !ok || tenantID == "" {
|
|
||||||
|
// 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)
|
http.Error(w, "Tenant ID not found in context", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -380,7 +391,18 @@ func (h *CoreHandlers) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
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)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -750,8 +772,10 @@ func (h *CoreHandlers) UpdateMyProfile(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// userID (string) passed directly as first arg
|
// 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)
|
resp, err := h.updateUserUC.Execute(ctx, userID, tenantID, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("[UpdateMyProfile] Error: %v", err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -835,13 +859,16 @@ func (h *CoreHandlers) Me(w http.ResponseWriter, r *http.Request) {
|
||||||
userID = strconv.Itoa(int(v))
|
userID = strconv.Itoa(int(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("[Me Handler] Processing request for UserID: %s", userID)
|
||||||
|
|
||||||
user, err := h.adminService.GetUser(ctx, userID)
|
user, err := h.adminService.GetUser(ctx, userID)
|
||||||
if err != nil {
|
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)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
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)
|
company, _ := h.adminService.GetCompanyByUserID(ctx, userID)
|
||||||
if company != nil {
|
if company != nil {
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,12 @@ func (uc *DeleteUserUseCase) Execute(ctx context.Context, userID, tenantID strin
|
||||||
return err
|
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")
|
return errors.New("user not found in this tenant")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Delete
|
// 3. Delete
|
||||||
return uc.userRepo.Delete(ctx, userID)
|
return uc.userRepo.Delete(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,7 @@ func NewRouter() http.Handler {
|
||||||
|
|
||||||
// Public /api/v1/users/me (Authenticated)
|
// Public /api/v1/users/me (Authenticated)
|
||||||
mux.Handle("GET /api/v1/users/me", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.Me)))
|
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)
|
// /api/v1/admin/companies -> Handled by coreHandlers.ListCompanies (Smart Branching)
|
||||||
// Needs to be wired with Optional Auth to support both Public and Admin.
|
// Needs to be wired with Optional Auth to support both Public and Admin.
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Plus, Search, Loader2, RefreshCw, Building2, CheckCircle, XCircle, Eye, Trash2, Pencil } from "lucide-react"
|
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 { adminCompaniesApi, type AdminCompany } from "@/lib/api"
|
||||||
import { getCurrentUser, isAdminUser } from "@/lib/auth"
|
import { getCurrentUser, isAdminUser } from "@/lib/auth"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
@ -82,6 +83,8 @@ export default function AdminCompaniesPage() {
|
||||||
document: "",
|
document: "",
|
||||||
address: "",
|
address: "",
|
||||||
description: "",
|
description: "",
|
||||||
|
active: false,
|
||||||
|
verified: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -184,6 +187,8 @@ export default function AdminCompaniesPage() {
|
||||||
document: company.document || "",
|
document: company.document || "",
|
||||||
address: company.address || "",
|
address: company.address || "",
|
||||||
description: company.description || "",
|
description: company.description || "",
|
||||||
|
active: company.active || false,
|
||||||
|
verified: company.verified || false,
|
||||||
})
|
})
|
||||||
setIsEditDialogOpen(true)
|
setIsEditDialogOpen(true)
|
||||||
setIsViewDialogOpen(false)
|
setIsViewDialogOpen(false)
|
||||||
|
|
@ -193,7 +198,26 @@ export default function AdminCompaniesPage() {
|
||||||
if (!selectedCompany) return
|
if (!selectedCompany) return
|
||||||
try {
|
try {
|
||||||
setUpdating(true)
|
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")
|
toast.success("Company updated successfully")
|
||||||
setIsEditDialogOpen(false)
|
setIsEditDialogOpen(false)
|
||||||
loadCompanies()
|
loadCompanies()
|
||||||
|
|
@ -434,7 +458,7 @@ export default function AdminCompaniesPage() {
|
||||||
</Card>
|
</Card>
|
||||||
{/* View Company Modal */}
|
{/* View Company Modal */}
|
||||||
<Dialog open={isViewDialogOpen} onOpenChange={setIsViewDialogOpen}>
|
<Dialog open={isViewDialogOpen} onOpenChange={setIsViewDialogOpen}>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<Building2 className="h-5 w-5" />
|
<Building2 className="h-5 w-5" />
|
||||||
|
|
@ -561,6 +585,24 @@ export default function AdminCompaniesPage() {
|
||||||
<DialogDescription>Update company information</DialogDescription>
|
<DialogDescription>Update company information</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="flex items-center gap-4 border p-4 rounded-md">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
checked={editFormData.active}
|
||||||
|
onCheckedChange={(checked) => setEditFormData({ ...editFormData, active: checked })}
|
||||||
|
id="edit-active"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="edit-active">Active</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
checked={editFormData.verified}
|
||||||
|
onCheckedChange={(checked) => setEditFormData({ ...editFormData, verified: checked })}
|
||||||
|
id="edit-verified"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="edit-verified">Verified</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="edit-name">Company name</Label>
|
<Label htmlFor="edit-name">Company name</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,10 @@ export default function ProfilePage() {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const loadProfile = async () => {
|
const loadProfile = async () => {
|
||||||
|
console.log("[PROFILE_FLOW] Loading profile...")
|
||||||
try {
|
try {
|
||||||
const userData = await authApi.getCurrentUser()
|
const userData = await authApi.getCurrentUser()
|
||||||
|
console.log("[PROFILE_FLOW] Profile loaded:", userData)
|
||||||
setUser(userData)
|
setUser(userData)
|
||||||
setFormData({
|
setFormData({
|
||||||
fullName: userData.fullName || "",
|
fullName: userData.fullName || "",
|
||||||
|
|
@ -38,6 +40,7 @@ export default function ProfilePage() {
|
||||||
bio: userData.bio || ""
|
bio: userData.bio || ""
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("[PROFILE_FLOW] Error loading profile:", error)
|
||||||
toast.error("Failed to load profile")
|
toast.error("Failed to load profile")
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
@ -51,14 +54,17 @@ export default function ProfilePage() {
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
|
console.log("[PROFILE_FLOW] Updating profile:", formData)
|
||||||
try {
|
try {
|
||||||
await profileApi.update({
|
await profileApi.update({
|
||||||
name: formData.fullName,
|
name: formData.fullName,
|
||||||
phone: formData.phone,
|
phone: formData.phone,
|
||||||
bio: formData.bio
|
bio: formData.bio
|
||||||
})
|
})
|
||||||
|
console.log("[PROFILE_FLOW] Profile updated successfully")
|
||||||
toast.success("Profile updated")
|
toast.success("Profile updated")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("[PROFILE_FLOW] Error updating profile:", error)
|
||||||
toast.error("Failed to update profile")
|
toast.error("Failed to update profile")
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false)
|
setSaving(false)
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import {
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
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 { usersApi, adminCompaniesApi, type ApiUser, type AdminCompany } from "@/lib/api"
|
||||||
import { getCurrentUser, isAdminUser } from "@/lib/auth"
|
import { getCurrentUser, isAdminUser } from "@/lib/auth"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
@ -36,13 +36,23 @@ export default function AdminUsersPage() {
|
||||||
const [searchTerm, setSearchTerm] = useState("")
|
const [searchTerm, setSearchTerm] = useState("")
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [totalUsers, setTotalUsers] = useState(0)
|
const [totalUsers, setTotalUsers] = useState(0)
|
||||||
|
|
||||||
|
// Dialog States
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
||||||
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
|
||||||
|
|
||||||
|
// Action States
|
||||||
const [creating, setCreating] = useState(false)
|
const [creating, setCreating] = useState(false)
|
||||||
const [updating, setUpdating] = useState(false)
|
const [updating, setUpdating] = useState(false)
|
||||||
|
const [deleting, setDeleting] = useState(false)
|
||||||
|
const [viewing, setViewing] = useState(false)
|
||||||
|
|
||||||
const [selectedUser, setSelectedUser] = useState<ApiUser | null>(null)
|
const [selectedUser, setSelectedUser] = useState<ApiUser | null>(null)
|
||||||
const [companies, setCompanies] = useState<AdminCompany[]>([])
|
const [companies, setCompanies] = useState<AdminCompany[]>([])
|
||||||
const [currentUser, setCurrentUser] = useState<ApiUser | null>(null)
|
const [currentUser, setCurrentUser] = useState<ApiUser | null>(null)
|
||||||
|
|
||||||
|
// Form Data
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
email: "",
|
email: "",
|
||||||
|
|
@ -64,7 +74,7 @@ export default function AdminUsersPage() {
|
||||||
router.push("/dashboard")
|
router.push("/dashboard")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setCurrentUser(user as ApiUser) // Casting safe here due to check
|
setCurrentUser(user as ApiUser)
|
||||||
loadUsers()
|
loadUsers()
|
||||||
|
|
||||||
if (user?.role === 'superadmin') {
|
if (user?.role === 'superadmin') {
|
||||||
|
|
@ -85,14 +95,16 @@ export default function AdminUsersPage() {
|
||||||
const totalPages = Math.max(1, Math.ceil(totalUsers / limit))
|
const totalPages = Math.max(1, Math.ceil(totalUsers / limit))
|
||||||
|
|
||||||
const loadUsers = async (targetPage = page) => {
|
const loadUsers = async (targetPage = page) => {
|
||||||
|
console.log(`[USER_FLOW] Loading users page: ${targetPage}`)
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const data = await usersApi.list({ page: targetPage, limit })
|
const data = await usersApi.list({ page: targetPage, limit })
|
||||||
|
console.log("[USER_FLOW] Users loaded:", data)
|
||||||
setUsers(data?.data || [])
|
setUsers(data?.data || [])
|
||||||
setTotalUsers(data?.pagination?.total || 0)
|
setTotalUsers(data?.pagination?.total || 0)
|
||||||
setPage(data?.pagination?.page || targetPage)
|
setPage(data?.pagination?.page || targetPage)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading users:", error)
|
console.error("[USER_FLOW] Error loading users:", error)
|
||||||
toast.error("Failed to load users")
|
toast.error("Failed to load users")
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
@ -100,11 +112,12 @@ export default function AdminUsersPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
|
console.log("[USER_FLOW] Creating user with data:", formData)
|
||||||
try {
|
try {
|
||||||
setCreating(true)
|
setCreating(true)
|
||||||
const payload = {
|
const payload = {
|
||||||
...formData,
|
...formData,
|
||||||
roles: [formData.role], // Backend expects array
|
roles: [formData.role],
|
||||||
}
|
}
|
||||||
await usersApi.create(payload)
|
await usersApi.create(payload)
|
||||||
toast.success("User created successfully!")
|
toast.success("User created successfully!")
|
||||||
|
|
@ -113,14 +126,15 @@ export default function AdminUsersPage() {
|
||||||
setPage(1)
|
setPage(1)
|
||||||
loadUsers(1)
|
loadUsers(1)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating user:", error)
|
console.error("[USER_FLOW] Error creating user:", error)
|
||||||
toast.error("Failed to create user")
|
toast.error("Failed to create user")
|
||||||
} finally {
|
} finally {
|
||||||
setCreating(false)
|
setCreating(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEdit = (user: ApiUser) => {
|
const handleView = (user: ApiUser) => {
|
||||||
|
console.log("[USER_FLOW] Viewing user:", user)
|
||||||
setSelectedUser(user)
|
setSelectedUser(user)
|
||||||
setEditFormData({
|
setEditFormData({
|
||||||
name: user.name,
|
name: user.name,
|
||||||
|
|
@ -128,54 +142,78 @@ export default function AdminUsersPage() {
|
||||||
role: user.role,
|
role: user.role,
|
||||||
status: user.status || "active",
|
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)
|
setIsEditDialogOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpdate = async () => {
|
const handleUpdate = async () => {
|
||||||
if (!selectedUser) return
|
if (!selectedUser) return
|
||||||
|
console.log("[USER_FLOW] Updating user:", selectedUser.id, "with data:", editFormData)
|
||||||
try {
|
try {
|
||||||
setUpdating(true)
|
setUpdating(true)
|
||||||
const payload = {
|
const payload = {
|
||||||
...editFormData,
|
...editFormData,
|
||||||
roles: [editFormData.role], // Backend expects array of roles
|
roles: [editFormData.role],
|
||||||
}
|
}
|
||||||
await usersApi.update(selectedUser.id, payload)
|
await usersApi.update(selectedUser.id, payload)
|
||||||
toast.success("User updated successfully!")
|
toast.success("User updated successfully!")
|
||||||
setIsEditDialogOpen(false)
|
setIsEditDialogOpen(false)
|
||||||
loadUsers() // Refresh list
|
loadUsers()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating user:", error)
|
console.error("[USER_FLOW] Error updating user:", error)
|
||||||
toast.error("Failed to update user")
|
toast.error("Failed to update user")
|
||||||
} finally {
|
} finally {
|
||||||
setUpdating(false)
|
setUpdating(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const handleDeleteClick = (user: ApiUser) => {
|
||||||
// Optimistic UI update or wait? User asked for no full reload.
|
console.log("[USER_FLOW] Delete click for user:", user)
|
||||||
// We can remove it from state immediately.
|
setSelectedUser(user)
|
||||||
// We should use a proper dialog but standard confirm is quick.
|
setIsDeleteDialogOpen(true)
|
||||||
if (!confirm("Are you sure you want to delete this user?")) return
|
}
|
||||||
|
|
||||||
// Optimistic update
|
const confirmDelete = async () => {
|
||||||
const originalUsers = [...users]
|
if (!selectedUser) return
|
||||||
setUsers(users.filter(u => u.id !== id))
|
console.log("[USER_FLOW] Confirming delete for user:", selectedUser.id)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await usersApi.delete(id)
|
setDeleting(true)
|
||||||
|
await usersApi.delete(selectedUser.id)
|
||||||
toast.success("User deleted!")
|
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) {
|
if (users.length === 1 && page > 1) {
|
||||||
setPage(page - 1)
|
setPage(page - 1)
|
||||||
loadUsers(page - 1)
|
loadUsers(page - 1)
|
||||||
} else {
|
} else {
|
||||||
// Background revalidate to ensure count is correct
|
|
||||||
loadUsers(page)
|
loadUsers(page)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
setIsDeleteDialogOpen(false)
|
||||||
console.error("Error deleting user:", error)
|
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")
|
toast.error("Failed to delete user")
|
||||||
setUsers(originalUsers) // Revert on failure
|
}
|
||||||
|
} finally {
|
||||||
|
setDeleting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -312,11 +350,13 @@ export default function AdminUsersPage() {
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Edit / View Dialog */}
|
||||||
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
|
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
|
||||||
<DialogContent>
|
<DialogContent className="max-w-md max-h-[85vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Edit User</DialogTitle>
|
<DialogTitle>{viewing ? "User Details" : "Edit User"}</DialogTitle>
|
||||||
<DialogDescription>Update user details</DialogDescription>
|
<DialogDescription>{viewing ? "View user information" : "Update user details"}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 py-4">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
|
|
@ -326,6 +366,8 @@ export default function AdminUsersPage() {
|
||||||
value={editFormData.name}
|
value={editFormData.name}
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, name: e.target.value })}
|
onChange={(e) => setEditFormData({ ...editFormData, name: e.target.value })}
|
||||||
placeholder="Full name"
|
placeholder="Full name"
|
||||||
|
readOnly={viewing}
|
||||||
|
disabled={viewing}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
|
|
@ -336,11 +378,17 @@ export default function AdminUsersPage() {
|
||||||
value={editFormData.email}
|
value={editFormData.email}
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, email: e.target.value })}
|
onChange={(e) => setEditFormData({ ...editFormData, email: e.target.value })}
|
||||||
placeholder="email@example.com"
|
placeholder="email@example.com"
|
||||||
|
readOnly={viewing}
|
||||||
|
disabled={viewing}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="edit-role">Role</Label>
|
<Label htmlFor="edit-role">Role</Label>
|
||||||
<Select value={editFormData.role} onValueChange={(v) => setEditFormData({ ...editFormData, role: v })}>
|
<Select
|
||||||
|
value={editFormData.role}
|
||||||
|
onValueChange={(v) => setEditFormData({ ...editFormData, role: v })}
|
||||||
|
disabled={viewing}
|
||||||
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
@ -354,7 +402,11 @@ export default function AdminUsersPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="edit-status">Status</Label>
|
<Label htmlFor="edit-status">Status</Label>
|
||||||
<Select value={editFormData.status} onValueChange={(v) => setEditFormData({ ...editFormData, status: v })}>
|
<Select
|
||||||
|
value={editFormData.status}
|
||||||
|
onValueChange={(v) => setEditFormData({ ...editFormData, status: v })}
|
||||||
|
disabled={viewing}
|
||||||
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
@ -366,11 +418,45 @@ export default function AdminUsersPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>Cancel</Button>
|
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>
|
||||||
|
{viewing ? "Close" : "Cancel"}
|
||||||
|
</Button>
|
||||||
|
{!viewing && (
|
||||||
<Button onClick={handleUpdate} disabled={updating}>
|
<Button onClick={handleUpdate} disabled={updating}>
|
||||||
{updating && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
{updating && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||||
Save Changes
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Delete Confirmation Dialog */}
|
||||||
|
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Delete User</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Are you sure you want to delete this user? This action cannot be undone.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="py-4">
|
||||||
|
{selectedUser && (
|
||||||
|
<div className="text-sm border p-4 rounded-md bg-muted/50">
|
||||||
|
<p><strong>Name:</strong> {selectedUser.name}</p>
|
||||||
|
<p><strong>Email:</strong> {selectedUser.email}</p>
|
||||||
|
<p><strong>ID:</strong> {selectedUser.id}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setIsDeleteDialogOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" onClick={confirmDelete} disabled={deleting}>
|
||||||
|
{deleting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||||
|
Delete User
|
||||||
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
@ -458,8 +544,15 @@ export default function AdminUsersPage() {
|
||||||
<TableCell>{user.email}</TableCell>
|
<TableCell>{user.email}</TableCell>
|
||||||
<TableCell>{getRoleBadge(user.role)}</TableCell>
|
<TableCell>{getRoleBadge(user.role)}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={user.status === "active" ? "default" : "secondary"}>
|
<Badge
|
||||||
{user.status === "active" ? "Active" : user.status}
|
variant="outline"
|
||||||
|
className={
|
||||||
|
user.status === "active"
|
||||||
|
? "border-transparent bg-green-500/15 text-green-700 hover:bg-green-500/25 dark:text-green-400"
|
||||||
|
: "border-transparent bg-red-500/15 text-red-700 hover:bg-red-500/25 dark:text-red-400"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{user.status ? user.status.toUpperCase() : "UNKNOWN"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|
@ -467,6 +560,14 @@ export default function AdminUsersPage() {
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => handleView(user)}
|
||||||
|
title="View Details"
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|
@ -478,7 +579,7 @@ export default function AdminUsersPage() {
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => handleDelete(user.id)}
|
onClick={() => handleDeleteClick(user)}
|
||||||
disabled={user.role === "superadmin"}
|
disabled={user.role === "superadmin"}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue