feat(intake-links): picker project list and resolver
Phase 9b Task 3. listPickerProjects reads the public-eligible projects on a client; resolveProjectForClient checks that a given projectId belongs to the client and is opted in. Both fail closed on any DB read error.
This commit is contained in:
parent
34c36eee15
commit
78c3b9e401
1 changed files with 55 additions and 0 deletions
55
src/lib/intake-links/client-projects.ts
Normal file
55
src/lib/intake-links/client-projects.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import "server-only";
|
||||
|
||||
import { getAdminPb } from "@/lib/pb/admin";
|
||||
import type { ProjectRecord } from "@/lib/pb/types";
|
||||
|
||||
/**
|
||||
* List every project on the given client that is eligible to appear
|
||||
* in the public intake picker.
|
||||
*
|
||||
* Eligible = (status = "active") AND (show_in_intake = true).
|
||||
*
|
||||
* Reads via the superuser connection. The route that uses this is a
|
||||
* public server component — no session, no user, so the only authority
|
||||
* is the superuser.
|
||||
*
|
||||
* Sorted by project name so the picker is alphabetical and predictable.
|
||||
*/
|
||||
export async function listPickerProjects(
|
||||
clientId: string
|
||||
): Promise<ProjectRecord[]> {
|
||||
const pb = await getAdminPb();
|
||||
return pb
|
||||
.collection("projects")
|
||||
.getFullList<ProjectRecord>({
|
||||
filter: `client = "${clientId}" && status = "active" && show_in_intake = true`,
|
||||
sort: "name",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a single project, scoped to the client. Used by the
|
||||
* `/intake/[token]/p/[projectId]` page to verify the URL's projectId
|
||||
* is one the picker could have shown.
|
||||
*
|
||||
* Returns null if the project is missing, archived, opted out, or
|
||||
* belongs to a different client. The caller 404s on null — same
|
||||
* wording as the wizard, so a hostile body cannot probe.
|
||||
*/
|
||||
export async function resolveProjectForClient(
|
||||
clientId: string,
|
||||
projectId: string
|
||||
): Promise<ProjectRecord | null> {
|
||||
const pb = await getAdminPb();
|
||||
try {
|
||||
const project = await pb
|
||||
.collection("projects")
|
||||
.getOne<ProjectRecord>(projectId);
|
||||
if (project.client !== clientId) return null;
|
||||
if (project.status !== "active") return null;
|
||||
if (!project.show_in_intake) return null;
|
||||
return project;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue