import { useState, useCallback, useEffect } from "react"; import { Search, User, CheckCircle, XCircle, Key, Loader2, Plus, ChevronRight, ShieldCheck, Shield, Ban, UserCheck, AlertTriangle, Trash2, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Textarea } from "@/components/ui/textarea"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from "@/components/ui/sheet"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { useToast } from "@/hooks/use-toast"; import { api, User as ApiUser, SSHKey, ApiError } from "@/lib/api"; import { useAuth } from "@/contexts/AuthContext"; function formatDate(d: string | null) { if (!d) return "—"; return new Date(d).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }); } function isSuspended(status: string | undefined) { return status === "suspended" || status === "compliance_suspended"; } function RoleBadge({ role }: { role: string }) { const r = (role || "").toLowerCase(); if (r === "owner") { return ( Owner ); } if (r === "admin") { return ( Admin ); } return ( Member ); } export default function AdminUsersPage() { const { toast } = useToast(); const { user: currentUser } = useAuth(); // User list const [users, setUsers] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [pages, setPages] = useState(1); const [isLoading, setIsLoading] = useState(false); const [search, setSearch] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState(""); const [roleFilter, setRoleFilter] = useState("all"); // Debounce search useEffect(() => { const t = setTimeout(() => setDebouncedSearch(search), 300); return () => clearTimeout(t); }, [search]); // User detail drawer const [selectedUser, setSelectedUser] = useState(null); const [userSshKeys, setUserSshKeys] = useState([]); const [isDrawerLoading, setIsDrawerLoading] = useState(false); // Role update const [isUpdatingRole, setIsUpdatingRole] = useState(false); // Admin add SSH key dialog const [showAddKey, setShowAddKey] = useState(false); const [addKeyPublicKey, setAddKeyPublicKey] = useState(""); const [addKeyDescription, setAddKeyDescription] = useState(""); const [isAddingKey, setIsAddingKey] = useState(false); const [addKeyError, setAddKeyError] = useState(null); // Suspend / unsuspend const [isSuspending, setIsSuspending] = useState(false); const [showSuspendConfirm, setShowSuspendConfirm] = useState(false); // Hard delete const [showHardDelete, setShowHardDelete] = useState(false); const [hardDeleteConfirmEmail, setHardDeleteConfirmEmail] = useState(""); const [isHardDeleting, setIsHardDeleting] = useState(false); // ── Fetch users ───────────────────────────────────────────────────────────── const fetchUsers = useCallback(async (q: string, pg: number) => { setIsLoading(true); try { const params: Record = { page: String(pg), per_page: "50" }; if (q) params.q = q; const data = await api.admin.listUsers(params); setUsers(data.users); setTotal(data.count); setPages(data.pages); } catch (err) { if (err instanceof ApiError && err.code === 403) { toast({ variant: "destructive", title: "Access denied", description: "Admin or owner role required to view all users.", }); } else { toast({ variant: "destructive", title: "Failed to load users" }); } } finally { setIsLoading(false); } }, [toast]); useEffect(() => { setPage(1); fetchUsers(debouncedSearch, 1); }, [debouncedSearch, fetchUsers]); useEffect(() => { fetchUsers(debouncedSearch, page); }, [page]); // eslint-disable-line react-hooks/exhaustive-deps // ── Open user drawer ───────────────────────────────────────────────────────── const openUserDrawer = async (user: ApiUser) => { setSelectedUser(user); setUserSshKeys([]); setIsDrawerLoading(true); try { const data = await api.admin.getUser(user.id); setUserSshKeys(data.ssh_keys); } catch { // Non-fatal — drawer still shows basic user info } finally { setIsDrawerLoading(false); } }; // ── Update role ────────────────────────────────────────────────────────────── const handleRoleChange = async (newRole: string) => { if (!selectedUser || !selectedUser.org_id) return; setIsUpdatingRole(true); try { await api.admin.updateUserRole(selectedUser.org_id, selectedUser.id, newRole.toUpperCase()); const updated = { ...selectedUser, org_role: newRole }; setSelectedUser(updated); setUsers((prev) => prev.map((u) => (u.id === selectedUser.id ? { ...u, org_role: newRole } : u)) ); toast({ title: "Role updated", description: `${selectedUser.full_name || selectedUser.email} is now a ${newRole}.`, }); } catch (err) { toast({ variant: "destructive", title: "Failed to update role", description: err instanceof ApiError ? err.message : "Something went wrong", }); } finally { setIsUpdatingRole(false); } }; // ── Admin add SSH key ──────────────────────────────────────────────────────── const handleAddKey = async () => { if (!selectedUser) return; setAddKeyError(null); if (!addKeyPublicKey.trim()) { setAddKeyError("Public key is required"); return; } setIsAddingKey(true); try { const key = await api.ssh.adminAddKey(selectedUser.id, addKeyPublicKey.trim(), addKeyDescription.trim() || undefined); setUserSshKeys((prev) => [...prev, key]); toast({ title: "SSH key added", description: `Key added for ${selectedUser.email}` }); setShowAddKey(false); setAddKeyPublicKey(""); setAddKeyDescription(""); } catch (err) { setAddKeyError(err instanceof ApiError ? err.message : "Failed to add key"); } finally { setIsAddingKey(false); } }; // ── Suspend / Unsuspend user ───────────────────────────────────────────────── const handleSuspend = async () => { if (!selectedUser) return; setIsSuspending(true); try { const data = await api.admin.suspendUser(selectedUser.id); const updated = { ...selectedUser, status: data.user.status }; setSelectedUser(updated); setUsers((prev) => prev.map((u) => u.id === selectedUser.id ? { ...u, status: data.user.status } : u)); setShowSuspendConfirm(false); toast({ title: "User suspended", description: `${selectedUser.full_name || selectedUser.email} has been suspended.` }); } catch (err) { setShowSuspendConfirm(false); if (err instanceof ApiError && err.type === "OWNER_PROTECTION") { toast({ variant: "destructive", title: "Cannot suspend organization owner", description: "Transfer ownership to another member before suspending this account.", }); } else { toast({ variant: "destructive", title: "Failed to suspend user", description: err instanceof ApiError ? err.message : "Something went wrong" }); } } finally { setIsSuspending(false); } }; const handleUnsuspend = async () => { if (!selectedUser) return; setIsSuspending(true); try { const data = await api.admin.unsuspendUser(selectedUser.id); const updated = { ...selectedUser, status: data.user.status }; setSelectedUser(updated); setUsers((prev) => prev.map((u) => u.id === selectedUser.id ? { ...u, status: data.user.status } : u)); toast({ title: "User unsuspended", description: `${selectedUser.full_name || selectedUser.email} is now active.` }); } catch (err) { toast({ variant: "destructive", title: "Failed to unsuspend user", description: err instanceof ApiError ? err.message : "Something went wrong" }); } finally { setIsSuspending(false); } }; // ── Hard delete user ───────────────────────────────────────────────────────── const handleHardDelete = async () => { if (!selectedUser) return; setIsHardDeleting(true); try { const result = await api.admin.hardDeleteUser(selectedUser.id); setUsers((prev) => prev.filter((u) => u.id !== selectedUser.id)); setTotal((t) => t - 1); setShowHardDelete(false); setSelectedUser(null); toast({ title: "User permanently deleted", description: `${result.deleted_user_email} — ${result.certs_revoked} cert(s) revoked, ${result.ssh_keys_deleted} key(s) deleted.`, }); } catch (err) { toast({ variant: "destructive", title: "Failed to delete user", description: err instanceof ApiError ? err.message : "Something went wrong", }); } finally { setIsHardDeleting(false); } }; // Filter by role client-side const filteredUsers = users.filter((u) => { if (roleFilter === "all") return true; const r = (u.org_role || "member").toLowerCase(); return r === roleFilter; }); // ────────────────────────────────────────────────────────────────────────────── return (

User Management

View and manage users across your organizations

{/* Search + filter bar */}
setSearch(e.target.value)} />
Users {!isLoading && {total}} Click a user to view details and manage their role or SSH keys {isLoading ? (
) : filteredUsers.length === 0 ? (

{debouncedSearch ? "No users match your search" : "No users found"}

) : (
{filteredUsers.map((user) => ( ))}
)} {/* Pagination */} {pages > 1 && (

Page {page} of {pages} · {total} total

)}
{/* ── User detail drawer ─────────────────────────────────────────────────── */} { if (!open) setSelectedUser(null); }}> {selectedUser && ( <> {selectedUser.full_name || selectedUser.email} {selectedUser.email} {/* Basic info */}
Status {isSuspended(selectedUser.status) ? ( <>Suspended{selectedUser.status === "compliance_suspended" ? " (compliance)" : ""} ) : ( <>Active )} Joined {formatDate(selectedUser.created_at)} Activated {selectedUser.activated === false ? ( <> No ) : ( <> Yes )} Last login {formatDate(selectedUser.last_login_at)}
{/* Suspend / Unsuspend — only for other users */} {selectedUser.id !== currentUser?.id && (

Account Access

{isSuspended(selectedUser.status) ? (

{selectedUser.status === "compliance_suspended" ? "This account is suspended due to MFA compliance. The user cannot log in or request certificates." : "This account is suspended. The user cannot log in or request certificates."}

) : (

Suspending blocks this user from logging in and requesting SSH certificates.

)}
)} {/* Role management — only if not viewing yourself and user has org_id */} {selectedUser.org_id && selectedUser.id !== currentUser?.id && (

Organization Role

{isUpdatingRole && }
{(selectedUser.org_role || "").toLowerCase() === "owner" && (

Owner role cannot be changed here. Transfer ownership from the Members page.

)}
)} {/* SSH Keys section */}

SSH Keys

{isDrawerLoading ? (
) : userSshKeys.length === 0 ? (
No SSH keys registered
) : (
{userSshKeys.map((k) => (
{k.description || No description} {k.verified ? ( Verified ) : ( Unverified )}
{k.fingerprint ?? k.public_key.slice(0, 64) + "…"}
Added {formatDate(k.created_at)}
))}
)}
{/* Danger zone — Hard delete */} {selectedUser.id !== currentUser?.id && (

Danger Zone

Permanently delete this account. This cannot be undone — all SSH keys and certificates will be revoked immediately.

)} )}
{/* ── Admin add SSH key dialog ───────────────────────────────────────────── */} { setShowAddKey(open); setAddKeyError(null); }}> Add SSH Key for {selectedUser?.email} Add an SSH public key on behalf of this user (admin action).
{addKeyError && (
{addKeyError}
)}