import { useState, useEffect, useCallback } from "react"; import { Download, Globe, Lock, Search, Filter, RefreshCw, ChevronLeft, ChevronRight, LogIn, Key, UserPlus, Shield, Settings, AlertTriangle, Terminal, Loader2, X, CheckCircle2, XCircle, Link2, UserCog, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { api, AuditLogEntry, ApiError, OrganizationMember } from "@/lib/api"; import { useCurrentOrganizationId } from "@/hooks/useCurrentOrganization"; import { formatDateTime } from "@/lib/date"; // ─── category / display helpers ────────────────────────────────────────────── type Category = "auth" | "ssh" | "admin" | "member" | "policy" | "security" | "oauth" | "other"; const getCategory = (action: string): Category => { const a = action.toLowerCase(); if (a.startsWith("session") || a === "user.login" || a === "user.logout") return "auth"; if (a.startsWith("ssh")) return "ssh"; if (a.startsWith("admin.")) return "admin"; if (a.includes("member") || a.includes("invite") || a.startsWith("org.member")) return "member"; if (a.includes("policy") || a.includes("mfa.policy") || a.startsWith("org.security")) return "policy"; if (a.includes("mfa") || a.includes("totp") || a.includes("webauthn") || a.includes("passkey") || a.includes("password")) return "security"; if (a.startsWith("external_auth")) return "oauth"; return "other"; }; const CATEGORY_META: Record = { auth: { label: "Auth", color: "bg-blue-500/10 text-blue-600 dark:text-blue-400" }, ssh: { label: "SSH", color: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400" }, admin: { label: "Admin", color: "bg-red-500/10 text-red-600 dark:text-red-400" }, member: { label: "Member", color: "bg-violet-500/10 text-violet-600 dark:text-violet-400" }, policy: { label: "Policy", color: "bg-amber-500/10 text-amber-600 dark:text-amber-400" }, security: { label: "Security", color: "bg-orange-500/10 text-orange-600 dark:text-orange-400" }, oauth: { label: "OAuth", color: "bg-cyan-500/10 text-cyan-600 dark:text-cyan-400" }, other: { label: "Other", color: "bg-muted text-muted-foreground" }, }; const getCategoryIcon = (cat: Category) => { const cls = "w-4 h-4"; switch (cat) { case "auth": return ; case "ssh": return ; case "admin": return ; case "member": return ; case "policy": return ; case "security": return ; case "oauth": return ; default: return ; } }; const ACTION_LABELS: Record = { // Sessions "session.create": "Signed in", "session.revoke": "Signed out", "user.login": "Signed in", "user.logout": "Signed out", // Members "org.member.add": "Member added", "org.member.remove": "Member removed", "org.member.role_change": "Member role changed", "org.ownership.transferred": "Ownership transferred", // Admin actions "admin.mfa.remove": "MFA removed by admin", "admin.oauth.unlink": "OAuth unlinked by admin", "admin.password.set": "Password set by admin", "admin.email.verify": "Email verified by admin", // Security / policy "org.security_policy.update": "Security policy updated", "user.security_policy.override_update":"User policy override updated", "mfa.policy.user_suspended": "User suspended (MFA policy)", "mfa.policy.user_compliant": "User MFA compliant", // Password "user.password_change": "Password changed", "user.password_reset": "Password reset", // SSH "ssh.key.added": "SSH key added", "ssh.key.verified": "SSH key verified", "ssh.key.deleted": "SSH key removed", "ssh.cert.requested": "SSH certificate requested", "ssh.cert.issued": "SSH certificate issued", "ssh.cert.failed": "SSH certificate request failed", "ssh.cert.revoked": "SSH certificate revoked", // WebAuthn / Passkey "webauthn.register.completed": "Passkey registered", "webauthn.credential.deleted": "Passkey removed", "webauthn.login.success": "Signed in with passkey", "webauthn.login.failed": "Passkey login failed", // TOTP "totp.enroll.completed": "TOTP enrolled", "totp.disabled": "TOTP disabled", "totp.verify.failed": "TOTP verification failed", // External auth "external_auth.link.completed": "OAuth account linked", "external_auth.unlink": "OAuth account unlinked", "external_auth.login": "Signed in via OAuth", "external_auth.login.failed": "OAuth login failed", // Org "org.create": "Organisation created", "org.update": "Organisation updated", "org.delete": "Organisation deleted", // User lifecycle "user.register": "User registered", "user.suspend": "User suspended", "user.unsuspend":"User unsuspended", "user.delete": "User deleted", }; const getActionLabel = (action: string) => ACTION_LABELS[action] ?? action.replace(/[._]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); // ─── action filter options (value = enum dot-notation) ─────────────────────── const ACTION_FILTER_OPTIONS = [ { value: "all", label: "All actions" }, // Auth { value: "session.create", label: "Sign in" }, { value: "session.revoke", label: "Sign out" }, { value: "external_auth.login", label: "OAuth login" }, // Members { value: "org.member.add", label: "Member added" }, { value: "org.member.remove", label: "Member removed" }, { value: "org.member.role_change", label: "Role changed" }, // Admin actions { value: "admin.mfa.remove", label: "MFA removed (admin)" }, { value: "admin.oauth.unlink", label: "OAuth unlinked (admin)" }, { value: "admin.password.set", label: "Password set (admin)" }, // Security / policy { value: "org.security_policy.update", label: "Security policy changed" }, { value: "user.password_change", label: "Password changed" }, { value: "user.password_reset", label: "Password reset" }, // SSH { value: "ssh.key.added", label: "SSH key added" }, { value: "ssh.key.verified", label: "SSH key verified" }, { value: "ssh.cert.issued", label: "SSH cert issued" }, { value: "ssh.cert.revoked", label: "SSH cert revoked" }, // MFA { value: "totp.enroll.completed", label: "TOTP enrolled" }, { value: "totp.disabled", label: "TOTP disabled" }, { value: "webauthn.register.completed", label: "Passkey registered" }, { value: "webauthn.credential.deleted", label: "Passkey removed" }, // User lifecycle { value: "user.register", label: "User registered" }, { value: "user.suspend", label: "User suspended" }, ]; const PER_PAGE = 50; const getUserLabel = (log: AuditLogEntry) => log.user?.email || (log.user_id ? `${log.user_id.slice(0, 8)}…` : null); // ─── filter chip helpers ────────────────────────────────────────────────────── const ACTION_CHIP_LABELS: Record = { ...ACTION_LABELS, all: "All actions", }; // ─── cert metadata detail ───────────────────────────────────────────────────── function CertDetail({ metadata }: { metadata?: Record | null }) { if (!metadata) return null; const principal = metadata.principal as string | undefined; const principals = metadata.principals as string[] | undefined; const serial = metadata.serial_number ?? metadata.serial ?? metadata.cert_serial; const principalList = principal ? [principal] : Array.isArray(principals) ? principals : []; if (!principalList.length && !serial) return null; return ( {principalList.length > 0 && <>principal: {principalList.join(", ")}} {principalList.length > 0 && serial && " · "} {serial != null && <>serial: {String(serial)}} ); } // ─── component ──────────────────────────────────────────────────────────────── export default function OrgAuditPage() { const { orgId } = useCurrentOrganizationId(); const [search, setSearch] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState(""); const [actionFilter, setActionFilter] = useState("all"); const [successFilter, setSuccessFilter] = useState("all"); const [userFilter, setUserFilter] = useState(null); const [userFilterLabel, setUserFilterLabel] = useState(null); const [viewMode, setViewMode] = useState<"org" | "user">("org"); const [selectedUserId, setSelectedUserId] = useState(null); const [selectedUserLabel, setSelectedUserLabel] = useState(null); const [orgMembers, setOrgMembers] = useState([]); const [isMembersLoading, setIsMembersLoading] = useState(false); const [accessDenied, setAccessDenied] = useState(false); const [page, setPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [totalCount, setTotalCount] = useState(0); const [auditLogs, setAuditLogs] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isExporting, setIsExporting] = useState(false); const [error, setError] = useState(null); // debounce search useEffect(() => { const t = setTimeout(() => setDebouncedSearch(search), 400); return () => clearTimeout(t); }, [search]); // reset page on filter change useEffect(() => { setPage(1); }, [actionFilter, successFilter, userFilter, debouncedSearch, viewMode, selectedUserId]); // fetch org members for user selector useEffect(() => { if (viewMode !== "user" || !orgId) return; setIsMembersLoading(true); api.organizations.getMembers(orgId) .then((resp) => setOrgMembers(resp.members ?? [])) .catch(() => {}) .finally(() => setIsMembersLoading(false)); }, [viewMode, orgId]); const fetchLogs = useCallback(async () => { setIsLoading(true); setError(null); setAccessDenied(false); try { const params: Record = { page: String(page), per_page: String(PER_PAGE), }; if (actionFilter !== "all") params.action = actionFilter; if (successFilter !== "all") params.success = successFilter; if (userFilter) params.user_id = userFilter; if (debouncedSearch) params.q = debouncedSearch; if (viewMode === "user") { if (!selectedUserId) { setIsLoading(false); return; } const resp = await api.superadmin.getUserAuditLogs(selectedUserId, params); setAuditLogs(resp.audit_logs ?? []); setTotalCount(resp.count ?? 0); setTotalPages(resp.pages ?? 1); } else { if (!orgId) { setIsLoading(false); return; } const resp = await api.organizations.getAuditLogs(orgId, params); setAuditLogs(resp.audit_logs ?? []); setTotalCount(resp.count ?? 0); setTotalPages(resp.pages ?? 1); } } catch (err) { if (err instanceof ApiError && err.code === 403) { setAccessDenied(true); } else { console.error("Failed to fetch audit logs:", err); setError("Failed to load audit logs. Please try again."); } } finally { setIsLoading(false); } }, [orgId, page, actionFilter, successFilter, userFilter, debouncedSearch, viewMode, selectedUserId]); useEffect(() => { fetchLogs(); }, [fetchLogs]); const handleExport = useCallback(async () => { setIsExporting(true); try { const EXPORT_PER_PAGE = 200; const buildParams = (p: number) => { const params: Record = { page: String(p), per_page: String(EXPORT_PER_PAGE) }; if (actionFilter !== "all") params.action = actionFilter; if (successFilter !== "all") params.success = successFilter; if (userFilter) params.user_id = userFilter; if (debouncedSearch) params.q = debouncedSearch; return params; }; if (viewMode === "user") { if (!selectedUserId) return; await api.superadmin.exportUserAuditLogs(selectedUserId, buildParams(1)); } else { if (!orgId) return; const first = await api.organizations.getAuditLogs(orgId, buildParams(1)); const allLogs = [...(first.audit_logs ?? [])]; const totalPages = first.pages ?? 1; if (totalPages > 1) { const remaining = await Promise.all( Array.from({ length: totalPages - 1 }, (_, i) => api.organizations.getAuditLogs(orgId, buildParams(i + 2)) ) ); for (const r of remaining) allLogs.push(...(r.audit_logs ?? [])); } const esc = (v: string) => `"${v.replace(/"/g, '""')}"`; const header = ["ID","Action","Description","User Email","User ID","Resource Type","Resource ID","IP Address","User Agent","Success","Error Message","Created At","Updated At"]; const rows = allLogs.map((l) => [ l.id, l.action, l.description ?? "", l.user?.email ?? "", l.user_id ?? "", l.resource_type ?? "", l.resource_id ?? "", l.ip_address ?? "", l.user_agent ?? "", l.success ? "Yes" : "No", l.error_message ?? "", l.created_at, l.updated_at ?? "", ].map(esc).join(",")); const csv = [header.map(esc).join(","), ...rows].join("\n"); const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `audit-logs-${new Date().toISOString().slice(0, 10)}.csv`; a.click(); URL.revokeObjectURL(url); } } catch (err) { console.error("Export failed:", err); } finally { setIsExporting(false); } }, [orgId, viewMode, selectedUserId, actionFilter, successFilter, userFilter, debouncedSearch]); return (
{/* Header */}

Admin Audit Log

{viewMode === "user" ? `User events for ${selectedUserLabel ?? "selected user"}` : "Organisation activity — user events, admin actions, policy changes" } {totalCount > 0 && ` · ${totalCount.toLocaleString()} total`}

{/* View mode toggle */}
{/* User selector (user mode only) */} {viewMode === "user" && (
)} {/* Filters */}
setSearch(e.target.value)} className="pl-10" />
{/* Active filter chips */} {(actionFilter !== "all" || successFilter !== "all" || userFilter) && (
{actionFilter !== "all" && ( Action: {ACTION_CHIP_LABELS[actionFilter] ?? actionFilter} setActionFilter("all")} /> )} {userFilter && ( User: {userFilterLabel ?? userFilter.slice(0, 8) + "…"} { setUserFilter(null); setUserFilterLabel(null); }} /> )} {successFilter !== "all" && ( Status: {successFilter === "true" ? "Success only" : "Failures only"} setSuccessFilter("all")} /> )}
)} {/* Table */} {isLoading ? (
Loading…
) : accessDenied ? (

Access Restricted

You don't have permission to view user audit logs. Contact your administrator to request access.

) : error ? (

{error}

) : viewMode === "user" && !selectedUserId ? (

No user selected

Select a user above to view their audit events.

) : auditLogs.length === 0 ? (
No audit events match the current filters.
) : (
{auditLogs.map((log) => { const cat = getCategory(log.action); const meta = CATEGORY_META[cat]; const isCert = log.action.startsWith("ssh.cert"); return (
{/* Icon */}
{log.success ? getCategoryIcon(cat) : }
{/* Body */}
setActionFilter((prev) => (prev === log.action ? "all" : log.action)) } > {getActionLabel(log.action)} {meta.label} {!log.success && ( Failed )}
{/* Description */} {log.description && (

{log.description} {isCert && }

)} {log.error_message && (

{log.error_message}

)} {/* Actor / meta row */}
{log.user?.email ? ( { if (log.user_id) { setUserFilter((prev) => (prev === log.user_id ? null : log.user_id)); setUserFilterLabel((prev) => (prev === log.user.email ? null : log.user.email)); } }} >{log.user.email} ) : log.user_id ? ( { setUserFilter((prev) => (prev === log.user_id ? null : log.user_id)); setUserFilterLabel((prev) => prev === log.user_id ? null : `${log.user_id!.slice(0, 8)}…`); }} >{log.user_id.slice(0, 8)}… ) : ( System )} {log.ip_address && ( {log.ip_address} )} {log.resource_type && ( {log.resource_type} )}
{/* Timestamp */}

{formatDateTime(log.created_at)}

{log.success ? ( ) : ( )}
); })}
)}
{/* Pagination */} {totalPages > 1 && (

Page {page} of {totalPages}  ·  {totalCount.toLocaleString()} events

)}
); }