init
This commit is contained in:
119
src/api/client.ts
Normal file
119
src/api/client.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import axios from 'axios';
|
||||
import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios';
|
||||
import { Api, HttpClient } from './api';
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000';
|
||||
|
||||
// ─── Raw axios instance with interceptors ────────────────────────────────────
|
||||
|
||||
export const axiosInstance: AxiosInstance = axios.create({
|
||||
baseURL: BASE_URL,
|
||||
timeout: 15_000,
|
||||
});
|
||||
|
||||
// Request interceptor — inject access token
|
||||
axiosInstance.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
const token = _getAccessToken();
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error),
|
||||
);
|
||||
|
||||
// Response interceptor — silent token refresh on 401
|
||||
let _isRefreshing = false;
|
||||
let _failedQueue: Array<{ resolve: (v: unknown) => void; reject: (r: unknown) => void }> = [];
|
||||
|
||||
function _processQueue(error: unknown, token: string | null) {
|
||||
_failedQueue.forEach(({ resolve, reject }) => {
|
||||
if (error) reject(error);
|
||||
else resolve(token);
|
||||
});
|
||||
_failedQueue = [];
|
||||
}
|
||||
|
||||
axiosInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
|
||||
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
if (_isRefreshing) {
|
||||
return new Promise((resolve, reject) => {
|
||||
_failedQueue.push({ resolve, reject });
|
||||
}).then((token) => {
|
||||
originalRequest.headers.Authorization = `Bearer ${token}`;
|
||||
return axiosInstance(originalRequest);
|
||||
});
|
||||
}
|
||||
|
||||
originalRequest._retry = true;
|
||||
_isRefreshing = true;
|
||||
|
||||
const refreshToken = localStorage.getItem('refreshToken');
|
||||
if (!refreshToken) {
|
||||
_processQueue(error, null);
|
||||
_isRefreshing = false;
|
||||
_redirectToLogin();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await axios.post<{ accessToken: string; refreshToken: string }>(
|
||||
`${BASE_URL}/api/v1/auth/refresh`,
|
||||
{ refreshToken },
|
||||
);
|
||||
const { accessToken, refreshToken: newRefresh } = res.data;
|
||||
_setAccessToken(accessToken);
|
||||
localStorage.setItem('refreshToken', newRefresh);
|
||||
_processQueue(null, accessToken);
|
||||
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
|
||||
return axiosInstance(originalRequest);
|
||||
} catch (refreshError) {
|
||||
_processQueue(refreshError, null);
|
||||
_clearAuth();
|
||||
_redirectToLogin();
|
||||
return Promise.reject(refreshError);
|
||||
} finally {
|
||||
_isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
// ─── In-memory token storage ─────────────────────────────────────────────────
|
||||
// Access token lives only in memory; refresh token lives in localStorage
|
||||
|
||||
let _accessToken: string | null = null;
|
||||
|
||||
export function _getAccessToken() { return _accessToken; }
|
||||
export function _setAccessToken(token: string) { _accessToken = token; }
|
||||
export function _clearAuth() {
|
||||
_accessToken = null;
|
||||
localStorage.removeItem('refreshToken');
|
||||
}
|
||||
|
||||
function _redirectToLogin() {
|
||||
// Dynamic import to avoid circular dependency with router
|
||||
import('@/router').then(({ router }) => router.replace('/login'));
|
||||
}
|
||||
|
||||
// ─── Typed API client ─────────────────────────────────────────────────────────
|
||||
|
||||
const httpClient = new HttpClient({
|
||||
baseURL: BASE_URL,
|
||||
securityWorker: () => {
|
||||
const token = _getAccessToken();
|
||||
return token ? { headers: { Authorization: `Bearer ${token}` } } : {};
|
||||
},
|
||||
});
|
||||
|
||||
// Plug our axios instance into the generated client
|
||||
httpClient.instance = axiosInstance;
|
||||
|
||||
export const apiClient = new Api(httpClient);
|
||||
Reference in New Issue
Block a user