feat(admin): admin panel with overview, users, audit logs, storage, AI config, settings
Sticky sidebar with back-to-app link. Overview: user counts, recipe photos, AI calls/month. User management with role/tier editing. Audit log (all admin actions). Storage page with photo breakdown by tier. AI config showing DB vs .env key source. Site settings page to override .env values at runtime (encrypted in DB).
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import type { Metadata } from "next";
|
||||
import { db, auditLogs, users, eq, desc } from "@epicure/db";
|
||||
|
||||
export const metadata: Metadata = { title: "Audit Logs" };
|
||||
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
interface PageProps {
|
||||
searchParams: Promise<{ page?: string }>;
|
||||
}
|
||||
|
||||
export default async function AdminAuditLogsPage({ searchParams }: PageProps) {
|
||||
const { page: pageParam } = await searchParams;
|
||||
const page = Math.max(1, parseInt(pageParam ?? "1", 10));
|
||||
const offset = (page - 1) * PAGE_SIZE;
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
id: auditLogs.id,
|
||||
action: auditLogs.action,
|
||||
targetType: auditLogs.targetType,
|
||||
targetId: auditLogs.targetId,
|
||||
metadata: auditLogs.metadata,
|
||||
createdAt: auditLogs.createdAt,
|
||||
actorName: users.name,
|
||||
actorEmail: users.email,
|
||||
})
|
||||
.from(auditLogs)
|
||||
.leftJoin(users, eq(auditLogs.userId, users.id))
|
||||
.orderBy(desc(auditLogs.createdAt))
|
||||
.limit(PAGE_SIZE)
|
||||
.offset(offset);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">Audit Logs</h1>
|
||||
<p className="text-muted-foreground text-sm mt-1">
|
||||
Admin actions recorded for accountability. Page {page}.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="border-b bg-muted/50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left font-medium whitespace-nowrap">Actor</th>
|
||||
<th className="px-4 py-3 text-left font-medium whitespace-nowrap">Action</th>
|
||||
<th className="px-4 py-3 text-left font-medium whitespace-nowrap">Target</th>
|
||||
<th className="px-4 py-3 text-left font-medium whitespace-nowrap">Metadata</th>
|
||||
<th className="px-4 py-3 text-left font-medium whitespace-nowrap">Timestamp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-4 py-8 text-center text-muted-foreground">
|
||||
No audit log entries yet.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
rows.map((row) => (
|
||||
<tr key={row.id} className="border-b last:border-0 hover:bg-muted/30">
|
||||
<td className="px-4 py-3">
|
||||
<div className="font-medium">{row.actorName ?? "System"}</div>
|
||||
{row.actorEmail && (
|
||||
<div className="text-xs text-muted-foreground">{row.actorEmail}</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 font-mono text-xs">{row.action}</td>
|
||||
<td className="px-4 py-3 text-muted-foreground text-xs">
|
||||
{row.targetType ? (
|
||||
<span>
|
||||
{row.targetType}
|
||||
{row.targetId ? `: ${row.targetId.slice(0, 8)}…` : ""}
|
||||
</span>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 font-mono text-xs text-muted-foreground max-w-xs truncate">
|
||||
{row.metadata ?? "—"}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground whitespace-nowrap">
|
||||
{row.createdAt.toLocaleString()}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{page > 1 && (
|
||||
<a
|
||||
href={`/admin/audit-logs?page=${page - 1}`}
|
||||
className="rounded-md border px-3 py-1 text-sm hover:bg-accent"
|
||||
>
|
||||
Previous
|
||||
</a>
|
||||
)}
|
||||
{rows.length === PAGE_SIZE && (
|
||||
<a
|
||||
href={`/admin/audit-logs?page=${page + 1}`}
|
||||
className="rounded-md border px-3 py-1 text-sm hover:bg-accent"
|
||||
>
|
||||
Next
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user