SPB Git

Repository Public MIT

page.tsx 3.6 KB 50 lines tsx

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
Files main
Menu

Theme