import { InteractionRequiredAuthError, PublicClientApplication, } from "@azure/msal-browser"; import { config } from "../config"; export const msalInstance = config.authMode === "entra" ? new PublicClientApplication({ auth: { clientId: config.clientId, authority: `https://login.microsoftonline.com/${config.tenantId}`, redirectUri: window.location.origin, postLogoutRedirectUri: window.location.origin, }, cache: { cacheLocation: "sessionStorage" }, }) : null; const DEV_USER_KEY = "pesco-ncr-dev-user"; export function getDevUser(): string { return localStorage.getItem(DEV_USER_KEY) || "admin@pescoinc.biz"; } export function setDevUser(email: string): void { localStorage.setItem(DEV_USER_KEY, email); } async function authHeaders(): Promise> { if (config.authMode === "dev") { return { "X-Dev-User": getDevUser() }; } const instance = msalInstance!; const account = instance.getActiveAccount() ?? instance.getAllAccounts()[0]; if (!account) { await instance.loginRedirect({ scopes: [config.apiScope] }); throw new Error("Redirecting to sign in…"); } try { const result = await instance.acquireTokenSilent({ scopes: [config.apiScope], account, }); return { Authorization: `Bearer ${result.accessToken}` }; } catch (err) { if (err instanceof InteractionRequiredAuthError) { await instance.acquireTokenRedirect({ scopes: [config.apiScope], account }); } throw err; } } export class ApiError extends Error { status: number; constructor(status: number, detail: string) { super(detail); this.status = status; } } async function parseError(resp: Response): Promise { let detail = `Request failed (${resp.status})`; try { const body = await resp.json(); if (typeof body.detail === "string") detail = body.detail; else if (Array.isArray(body.detail) && body.detail[0]?.msg) detail = body.detail .map((d: { loc?: unknown[]; msg: string }) => d.msg) .join("; "); } catch { /* non-JSON body */ } return new ApiError(resp.status, detail); } export async function api( path: string, options: { method?: string; body?: unknown } = {}, ): Promise { const headers: Record = await authHeaders(); const init: RequestInit = { method: options.method ?? "GET", headers }; if (options.body !== undefined) { headers["Content-Type"] = "application/json"; init.body = JSON.stringify(options.body); } const resp = await fetch(path, init); if (!resp.ok) throw await parseError(resp); if (resp.status === 204) return undefined as T; return (await resp.json()) as T; } export async function apiUpload(path: string, form: FormData): Promise { const headers = await authHeaders(); const resp = await fetch(path, { method: "POST", headers, body: form }); if (!resp.ok) throw await parseError(resp); return (await resp.json()) as T; } export async function apiBlob(path: string): Promise { const headers = await authHeaders(); const resp = await fetch(path, { headers }); if (!resp.ok) throw await parseError(resp); return resp.blob(); } /** Fetch a protected file and hand it to the browser (download or new tab). */ export async function openBlob( path: string, filename: string, mode: "download" | "open", ): Promise { const blob = await apiBlob(path); const url = URL.createObjectURL(blob); if (mode === "open") { window.open(url, "_blank"); } else { const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); } setTimeout(() => URL.revokeObjectURL(url), 60_000); } export function buildQuery(params: Record): string { const q = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== null && value !== "") { q.set(key, String(value)); } } const s = q.toString(); return s ? `?${s}` : ""; }