Files
carmanagement/apps/admin/src/app/dashboard/audit-logs/page.tsx
T
2026-05-06 22:58:23 -04:00

101 lines
4.3 KiB
TypeScript

'use client'
import { useEffect, useState } from 'react'
const API_BASE = '/api/v1'
interface AuditLog {
id: string
action: string
resource: string
resourceId: string | null
createdAt: string
adminUser: { email: string; firstName: string; lastName: string } | null
}
const ACTION_COLORS: Record<string, string> = {
CREATE: 'text-emerald-400 bg-emerald-950/40',
UPDATE: 'text-sky-400 bg-sky-950/40',
DELETE: 'text-red-400 bg-red-950/40',
SUSPEND: 'text-amber-400 bg-amber-950/40',
ACTIVATE: 'text-emerald-400 bg-emerald-950/40',
LOGIN: 'text-zinc-400 bg-zinc-800',
}
export default function AdminAuditLogsPage() {
const [logs, setLogs] = useState<AuditLog[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [filter, setFilter] = useState('')
useEffect(() => {
const token = localStorage.getItem('admin_token') ?? ''
fetch(`${API_BASE}/admin/audit-logs`, {
headers: { Authorization: `Bearer ${token}` },
cache: 'no-store',
})
.then((r) => r.json())
.then((json) => setLogs(json.data ?? []))
.catch((err) => setError(err.message))
.finally(() => setLoading(false))
}, [])
const filtered = filter
? logs.filter((l) => l.action.toLowerCase().includes(filter.toLowerCase()) || l.resource.toLowerCase().includes(filter.toLowerCase()))
: logs
return (
<div className="shell py-8 space-y-6">
<div className="flex items-center justify-between">
<div>
<p className="text-xs uppercase tracking-[0.2em] text-emerald-400">Platform</p>
<h1 className="mt-1 text-3xl font-black">Audit Logs</h1>
</div>
<input
className="w-64 px-3 py-2 rounded-xl bg-zinc-800 border border-zinc-700 text-zinc-100 text-sm placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-emerald-500"
placeholder="Filter by action or entity…"
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
</div>
{error && <div className="panel p-4 text-sm text-red-400">{error}</div>}
<div className="panel overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-zinc-800">
<th className="text-left px-6 py-3 text-xs font-medium text-zinc-500 uppercase tracking-wider">Action</th>
<th className="text-left px-6 py-3 text-xs font-medium text-zinc-500 uppercase tracking-wider">Entity</th>
<th className="text-left px-6 py-3 text-xs font-medium text-zinc-500 uppercase tracking-wider">Entity ID</th>
<th className="text-left px-6 py-3 text-xs font-medium text-zinc-500 uppercase tracking-wider">Admin</th>
<th className="text-left px-6 py-3 text-xs font-medium text-zinc-500 uppercase tracking-wider">Time</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-800/60">
{loading ? (
<tr><td colSpan={5} className="px-6 py-12 text-center text-zinc-500">Loading</td></tr>
) : filtered.length === 0 ? (
<tr><td colSpan={5} className="px-6 py-12 text-center text-zinc-500">No logs found</td></tr>
) : filtered.map((log) => (
<tr key={log.id} className="hover:bg-zinc-800/30 transition-colors">
<td className="px-6 py-3">
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${ACTION_COLORS[log.action] ?? 'text-zinc-400 bg-zinc-800'}`}>
{log.action}
</span>
</td>
<td className="px-6 py-3 text-zinc-300">{log.resource}</td>
<td className="px-6 py-3 text-zinc-500 font-mono text-xs">{log.resourceId ? `${log.resourceId.slice(0, 12)}` : '—'}</td>
<td className="px-6 py-3 text-zinc-400">{log.adminUser ? `${log.adminUser.firstName} ${log.adminUser.lastName}` : '—'}</td>
<td className="px-6 py-3 text-zinc-500 text-xs">{new Date(log.createdAt).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}