diff --git a/src/auth/access-snapshot.integration.test.ts b/src/auth/access-snapshot.integration.test.ts index 39aac1b..a49b05f 100644 --- a/src/auth/access-snapshot.integration.test.ts +++ b/src/auth/access-snapshot.integration.test.ts @@ -157,7 +157,8 @@ describe.skipIf(!baseURL || !databaseURL || !clientId || !audience)( { sub: "snapshot-integration-user", name: "Snapshot User", - email: "snapshot@identity.invalid", + // A sign-in placeholder is never sent. + email: null, }, ]), ); diff --git a/src/auth/contact-email.ts b/src/auth/contact-email.ts index 26ad99e..baa3b6a 100644 --- a/src/auth/contact-email.ts +++ b/src/auth/contact-email.ts @@ -74,10 +74,14 @@ export function directoryEmail(email: string, tokens: SignInToken[]) { return providerEmail("pn-entra", tokens) ?? contactEmail(email, tokens); } -/** `directoryEmail` for many people at once, keyed by user ID. Null when none is known. */ +/** + * `directoryEmail` for many people at once, keyed by user ID. Null when none is known. + * `everyone` reads every account instead of naming each person, for lists of all users. + */ export async function contactEmails( reader: Reader, people: { id: string; email: string }[], + scope: "listed" | "everyone" = "listed", ): Promise> { const tokens = people.length ? await reader @@ -98,21 +102,23 @@ export async function contactEmails( ) .where( and( - inArray( - account.userId, - people.map((person) => person.id), - ), + scope === "listed" + ? inArray( + account.userId, + people.map((person) => person.id), + ) + : undefined, inArray(account.providerId, EMAIL_PROVIDERS), ), ) : []; + const byUser = new Map(); + for (const token of tokens) + byUser.set(token.userId, [...(byUser.get(token.userId) ?? []), token]); return new Map( people.map((person) => [ person.id, - directoryEmail( - person.email, - tokens.filter((token) => token.userId === person.id), - ) ?? null, + directoryEmail(person.email, byUser.get(person.id) ?? []) ?? null, ]), ); } diff --git a/src/routes/api/internal/esign-users.ts b/src/routes/api/internal/esign-users.ts index 2424eec..c3e1799 100644 --- a/src/routes/api/internal/esign-users.ts +++ b/src/routes/api/internal/esign-users.ts @@ -1,6 +1,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { asc } from "drizzle-orm"; import { snapshotClientId } from "@/auth/access-snapshot-token"; +import { contactEmails } from "@/auth/contact-email"; import { projectionForClient } from "@/auth/access-projections"; import { db } from "@/db"; import { user } from "@/db/schema"; @@ -8,7 +9,10 @@ import { env } from "@/env"; const headers = { "Cache-Control": "private, no-store", Vary: "Authorization" }; -/** Registered identities for the backend's e-signing picker; access is checked by the backend's live snapshot. */ +/** + * Registered identities for the backend's e-signing picker; access is checked by the backend's live snapshot. + * The email is the one administrators see (PoliNetwork first), never a sign-in placeholder. + */ export const Route = createFileRoute("/api/internal/esign-users")({ server: { handlers: { @@ -20,10 +24,18 @@ export const Route = createFileRoute("/api/internal/esign-users")({ if (projectionForClient(clientId) !== "backend") return Response.json({ error: "Forbidden" }, { status: 403, headers }); const people = await db - .select({ sub: user.id, name: user.name, email: user.email }) + .select({ id: user.id, name: user.name, email: user.email }) .from(user) .orderBy(asc(user.name), asc(user.id)); - return Response.json(people, { headers }); + const emails = await contactEmails(db, people, "everyone"); + return Response.json( + people.map((person) => ({ + sub: person.id, + name: person.name, + email: emails.get(person.id) ?? null, + })), + { headers }, + ); }, }, },