196 lines
5.7 KiB
TypeScript
Executable File
196 lines
5.7 KiB
TypeScript
Executable File
// apps/web/lib/hooks/use-token.ts
|
||
'use client';
|
||
import { createContext, useContext, useEffect, useState, useCallback, ReactNode } from 'react';
|
||
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
|
||
import { TokenManager } from '@/lib/auth/token-manager';
|
||
|
||
interface TokenContextType {
|
||
accessToken: string | null;
|
||
isTokenValid: boolean;
|
||
refreshToken: () => Promise<void>;
|
||
clearTokens: () => void;
|
||
forceRefresh: () => Promise<void>;
|
||
}
|
||
|
||
const TokenContext = createContext<TokenContextType | null>(null);
|
||
|
||
interface TokenProviderProps {
|
||
children: ReactNode;
|
||
}
|
||
|
||
export function TokenProvider({ children }: TokenProviderProps) {
|
||
const router = useRouter();
|
||
const pathname = usePathname();
|
||
const searchParams = useSearchParams();
|
||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||
const [isTokenValid, setIsTokenValid] = useState(false);
|
||
|
||
// 加载和验证token
|
||
const loadToken = useCallback(async () => {
|
||
try {
|
||
const token = await TokenManager.getValidAccessToken();
|
||
setAccessToken(token);
|
||
setIsTokenValid(!!token && !TokenManager.isTokenExpired());
|
||
} catch (error) {
|
||
console.error('加载token失败:', error);
|
||
setAccessToken(null);
|
||
setIsTokenValid(false);
|
||
}
|
||
}, []);
|
||
|
||
// 刷新token
|
||
const refreshToken = useCallback(async () => {
|
||
await loadToken();
|
||
}, [loadToken]);
|
||
|
||
// 强制刷新token状态(用于手动同步)
|
||
const forceRefresh = useCallback(async () => {
|
||
await loadToken();
|
||
}, [loadToken]);
|
||
|
||
// 清除token
|
||
const clearTokens = useCallback(() => {
|
||
TokenManager.clearTokens();
|
||
setAccessToken(null);
|
||
setIsTokenValid(false);
|
||
}, []);
|
||
|
||
// 清除登录状态并跳转到登录页面
|
||
const handleTokenExpired = useCallback(() => {
|
||
// 处理token过期重新登录
|
||
clearTokens();
|
||
|
||
// 检查当前是否已经在登录页面,避免循环重定向
|
||
const currentPath = pathname + (searchParams.toString() ? `?${searchParams.toString()}` : '');
|
||
|
||
// 如果已经在登录页面,不进行重定向
|
||
if (pathname.includes('/auth/login') || pathname.includes('/auth/register')) {
|
||
return;
|
||
}
|
||
|
||
// 获取原始重定向URL,避免嵌套
|
||
const getOriginalRedirectUrl = (): string => {
|
||
const existingRedirectUrl = searchParams.get('redirectUrl');
|
||
|
||
if (existingRedirectUrl) {
|
||
try {
|
||
const decoded = decodeURIComponent(existingRedirectUrl);
|
||
// 如果已存在的redirectUrl包含登录页面,提取其中的原始redirectUrl
|
||
if (decoded.includes('/auth/login')) {
|
||
const nestedUrl = new URL(decoded, window.location.origin);
|
||
return nestedUrl.searchParams.get('redirectUrl') || '/dashboard';
|
||
}
|
||
return decoded;
|
||
} catch {
|
||
return '/dashboard';
|
||
}
|
||
}
|
||
return currentPath;
|
||
};
|
||
|
||
const originalRedirectUrl = getOriginalRedirectUrl();
|
||
const loginUrl = `/auth/login?redirectUrl=${encodeURIComponent(originalRedirectUrl)}`;
|
||
|
||
// 跳转到登录页面
|
||
router.push(loginUrl);
|
||
}, [clearTokens, router, pathname, searchParams]);
|
||
|
||
// 页面获得焦点时检测token有效性
|
||
const checkTokenOnFocus = useCallback(async () => {
|
||
// 如果当前在登录页面,不执行token检查
|
||
if (pathname.includes('/auth/login')) {
|
||
return;
|
||
}
|
||
|
||
const currentToken = TokenManager.getAccessToken();
|
||
if (!currentToken) {
|
||
// 未找到token,跳转到登录页面
|
||
console.log('未找到token,跳转到登录页面')
|
||
handleTokenExpired();
|
||
return;
|
||
}
|
||
|
||
// 检查token是否过期
|
||
if (TokenManager.isTokenExpired()) {
|
||
// Token已过期,尝试刷新...
|
||
console.log('token已经过期')
|
||
try {
|
||
// 尝试获取新的有效token
|
||
const newToken = await TokenManager.getValidAccessToken();
|
||
|
||
if (newToken) {
|
||
// Token刷新成功
|
||
console.log('Token刷新成功')
|
||
setAccessToken(newToken);
|
||
setIsTokenValid(true);
|
||
} else {
|
||
// Token刷新失败,清除登录状态
|
||
console.log('Token刷新失败,清除登录状态')
|
||
handleTokenExpired();
|
||
}
|
||
} catch (error) {
|
||
console.error('Token刷新过程中出错:', error);
|
||
handleTokenExpired();
|
||
}
|
||
} else {
|
||
// Token仍然有效,确保状态同步
|
||
console.log('Token仍然有效,确保状态同步')
|
||
setAccessToken(currentToken);
|
||
setIsTokenValid(true);
|
||
}
|
||
}, [handleTokenExpired, pathname]);
|
||
|
||
// 初始化加载
|
||
useEffect(() => {
|
||
loadToken();
|
||
}, [loadToken]);
|
||
|
||
// 监听localStorage变化(跨标签页同步)
|
||
useEffect(() => {
|
||
const handleStorageChange = (e: StorageEvent) => {
|
||
if (e.key === 'oidc_access_token') {
|
||
loadToken();
|
||
}
|
||
};
|
||
|
||
window.addEventListener('storage', handleStorageChange);
|
||
return () => window.removeEventListener('storage', handleStorageChange);
|
||
}, [loadToken]);
|
||
|
||
// 监听页面获得焦点事件
|
||
useEffect(() => {
|
||
const handleWindowFocus = () => {
|
||
// checkTokenOnFocus();
|
||
};
|
||
|
||
// 添加焦点事件监听器
|
||
window.addEventListener('focus', handleWindowFocus);
|
||
|
||
// 清理函数
|
||
return () => {
|
||
window.removeEventListener('focus', handleWindowFocus);
|
||
};
|
||
}, [checkTokenOnFocus]);
|
||
|
||
const value = {
|
||
accessToken,
|
||
isTokenValid,
|
||
refreshToken,
|
||
clearTokens,
|
||
forceRefresh,
|
||
};
|
||
|
||
return (
|
||
<TokenContext.Provider value={value}>
|
||
{children}
|
||
</TokenContext.Provider>
|
||
);
|
||
}
|
||
|
||
export function useToken() {
|
||
const context = useContext(TokenContext);
|
||
if (!context) {
|
||
throw new Error('useToken must be used within a TokenProvider');
|
||
}
|
||
return context;
|
||
} |