feat(auth): revalidar token automaticamente

- Decodifica exp do JWT a partir do cookie access_token
- Aciona refresh antes das requisições quando expiração estiver próxima
- Mantém header Authorization atualizado após renovar o token
master
Artur Oliveira 2025-12-16 13:25:56 -03:00
parent 7b8112d73c
commit f73a6accb9
1 changed files with 100 additions and 13 deletions

View File

@ -1,21 +1,108 @@
import axios from 'axios'
import axios from "axios";
const baseURL = (import.meta.env.VITE_BACKEND_URL as string) || 'http://localhost:8080';
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',
},
baseURL,
withCredentials: true,
headers: {
"Content-Type": "application/json",
},
});
export const setAuthToken = (token?: string) => {
if (token) {
api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
} else {
delete api.defaults.headers.common['Authorization'];
}
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;