diff --git a/src/components/TokenRefreshManager.tsx b/src/components/TokenRefreshManager.tsx index 21f3cd2..428b66b 100644 --- a/src/components/TokenRefreshManager.tsx +++ b/src/components/TokenRefreshManager.tsx @@ -3,6 +3,7 @@ import { useEffect } from 'react'; import { getAuthInfoFromBrowserCookie, clearAuthCookie } from '@/lib/auth'; +import { TOKEN_CONFIG } from '@/lib/refresh-token'; /** * Token 自动刷新管理器 @@ -52,6 +53,12 @@ export function TokenRefreshManager() { // 刷新失败,先登出再跳转登录 if (response.status === 401 || response.status === 403) { + // 如果在登录页面,跳过登出和跳转逻辑 + if (window.location.pathname === '/login') { + console.log('[Token] On login page, skipping logout and redirect'); + return false; + } + try { await window.fetch('/api/logout', { method: 'POST', @@ -105,12 +112,12 @@ export function TokenRefreshManager() { } // 计算 Access Token 剩余时间 - const ACCESS_TOKEN_AGE = 4 * 60 * 60 * 1000; // 4 小时 + const ACCESS_TOKEN_AGE = TOKEN_CONFIG.ACCESS_TOKEN_AGE; const age = now - authInfo.timestamp; const remaining = ACCESS_TOKEN_AGE - age; - // 剩余时间 < 10 分钟时需要刷新(包括已过期的情况) - const REFRESH_THRESHOLD = 10 * 60 * 1000; // 10 分钟 + // 剩余时间 < 刷新阈值时需要刷新(包括已过期的情况) + const REFRESH_THRESHOLD = TOKEN_CONFIG.RENEWAL_THRESHOLD; return remaining < REFRESH_THRESHOLD; }; @@ -134,7 +141,7 @@ export function TokenRefreshManager() { } // 请求前检查:Token 即将过期时主动刷新 - if (shouldRefreshToken() && !isRefreshing) { + if (shouldRefreshToken()) { console.log('[Token] Expiring soon, refreshing proactively...'); await refreshToken(); } @@ -143,30 +150,57 @@ export function TokenRefreshManager() { let response = await originalFetch(input, init); // 响应拦截:401 错误时刷新 Token 并重试(仅重试一次) - if (response.status === 401 && !isRefreshing) { - console.log('[Token] Received 401, attempting refresh and retry...'); + if (response.status === 401) { + // 如果在登录页面,跳过刷新逻辑 + if (window.location.pathname === '/login') { + console.log('[Token] On login page, skipping refresh logic'); + return response; + } - const refreshed = await refreshToken(); + // 克隆响应以便读取响应体 + const clonedResponse = response.clone(); - if (refreshed) { - // 刷新成功,重试原请求(仅此一次) - response = await originalFetch(input, init); + try { + const responseText = await clonedResponse.text(); - // 如果重试后仍然是 401,说明有问题,先登出再跳转登录 - if (response.status === 401) { - console.error('[Token] Still 401 after refresh, redirecting to login'); - try { - await originalFetch('/api/logout', { - method: 'POST', - credentials: 'include', - }); - } catch (error) { - console.error('[Token] Logout error:', error); - // 登出失败时清除前端cookie - clearAuthCookie(); + // 只有当响应体包含 "Unauthorized" 或 "Refresh token expired" 或 "Access token expired" 时才刷新 + if (responseText.includes('Unauthorized') || responseText.includes('Refresh token expired') || responseText.includes('Access token expired')) { + console.log('[Token] Received 401 with auth error, attempting refresh and retry...'); + + const refreshed = await refreshToken(); + + if (refreshed) { + // 刷新成功,重试原请求(仅此一次) + response = await originalFetch(input, init); + + // 如果重试后仍然是 401,说明有问题,先登出再跳转登录 + if (response.status === 401) { + console.error('[Token] Still 401 after refresh, redirecting to login'); + + // 如果在登录页面,跳过登出和跳转逻辑 + if (window.location.pathname === '/login') { + console.log('[Token] On login page, skipping logout and redirect'); + return response; + } + + try { + await originalFetch('/api/logout', { + method: 'POST', + credentials: 'include', + }); + } catch (error) { + console.error('[Token] Logout error:', error); + // 登出失败时清除前端cookie + clearAuthCookie(); + } + window.location.href = `/login?redirect=${encodeURIComponent(window.location.pathname + window.location.search)}`; + } } - window.location.href = `/login?redirect=${encodeURIComponent(window.location.pathname + window.location.search)}`; + } else { + console.log('[Token] Received 401 but not an auth error, skipping refresh'); } + } catch (error) { + console.error('[Token] Failed to read response body:', error); } } diff --git a/src/components/WatchRoomProvider.tsx b/src/components/WatchRoomProvider.tsx index ee1e048..930ec6a 100644 --- a/src/components/WatchRoomProvider.tsx +++ b/src/components/WatchRoomProvider.tsx @@ -7,6 +7,8 @@ import { useWatchRoom } from '@/hooks/useWatchRoom'; import Toast, { ToastProps } from '@/components/Toast'; +import { getAuthInfoFromBrowserCookie } from '@/lib/auth'; + import type { ChatMessage, Member, Room, WatchRoomConfig } from '@/types/watch-room'; // Import type from watch-room-socket @@ -79,6 +81,7 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) { const [isEnabled, setIsEnabled] = useState(false); const [toast, setToast] = useState(null); const [reconnectFailed, setReconnectFailed] = useState(false); + const [isLoggedIn, setIsLoggedIn] = useState(false); // 处理房间删除的回调 const handleRoomDeleted = useCallback((data?: { reason?: string }) => { @@ -116,6 +119,23 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) { const watchRoom = useWatchRoom(handleRoomDeleted, handleStateCleared); + // 检查登录状态 + useEffect(() => { + const checkLoginStatus = () => { + const authInfo = getAuthInfoFromBrowserCookie(); + const loggedIn = !!(authInfo && authInfo.username); + setIsLoggedIn(loggedIn); + }; + + // 初始检查 + checkLoginStatus(); + + // 定期检查登录状态(每秒检查一次) + const interval = setInterval(checkLoginStatus, 1000); + + return () => clearInterval(interval); + }, []); + // 手动重连 const manualReconnect = useCallback(async () => { console.log('[WatchRoomProvider] Manual reconnect initiated'); @@ -164,20 +184,26 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) { // 如果使用外部服务器,需要获取认证信息(需要登录) if (watchRoomConfig.serverType === 'external' && watchRoomConfig.enabled) { - try { - const authResponse = await fetch('/api/watch-room-auth'); - if (authResponse.ok) { - const authData = await authResponse.json(); - watchRoomConfig.externalServerAuth = authData.externalServerAuth; - } else { - console.error('[WatchRoom] Failed to load auth info:', authResponse.status); + // 检查用户是否已登录 + if (!isLoggedIn) { + console.log('[WatchRoom] User not logged in, skipping auth info request'); + // 用户未登录,不调用认证接口 + } else { + try { + const authResponse = await fetch('/api/watch-room-auth'); + if (authResponse.ok) { + const authData = await authResponse.json(); + watchRoomConfig.externalServerAuth = authData.externalServerAuth; + } else { + console.error('[WatchRoom] Failed to load auth info:', authResponse.status); + // 如果无法获取认证信息,禁用观影室 + watchRoomConfig.enabled = false; + } + } catch (error) { + console.error('[WatchRoom] Error loading auth info:', error); // 如果无法获取认证信息,禁用观影室 watchRoomConfig.enabled = false; } - } catch (error) { - console.error('[WatchRoom] Error loading auth info:', error); - // 如果无法获取认证信息,禁用观影室 - watchRoomConfig.enabled = false; } } @@ -232,7 +258,7 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) { return () => { watchRoom.disconnect(); }; - }, []); + }, [isLoggedIn]); // 添加 isLoggedIn 作为依赖 const contextValue: WatchRoomContextType = { socket: watchRoom.socket, diff --git a/src/lib/db.client.ts b/src/lib/db.client.ts index bd2b532..92d0861 100644 --- a/src/lib/db.client.ts +++ b/src/lib/db.client.ts @@ -526,7 +526,15 @@ async function fetchWithAuth( // 如果是 401 且是 token 过期,尝试刷新并重试 if (res.status === 401) { const text = await res.clone().text(); - if (text === 'Access token expired') { + + // 只有当响应体包含 "Unauthorized" 或 "Refresh token expired" 或 "Access token expired" 时才处理 + if (text.includes('Unauthorized') || text.includes('Refresh token expired') || text.includes('Access token expired')) { + // 如果在登录页面,跳过刷新逻辑 + if (typeof window !== 'undefined' && window.location.pathname === '/login') { + console.log('[fetchWithAuth] On login page, skipping refresh logic'); + return res; + } + // 检查是否是登录相关的接口,如果是则不刷新 if ( url.includes('/api/login') || @@ -547,29 +555,37 @@ async function fetchWithAuth( // 刷新成功,重试原请求 res = await fetch(url, options); } + } else { + // 不是认证错误的401,直接返回 + console.log('[fetchWithAuth] Received 401 but not an auth error, skipping refresh'); + return res; } // 如果刷新后仍然是 401,或者是其他 401 错误,跳转登录 if (res.status === 401) { - // 检查当前页面是否已经是登录页,避免重复跳转 - if (typeof window !== 'undefined' && !window.location.pathname.startsWith('/login')) { - // 调用 logout 接口 - try { - await fetch('/api/logout', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - }); - } catch (error) { - console.error('注销请求失败:', error); - // 登出失败时清除前端cookie - clearAuthCookie(); + const text2 = await res.clone().text(); + // 再次检查响应体 + if (text2.includes('Unauthorized') || text2.includes('Refresh token expired') || text2.includes('Access token expired')) { + // 检查当前页面是否已经是登录页,避免重复跳转 + if (typeof window !== 'undefined' && !window.location.pathname.startsWith('/login')) { + // 调用 logout 接口 + try { + await fetch('/api/logout', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); + } catch (error) { + console.error('注销请求失败:', error); + // 登出失败时清除前端cookie + clearAuthCookie(); + } + const currentUrl = window.location.pathname + window.location.search; + const loginUrl = new URL('/login', window.location.origin); + loginUrl.searchParams.set('redirect', currentUrl); + window.location.href = loginUrl.toString(); } - const currentUrl = window.location.pathname + window.location.search; - const loginUrl = new URL('/login', window.location.origin); - loginUrl.searchParams.set('redirect', currentUrl); - window.location.href = loginUrl.toString(); + throw new Error('用户未授权,已跳转到登录页面'); } - throw new Error('用户未授权,已跳转到登录页面'); } }