SPB Git

Repository Public MIT

page.tsx 3.7 KB 57 lines tsx

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

Theme