91 lines
2.3 KiB
TypeScript
91 lines
2.3 KiB
TypeScript
import createClient from "openapi-fetch";
|
|
import createQueryClient from "openapi-react-query";
|
|
|
|
import type { paths } from "./generated/schema";
|
|
|
|
const HTTP_UNAUTHORIZED = 401;
|
|
|
|
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:3000";
|
|
|
|
type AuthHandlers = {
|
|
accessToken: () => string | null;
|
|
refresh: () => Promise<boolean>;
|
|
clear: () => void;
|
|
};
|
|
|
|
let auth: AuthHandlers | null = null;
|
|
let hasAuthenticatedSession = false;
|
|
const authSessionListeners = new Set<() => void>();
|
|
|
|
function setAuthenticatedSession(next: boolean) {
|
|
if (next === hasAuthenticatedSession) return;
|
|
hasAuthenticatedSession = next;
|
|
for (const listener of authSessionListeners) listener();
|
|
}
|
|
|
|
function configureAuthClient(handlers: AuthHandlers | null) {
|
|
auth = handlers;
|
|
setAuthenticatedSession(Boolean(auth?.accessToken()));
|
|
}
|
|
|
|
function getAuthenticatedSessionSnapshot() {
|
|
return hasAuthenticatedSession;
|
|
}
|
|
|
|
function subscribeAuthenticatedSession(listener: () => void) {
|
|
authSessionListeners.add(listener);
|
|
return () => authSessionListeners.delete(listener);
|
|
}
|
|
|
|
async function authenticatedFetch(request: Request): Promise<Response> {
|
|
const isAuthRequest = new URL(request.url).pathname.startsWith(
|
|
"/api/v1/auth/",
|
|
);
|
|
const token = auth?.accessToken();
|
|
const withToken = (source: Request, value: string) => {
|
|
const headers = new Headers(source.headers);
|
|
headers.set("Authorization", `Bearer ${value}`);
|
|
return new Request(source, { headers });
|
|
};
|
|
const authorized =
|
|
token && !isAuthRequest ? withToken(request, token) : request;
|
|
const retryRequest = authorized.clone();
|
|
const response = await fetch(authorized);
|
|
if (
|
|
response.status !== HTTP_UNAUTHORIZED ||
|
|
isAuthRequest ||
|
|
!auth ||
|
|
(
|
|
await response
|
|
.clone()
|
|
.json()
|
|
.catch(() => null)
|
|
)?.code !== "UNAUTHORIZED"
|
|
)
|
|
return response;
|
|
if (!(await auth.refresh())) {
|
|
auth.clear();
|
|
return response;
|
|
}
|
|
const refreshedToken = auth.accessToken();
|
|
return refreshedToken
|
|
? fetch(withToken(retryRequest, refreshedToken))
|
|
: response;
|
|
}
|
|
|
|
const apiClient = createClient<paths>({
|
|
baseUrl: apiBaseUrl,
|
|
credentials: "include",
|
|
fetch: authenticatedFetch,
|
|
});
|
|
|
|
const apiQueryClient = createQueryClient(apiClient);
|
|
|
|
export {
|
|
apiClient,
|
|
apiQueryClient,
|
|
configureAuthClient,
|
|
getAuthenticatedSessionSnapshot,
|
|
subscribeAuthenticatedSession,
|
|
};
|