Repository Public MIT
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