SPB Git

Repository Public MIT

page.tsx 6.6 KB 92 lines tsx

1import Link from "next/link";2import { notFound } from "next/navigation";3import type { Finding } from "@skills/registry-core";4import { ActionForm, Field } from "@/components/action-form";5import { dateTime } from "@/lib/format";6import { requireSession } from "@/lib/session";7import { listQuarantined, listReports } from "@/server/community";8import { moderateArtifactAction, resolveReportAction, reviewReleaseAction } from "../actions";910export const metadata = { title: "Moderation" };11const PW = "Needed only if you signed in more than 15 minutes ago.";1213export default async function Page() {14  const { ctx, isAdmin } = await requireSession();15  if (!isAdmin) notFound();16  const [reports, quarantine] = await Promise.all([listReports(ctx), listQuarantined(ctx)]);17  return (18    <div className="space-y-12">19      <header>20        <h1 className="text-2xl font-semibold tracking-tight">Moderation</h1>21        <p className="mt-2 max-w-2xl text-sm text-muted">Platform administrators only. Every decision needs a written reason and is recorded in the audit log and the moderation ledger.</p>22      </header>2324      <section aria-labelledby="q-h">25        <h2 id="q-h" className="mb-3 text-base font-semibold">Security review queue ({quarantine.length})</h2>26        {quarantine.length === 0 ? <p className="text-sm text-muted">No release is waiting for review.</p> : (27          <ul className="space-y-4">28            {quarantine.map((r) => {29              const findings = ((r.findings as Finding[]) ?? []).filter((f) => f.severity !== "info");30              return (31                <li key={r.id} className="card p-5">32                  <p className="font-mono text-sm">@{r.namespace}/{r.slug}@{r.version} <span className="text-[11px] text-faint">· {dateTime(r.published_at)} · sha256 {r.sha256.slice(0, 16)}…</span></p>33                  <ul className="mt-3 divide-y divide-line rounded-lg border border-line text-sm">34                    {findings.map((f, i) => (<li key={i} className="flex flex-wrap gap-x-3 px-3 py-2"><span className="w-16 font-mono text-[11px] uppercase text-warn">{f.severity}</span><span className="min-w-0 flex-1 text-muted">{f.message}</span><code className="font-mono text-[11px] text-faint">{f.path}{f.line ? `:${f.line}` : ""}</code></li>))}35                  </ul>36                  <p className="mt-3 text-xs text-muted">Inspect the files through the API with your session before deciding: <code className="font-mono">/api/v1/artifacts/{r.namespace}/{r.slug}/versions/{r.version}</code>. Never run the package.</p>37                  <ActionForm action={reviewReleaseAction} submit="Record decision" className="mt-4 grid gap-3 sm:grid-cols-[10rem_minmax(0,1fr)_12rem]">38                    <input type="hidden" name="releaseId" value={r.id} />39                    <Field label="Decision" name="decision"><select id="f-decision" name="decision" className="input"><option value="approve">Approve and publish</option><option value="reject">Reject (revoke)</option></select></Field>40                    <Field label="Review note" name="reason" required minLength={5} maxLength={1000} />41                    <Field label="Password" name="password" type="password" autoComplete="current-password" hint={PW} />42                  </ActionForm>43                </li>44              );45            })}46          </ul>47        )}48      </section>4950      <section aria-labelledby="r-h">51        <h2 id="r-h" className="mb-3 text-base font-semibold">Reports ({reports.filter((r) => r.state === "open").length} open)</h2>52        {reports.length === 0 ? <p className="text-sm text-muted">No reports have been filed.</p> : (53          <ul className="space-y-4">54            {reports.map((r) => (55              <li key={r.id} className="card p-5">56                <p className="flex flex-wrap items-baseline gap-x-3 font-mono text-sm">57                  <Link className="link" href={`/@${r.namespace}/${r.slug}`}>@{r.namespace}/{r.slug}{r.version ? `@${r.version}` : ""}</Link>58                  <span className="chip">{r.category}</span>59                  <span className={`text-[11px] ${r.state === "open" ? "text-warn" : "text-faint"}`}>{r.state}</span>60                  <span className="text-[11px] text-faint">{dateTime(r.created_at)}</span>61                </p>62                <p className="mt-3 whitespace-pre-wrap break-words text-sm text-muted">{r.details}</p>63                {r.reporter_contact && <p className="mt-2 font-mono text-[11px] text-faint">contact: {r.reporter_contact}</p>}64                {r.resolution_note && <p className="mt-2 text-sm text-text">Resolution: {r.resolution_note}</p>}65                {r.state !== "resolved" && r.state !== "dismissed" && (66                  <div className="mt-4 grid gap-6 lg:grid-cols-2">67                    <ActionForm action={resolveReportAction} submit="Update report" className="space-y-3">68                      <input type="hidden" name="id" value={r.id} />69                      <Field label="Outcome" name="state"><select id="f-state" name="state" className="input"><option value="triaged">Triaged (keep open)</option><option value="resolved">Resolved</option><option value="dismissed">Dismissed</option></select></Field>70                      <Field label="Note" name="note" required minLength={5} maxLength={2000} />71                    </ActionForm>72                    <details>73                      <summary className="cursor-pointer text-sm text-danger">Act on the artifact…</summary>74                      <ActionForm action={moderateArtifactAction} submit="Apply" danger className="mt-3 space-y-3" confirm="Apply this moderation action?">75                        <input type="hidden" name="artifactId" value={r.artifact_id} /><input type="hidden" name="reportId" value={r.id} />76                        <Field label="Action" name="action"><select id="f-action" name="action" className="input"><option value="takedown">Take down (hide from everyone)</option><option value="restore">Restore</option><option value="verify-source">Mark source provenance verified</option><option value="unverify-source">Remove source verification</option></select></Field>77                        <Field label="Reason" name="reason" required minLength={5} maxLength={1000} />78                        <label className="flex items-start gap-2 text-sm text-muted"><input type="checkbox" name="confirm" className="mt-1" /> Confirm (required for takedown).</label>79                        <Field label="Password" name="password" type="password" autoComplete="current-password" hint={PW} />80                      </ActionForm>81                    </details>82                  </div>83                )}84              </li>85            ))}86          </ul>87        )}88      </section>89    </div>90  );91}92
Files main
Menu

Theme