diff --git a/src/app/actions/intake-links.ts b/src/app/actions/intake-links.ts new file mode 100644 index 0000000..14c1df7 --- /dev/null +++ b/src/app/actions/intake-links.ts @@ -0,0 +1,99 @@ +"use server"; + +import { revalidatePath } from "next/cache"; + +import { getAdminPb } from "@/lib/pb/admin"; +import { requireSession } from "@/lib/auth/session"; +import { generateIntakeToken } from "@/lib/intake/token"; +import type { IntakeLinkRecord } from "@/lib/pb/types"; + +/** Owner-only entry point. See src/app/actions/log-work.ts for the pattern. */ +async function requireOwner() { + const session = await requireSession(); + if (session.role !== "owner") throw new Error("Not found"); + return session; +} + +export async function createIntakeLinkAction(input: { + projectId: string; + label: string; + expiresAt?: string; + maxSubmissionsPerDay?: number; + notes?: string; +}): Promise<{ id: string; token: string; url: string }> { + await requireOwner(); + const pb = await getAdminPb(); + + const label = input.label.trim().slice(0, 80); + if (!label) throw new Error("A label is required"); + + const record = await pb.collection("intake_links").create({ + project: input.projectId, + label, + token: generateIntakeToken(), + active: true, + expires_at: input.expiresAt ?? "", + max_submissions_per_day: input.maxSubmissionsPerDay ?? 100, + submission_count: 0, + submission_count_today: 0, + submission_count_today_date: "", + last_submission_at: "", + notes: (input.notes ?? "").slice(0, 1000), + }); + + revalidatePath(`/admin/projects/${input.projectId}`); + return { + id: record.id, + token: record.token, + url: intakeUrlFor(record.token), + }; +} + +/** Rotate the token on an existing link — the old URL stops working + * immediately. */ +export async function rotateIntakeTokenAction(input: { + linkId: string; + projectId: string; +}): Promise<{ token: string; url: string }> { + await requireOwner(); + const pb = await getAdminPb(); + + const token = generateIntakeToken(); + await pb.collection("intake_links").update(input.linkId, { token }); + revalidatePath(`/admin/projects/${input.projectId}`); + + return { token, url: intakeUrlFor(token) }; +} + +export async function setIntakeLinkActiveAction(input: { + linkId: string; + projectId: string; + active: boolean; +}): Promise { + await requireOwner(); + const pb = await getAdminPb(); + + await pb.collection("intake_links").update(input.linkId, { + active: input.active, + }); + revalidatePath(`/admin/projects/${input.projectId}`); +} + +export async function setIntakeLinkNotesAction(input: { + linkId: string; + projectId: string; + notes: string; +}): Promise { + await requireOwner(); + const pb = await getAdminPb(); + + await pb.collection("intake_links").update(input.linkId, { + notes: input.notes.slice(0, 1000), + }); + revalidatePath(`/admin/projects/${input.projectId}`); +} + +function intakeUrlFor(token: string): string { + const base = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"; + return `${base}/intake/${token}`; +}