This commit is contained in:
Oscar
2026-06-08 13:23:20 +03:00
commit 637dddf656
160 changed files with 56097 additions and 0 deletions

119
src/api/client.ts Normal file
View 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);