import type {NextRequest} from 'next/server'; import {upstreamRaw} from '@/services/api/apiClient'; import {withUpstream} from '@/features/auth/services/upstreamSession'; import {failResponse} from '@/shared/services/bff'; export const dynamic = 'force-dynamic'; /** * GET /api/faces?src=/api/faces/.jpg — an authenticated photo, proxied. * * A browser `` cannot send an Authorization header, and the platform's * own image URLs require one. The alternatives were fetch + createObjectURL + * revoke-on-unmount at every avatar — which leaks hundreds of copies of one * photograph on a screen left open all afternoon — or this: one hop through * the origin that already holds the token. * * ── Why `src` is validated rather than trusted ─────────────────────────── * An unchecked pass-through would be an open proxy that attaches the * merchant's bearer token to any URL an attacker can get into a page. Only * same-origin platform paths under /api/faces/ are forwarded. * * Every hand-out of a photo is written to the platform's audit log, so this * must be requested once per screen rather than once per component: two * components asking for the same face puts two rows in "who looked at my * customers" for one glance at one person. */ export async function GET(req: NextRequest) { const src = new URL(req.url).searchParams.get('src') ?? ''; // Relative, no traversal, and inside the faces namespace. Anything else is // refused rather than sanitised — a "cleaned" attacker-supplied URL is still // attacker-supplied. if (!src.startsWith('/api/faces/') || src.includes('..')) { return Response.json( {error: {code: 'bad_request', message: 'Not a valid image reference.'}}, {status: 400}, ); } try { const upstream = await withUpstream((token) => upstreamRaw({path: src, accessToken: token}), ); return new Response(upstream.body, { status: 200, headers: { 'content-type': upstream.headers.get('content-type') ?? 'image/jpeg', // Private: this is one merchant's customer, and a shared cache holding // it would serve it across tenants. 'cache-control': 'private, max-age=300', }, }); } catch (err) { return failResponse(err); } }