136 lines
4.0 KiB
TypeScript
136 lines
4.0 KiB
TypeScript
|
|
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<Record<string, string>> {
|
||
|
|
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<ApiError> {
|
||
|
|
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<T>(
|
||
|
|
path: string,
|
||
|
|
options: { method?: string; body?: unknown } = {},
|
||
|
|
): Promise<T> {
|
||
|
|
const headers: Record<string, string> = 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<T>(path: string, form: FormData): Promise<T> {
|
||
|
|
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<Blob> {
|
||
|
|
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<void> {
|
||
|
|
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, unknown>): 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}` : "";
|
||
|
|
}
|