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 tokenmaster
parent
7b8112d73c
commit
f73a6accb9
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue