SPB Git

Repository Public MIT

actions.ts 6.6 KB 129 lines typescript

1"use server";2import { revalidatePath } from "next/cache";3import { redirect } from "next/navigation";4import type { ActionState } from "@/components/action-form";5import { requireSession } from "@/lib/session";6import { changePassword, createToken, hasRecentReauth, reauthenticate, revokeToken, rotateToken } from "@/server/auth";7import { moderateArtifact, resolveReport } from "@/server/community";8import type { Ctx } from "@/server/context";9import { ApiError } from "@/server/errors";10import { reviewRelease, revokeRelease, updateReleaseNotes } from "@/server/publish";11import { createArtifact, setArtifactState, updateArtifact } from "@/server/registry";1213const s = (d: FormData, k: string) => String(d.get(k) ?? "").trim();1415/** Sensitive actions need a password entered within the last 15 minutes (step-up authentication). */16async function stepUp(ctx: Ctx, data: FormData) {17  if (hasRecentReauth(ctx.principal)) return;18  const pw = String(data.get("password") ?? "");19  if (!pw) throw new ApiError(403, "reauth_required", "Enter your password to confirm this action.");20  await reauthenticate(ctx, pw);21}2223async function run(data: FormData | null, fn: (ctx: Ctx) => Promise<ActionState | void>, opts: { sensitive?: boolean; path?: string } = {}): Promise<ActionState> {24  const { ctx } = await requireSession();25  try {26    if (opts.sensitive && data) await stepUp(ctx, data);27    const out = await fn(ctx);28    revalidatePath(opts.path ?? "/dashboard", "layout");29    return out ?? { ok: "Done." };30  } catch (e) {31    if (e instanceof ApiError) return { error: e.message };32    throw e;33  }34}3536export async function changePasswordAction(_p: ActionState, d: FormData): Promise<ActionState> {37  const { ctx } = await requireSession({ allowPasswordChange: true });38  try {39    if (String(d.get("next") ?? "") !== String(d.get("confirm") ?? "")) return { error: "The two new passwords do not match." };40    await changePassword(ctx, String(d.get("current") ?? ""), String(d.get("next") ?? ""));41  } catch (e) {42    if (e instanceof ApiError) return { error: e.message };43    throw e;44  }45  redirect("/dashboard?password=changed");46}4748export async function createTokenAction(_p: ActionState, d: FormData): Promise<ActionState> {49  return run(d, async (ctx) => {50    const days = Number(s(d, "days") || "90");51    const { token, row } = await createToken(ctx, {52      name: s(d, "name"), scopes: d.getAll("scopes").map(String), namespaces: d.getAll("namespaces").map(String),53      expiresInHours: s(d, "hours") ? Number(s(d, "hours")) : days * 24, ipAllowlist: s(d, "ip") ? s(d, "ip").split(/[\s,]+/).filter(Boolean) : [],54    });55    return { ok: `Token “${row.name}” (${row.prefix}) created. It expires ${row.expires_at.toISOString().slice(0, 10)}.`, secret: token };56  }, { sensitive: true });57}58export async function revokeTokenAction(_p: ActionState, d: FormData): Promise<ActionState> {59  return run(d, async (ctx) => {60    await revokeToken(ctx, s(d, "id"), "revoked from dashboard");61    return { ok: "Token revoked. It stops working immediately." };62  });63}64export async function rotateTokenAction(_p: ActionState, d: FormData): Promise<ActionState> {65  return run(d, async (ctx) => {66    const { token, row } = await rotateToken(ctx, s(d, "id"));67    return { ok: `Rotated. The previous secret is revoked; new prefix ${row.prefix}.`, secret: token };68  }, { sensitive: true });69}7071export async function createArtifactAction(_p: ActionState, d: FormData): Promise<ActionState> {72  let id = "";73  const res = await run(d, async (ctx) => {74    const visibility = s(d, "visibility") || "private";75    const a = await createArtifact(ctx, { namespace: s(d, "namespace"), slug: s(d, "slug"), type: s(d, "type"), summary: s(d, "summary"), visibility, confirmPublic: d.get("confirmPublic") === "on" });76    id = a.id;77  }, { sensitive: s(d, "visibility") === "public" });78  if (id) redirect(`/dashboard/artifacts/${id}`);79  return res;80}81export async function updateArtifactAction(_p: ActionState, d: FormData): Promise<ActionState> {82  const visibility = s(d, "visibility");83  return run(d, async (ctx) => {84    await updateArtifact(ctx, s(d, "id"), {85      summary: s(d, "summary"), repository: s(d, "repository") || null, homepage: s(d, "homepage") || null, issues: s(d, "issues") || null,86      keywords: s(d, "keywords") ? s(d, "keywords").split(/[\s,]+/).filter(Boolean) : [], visibility, confirmPublic: d.get("confirmPublic") === "on",87    });88    return { ok: "Metadata saved." };89  }, { sensitive: visibility !== "private" && s(d, "previousVisibility") !== visibility });90}91export async function artifactStateAction(_p: ActionState, d: FormData): Promise<ActionState> {92  const action = s(d, "action") as "deprecate" | "undeprecate" | "archive";93  return run(d, async (ctx) => {94    await setArtifactState(ctx, s(d, "id"), action, { reason: s(d, "reason"), confirm: d.get("confirm") === "on" || action === "undeprecate" });95    return { ok: `Artifact ${action}d.` };96  }, { sensitive: action !== "undeprecate" });97}98export async function revokeReleaseAction(_p: ActionState, d: FormData): Promise<ActionState> {99  return run(d, async (ctx) => {100    await revokeRelease(ctx, s(d, "releaseId"), { reason: s(d, "reason"), confirm: d.get("confirm") === "on" });101    return { ok: "Release revoked. Downloads are blocked; the record remains." };102  }, { sensitive: true });103}104export async function releaseNotesAction(_p: ActionState, d: FormData): Promise<ActionState> {105  return run(d, async (ctx) => {106    await updateReleaseNotes(ctx, s(d, "releaseId"), { releaseNotes: String(d.get("releaseNotes") ?? "") || null });107    return { ok: "Release notes saved." };108  });109}110111export async function resolveReportAction(_p: ActionState, d: FormData): Promise<ActionState> {112  return run(d, async (ctx) => {113    await resolveReport(ctx, s(d, "id"), { state: s(d, "state"), note: s(d, "note") });114    return { ok: "Report updated." };115  });116}117export async function reviewReleaseAction(_p: ActionState, d: FormData): Promise<ActionState> {118  return run(d, async (ctx) => {119    await reviewRelease(ctx, s(d, "releaseId"), { decision: s(d, "decision"), reason: s(d, "reason") });120    return { ok: `Release ${s(d, "decision") === "approve" ? "approved and published" : "rejected"}.` };121  }, { sensitive: true });122}123export async function moderateArtifactAction(_p: ActionState, d: FormData): Promise<ActionState> {124  return run(d, async (ctx) => {125    await moderateArtifact(ctx, s(d, "artifactId"), { action: s(d, "action"), reason: s(d, "reason"), confirm: d.get("confirm") === "on", reportId: s(d, "reportId") || undefined });126    return { ok: `Moderation action “${s(d, "action")}” applied.` };127  }, { sensitive: true });128}129
Files main
Menu

Theme