"use server"; // Managing a catalog from the console: settings, key, team, members, invitations, what it hosts, suspension. import { revalidatePath } from "next/cache"; import { api, unwrap } from "@/lib/api"; import { attempt, field, type ActionState } from "@/lib/actions"; /** Runs a change to a catalog, then shows its pages again. */ async function onCatalog(form: FormData, change: (id: string) => Promise): Promise { const id = field(form, "id"); return attempt(async () => { const notice = await change(id); revalidatePath(`/console/catalogs/${id}`); revalidatePath(`/catalogs/${id}`); revalidatePath("/console/catalogs"); revalidatePath("/catalogs"); return notice; }); } const pick = (v: string, options: readonly T[]): T | undefined => options.find((o) => o === v); export async function saveCatalog(_: ActionState, form: FormData): Promise { return onCatalog(form, async (id) => { unwrap( await api.catalogs({ id }).patch({ name: field(form, "name"), url: field(form, "url"), contact: field(form, "contact"), visibility: pick(field(form, "visibility"), ["public", "private"] as const), membership: pick(field(form, "membership"), ["open", "approval", "invite"] as const), maxRating: pick(field(form, "maxRating"), ["general", "adult"] as const), uploads: pick(field(form, "uploads"), ["anyone", "members", "team"] as const), mirrors: pick(field(form, "mirrors"), ["auto", "approve", "off"] as const), }), ); return "Saved."; }); } export async function rotateKey(_: ActionState, form: FormData): Promise { return onCatalog(form, async (id) => { const { key } = unwrap(await api.catalogs({ id }).key.post()); return `New key (shown once; put it in the catalog's CATALOG_KEY and restart it): ${key}`; }); } export async function setSuspended(_: ActionState, form: FormData): Promise { return onCatalog(form, async (id) => { if (field(form, "intent") === "reinstate") unwrap(await api.catalogs({ id }).reinstate.post()); else unwrap(await api.catalogs({ id }).suspend.post({ reason: field(form, "reason") })); }); } export async function addOperator(_: ActionState, form: FormData): Promise { return onCatalog(form, async (id) => { const username = field(form, "username"); unwrap(await api.catalogs({ id }).team({ username }).put({ role: "catalog-operator" })); return `${username} is an operator now.`; }); } export async function removeFromTeam(_: ActionState, form: FormData): Promise { return onCatalog(form, async (id) => { unwrap(await api.catalogs({ id }).team({ username: field(form, "username") }).delete({}, { query: { role: "catalog-operator" } })); }); } /** A member or request: approve, decline, add, ban, unban or remove. */ export async function decideMember(_: ActionState, form: FormData): Promise { return onCatalog(form, async (id) => { const member = api.catalogs({ id }).members({ username: field(form, "username") }); switch (field(form, "decision")) { case "approve": case "add": unwrap(await member.put({ status: "member" })); break; case "ban": unwrap(await member.put({ status: "banned" })); break; default: // decline, unban, remove unwrap(await member.delete()); } }); } export async function createInvite(_: ActionState, form: FormData): Promise { return onCatalog(form, async (id) => { const maxUses = Number(field(form, "maxUses")) || undefined; const expiresInDays = Number(field(form, "expiresInDays")) || undefined; const { invite } = unwrap(await api.catalogs({ id }).invites.post({ maxUses, expiresInDays })); return `Invitation link: ${invite.url}`; }); } export async function revokeInvite(_: ActionState, form: FormData): Promise { return onCatalog(form, async (id) => { unwrap(await api.catalogs({ id }).invites({ code: field(form, "code") }).delete()); }); } export async function delist(_: ActionState, form: FormData): Promise { return onCatalog(form, async (id) => { const product = field(form, "product"); unwrap(await api.catalogs({ id }).products({ product }).delete({}, { query: { reason: field(form, "reason") } })); revalidatePath(`/products/${product}`); }); } /** Someone joins a catalog (or asks to), with an invitation code if they have one; or leaves it. */ export async function membership(_: ActionState, form: FormData): Promise { return onCatalog(form, async (id) => { if (field(form, "intent") === "leave") { unwrap(await api.catalogs({ id }).join.delete()); revalidatePath("/library"); return "You left."; } const invite = field(form, "invite"); const { membership } = unwrap(await api.catalogs({ id }).join.post(invite ? { invite } : {})); revalidatePath("/library"); return membership === "member" ? "You're a member." : "Asked. The catalog's team will decide."; }); }