From 20e300587973f55e0392299e49b3c81deb8e2467 Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 18 Jan 2026 05:21:13 +0300 Subject: [PATCH] Update token.ts --- src/utils/token.ts | 104 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/src/utils/token.ts b/src/utils/token.ts index d5b0309..7018db9 100644 --- a/src/utils/token.ts +++ b/src/utils/token.ts @@ -149,3 +149,107 @@ export const tokenStorage = { } }, } + +/** + * Централизованный менеджер обновления токенов + * Предотвращает множественные параллельные refresh запросы + */ +class TokenRefreshManager { + private isRefreshing = false + private refreshPromise: Promise | null = null + private subscribers: ((token: string | null) => void)[] = [] + private refreshEndpoint = '/api/cabinet/auth/refresh' + + setRefreshEndpoint(endpoint: string): void { + this.refreshEndpoint = endpoint + } + + /** + * Обновляет access token используя refresh token + * При множественных вызовах возвращает один и тот же Promise + */ + async refreshAccessToken(): Promise { + // Если уже идёт refresh - возвращаем существующий Promise + if (this.isRefreshing && this.refreshPromise) { + return this.refreshPromise + } + + const refreshToken = tokenStorage.getRefreshToken() + if (!refreshToken) { + return null + } + + this.isRefreshing = true + this.refreshPromise = this.doRefresh(refreshToken) + + try { + const result = await this.refreshPromise + this.notifySubscribers(result) + return result + } finally { + this.isRefreshing = false + this.refreshPromise = null + } + } + + private async doRefresh(refreshToken: string): Promise { + try { + const response = await fetch(this.refreshEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refresh_token: refreshToken }), + }) + + if (!response.ok) { + throw new Error(`Refresh failed: ${response.status}`) + } + + const data = await response.json() + const newAccessToken = data.access_token + + if (newAccessToken) { + tokenStorage.setAccessToken(newAccessToken) + return newAccessToken + } + + return null + } catch (error) { + console.error('[TokenRefreshManager] Refresh failed:', error) + return null + } + } + + /** + * Подписка на результат refresh (для ожидающих запросов) + */ + subscribe(callback: (token: string | null) => void): () => void { + this.subscribers.push(callback) + return () => { + this.subscribers = this.subscribers.filter((cb) => cb !== callback) + } + } + + private notifySubscribers(token: string | null): void { + this.subscribers.forEach((cb) => cb(token)) + this.subscribers = [] + } + + /** + * Проверяет, идёт ли сейчас refresh + */ + get isRefreshInProgress(): boolean { + return this.isRefreshing + } + + /** + * Ожидает завершения текущего refresh (если есть) + */ + async waitForRefresh(): Promise { + if (this.refreshPromise) { + return this.refreshPromise + } + return tokenStorage.getAccessToken() + } +} + +export const tokenRefreshManager = new TokenRefreshManager()