91 lines
3.0 KiB
TypeScript
91 lines
3.0 KiB
TypeScript
import axios, { type AxiosInstance, type InternalAxiosRequestConfig } from 'axios';
|
|
import * as SecureStore from 'expo-secure-store';
|
|
import { API_BASE_URL, SECURE_STORE_ACCESS_TOKEN, SECURE_STORE_REFRESH_TOKEN } from '@shared/constants';
|
|
|
|
/**
|
|
* Cliente HTTP centralizado.
|
|
*
|
|
* - Injeta o Bearer token automaticamente em todas as requisições.
|
|
* - Intercepta 401 para tentar refresh do token automaticamente.
|
|
* - Garante que todas as chamadas apontem para a base URL da API.
|
|
*/
|
|
const httpClient: AxiosInstance = axios.create({
|
|
baseURL: API_BASE_URL,
|
|
timeout: 15_000,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
// ── Interceptor de request: anexa Bearer token ──────────────
|
|
httpClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
|
|
const token = await SecureStore.getItemAsync(SECURE_STORE_ACCESS_TOKEN);
|
|
if (token && config.headers) {
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
return config;
|
|
});
|
|
|
|
// ── Interceptor de response: refresh automático em 401 ──────
|
|
let isRefreshing = false;
|
|
let pendingQueue: Array<{
|
|
resolve: (token: string) => void;
|
|
reject: (err: unknown) => void;
|
|
}> = [];
|
|
|
|
function processQueue(error: unknown, token: string | null): void {
|
|
pendingQueue.forEach(({ resolve, reject }) => {
|
|
if (error) reject(error);
|
|
else if (token) resolve(token);
|
|
});
|
|
pendingQueue = [];
|
|
}
|
|
|
|
httpClient.interceptors.response.use(
|
|
(response) => response,
|
|
async (error) => {
|
|
const originalRequest = error.config;
|
|
|
|
if (error.response?.status === 401 && !originalRequest._retry) {
|
|
if (isRefreshing) {
|
|
return new Promise((resolve, reject) => {
|
|
pendingQueue.push({ resolve, reject });
|
|
}).then((token) => {
|
|
originalRequest.headers.Authorization = `Bearer ${token}`;
|
|
return httpClient(originalRequest);
|
|
});
|
|
}
|
|
|
|
originalRequest._retry = true;
|
|
isRefreshing = true;
|
|
|
|
try {
|
|
const refreshToken = await SecureStore.getItemAsync(SECURE_STORE_REFRESH_TOKEN);
|
|
if (!refreshToken) throw new Error('No refresh token');
|
|
|
|
const { data } = await axios.post(`${API_BASE_URL}/auth/refresh`, { refresh_token: refreshToken });
|
|
|
|
await SecureStore.setItemAsync(SECURE_STORE_ACCESS_TOKEN, data.access_token);
|
|
await SecureStore.setItemAsync(SECURE_STORE_REFRESH_TOKEN, data.refresh_token);
|
|
|
|
processQueue(null, data.access_token);
|
|
originalRequest.headers.Authorization = `Bearer ${data.access_token}`;
|
|
|
|
return httpClient(originalRequest);
|
|
} catch (refreshError) {
|
|
processQueue(refreshError, null);
|
|
// Limpa tokens inválidos — a store de auth detecta e redireciona para login
|
|
await SecureStore.deleteItemAsync(SECURE_STORE_ACCESS_TOKEN);
|
|
await SecureStore.deleteItemAsync(SECURE_STORE_REFRESH_TOKEN);
|
|
return Promise.reject(refreshError);
|
|
} finally {
|
|
isRefreshing = false;
|
|
}
|
|
}
|
|
|
|
return Promise.reject(error);
|
|
},
|
|
);
|
|
|
|
export { httpClient };
|