196 lines
5.7 KiB
TypeScript
196 lines
5.7 KiB
TypeScript
|
|
// 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;
|
|||
|
|
}
|