From 34e74e44c46a72e4aef817e734549eca31c3edb2 Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 18 Jan 2026 05:15:54 +0300 Subject: [PATCH] Update client.ts --- src/api/client.ts | 114 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 79 insertions(+), 35 deletions(-) diff --git a/src/api/client.ts b/src/api/client.ts index 9a9221a..40943c8 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -1,27 +1,18 @@ import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios' +import { tokenStorage, isTokenExpired } from '../utils/token' const API_BASE_URL = import.meta.env.VITE_API_URL || '/api' -const TELEGRAM_INIT_STORAGE_KEY = 'telegram_init_data' - const getTelegramInitData = (): string | null => { if (typeof window === 'undefined') return null const initData = window.Telegram?.WebApp?.initData if (initData) { - try { - localStorage.setItem(TELEGRAM_INIT_STORAGE_KEY, initData) - } catch { - /* ignore storage errors */ - } + tokenStorage.setTelegramInitData(initData) return initData } - try { - return localStorage.getItem(TELEGRAM_INIT_STORAGE_KEY) - } catch { - return null - } + return tokenStorage.getTelegramInitData() } export const apiClient = axios.create({ @@ -31,9 +22,64 @@ export const apiClient = axios.create({ }, }) -// Request interceptor - add auth token -apiClient.interceptors.request.use((config: InternalAxiosRequestConfig) => { - const token = localStorage.getItem('access_token') +// Флаг для предотвращения множественных refresh запросов +let isRefreshing = false +let refreshSubscribers: ((token: string) => void)[] = [] + +const subscribeTokenRefresh = (cb: (token: string) => void) => { + refreshSubscribers.push(cb) +} + +const onTokenRefreshed = (token: string) => { + refreshSubscribers.forEach((cb) => cb(token)) + refreshSubscribers = [] +} + +const refreshAccessToken = async (): Promise => { + const refreshToken = tokenStorage.getRefreshToken() + if (!refreshToken) return null + + try { + const response = await axios.post(`${API_BASE_URL}/cabinet/auth/refresh`, { + refresh_token: refreshToken, + }) + + const { access_token } = response.data + tokenStorage.setAccessToken(access_token) + return access_token + } catch { + tokenStorage.clearTokens() + window.location.href = '/login' + return null + } +} + +// Request interceptor - add auth token with expiration check +apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) => { + let token = tokenStorage.getAccessToken() + + // Проверяем срок действия токена перед запросом + if (token && isTokenExpired(token)) { + // Токен истёк или скоро истечёт - обновляем + if (!isRefreshing) { + isRefreshing = true + const newToken = await refreshAccessToken() + isRefreshing = false + + if (newToken) { + token = newToken + onTokenRefreshed(newToken) + } else { + return config + } + } else { + // Другой запрос уже обновляет токен - ждём + token = await new Promise((resolve) => { + subscribeTokenRefresh(resolve) + }) + } + } + if (token && config.headers) { config.headers.Authorization = `Bearer ${token}` } @@ -45,38 +91,37 @@ apiClient.interceptors.request.use((config: InternalAxiosRequestConfig) => { return config }) -// Response interceptor - handle 401, refresh token +// Response interceptor - handle 401 as fallback apiClient.interceptors.response.use( (response) => response, async (error: AxiosError) => { const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean } + // Если получили 401 и ещё не пробовали refresh (на случай если проверка exp не сработала) if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true - const refreshToken = localStorage.getItem('refresh_token') - if (refreshToken) { - try { - const response = await axios.post(`${API_BASE_URL}/cabinet/auth/refresh`, { - refresh_token: refreshToken, - }) - - const { access_token } = response.data - localStorage.setItem('access_token', access_token) + if (!isRefreshing) { + isRefreshing = true + const newToken = await refreshAccessToken() + isRefreshing = false + if (newToken) { + onTokenRefreshed(newToken) if (originalRequest.headers) { - originalRequest.headers.Authorization = `Bearer ${access_token}` + originalRequest.headers.Authorization = `Bearer ${newToken}` } - return apiClient(originalRequest) - } catch (refreshError) { - // Refresh failed, clear tokens and redirect to login - localStorage.removeItem('access_token') - localStorage.removeItem('refresh_token') - localStorage.removeItem('user') - window.location.href = '/login' - return Promise.reject(refreshError) } + } else { + // Ждём завершения refresh от другого запроса + const token = await new Promise((resolve) => { + subscribeTokenRefresh(resolve) + }) + if (originalRequest.headers) { + originalRequest.headers.Authorization = `Bearer ${token}` + } + return apiClient(originalRequest) } } @@ -85,4 +130,3 @@ apiClient.interceptors.response.use( ) export default apiClient -