TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import Link from "next/link";2import { ChevronLeft, ChevronRight, ScrollText } from "lucide-react";3import { getWorkspace } from "@/lib/session";4import { listAuditLogs } from "@/lib/queries/account";5import { formatDate, titleCase, truncate } from "@/lib/format";6import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";7import { EmptyState } from "@/components/ui/empty-state";8import { Badge } from "@/components/ui/badge";9import { Button } from "@/components/ui/button";10import { SimpleTooltip } from "@/components/ui/tooltip";1112export const dynamic = "force-dynamic";1314const PAGE_SIZE = 25;1516function actionVariant(action: string): "default" | "success" | "danger" | "warning" | "info" | "accent" {17 if (action.endsWith(".revoked") || action.endsWith(".deleted") || action.endsWith(".archived")) return "danger";18 if (action.endsWith(".created") || action === "login" || action === "email.verified") return "success";19 if (action.startsWith("password") || action.startsWith("email") || action.startsWith("session")) return "warning";20 if (action.startsWith("admin")) return "accent";21 return "default";22}2324function summarize(meta: Record<string, unknown> | null): string {25 if (!meta) return "—";26 const parts = Object.entries(meta)27 .filter(([, v]) => v !== null && v !== undefined && typeof v !== "object")28 .map(([k, v]) => `${k}: ${String(v)}`);29 return parts.length ? truncate(parts.join(" · "), 72) : "—";30}3132export default async function AuditLogPage({ searchParams }: { searchParams: Promise<{ page?: string }> }) {33 const sp = await searchParams;34 const page = Math.max(1, Number.parseInt(sp.page ?? "1", 10) || 1);35 const ws = await getWorkspace();36 const { rows, total } = await listAuditLogs(ws.user.id, ws.organization.id, page, PAGE_SIZE);37 const pages = Math.max(1, Math.ceil(total / PAGE_SIZE));3839 return (40 <div className="grid gap-4">41 <div className="flex flex-wrap items-end justify-between gap-3">42 <div>43 <h2 className="text-[15px] font-semibold tracking-tight">Audit log</h2>44 <p className="text-[13px] text-fg-muted">Security-relevant events for your account and organization. Retained for 365 days.</p>45 </div>46 <p className="text-[12.5px] text-fg-subtle tabular">{total.toLocaleString("en-US")} events</p>47 </div>4849 {rows.length === 0 ? (50 <EmptyState icon={ScrollText} title={page > 1 ? "Nothing on this page" : "No events yet"} description={page > 1 ? "Go back to the first page." : "Logins, key changes and settings updates will appear here."} action={page > 1 ? <Button asChild variant="outline" size="sm"><Link href="/dashboard/settings/audit">First page</Link></Button> : undefined} />51 ) : (52 <div className="overflow-hidden rounded-lg border border-border bg-bg-elevated shadow-xs">53 <Table>54 <TableHeader>55 <TableRow className="hover:bg-transparent">56 <TableHead>Time</TableHead>57 <TableHead>Action</TableHead>58 <TableHead>Target</TableHead>59 <TableHead>Details</TableHead>60 <TableHead>IP</TableHead>61 <TableHead>User agent</TableHead>62 </TableRow>63 </TableHeader>64 <TableBody>65 {rows.map((r) => (66 <TableRow key={r.id}>67 <TableCell className="whitespace-nowrap tabular text-fg-muted">{formatDate(r.createdAt)}</TableCell>68 <TableCell>69 <Badge variant={actionVariant(r.action)}>{titleCase(r.action.replace(/\./g, " "))}</Badge>70 </TableCell>71 <TableCell className="font-mono text-[12px] text-fg-muted">{r.target ? truncate(r.target, 28) : "—"}</TableCell>72 <TableCell className="max-w-[280px] truncate text-[12.5px] text-fg-muted" title={r.metadata ? JSON.stringify(r.metadata) : undefined}>73 {summarize(r.metadata)}74 </TableCell>75 <TableCell className="font-mono text-[12px] tabular">{r.ipAddress ?? "—"}</TableCell>76 <TableCell className="text-[12px] text-fg-subtle">77 {r.userAgent ? (78 <SimpleTooltip content={<span className="font-mono text-[11px]">{r.userAgent}</span>}>79 <span className="font-mono">{truncate(r.userAgent, 36)}</span>80 </SimpleTooltip>81 ) : (82 "—"83 )}84 </TableCell>85 </TableRow>86 ))}87 </TableBody>88 </Table>89 <div className="flex items-center justify-between border-t border-border px-4 py-2 text-[12.5px] text-fg-muted">90 <span className="tabular">91 Page {page} of {pages}92 </span>93 <div className="flex items-center gap-1">94 <Button asChild variant="ghost" size="xs" aria-disabled={page <= 1}>95 <Link href={`/dashboard/settings/audit?page=${page - 1}`} aria-label="Previous page" className={page <= 1 ? "pointer-events-none opacity-50" : undefined}>96 <ChevronLeft /> Prev97 </Link>98 </Button>99 <Button asChild variant="ghost" size="xs" aria-disabled={page >= pages}>100 <Link href={`/dashboard/settings/audit?page=${page + 1}`} aria-label="Next page" className={page >= pages ? "pointer-events-none opacity-50" : undefined}>101 Next <ChevronRight />102 </Link>103 </Button>104 </div>105 </div>106 </div>107 )}108 </div>109 );110}111