From a591f352c82d32627b7d9d319bee2a6b14f547a0 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 21 Sep 2026 02:21:32 +0200 Subject: [PATCH] feat(intake): public intake page resolves token to project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 8a Task 10. Server component 404s on unknown/revoked/expired tokens with the same wording so the response cannot probe token existence. Only the link's label and the project name cross into the rendered HTML; counts, notes and the token itself stay owner-only. The IntakePublic component is a placeholder — Task 11 replaces it with the real wizard. Keeping the placeholder lets the build stay green between the two commits. --- src/app/intake/[token]/page.tsx | 56 +++++++++++++++++++++++++ src/components/intake/intake-public.tsx | 13 ++++++ 2 files changed, 69 insertions(+) create mode 100644 src/app/intake/[token]/page.tsx create mode 100644 src/components/intake/intake-public.tsx diff --git a/src/app/intake/[token]/page.tsx b/src/app/intake/[token]/page.tsx new file mode 100644 index 0000000..d1e9034 --- /dev/null +++ b/src/app/intake/[token]/page.tsx @@ -0,0 +1,56 @@ +import { notFound } from "next/navigation"; + +import { getAdminPb } from "@/lib/pb/admin"; +import { IntakePublic } from "@/components/intake/intake-public"; +import { resolveIntakeLink } from "@/lib/intake-links"; +import type { ProjectRecord } from "@/lib/pb/types"; + +/** + * The unauthenticated intake page. + * + * Resolves the token server-side. Unknown, revoked and expired tokens all + * 404 — same wording so the response cannot be used to probe existence. The + * link's label is shown to the submitter; nothing else from `intake_links` + * crosses into the rendered HTML (notes, counts and the token itself are + * owner-only). + */ +export default async function IntakePage({ + params, +}: { + params: Promise<{ token: string }>; +}) { + const { token } = await params; + + const link = await resolveIntakeLink(token); + if (!link || !link.active) notFound(); + + if (link.expires_at && link.expires_at < new Date().toISOString().slice(0, 10)) { + notFound(); + } + + const pb = await getAdminPb(); + const project = await pb + .collection("projects") + .getOne(link.project) + .catch(() => null); + + if (!project) notFound(); + + return ( +
+
+

+ Request to {project.name} +

+

+ {link.label} +

+
+ +
+ ); +} diff --git a/src/components/intake/intake-public.tsx b/src/components/intake/intake-public.tsx new file mode 100644 index 0000000..7aa10a4 --- /dev/null +++ b/src/components/intake/intake-public.tsx @@ -0,0 +1,13 @@ +"use client"; + +/** + * Placeholder. Task 11 replaces this with the full public wizard and adds + * the public BriefPreview component. + */ +export function IntakePublic(_props: { + projectId: string; + projectName: string; + token: string; +}) { + return

Loading…

; +}