101 lines
4.3 KiB
TypeScript
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>
|
|
)
|
|
}
|