109 lines
2.9 KiB
TypeScript
109 lines
2.9 KiB
TypeScript
import axios from "axios";
|
|
|
|
const baseURL = (import.meta.env.VITE_BACKEND_URL as string) || "http://localhost:8080";
|
|
const REFRESH_ENDPOINT = "/api/auth/refresh";
|
|
const ACCESS_TOKEN_COOKIE = "access_token";
|
|
const REFRESH_THRESHOLD_MS = 60_000;
|
|
|
|
const api = axios.create({
|
|
baseURL,
|
|
withCredentials: true,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
});
|
|
|
|
type JwtPayload = {
|
|
exp?: number;
|
|
};
|
|
|
|
const getCookieValue = (name: string): string | undefined => {
|
|
if (typeof document === "undefined") {
|
|
return undefined;
|
|
}
|
|
|
|
const cookies = document.cookie?.split(";") ?? [];
|
|
const cookie = cookies
|
|
.map((c) => c.trim())
|
|
.find((c) => c.startsWith(`${name}=`));
|
|
|
|
return cookie ? decodeURIComponent(cookie.split("=")[1]) : undefined;
|
|
};
|
|
|
|
const decodeJwtPayload = (token: string): JwtPayload | null => {
|
|
try {
|
|
const [, payload] = token.split(".");
|
|
if (!payload) return null;
|
|
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
const decoded = typeof atob === "function" ? atob(base64) : "";
|
|
return decoded ? JSON.parse(decoded) : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const isTokenExpiringSoon = (token?: string) => {
|
|
if (!token) return false;
|
|
const payload = decodeJwtPayload(token);
|
|
if (!payload?.exp) return false;
|
|
|
|
const expiresAt = payload.exp * 1000;
|
|
const millisUntilExpiry = expiresAt - Date.now();
|
|
return millisUntilExpiry <= REFRESH_THRESHOLD_MS;
|
|
};
|
|
|
|
export const setAuthToken = (token?: string) => {
|
|
if (token) {
|
|
api.defaults.headers.common["Authorization"] = `Bearer ${token}`;
|
|
} else {
|
|
delete api.defaults.headers.common["Authorization"];
|
|
}
|
|
};
|
|
|
|
let refreshPromise: Promise<void> | null = null;
|
|
|
|
const refreshAccessToken = async () => {
|
|
if (!refreshPromise) {
|
|
refreshPromise = api
|
|
.post(REFRESH_ENDPOINT)
|
|
.then(() => {
|
|
const token = getCookieValue(ACCESS_TOKEN_COOKIE);
|
|
setAuthToken(token);
|
|
})
|
|
.catch((error) => {
|
|
throw error;
|
|
})
|
|
.finally(() => {
|
|
refreshPromise = null;
|
|
});
|
|
}
|
|
|
|
return refreshPromise;
|
|
};
|
|
|
|
api.interceptors.request.use(async (config) => {
|
|
const isRefreshCall = config.url?.includes(REFRESH_ENDPOINT);
|
|
|
|
if (!isRefreshCall) {
|
|
const token = getCookieValue(ACCESS_TOKEN_COOKIE);
|
|
if (isTokenExpiringSoon(token)) {
|
|
try {
|
|
await refreshAccessToken();
|
|
} catch (error) {
|
|
refreshPromise = null;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
const updatedToken = getCookieValue(ACCESS_TOKEN_COOKIE);
|
|
if (updatedToken) {
|
|
config.headers = config.headers ?? {};
|
|
config.headers.Authorization = `Bearer ${updatedToken}`;
|
|
}
|
|
}
|
|
|
|
return config;
|
|
});
|
|
|
|
export default api;
|