"use client"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useAuth } from "@/hooks/useAuth"; import { useRouter } from "next/navigation"; import { apiFetch } from "@/lib/api"; import { useDismissingState } from "@/hooks/useDismissingState"; import { History } from "lucide-react"; import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "@/components/ui/table"; const ACTIONS = [ "refund_created", "donation_assigned", "donation_unassigned", "registration_created_manual", "registration_cancelled", "event_created", "event_updated", "event_deleted", "settings_updated", ] as const; const ACTION_LABELS: Record = { refund_created: "Refund created", donation_assigned: "Donation assigned", donation_unassigned: "Donation unassigned", registration_created_manual: "Manual registration created", registration_cancelled: "Registration cancelled (staff)", event_created: "Event created", event_updated: "Event updated", event_deleted: "Event deactivated", settings_updated: "Settings updated", }; interface AuditLogEntry { id: string; actorId: string | null; actorRole: string; action: string; targetType: string; targetId: string | null; metadata: Record | null; ip: string | null; createdAt: string; actor: { id: string; name: string; email: string } | null; } function formatMetadata(metadata: Record | null): string { if (!metadata) return ""; try { return Object.entries(metadata) .filter(([, v]) => v !== null && v !== undefined && v !== "") .map(([k, v]) => `${k}: ${Array.isArray(v) ? v.join(", ") : String(v)}`) .join(" · "); } catch { return ""; } } export default function AdminAuditLogPage() { const { user, loading, token } = useAuth(); const router = useRouter(); const isAdmin = useMemo(() => user?.role === "admin", [user]); useEffect(() => { if (loading) return; if (!user) router.replace("/login"); }, [user, loading, router]); const [entries, setEntries] = useState([]); const [fetching, setFetching] = useState(false); const [error, setError] = useDismissingState(null); const [page, setPage] = useState(1); const [total, setTotal] = useState(0); const pageSize = 50; const totalPages = Math.max(1, Math.ceil(total / pageSize)); const [actionFilter, setActionFilter] = useState(""); const [fromFilter, setFromFilter] = useState(""); const [toFilter, setToFilter] = useState(""); const buildQuery = useCallback((p: number) => { const qs = new URLSearchParams({ page: String(p), limit: String(pageSize) }); if (actionFilter) qs.set("action", actionFilter); if (fromFilter) qs.set("from", fromFilter); if (toFilter) qs.set("to", toFilter); return `/api/admin/audit-log?${qs.toString()}`; }, [actionFilter, fromFilter, toFilter]); const load = useCallback(async (p = 1) => { if (!token) return; setError(null); setFetching(true); try { const res = await apiFetch<{ rows: AuditLogEntry[]; total: number; page: number }>(buildQuery(p), { authToken: token }); setEntries(res?.rows || []); setTotal(res?.total ?? 0); setPage(p); } catch (e: any) { setError(e?.message || "Failed to load audit log"); } finally { setFetching(false); } }, [token, buildQuery]); useEffect(() => { if (token) load(1); }, [token, actionFilter, fromFilter, toFilter]); return (

Admin Audit Log

{total} action{total !== 1 ? "s" : ""} recorded

{!isAdmin && (
You need admin access to view the audit log.
)} {error &&
{error}
}
setFromFilter(e.target.value)} />
setToFilter(e.target.value)} />
When Actor Action Target Details {entries.map(e => ( {new Date(e.createdAt).toLocaleString()} {e.actor ? ( <>
{e.actor.name}
{e.actor.email}
) : ( Deleted user )}
{e.actorRole}
{ACTION_LABELS[e.action] || e.action} {e.targetType}{e.targetId ? ` #${e.targetId.slice(0, 8)}` : ""} {formatMetadata(e.metadata)}
))} {entries.length === 0 && !fetching && ( No matching audit entries. )} {fetching && ( Loading… )}
{totalPages > 1 && (
Page {page} of {totalPages}
)}
); }