✨ feat(src/components/layout/TauriTitlebar.vue): изменяет название приложения с "Daiting" на "Dating" ✨ feat(src/components/layout/SideNav.vue): изменяет название приложения с "Daiting" на "Dating" ✨ fix(README.md): исправляет название приложения с "Daiting" на "Dating" ✨ fix(src/api/client.ts): обновляет базовый URL API с 'localhost:3000' на 'localhost:1337' ✨ fix(src/views/auth/LoginView.vue): изменяет название приложения с "Daiting" на "Dating" ✨ fix(src/views/auth/RegisterView.vue): изменяет название приложения с "Daiting" на "Dating" ✨ fix(src-tauri/Cargo.toml): исправляет описание приложения с "Daiting" на "Dating" ✨ fix(src-tauri/tauri.conf.json): изменяет имя продукта с "Daiting" на "Dating" ✨ fix(vite.config.ts): обновляет порт разработки с 1420 на 3000 ✨ fix(index.html): изменяет заголовок страницы с "Daiting" на "Dating" ✨ fix(dating-app-frontend-prompt.md): исправляет название приложения с "Daiting" на "Dating" ✨ fix(PRODUCT.md): изменяет название приложения с "Daiting" на "Dating"
125 lines
4.1 KiB
TypeScript
125 lines
4.1 KiB
TypeScript
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:1337';
|
|
|
|
// ─── 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) => {
|
|
if (response.data !== null && typeof response.data === 'object' && 'data' in response.data) {
|
|
response.data = response.data.data;
|
|
}
|
|
return 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<{ data: { accessToken: string; refreshToken: string } }>(
|
|
`${BASE_URL}/api/v1/auth/refresh`,
|
|
{ refreshToken },
|
|
);
|
|
const { accessToken, refreshToken: newRefresh } = res.data.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);
|