import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { getAuditEvents } from '../api/client'; import PageHeader from '../components/PageHeader'; import DataTable from '../components/DataTable'; import type { Column } from '../components/DataTable'; import ErrorState from '../components/ErrorState'; import { formatDateTime } from '../api/utils'; import type { AuditEvent } from '../api/types'; const actionColors: Record = { certificate_created: 'text-emerald-600', renewal_triggered: 'text-brand-500', renewal_job_created: 'text-brand-500', renewal_completed: 'text-emerald-600', deployment_completed: 'text-emerald-600', deployment_failed: 'text-red-600', expiration_alert_sent: 'text-amber-600', agent_registered: 'text-brand-500', policy_violated: 'text-red-600', certificate_revoked: 'text-red-600', }; const RESOURCE_TYPES = ['', 'certificate', 'agent', 'job', 'notification', 'policy', 'target', 'issuer']; const TIME_RANGES = [ { label: 'All time', value: '' }, { label: 'Last hour', value: '1h' }, { label: 'Last 24h', value: '24h' }, { label: 'Last 7 days', value: '7d' }, { label: 'Last 30 days', value: '30d' }, ]; function downloadFile(content: string, filename: string, type: string) { const blob = new Blob([content], { type }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function exportCSV(events: AuditEvent[]) { const headers = ['ID', 'Action', 'Actor', 'Actor Type', 'Resource Type', 'Resource ID', 'Details', 'Timestamp']; const rows = events.map(e => [ e.id, e.action, e.actor, e.actor_type, e.resource_type, e.resource_id, JSON.stringify(e.details || {}), e.timestamp, ]); const csv = [headers, ...rows].map(row => row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(',')).join('\n'); downloadFile(csv, `audit-trail-${new Date().toISOString().slice(0, 10)}.csv`, 'text/csv'); } function exportJSON(events: AuditEvent[]) { const json = JSON.stringify(events, null, 2); downloadFile(json, `audit-trail-${new Date().toISOString().slice(0, 10)}.json`, 'application/json'); } // Bundle 1 Phase 8 + Phase 10 — event_category filter exposed via the // category query param. Allowed values match the server's CHECK // constraint; the auditor role uses category=auth to surface only // authentication / authorization rows. const CATEGORIES = [ { label: 'All categories', value: '' }, { label: 'Cert lifecycle', value: 'cert_lifecycle' }, { label: 'Auth', value: 'auth' }, { label: 'Config', value: 'config' }, ]; export default function AuditPage() { const [resourceType, setResourceType] = useState(''); const [actorFilter, setActorFilter] = useState(''); const [timeRange, setTimeRange] = useState(''); const [actionFilter, setActionFilter] = useState(''); const [category, setCategory] = useState(''); const params: Record = {}; if (resourceType) params.resource_type = resourceType; if (actorFilter) params.actor = actorFilter; if (actionFilter) params.action = actionFilter; if (category) params.category = category; const { data, isLoading, error, refetch } = useQuery({ queryKey: ['audit', params], queryFn: () => getAuditEvents(params), refetchInterval: 30000, }); // Client-side time range filtering (server may not support time params) const filtered = (data?.data || []).filter((e) => { if (!timeRange) return true; const ts = new Date(e.timestamp).getTime(); const now = Date.now(); const hours = timeRange === '1h' ? 1 : timeRange === '24h' ? 24 : timeRange === '7d' ? 168 : 720; return now - ts < hours * 3600 * 1000; }); const columns: Column[] = [ { key: 'action', label: 'Action', render: (e) => ( {e.action.replace(/_/g, ' ')} ), }, { key: 'actor', label: 'Actor', render: (e) => (
{e.actor}
{e.actor_type}
), }, { key: 'resource', label: 'Resource', render: (e) => (
{e.resource_type}
{e.resource_id}
), }, { key: 'details', label: 'Details', render: (e) => { if (!e.details || Object.keys(e.details).length === 0) return ; return ( {JSON.stringify(e.details).slice(0, 60)} ); }, }, { key: 'time', label: 'Time', render: (e) => {formatDateTime(e.timestamp)} }, ]; const hasFilters = resourceType || actorFilter || timeRange || actionFilter || category; return ( <> 0 ? (
) : undefined } />
setActorFilter(e.target.value)} className="bg-surface border border-surface-border rounded px-3 py-1.5 text-xs text-ink placeholder-ink-faint focus:outline-none focus:border-brand-400 w-40" /> setActionFilter(e.target.value)} className="bg-surface border border-surface-border rounded px-3 py-1.5 text-xs text-ink placeholder-ink-faint focus:outline-none focus:border-brand-400 w-40" /> {hasFilters && ( )}
{error ? ( refetch()} /> ) : ( )}
); }