169 lines
4.3 KiB
TypeScript
169 lines
4.3 KiB
TypeScript
import { toast } from "react-toastify";
|
|
import { API_BASE_URL } from "../constant";
|
|
import { clearAuthCookies, refreshLoggedInFlag } from "./authCookies";
|
|
|
|
type ApiRequestOptions = Omit<RequestInit, "body"> & {
|
|
body?: unknown;
|
|
toast?: boolean;
|
|
successMessage?: string;
|
|
errorMessage?: string;
|
|
silent?: boolean;
|
|
};
|
|
|
|
let isRefreshing = false;
|
|
let refreshQueue: (() => void)[] = [];
|
|
const API_TOAST_OPTIONS = { autoClose: 2000 };
|
|
|
|
const hardLogout = () => {
|
|
clearAuthCookies();
|
|
toast.error(
|
|
"Your session has expired. Please sign in again.",
|
|
API_TOAST_OPTIONS
|
|
);
|
|
window.location.href = "/signin";
|
|
};
|
|
|
|
const refreshAccessToken = async () => {
|
|
const res = await fetch(`${API_BASE_URL}/api/auth/refresh`, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({}),
|
|
});
|
|
|
|
if (!res.ok) throw new Error("Refresh failed");
|
|
};
|
|
|
|
const waitForRefresh = (): Promise<void> =>
|
|
new Promise<void>((resolve) => refreshQueue.push(resolve));
|
|
|
|
const doRefresh = async () => {
|
|
if (isRefreshing) {
|
|
await waitForRefresh();
|
|
return;
|
|
}
|
|
isRefreshing = true;
|
|
try {
|
|
await refreshAccessToken();
|
|
refreshLoggedInFlag();
|
|
} finally {
|
|
isRefreshing = false;
|
|
refreshQueue.forEach((cb) => cb());
|
|
refreshQueue = [];
|
|
}
|
|
};
|
|
|
|
const request = async <T>(
|
|
path: string,
|
|
options: ApiRequestOptions = {},
|
|
_isRetry = false
|
|
): Promise<T> => {
|
|
const {
|
|
body,
|
|
headers,
|
|
toast: shouldToast = true,
|
|
successMessage,
|
|
errorMessage,
|
|
silent = false,
|
|
...rest
|
|
} = options;
|
|
|
|
const showToast = shouldToast && !silent;
|
|
|
|
const requestHeaders = new Headers(headers);
|
|
let requestBody: BodyInit | undefined;
|
|
|
|
if (body !== undefined) {
|
|
if (body instanceof FormData) {
|
|
requestBody = body;
|
|
} else {
|
|
requestHeaders.set("Content-Type", "application/json");
|
|
requestBody = JSON.stringify(body);
|
|
}
|
|
}
|
|
|
|
const url = path.startsWith("http")
|
|
? path
|
|
: `${API_BASE_URL.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`
|
|
}`;
|
|
|
|
const response = await fetch(url, {
|
|
...rest,
|
|
headers: requestHeaders,
|
|
credentials: "include",
|
|
body: requestBody,
|
|
});
|
|
|
|
if (response.status === 401 && !url.includes("/api/auth/signin")) {
|
|
if (!_isRetry) {
|
|
try {
|
|
await doRefresh();
|
|
return request<T>(path, options, true);
|
|
} catch {
|
|
hardLogout();
|
|
throw new Error("Session expired");
|
|
}
|
|
}
|
|
hardLogout();
|
|
throw new Error("Unauthorized");
|
|
}
|
|
|
|
const contentType = response.headers.get("content-type") || "";
|
|
const data =
|
|
response.status === 204
|
|
? null
|
|
: contentType.includes("application/json")
|
|
? await response.json()
|
|
: await response.text();
|
|
|
|
if (!response.ok) {
|
|
const backendError =
|
|
typeof data === "object" && data !== null
|
|
? (data as any).detail || (data as any).message
|
|
: undefined;
|
|
|
|
const finalErrorMessage = backendError || errorMessage || "Request failed";
|
|
|
|
if (showToast) {
|
|
toast.error(finalErrorMessage, API_TOAST_OPTIONS);
|
|
}
|
|
|
|
throw new Error(finalErrorMessage);
|
|
}
|
|
|
|
const method = String(rest.method || "GET").toUpperCase();
|
|
|
|
if (showToast && method !== "GET") {
|
|
const defaultSuccess =
|
|
method === "POST"
|
|
? "Created successfully"
|
|
: method === "PUT" || method === "PATCH"
|
|
? "Updated successfully"
|
|
: method === "DELETE"
|
|
? "Deleted successfully"
|
|
: "Action successful";
|
|
|
|
const msg =
|
|
(successMessage as string) ?? (data?.message as string) ?? defaultSuccess;
|
|
toast.success(msg, API_TOAST_OPTIONS);
|
|
}
|
|
|
|
return data as T;
|
|
};
|
|
|
|
export const apiClient = {
|
|
get: <T>(path: string, options?: ApiRequestOptions) =>
|
|
request<T>(path, { ...options, method: "GET" }),
|
|
|
|
post: <T>(path: string, body?: unknown, options?: ApiRequestOptions) =>
|
|
request<T>(path, { ...options, method: "POST", body }),
|
|
|
|
put: <T>(path: string, body?: unknown, options?: ApiRequestOptions) =>
|
|
request<T>(path, { ...options, method: "PUT", body }),
|
|
|
|
patch: <T>(path: string, body?: unknown, options?: ApiRequestOptions) =>
|
|
request<T>(path, { ...options, method: "PATCH", body }),
|
|
|
|
delete: <T>(path: string, options?: ApiRequestOptions) =>
|
|
request<T>(path, { ...options, method: "DELETE" }),
|
|
}; |