File size: 2,703 Bytes
b24b684 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | import axios, { AxiosError, type InternalAxiosRequestConfig } from "axios";
const TOKEN_KEY = "lc_admin_token";
const REFRESH_KEY = "lc_admin_refresh";
export const tokenStore = {
get: () => localStorage.getItem(TOKEN_KEY),
getRefresh: () => localStorage.getItem(REFRESH_KEY),
set: (access: string, refresh?: string) => {
localStorage.setItem(TOKEN_KEY, access);
if (refresh) localStorage.setItem(REFRESH_KEY, refresh);
},
clear: () => {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(REFRESH_KEY);
},
};
const baseURL =
import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000/api/v1";
const api = axios.create({ baseURL, timeout: 20000 });
api.interceptors.request.use((config) => {
const token = tokenStore.get();
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
let onUnauthorized: (() => void) | null = null;
export const setUnauthorizedHandler = (fn: () => void) => {
onUnauthorized = fn;
};
// Single-flight refresh: queue concurrent 401s behind one refresh call.
let refreshing: Promise<string | null> | null = null;
async function doRefresh(): Promise<string | null> {
const refresh = tokenStore.getRefresh();
if (!refresh) return null;
try {
const res = await axios.post(`${baseURL}/auth/refresh`, {
refresh_token: refresh,
});
tokenStore.set(res.data.access_token, res.data.refresh_token);
return res.data.access_token as string;
} catch {
return null;
}
}
api.interceptors.response.use(
(res) => res,
async (error: AxiosError) => {
const original = error.config as InternalAxiosRequestConfig & {
_retry?: boolean;
};
const isAuthCall = original?.url?.includes("/auth/");
if (error.response?.status === 401 && original && !original._retry && !isAuthCall) {
original._retry = true;
refreshing = refreshing ?? doRefresh();
const newToken = await refreshing;
refreshing = null;
if (newToken) {
original.headers.Authorization = `Bearer ${newToken}`;
return api(original);
}
tokenStore.clear();
onUnauthorized?.();
}
return Promise.reject(error);
},
);
export function apiErrorMessage(error: unknown): string {
if (axios.isAxiosError(error)) {
const detail = error.response?.data?.detail;
if (typeof detail === "string") return detail;
if (Array.isArray(detail) && detail[0]?.msg) return detail[0].msg as string;
if (error.code === "ERR_NETWORK") return "Cannot reach the server.";
}
return "Something went wrong. Please try again.";
}
export default api;
|