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:
parent
5137a6dda2
commit
a591f352c8
2 changed files with 69 additions and 0 deletions
56
src/app/intake/[token]/page.tsx
Normal file
56
src/app/intake/[token]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
13
src/components/intake/intake-public.tsx
Normal file
13
src/components/intake/intake-public.tsx
Normal 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>;
|
||||
}
|
||||
Loading…
Reference in a new issue