Repository Public MIT
1import Link from "next/link";2import { dateTime } from "@/lib/format";3import { requireSession } from "@/lib/session";4import { listWritableNamespaces } from "@/server/auth";5import { listAudit } from "@/server/community";67export default async function Page({ searchParams }: { searchParams: Promise<{ password?: string }> }) {8 const { ctx, principal, isAdmin } = await requireSession();9 const u = principal.user;10 const [namespaces, audit, counts] = await Promise.all([11 listWritableNamespaces(ctx, u),12 listAudit(ctx, { limit: 8 }),13 ctx.sql<{ artifacts: number; releases: number; tokens: number; expiring: number }[]>`14 SELECT (SELECT count(*)::int FROM artifacts a JOIN namespaces n ON n.id = a.namespace_id WHERE n.owner_user_id = ${u.id}) AS artifacts,15 (SELECT count(*)::int FROM releases r WHERE r.published_by = ${u.id}) AS releases,16 (SELECT count(*)::int FROM api_tokens WHERE user_id = ${u.id} AND revoked_at IS NULL AND expires_at > now()) AS tokens,17 (SELECT count(*)::int FROM api_tokens WHERE user_id = ${u.id} AND revoked_at IS NULL AND expires_at > now() AND expires_at < now() + interval '14 days') AS expiring`,18 ]);19 const c = counts[0]!;20 return (21 <div>22 <h1 className="text-2xl font-semibold tracking-tight">Overview</h1>23 {(await searchParams).password === "changed" && <p role="status" className="mt-3 rounded-lg border border-ok/40 bg-ok/5 p-3 text-sm text-ok">Password changed. Other sessions were signed out.</p>}24 <dl className="mt-6 grid gap-px overflow-hidden rounded-[10px] border border-line bg-line sm:grid-cols-4">25 {[["Artifacts", c.artifacts, "/dashboard/artifacts"], ["Releases published", c.releases, "/dashboard/artifacts"], ["Active tokens", c.tokens, "/dashboard/tokens"], ["Tokens expiring ≤ 14 d", c.expiring, "/dashboard/tokens"]].map(([label, n, href]) => (26 <Link key={label as string} href={href as string} className="bg-graphite p-5 hover:bg-raised">27 <dd className="font-mono text-2xl">{n as number}</dd>28 <dt className="mt-1 text-sm text-muted">{label as string}</dt>29 </Link>30 ))}31 </dl>32 <section className="mt-8">33 <h2 className="mb-3 text-base font-semibold">Your namespaces</h2>34 {namespaces.length ? (35 <ul className="flex flex-wrap gap-2">{namespaces.map((n) => <li key={n}><Link href={`/@${n}`} className="chip text-text hover:border-line-strong">@{n}</Link></li>)}</ul>36 ) : (37 <p className="text-sm text-muted">You do not own a namespace yet. Ask the registry operator to create one.</p>38 )}39 {isAdmin && <p className="mt-3 text-xs text-faint">You are a platform {u.platform_role}. Administrative rights never include publishing into other people's namespaces.</p>}40 </section>41 <section className="mt-8">42 <div className="mb-3 flex items-baseline justify-between"><h2 className="text-base font-semibold">Recent activity</h2><Link className="link text-sm" href="/dashboard/audit">Full audit log</Link></div>43 <ul className="divide-y divide-line rounded-lg border border-line text-sm">44 {audit.items.map((e) => (45 <li key={e.id} className="flex flex-wrap gap-x-3 gap-y-0.5 px-3 py-2">46 <time className="w-36 shrink-0 font-mono text-[11px] text-faint">{dateTime(e.at)}</time>47 <span className={`font-mono text-[12px] ${e.result === "success" ? "text-text" : "text-danger"}`}>{e.action}</span>48 <span className="min-w-0 break-all text-muted">{e.target}</span>49 </li>50 ))}51 {audit.items.length === 0 && <li className="px-3 py-4 text-muted">No activity recorded yet.</li>}52 </ul>53 </section>54 </div>55 );56}57