feat(intake): public intake page resolves token to project

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.
This commit is contained in:
Dave 2026-09-21 02:21:32 +02:00
parent 5137a6dda2
commit a591f352c8
2 changed files with 69 additions and 0 deletions

View file

@ -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<ProjectRecord>(link.project)
.catch(() => null);
if (!project) notFound();
return (
<main className="mx-auto max-w-xl space-y-4 p-6">
<header className="space-y-1">
<p className="text-xs uppercase tracking-wide text-text-muted">
Request to {project.name}
</p>
<h1 className="text-lg font-semibold text-text-primary">
{link.label}
</h1>
</header>
<IntakePublic
projectId={project.id}
projectName={project.name}
token={token}
/>
</main>
);
}

View file

@ -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 <p className="text-sm text-text-muted">Loading…</p>;
}