fix: resolve token refresh race condition and pdf viewer empty token rendering
All checks were successful
Automated Container Build / build-and-push (push) Successful in 24s

This commit is contained in:
Elijah 2026-05-31 20:44:55 -07:00
parent f3ea79ab1f
commit e88243f097
2 changed files with 43 additions and 21 deletions

View file

@ -152,7 +152,11 @@ export default function FilePreview({ file, onClose, downloadToken }: FilePrevie
)} )}
{previewType === 'pdf' && ( {previewType === 'pdf' && (
!downloadToken ? (
<div className="flex items-center justify-center p-8"><Loader2 className="w-8 h-8 animate-spin text-accent" /></div>
) : (
<DynamicPdfViewer url={downloadUrl} /> <DynamicPdfViewer url={downloadUrl} />
)
)} )}
{(previewType === 'text' || previewType === 'code') && ( {(previewType === 'text' || previewType === 'code') && (

View file

@ -68,7 +68,7 @@ class ApiClient {
const refreshed = await this.refreshAccessToken(); const refreshed = await this.refreshAccessToken();
if (refreshed) { if (refreshed) {
headers['Authorization'] = `Bearer ${this.accessToken}`; headers['Authorization'] = `Bearer ${this.accessToken}`;
return fetch(`${API_BASE}${endpoint}`, { const retryResponse = await fetch(`${API_BASE}${endpoint}`, {
method: options.method || 'GET', method: options.method || 'GET',
headers, headers,
body: options.body instanceof FormData body: options.body instanceof FormData
@ -77,6 +77,13 @@ class ApiClient {
? JSON.stringify(options.body) ? JSON.stringify(options.body)
: undefined, : 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,8 +107,16 @@ class ApiClient {
return response; return response;
} }
private refreshPromise: Promise<boolean> | null = null;
private async refreshAccessToken(): Promise<boolean> { private async refreshAccessToken(): Promise<boolean> {
if (!this.refreshToken) return false; if (!this.refreshToken) return false;
if (this.refreshPromise) {
return this.refreshPromise;
}
this.refreshPromise = (async () => {
try { try {
const response = await fetch(`${API_BASE}/api/auth/refresh`, { const response = await fetch(`${API_BASE}/api/auth/refresh`, {
method: 'POST', method: 'POST',
@ -115,14 +130,17 @@ class ApiClient {
this.setTokens(data.access_token, data.refresh_token || this.refreshToken!); this.setTokens(data.access_token, data.refresh_token || this.refreshToken!);
return true; return true;
} }
// Refresh failed
this.clearTokens(); this.clearTokens();
return false; return false;
} catch (e) { } catch {
this.clearTokens(); this.clearTokens();
return false; return false;
} finally {
this.refreshPromise = null;
} }
})();
return this.refreshPromise;
} }
async ensureTokenFresh(): Promise<boolean> { async ensureTokenFresh(): Promise<boolean> {