/** * What the edge should do with a request, decided in one pure function. * * Extracted from the middleware so it can be tested: the rules here are short * but they are the only thing standing in front of the administration, and one * of them has already been got wrong once. */ import type { Role } from './roles'; export type AccessDecision = | { kind: 'allow' } /** Send a browser to the sign-in screen. */ | { kind: 'redirect' } /** Answer a fetch with a status code rather than an HTML page. */ | { kind: 'deny'; status: 401 | 403; error: string }; export type AccessRequest = { pathname: string; method: string; role: Role | undefined; }; export function decideAccess({ pathname, method, role }: AccessRequest): AccessDecision { const isApi = pathname.startsWith('/api/admin'); if (!role) { // A fetch that receives an HTML login page is a confusing way to learn // you are signed out. return isApi ? { kind: 'deny', status: 401, error: 'Non authentifié' } : { kind: 'redirect' }; } const isRead = method === 'GET' || method === 'HEAD'; // Only API routes are gated by method. A React server action POSTs to the // URL of the page it lives on, so gating pages this way would refuse every // form to a read-only account — including the sign-out button. Pages carry // their own check inside the action, where the intent is known. if (isApi && !isRead && role !== 'admin') { return { kind: 'deny', status: 403, error: 'Compte en lecture seule' }; } return { kind: 'allow' }; }