'use client'; import { useEffect, useState } from 'react'; import { apiFetch } from '@/lib/api'; import { Input } from '@/components/input'; import { Button } from '@/components/ui/button'; interface Row { entry: { id: string; orgId: string | null; userId: string | null; action: string; resourceType: string | null; resourceId: string | null; metadata: Record | null; ipAddress: string | null; createdAt: string; }; userEmail: string | null; } const ACTION_FILTERS = [ '', 'auth.login', 'auth.logout', 'admin.login', 'server.create', 'server.iterate', 'server.delete', 'admin.user.update', 'admin.user.delete', 'admin.org.update', 'admin.server.rebuild', 'admin.server.delete', 'admin.prompt.update', ]; export default function AdminAuditPage() { const [rows, setRows] = useState(null); const [action, setAction] = useState(''); const [search, setSearch] = useState(''); useEffect(() => { apiFetch<{ entries: Row[] }>(`/v1/admin/audit${action ? `?action=${action}` : ''}`) .then((r) => setRows(r.entries)) .catch(() => setRows([])); }, [action]); function exportCsv() { if (!rows) return; const header = ['when', 'action', 'user', 'resource', 'ip', 'metadata']; const lines = rows.map((r) => [ new Date(r.entry.createdAt).toISOString(), r.entry.action, r.userEmail ?? '', `${r.entry.resourceType ?? ''}/${r.entry.resourceId ?? ''}`, r.entry.ipAddress ?? '', r.entry.metadata ? JSON.stringify(r.entry.metadata).replace(/"/g, '""') : '', ] .map((v) => `"${String(v).replace(/"/g, '""')}"`) .join(','), ); const csv = [header.join(','), ...lines].join('\n'); const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `bmm-audit-${Date.now()}.csv`; a.click(); URL.revokeObjectURL(url); } const visible = rows?.filter((r) => search ? r.entry.action.includes(search) || r.userEmail?.includes(search) || r.entry.resourceId?.includes(search) || r.entry.ipAddress?.includes(search) : true, ); return (

Audit log

System-wide. {visible?.length ?? 0} visible.

setSearch(e.target.value)} placeholder="Filter by user email, resource id, or ip…" className="w-96" />
{!visible && (

Loading…

)} {visible && visible.length === 0 && (

No matches.

)} {visible && visible.length > 0 && ( {visible.map((r) => ( ))}
When Action User Resource IP Metadata
{new Date(r.entry.createdAt).toLocaleString()} {r.entry.action} {r.userEmail ?? '—'} {r.entry.resourceType ? `${r.entry.resourceType}/${r.entry.resourceId?.slice(0, 8) ?? '—'}` : '—'} {r.entry.ipAddress ?? '—'} {r.entry.metadata ? JSON.stringify(r.entry.metadata) : '—'}
)}
); }