Repository Public MIT
1import Link from "next/link";2import { dateTime } from "@/lib/format";3import { requireSession } from "@/lib/session";4import { listAudit } from "@/server/community";56export const metadata = { title: "Audit log" };78export default async function Page({ searchParams }: { searchParams: Promise<{ before?: string; scope?: string; action?: string }> }) {9 const { ctx, isAdmin } = await requireSession();10 const sp = await searchParams;11 const all = isAdmin && sp.scope === "all";12 const action = sp.action?.slice(0, 60);13 const log = await listAudit(ctx, { all, before: sp.before ? Number(sp.before) : undefined, limit: 50, action });14 const qs = (o: Record<string, string | undefined>) => { const p = new URLSearchParams(); for (const [k, v] of Object.entries({ scope: all ? "all" : undefined, action, ...o })) if (v) p.set(k, v); return `/dashboard/audit?${p}`; };15 return (16 <div>17 <h1 className="text-2xl font-semibold tracking-tight">Audit log</h1>18 <p className="mt-2 max-w-2xl text-sm text-muted">Every authentication, publication, modification and privileged action, attributed to a session or token. The log is append-only: the database refuses updates and deletions.</p>19 <div className="mt-5 flex flex-wrap items-center gap-3 text-sm">20 {isAdmin && (<><Link className={all ? "link" : "font-medium text-text"} href={qs({ scope: undefined, before: undefined })}>Mine</Link><Link className={all ? "font-medium text-text" : "link"} href="/dashboard/audit?scope=all">Platform-wide</Link></>)}21 <form className="ml-auto flex gap-2" action="/dashboard/audit">22 {all && <input type="hidden" name="scope" value="all" />}23 <label htmlFor="f-action" className="sr-only">Filter by action prefix</label>24 <input id="f-action" name="action" defaultValue={action} placeholder="release. · token. · auth." className="input min-h-9 w-56 font-mono text-xs" />25 <button className="btn min-h-9" type="submit">Filter</button>26 </form>27 </div>28 <div className="mt-4 overflow-x-auto rounded-lg border border-line">29 <table className="w-full text-left text-sm">30 <thead className="border-b border-line text-xs text-faint"><tr><th scope="col" className="px-3 py-2 font-medium">When</th><th scope="col" className="px-3 py-2 font-medium">Actor</th><th scope="col" className="px-3 py-2 font-medium">Action</th><th scope="col" className="px-3 py-2 font-medium">Target</th><th scope="col" className="px-3 py-2 font-medium">Result</th><th scope="col" className="px-3 py-2 font-medium">Origin</th></tr></thead>31 <tbody className="divide-y divide-line">32 {log.items.map((e) => (33 <tr key={e.id} className="align-top">34 <td className="whitespace-nowrap px-3 py-2 font-mono text-[11px] text-faint">{dateTime(e.at)}</td>35 <td className="px-3 py-2 font-mono text-[11px] text-muted">{e.actor}</td>36 <td className="px-3 py-2 font-mono text-[12px]">{e.action}</td>37 <td className="max-w-xs break-all px-3 py-2 text-muted">{e.target ?? "—"}</td>38 <td className={`px-3 py-2 ${e.result === "success" ? "text-ok" : "text-danger"}`}>{e.result}</td>39 <td className="px-3 py-2 font-mono text-[11px] text-faint" title={e.requestId ?? undefined}>{e.ip}</td>40 </tr>41 ))}42 {log.items.length === 0 && <tr><td colSpan={6} className="px-3 py-6 text-center text-muted">No events.</td></tr>}43 </tbody>44 </table>45 </div>46 {log.nextBefore && <Link className="btn mt-4" href={qs({ before: String(log.nextBefore) })}>Older events</Link>}47 </div>48 );49}50