2025-12-16 16:25:56 +00:00
|
|
|
import axios from "axios";
|
2025-12-16 13:40:08 +00:00
|
|
|
|
2025-12-16 16:25:56 +00:00
|
|
|
const baseURL = (import.meta.env.VITE_BACKEND_URL as string) || "http://localhost:8080";
|
|
|
|
|
const REFRESH_ENDPOINT = "/api/auth/refresh";
|
2025-12-16 18:36:02 +00:00
|
|
|
const LOGOUT_ENDPOINT = "/api/auth/logout";
|
2025-12-16 16:25:56 +00:00
|
|
|
const ACCESS_TOKEN_COOKIE = "access_token";
|
|
|
|
|
const REFRESH_THRESHOLD_MS = 60_000;
|
2025-12-16 13:40:08 +00:00
|
|
|
|
|
|
|
|
const api = axios.create({
|
2025-12-16 16:25:56 +00:00
|
|
|
baseURL,
|
|
|
|
|
withCredentials: true,
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
},
|
2025-12-16 13:40:08 +00:00
|
|
|
});
|
|
|
|
|
|
2025-12-16 16:48:47 +00:00
|
|
|
export type JwtPayload = {
|
2025-12-16 16:25:56 +00:00
|
|
|
exp?: number;
|
2025-12-16 16:48:47 +00:00
|
|
|
sub?: string;
|
|
|
|
|
[key: string]: unknown;
|
2025-12-16 16:25:56 +00:00
|
|
|
};
|
|
|
|
|
|
2025-12-16 16:48:47 +00:00
|
|
|
export const getCookieValue = (name: string): string | undefined => {
|
2025-12-16 16:25:56 +00:00
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-16 16:48:47 +00:00
|
|
|
export const decodeJwtPayload = (token: string): JwtPayload | null => {
|
2025-12-16 16:25:56 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-16 16:48:47 +00:00
|
|
|
export const getAccessToken = () => getCookieValue(ACCESS_TOKEN_COOKIE);
|
|
|
|
|
|
|
|
|
|
export const getCurrentUsername = () => {
|
|
|
|
|
const token = getAccessToken();
|
|
|
|
|
if (!token) return undefined;
|
|
|
|
|
const payload = decodeJwtPayload(token);
|
|
|
|
|
return typeof payload?.sub === "string" ? payload.sub : undefined;
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-16 16:25:56 +00:00
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-16 13:40:08 +00:00
|
|
|
export const setAuthToken = (token?: string) => {
|
2025-12-16 16:25:56 +00:00
|
|
|
if (token) {
|
|
|
|
|
api.defaults.headers.common["Authorization"] = `Bearer ${token}`;
|
|
|
|
|
} else {
|
|
|
|
|
delete api.defaults.headers.common["Authorization"];
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let refreshPromise: Promise<void> | null = null;
|
|
|
|
|
|
2025-12-16 18:36:02 +00:00
|
|
|
const clearAccessTokenCookie = () => {
|
|
|
|
|
if (typeof document === "undefined") {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
document.cookie = `${ACCESS_TOKEN_COOKIE}=; Max-Age=0; path=/`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const resetAuthState = () => {
|
|
|
|
|
clearAccessTokenCookie();
|
|
|
|
|
setAuthToken(undefined);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const refreshAccessToken = async () => {
|
2025-12-16 16:25:56 +00:00
|
|
|
if (!refreshPromise) {
|
|
|
|
|
refreshPromise = api
|
|
|
|
|
.post(REFRESH_ENDPOINT)
|
|
|
|
|
.then(() => {
|
2025-12-16 16:48:47 +00:00
|
|
|
const token = getAccessToken();
|
2025-12-16 16:25:56 +00:00
|
|
|
setAuthToken(token);
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
throw error;
|
|
|
|
|
})
|
|
|
|
|
.finally(() => {
|
|
|
|
|
refreshPromise = null;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return refreshPromise;
|
2025-12-16 13:40:08 +00:00
|
|
|
};
|
|
|
|
|
|
2025-12-16 18:36:02 +00:00
|
|
|
const ensureTokenFromRefresh = async () => {
|
|
|
|
|
await refreshAccessToken();
|
|
|
|
|
const updatedToken = getAccessToken();
|
|
|
|
|
if (!updatedToken) {
|
|
|
|
|
throw new Error("Token indisponível após atualização.");
|
|
|
|
|
}
|
|
|
|
|
setAuthToken(updatedToken);
|
|
|
|
|
return updatedToken;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const validateSession = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const token = getAccessToken();
|
|
|
|
|
if (!token) {
|
|
|
|
|
await ensureTokenFromRefresh();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const payload = decodeJwtPayload(token);
|
|
|
|
|
if (!payload?.sub) {
|
|
|
|
|
await ensureTokenFromRefresh();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (payload.exp && payload.exp * 1000 <= Date.now()) {
|
|
|
|
|
await ensureTokenFromRefresh();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setAuthToken(token);
|
|
|
|
|
|
|
|
|
|
if (isTokenExpiringSoon(token)) {
|
|
|
|
|
await ensureTokenFromRefresh();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
} catch {
|
|
|
|
|
resetAuthState();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const logout = async () => {
|
|
|
|
|
try {
|
|
|
|
|
await api.post(LOGOUT_ENDPOINT);
|
|
|
|
|
} finally {
|
|
|
|
|
resetAuthState();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-16 16:25:56 +00:00
|
|
|
api.interceptors.request.use(async (config) => {
|
|
|
|
|
const isRefreshCall = config.url?.includes(REFRESH_ENDPOINT);
|
|
|
|
|
|
|
|
|
|
if (!isRefreshCall) {
|
2025-12-16 16:48:47 +00:00
|
|
|
const token = getAccessToken();
|
2025-12-16 16:25:56 +00:00
|
|
|
if (isTokenExpiringSoon(token)) {
|
|
|
|
|
try {
|
|
|
|
|
await refreshAccessToken();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
refreshPromise = null;
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-16 16:48:47 +00:00
|
|
|
const updatedToken = getAccessToken();
|
2025-12-16 16:25:56 +00:00
|
|
|
if (updatedToken) {
|
|
|
|
|
config.headers = config.headers ?? {};
|
|
|
|
|
config.headers.Authorization = `Bearer ${updatedToken}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return config;
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-16 17:26:18 +00:00
|
|
|
api.interceptors.response.use(
|
|
|
|
|
(response) => response,
|
|
|
|
|
(error) => {
|
|
|
|
|
if (error?.response?.status === 401) {
|
2025-12-16 18:36:02 +00:00
|
|
|
resetAuthState();
|
2025-12-16 17:26:18 +00:00
|
|
|
const currentPath = window.location.pathname;
|
|
|
|
|
if (currentPath !== "/login") {
|
|
|
|
|
window.location.href = "/login";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return Promise.reject(error);
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
2025-12-16 13:40:08 +00:00
|
|
|
export default api;
|