From e88243f097b8236586baf51ad8668bdc4f348323 Mon Sep 17 00:00:00 2001 From: Elijah Date: Sun, 31 May 2026 20:44:55 -0700 Subject: [PATCH] fix: resolve token refresh race condition and pdf viewer empty token rendering --- frontend/src/components/FilePreview.tsx | 6 ++- frontend/src/lib/api.ts | 58 ++++++++++++++++--------- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/FilePreview.tsx b/frontend/src/components/FilePreview.tsx index 0dfd792..4c7677b 100644 --- a/frontend/src/components/FilePreview.tsx +++ b/frontend/src/components/FilePreview.tsx @@ -152,7 +152,11 @@ export default function FilePreview({ file, onClose, downloadToken }: FilePrevie )} {previewType === 'pdf' && ( - + !downloadToken ? ( +
+ ) : ( + + ) )} {(previewType === 'text' || previewType === 'code') && ( diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index de77b62..b1b92ad 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -68,7 +68,7 @@ class ApiClient { const refreshed = await this.refreshAccessToken(); if (refreshed) { headers['Authorization'] = `Bearer ${this.accessToken}`; - return fetch(`${API_BASE}${endpoint}`, { + const retryResponse = await fetch(`${API_BASE}${endpoint}`, { method: options.method || 'GET', headers, body: options.body instanceof FormData @@ -77,6 +77,13 @@ class ApiClient { ? JSON.stringify(options.body) : undefined, }); + + if (!retryResponse.ok && retryResponse.status === 401) { + if (typeof window !== 'undefined') window.dispatchEvent(new Event('auth_error')); + throw new Error(`HTTP 401`); + } + + return retryResponse; } } @@ -100,29 +107,40 @@ class ApiClient { return response; } + private refreshPromise: Promise | null = null; + private async refreshAccessToken(): Promise { if (!this.refreshToken) return false; - try { - const response = await fetch(`${API_BASE}/api/auth/refresh`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${this.refreshToken}`, - }, - }); - if (response.ok) { - const data = await response.json(); - this.setTokens(data.access_token, data.refresh_token || this.refreshToken!); - return true; - } - - // Refresh failed - this.clearTokens(); - return false; - } catch (e) { - this.clearTokens(); - return false; + if (this.refreshPromise) { + return this.refreshPromise; } + + this.refreshPromise = (async () => { + try { + const response = await fetch(`${API_BASE}/api/auth/refresh`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.refreshToken}`, + }, + }); + + if (response.ok) { + const data = await response.json(); + this.setTokens(data.access_token, data.refresh_token || this.refreshToken!); + return true; + } + this.clearTokens(); + return false; + } catch { + this.clearTokens(); + return false; + } finally { + this.refreshPromise = null; + } + })(); + + return this.refreshPromise; } async ensureTokenFresh(): Promise {