diff --git a/web/frontend/src/features/auth/hooks/useAuth.ts b/web/frontend/src/features/auth/hooks/useAuth.ts index 69f0645f2..63fcd09c3 100644 --- a/web/frontend/src/features/auth/hooks/useAuth.ts +++ b/web/frontend/src/features/auth/hooks/useAuth.ts @@ -1,6 +1,12 @@ import { useEffect, useRef, useCallback } from 'react'; import { useAuthStore } from '../store/authStore'; +declare global { + interface Window { + __scriberr_original_fetch?: typeof window.fetch; + } +} + export function useAuth() { const { token, @@ -49,8 +55,7 @@ export function useAuth() { if (window.location.pathname !== "/") { // Force navigation handled by RouterContext or window.location if critical window.history.pushState({ route: { path: 'home' } }, "", "/"); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - window.dispatchEvent(new PopStateEvent('popstate', { state: { route: { path: 'home' } } as any })); + window.dispatchEvent(new PopStateEvent('popstate', { state: { route: { path: 'home' } } })); } }, [token, storeLogout]); @@ -63,7 +68,8 @@ export function useAuth() { const tryRefresh = useCallback(async (): Promise => { try { - const res = await fetch('/api/v1/auth/refresh', { method: 'POST' }) + const fetchToUse = window.__scriberr_original_fetch || window.fetch; + const res = await fetchToUse('/api/v1/auth/refresh', { method: 'POST' }) if (!res.ok) return null const data = await res.json() if (data?.token) { @@ -80,17 +86,24 @@ export function useAuth() { // Consolidated token management useEffect(() => { if (!fetchWrapperSetupRef.current) { - const originalFetch = window.fetch.bind(window); + if (!window.__scriberr_original_fetch) { + window.__scriberr_original_fetch = window.fetch.bind(window); + } + + const originalFetch = window.__scriberr_original_fetch!; const wrappedFetch: typeof window.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { + const url = typeof input === 'string' ? input : (input instanceof URL ? input.href : input.url); + const isAuthEndpoint = url.includes('/api/v1/auth/'); + let res = await originalFetch(input, init); - if (res.status === 401) { + if (res.status === 401 && !isAuthEndpoint) { const newToken = await tryRefresh() if (newToken) { - const newInit: RequestInit | undefined = init ? { ...init } : undefined - if (newInit?.headers && typeof newInit.headers === 'object') { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (newInit.headers as any)['Authorization'] = `Bearer ${newToken}` - } + const newInit: RequestInit = init ? { ...init } : {}; + const headers = new Headers(newInit.headers); + headers.set('Authorization', `Bearer ${newToken}`); + newInit.headers = headers; + res = await originalFetch(input, newInit) if (res.status !== 401) return res } @@ -98,10 +111,11 @@ export function useAuth() { } return res; }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - window.fetch = wrappedFetch as any; + window.fetch = wrappedFetch; fetchWrapperSetupRef.current = true; - return () => { window.fetch = originalFetch; }; + // Note: We don't restore originalFetch on unmount because other components + // also use useAuth and expect the wrapped version. This is a bit hacky + // but safer than multiple re-wrapping/unwrapping. } if (tokenCheckIntervalRef.current) clearInterval(tokenCheckIntervalRef.current);