import { resolveTokenFromStorage } from '../session'; import { BASE_API_URL } from './base-query-with-reauth'; /** * Opens an authenticated binary endpoint (a rendered PDF) in a new tab. * * RTK Query is not used here: `fetchBaseQuery` parses responses as JSON, and * a plain `window.open` sends no Authorization header, so a guarded document * endpoint would answer 401. Fetching to a blob keeps the bearer token on the * request and still gives the browser something it can display. */ export async function openAuthedDocument( path: string, fallbackName = 'document.pdf', ): Promise { const token = resolveTokenFromStorage(); const response = await fetch(`${BASE_API_URL}${path}`, { headers: token ? { Authorization: `Bearer ${token}` } : {}, }); if (!response.ok) { // The body carries the API's error key, which callers surface verbatim. let message = `${response.status}`; try { const body = await response.json(); message = body?.message ?? message; } catch { /* non-JSON error body — the status is all we have */ } throw new Error(message); } const blob = await response.blob(); const url = URL.createObjectURL(blob); const opened = window.open(url, '_blank', 'noopener'); if (!opened) { // Pop-up blocked: fall back to a direct download so the click still works. const anchor = document.createElement('a'); anchor.href = url; anchor.download = fallbackName; anchor.click(); } // Revoking immediately would race the new tab's load. setTimeout(() => URL.revokeObjectURL(url), 60_000); } /** * Downloads an authenticated endpoint straight to a file. * * Same reason as `openAuthedDocument` for bypassing RTK Query — `fetchBaseQuery` * would parse a CSV body as JSON — but a spreadsheet is something you save, not * something the browser can display, so this always takes the anchor path. * * The server names the file via `Content-Disposition`, and the API's CORS * config exposes that header along with `X-Total-Rows` and `X-Truncated`; those * two are returned so a caller can say when an export was cut short instead of * handing over a silently partial file. */ export async function downloadAuthedFile( path: string, fallbackName: string, ): Promise<{ rowCount: number | null; truncated: boolean }> { const token = resolveTokenFromStorage(); const response = await fetch(`${BASE_API_URL}${path}`, { headers: token ? { Authorization: `Bearer ${token}` } : {}, }); if (!response.ok) { let message = `${response.status}`; try { const body = await response.json(); message = body?.message ?? message; } catch { /* non-JSON error body — the status is all we have */ } throw new Error(message); } const blob = await response.blob(); const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = filenameFrom(response.headers) ?? fallbackName; anchor.click(); setTimeout(() => URL.revokeObjectURL(url), 60_000); const rows = response.headers.get('X-Total-Rows'); return { rowCount: rows === null ? null : Number(rows), truncated: response.headers.get('X-Truncated') === 'true', }; } /** `attachment; filename="vessel-register-2026-08-18.csv"` → the file name. */ function filenameFrom(headers: Headers): string | null { const disposition = headers.get('Content-Disposition'); if (!disposition) return null; const match = /filename="?([^";]+)"?/.exec(disposition); return match?.[1] ?? null; }