Initial commit
This commit is contained in:
127
meal-plan-frontend/src/api/axiosInstance.ts
Normal file
127
meal-plan-frontend/src/api/axiosInstance.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import axios, {
|
||||
AxiosError,
|
||||
type AxiosInstance,
|
||||
type InternalAxiosRequestConfig,
|
||||
} from "axios";
|
||||
import {
|
||||
clearAuth,
|
||||
getAccessToken,
|
||||
getRefreshToken,
|
||||
setTokens,
|
||||
} from "@/store/authStore";
|
||||
import type { TokenResponse } from "@/types/auth.types";
|
||||
|
||||
// All calls go through "/api"; nginx (prod) and the Vite dev proxy forward it to the API.
|
||||
export const api: AxiosInstance = axios.create({
|
||||
baseURL: "/api",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
// A bare client for the refresh call so it never triggers the response interceptor (no loops).
|
||||
const refreshClient = axios.create({
|
||||
baseURL: "/api",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
api.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||
const token = getAccessToken();
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// --- Silent refresh handling -------------------------------------------------
|
||||
// While a refresh is in flight, queue concurrent 401s and replay them once done.
|
||||
let isRefreshing = false;
|
||||
let pendingQueue: Array<(token: string | null) => void> = [];
|
||||
|
||||
function flushQueue(token: string | null): void {
|
||||
pendingQueue.forEach((resolve) => resolve(token));
|
||||
pendingQueue = [];
|
||||
}
|
||||
|
||||
function redirectToLogin(): void {
|
||||
clearAuth();
|
||||
const path = window.location.pathname;
|
||||
if (path !== "/login" && path !== "/register") {
|
||||
window.location.assign("/login");
|
||||
}
|
||||
}
|
||||
|
||||
async function performRefresh(): Promise<string | null> {
|
||||
const refreshToken = getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await refreshClient.post<TokenResponse>("/auth/refresh", {
|
||||
refreshToken,
|
||||
});
|
||||
setTokens(data);
|
||||
return data.accessToken;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error: AxiosError) => {
|
||||
const original = error.config as
|
||||
| (InternalAxiosRequestConfig & { _retry?: boolean })
|
||||
| undefined;
|
||||
|
||||
const status = error.response?.status;
|
||||
const isAuthEndpoint =
|
||||
original?.url?.includes("/auth/login") ||
|
||||
original?.url?.includes("/auth/register") ||
|
||||
original?.url?.includes("/auth/refresh");
|
||||
|
||||
if (status !== 401 || !original || original._retry || isAuthEndpoint) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
original._retry = true;
|
||||
|
||||
if (isRefreshing) {
|
||||
// Wait for the in-flight refresh, then retry (or fail) with its result.
|
||||
return new Promise((resolve, reject) => {
|
||||
pendingQueue.push((token) => {
|
||||
if (!token) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
original.headers.Authorization = `Bearer ${token}`;
|
||||
resolve(api(original));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
isRefreshing = true;
|
||||
const newToken = await performRefresh();
|
||||
isRefreshing = false;
|
||||
flushQueue(newToken);
|
||||
|
||||
if (!newToken) {
|
||||
redirectToLogin();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
original.headers.Authorization = `Bearer ${newToken}`;
|
||||
return api(original);
|
||||
},
|
||||
);
|
||||
|
||||
/** Extracts a user-facing message from an API error response. */
|
||||
export function extractApiError(error: unknown, fallback = "Something went wrong."): string {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const data = error.response?.data as { error?: string; title?: string } | undefined;
|
||||
return data?.error ?? data?.title ?? error.message ?? fallback;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
Reference in New Issue
Block a user