import { cache } from "react"; import { cookies } from "next/headers"; import { getClientIdentityByToken, type ClientIdentity } from "@/lib/client-view"; import { sessionCookieName, verifySessionValue, type ClientSession } from "@/lib/client-session"; export type GateResult = | { client: null; session: null } | { client: ClientIdentity; session: ClientSession | null }; // cache(): layout e page risolvono lo stesso cliente nella stessa richiesta // senza fare due giri di query. const resolveClient = cache(getClientIdentityByToken); /** * Stato di accesso al portale per un token/slug. * * ⚠️ Va chiamata all'INIZIO della page, PRIMA di qualsiasi query sui dati del * progetto — e la page deve tornare il gate se `session` è null. * * Metterla solo nel layout NON basta e non è una svista: nell'App Router il * segmento `page` viene renderizzato in parallelo al layout, quindi un layout * che non renderizza `{children}` nasconde la dashboard a schermo ma la sua * query è già partita e il payload RSC finisce comunque nell'HTML. Verificato: * fasi, task e pagamenti erano leggibili nel sorgente della pagina di accesso. */ export async function getClientGate(tokenOrSlug: string): Promise { const client = await resolveClient(tokenOrSlug); if (!client) return { client: null, session: null }; const cookieStore = await cookies(); const session = await verifySessionValue( cookieStore.get(sessionCookieName(client.id))?.value, client.id ); // Revoca admin: una sessione firmata prima di sessions_valid_from non vale più. const revoked = session !== null && client.sessions_valid_from !== null && session.iat < client.sessions_valid_from.getTime(); return { client, session: revoked ? null : session }; }