修复tmdbkey错误和未登录时获取外部观影室密钥无限重定向
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { getAuthInfoFromBrowserCookie, clearAuthCookie } from '@/lib/auth';
|
import { getAuthInfoFromBrowserCookie, clearAuthCookie } from '@/lib/auth';
|
||||||
|
import { TOKEN_CONFIG } from '@/lib/refresh-token';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Token 自动刷新管理器
|
* Token 自动刷新管理器
|
||||||
@@ -52,6 +53,12 @@ export function TokenRefreshManager() {
|
|||||||
|
|
||||||
// 刷新失败,先登出再跳转登录
|
// 刷新失败,先登出再跳转登录
|
||||||
if (response.status === 401 || response.status === 403) {
|
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 {
|
try {
|
||||||
await window.fetch('/api/logout', {
|
await window.fetch('/api/logout', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -105,12 +112,12 @@ export function TokenRefreshManager() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 计算 Access Token 剩余时间
|
// 计算 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 age = now - authInfo.timestamp;
|
||||||
const remaining = ACCESS_TOKEN_AGE - age;
|
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;
|
return remaining < REFRESH_THRESHOLD;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -134,7 +141,7 @@ export function TokenRefreshManager() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 请求前检查:Token 即将过期时主动刷新
|
// 请求前检查:Token 即将过期时主动刷新
|
||||||
if (shouldRefreshToken() && !isRefreshing) {
|
if (shouldRefreshToken()) {
|
||||||
console.log('[Token] Expiring soon, refreshing proactively...');
|
console.log('[Token] Expiring soon, refreshing proactively...');
|
||||||
await refreshToken();
|
await refreshToken();
|
||||||
}
|
}
|
||||||
@@ -143,30 +150,57 @@ export function TokenRefreshManager() {
|
|||||||
let response = await originalFetch(input, init);
|
let response = await originalFetch(input, init);
|
||||||
|
|
||||||
// 响应拦截:401 错误时刷新 Token 并重试(仅重试一次)
|
// 响应拦截:401 错误时刷新 Token 并重试(仅重试一次)
|
||||||
if (response.status === 401 && !isRefreshing) {
|
if (response.status === 401) {
|
||||||
console.log('[Token] Received 401, attempting refresh and retry...');
|
// 如果在登录页面,跳过刷新逻辑
|
||||||
|
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) {
|
try {
|
||||||
// 刷新成功,重试原请求(仅此一次)
|
const responseText = await clonedResponse.text();
|
||||||
response = await originalFetch(input, init);
|
|
||||||
|
|
||||||
// 如果重试后仍然是 401,说明有问题,先登出再跳转登录
|
// 只有当响应体包含 "Unauthorized" 或 "Refresh token expired" 或 "Access token expired" 时才刷新
|
||||||
if (response.status === 401) {
|
if (responseText.includes('Unauthorized') || responseText.includes('Refresh token expired') || responseText.includes('Access token expired')) {
|
||||||
console.error('[Token] Still 401 after refresh, redirecting to login');
|
console.log('[Token] Received 401 with auth error, attempting refresh and retry...');
|
||||||
try {
|
|
||||||
await originalFetch('/api/logout', {
|
const refreshed = await refreshToken();
|
||||||
method: 'POST',
|
|
||||||
credentials: 'include',
|
if (refreshed) {
|
||||||
});
|
// 刷新成功,重试原请求(仅此一次)
|
||||||
} catch (error) {
|
response = await originalFetch(input, init);
|
||||||
console.error('[Token] Logout error:', error);
|
|
||||||
// 登出失败时清除前端cookie
|
// 如果重试后仍然是 401,说明有问题,先登出再跳转登录
|
||||||
clearAuthCookie();
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { useWatchRoom } from '@/hooks/useWatchRoom';
|
|||||||
|
|
||||||
import Toast, { ToastProps } from '@/components/Toast';
|
import Toast, { ToastProps } from '@/components/Toast';
|
||||||
|
|
||||||
|
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
|
||||||
|
|
||||||
import type { ChatMessage, Member, Room, WatchRoomConfig } from '@/types/watch-room';
|
import type { ChatMessage, Member, Room, WatchRoomConfig } from '@/types/watch-room';
|
||||||
|
|
||||||
// Import type from watch-room-socket
|
// Import type from watch-room-socket
|
||||||
@@ -79,6 +81,7 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) {
|
|||||||
const [isEnabled, setIsEnabled] = useState(false);
|
const [isEnabled, setIsEnabled] = useState(false);
|
||||||
const [toast, setToast] = useState<ToastProps | null>(null);
|
const [toast, setToast] = useState<ToastProps | null>(null);
|
||||||
const [reconnectFailed, setReconnectFailed] = useState(false);
|
const [reconnectFailed, setReconnectFailed] = useState(false);
|
||||||
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
|
|
||||||
// 处理房间删除的回调
|
// 处理房间删除的回调
|
||||||
const handleRoomDeleted = useCallback((data?: { reason?: string }) => {
|
const handleRoomDeleted = useCallback((data?: { reason?: string }) => {
|
||||||
@@ -116,6 +119,23 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) {
|
|||||||
|
|
||||||
const watchRoom = useWatchRoom(handleRoomDeleted, handleStateCleared);
|
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 () => {
|
const manualReconnect = useCallback(async () => {
|
||||||
console.log('[WatchRoomProvider] Manual reconnect initiated');
|
console.log('[WatchRoomProvider] Manual reconnect initiated');
|
||||||
@@ -164,20 +184,26 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) {
|
|||||||
|
|
||||||
// 如果使用外部服务器,需要获取认证信息(需要登录)
|
// 如果使用外部服务器,需要获取认证信息(需要登录)
|
||||||
if (watchRoomConfig.serverType === 'external' && watchRoomConfig.enabled) {
|
if (watchRoomConfig.serverType === 'external' && watchRoomConfig.enabled) {
|
||||||
try {
|
// 检查用户是否已登录
|
||||||
const authResponse = await fetch('/api/watch-room-auth');
|
if (!isLoggedIn) {
|
||||||
if (authResponse.ok) {
|
console.log('[WatchRoom] User not logged in, skipping auth info request');
|
||||||
const authData = await authResponse.json();
|
// 用户未登录,不调用认证接口
|
||||||
watchRoomConfig.externalServerAuth = authData.externalServerAuth;
|
} else {
|
||||||
} else {
|
try {
|
||||||
console.error('[WatchRoom] Failed to load auth info:', authResponse.status);
|
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;
|
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 () => {
|
return () => {
|
||||||
watchRoom.disconnect();
|
watchRoom.disconnect();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [isLoggedIn]); // 添加 isLoggedIn 作为依赖
|
||||||
|
|
||||||
const contextValue: WatchRoomContextType = {
|
const contextValue: WatchRoomContextType = {
|
||||||
socket: watchRoom.socket,
|
socket: watchRoom.socket,
|
||||||
|
|||||||
+34
-18
@@ -526,7 +526,15 @@ async function fetchWithAuth(
|
|||||||
// 如果是 401 且是 token 过期,尝试刷新并重试
|
// 如果是 401 且是 token 过期,尝试刷新并重试
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
const text = await res.clone().text();
|
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 (
|
if (
|
||||||
url.includes('/api/login') ||
|
url.includes('/api/login') ||
|
||||||
@@ -547,29 +555,37 @@ async function fetchWithAuth(
|
|||||||
// 刷新成功,重试原请求
|
// 刷新成功,重试原请求
|
||||||
res = await fetch(url, options);
|
res = await fetch(url, options);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 不是认证错误的401,直接返回
|
||||||
|
console.log('[fetchWithAuth] Received 401 but not an auth error, skipping refresh');
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果刷新后仍然是 401,或者是其他 401 错误,跳转登录
|
// 如果刷新后仍然是 401,或者是其他 401 错误,跳转登录
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
// 检查当前页面是否已经是登录页,避免重复跳转
|
const text2 = await res.clone().text();
|
||||||
if (typeof window !== 'undefined' && !window.location.pathname.startsWith('/login')) {
|
// 再次检查响应体
|
||||||
// 调用 logout 接口
|
if (text2.includes('Unauthorized') || text2.includes('Refresh token expired') || text2.includes('Access token expired')) {
|
||||||
try {
|
// 检查当前页面是否已经是登录页,避免重复跳转
|
||||||
await fetch('/api/logout', {
|
if (typeof window !== 'undefined' && !window.location.pathname.startsWith('/login')) {
|
||||||
method: 'POST',
|
// 调用 logout 接口
|
||||||
headers: { 'Content-Type': 'application/json' },
|
try {
|
||||||
});
|
await fetch('/api/logout', {
|
||||||
} catch (error) {
|
method: 'POST',
|
||||||
console.error('注销请求失败:', error);
|
headers: { 'Content-Type': 'application/json' },
|
||||||
// 登出失败时清除前端cookie
|
});
|
||||||
clearAuthCookie();
|
} 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;
|
throw new Error('用户未授权,已跳转到登录页面');
|
||||||
const loginUrl = new URL('/login', window.location.origin);
|
|
||||||
loginUrl.searchParams.set('redirect', currentUrl);
|
|
||||||
window.location.href = loginUrl.toString();
|
|
||||||
}
|
}
|
||||||
throw new Error('用户未授权,已跳转到登录页面');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user