Files
emaui/libs/api/src/lib/base-api/base-query-with-reauth.ts

65 lines
2.1 KiB
TypeScript

import { fetchBaseQuery, type BaseQueryFn } from "@reduxjs/toolkit/query/react";
import type { FetchArgs, FetchBaseQueryError } from "@reduxjs/toolkit/query";
import { resolveSessionContext } from "../session";
/**
* The one place the backend URL is resolved: VITE_BASE_API_URL from the env,
* falling back to the local dev API (3001 — the portal itself owns 3000 for
* the Fayda redirect). Import this; do not re-derive it.
*/
export const BASE_API_URL =
(import.meta as { env?: Record<string, string> }).env?.[
"VITE_BASE_API_URL"
]?.trim() || "http://localhost:3000/api";
let _onTokenExpired: (() => Promise<string>) | null = null;
let _onAuthFailure: (() => void) | null = null;
export function configureTokenRefresh(config: {
onTokenExpired: () => Promise<string>;
onAuthFailure: () => void;
}) {
_onTokenExpired = config.onTokenExpired;
_onAuthFailure = config.onAuthFailure;
}
export const baseQueryWithReauth: BaseQueryFn<
string | FetchArgs,
unknown,
FetchBaseQueryError
> = async (args, api, extraOptions) => {
const baseQuery = fetchBaseQuery({
baseUrl: BASE_API_URL,
prepareHeaders: (headers) => {
const { token, sessionHeaders } = resolveSessionContext(
api.getState() as { auth?: { token?: string } },
);
if (token) headers.set("Authorization", `Bearer ${token}`);
Object.entries(sessionHeaders).forEach(([k, v]) => headers.set(k, v));
return headers;
},
});
let result = await baseQuery(args, api, extraOptions);
if (result.error?.status === 401) {
if (_onTokenExpired) {
try {
await _onTokenExpired();
result = await baseQuery(args, api, extraOptions);
} catch (err) {
// Only a rejected refresh token ends the session. A network blip or a
// 5xx leaves the original 401 for the screen to report, rather than
// throwing the user out of a session that is still valid.
if ((err as { sessionExpired?: boolean })?.sessionExpired) {
_onAuthFailure?.();
}
}
} else {
_onAuthFailure?.();
}
}
return result;
};