feat():learning后台管理前端页面初始化
This commit is contained in:
415
src/hooks/useAuth.ts
Normal file
415
src/hooks/useAuth.ts
Normal file
@@ -0,0 +1,415 @@
|
||||
// import { message } from 'antd'; // 暂时注释掉,未使用
|
||||
import { useAtom } from 'jotai';
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { authStateAtom, authLoadingAtom } from '../atoms/auth';
|
||||
import { SSOApiService } from '../services/api';
|
||||
import type { AuthState } from '../atoms/auth';
|
||||
|
||||
export const useAuth = () => {
|
||||
const [authState, setAuthState] = useAtom(authStateAtom);
|
||||
const [loading, setLoading] = useAtom(authLoadingAtom);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const refreshTimerRef = useRef<number | null>(null);
|
||||
|
||||
// 公共的token更新和定时器设置逻辑
|
||||
const updateTokenAndSetTimer = (
|
||||
tokenData: {
|
||||
access_token: string;
|
||||
refresh_token?: string;
|
||||
expires_in?: number;
|
||||
}
|
||||
) => {
|
||||
const { access_token, refresh_token, expires_in } = tokenData;
|
||||
|
||||
// 更新本地存储
|
||||
localStorage.setItem('token', access_token);
|
||||
if (refresh_token) {
|
||||
localStorage.setItem('refreshToken', refresh_token);
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
const updatedState: Partial<AuthState> = {
|
||||
token: access_token,
|
||||
};
|
||||
if (refresh_token) {
|
||||
updatedState.refreshToken = refresh_token;
|
||||
}
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
...updatedState,
|
||||
}));
|
||||
|
||||
// 清除旧的定时器并设置新的定时器
|
||||
if (refreshTimerRef.current) {
|
||||
clearTimeout(refreshTimerRef.current);
|
||||
}
|
||||
|
||||
if (expires_in) {
|
||||
// 提前2分钟触发主动刷新
|
||||
refreshTimerRef.current = setTimeout(
|
||||
refreshToken,
|
||||
expires_in * 1000 - 2 * 60000
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 跳转到登录页面(若无URL则先请求再跳转)
|
||||
const redirectToLogin = async () => {
|
||||
console.log('🔄 [Redirect] 开始跳转登录流程');
|
||||
let loginUrl = authState.loginUrl;
|
||||
console.log('🔍 [Redirect] 当前loginUrl:', loginUrl);
|
||||
|
||||
if (!loginUrl) {
|
||||
console.log('🚀 [Redirect] 无登录URL,获取新的登录URL');
|
||||
try {
|
||||
const resp = await SSOApiService.initiateLogin();
|
||||
console.log('🚀 [Redirect] 登录URL响应:', {
|
||||
success: resp?.success,
|
||||
hasAuthUrl: !!resp?.auth_url,
|
||||
authUrl: resp?.auth_url
|
||||
});
|
||||
|
||||
if (resp?.auth_url) {
|
||||
loginUrl = resp.auth_url;
|
||||
setAuthState(prev => ({ ...prev, loginUrl }));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('❌ [Redirect] 获取登录URL失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (loginUrl && loginUrl.startsWith('http')) {
|
||||
console.log('🔄 [Redirect] 跳转到SSO登录页面:', loginUrl);
|
||||
window.location.href = loginUrl;
|
||||
} else {
|
||||
console.error('❌ [Redirect] 无效的登录URL:', loginUrl);
|
||||
}
|
||||
};
|
||||
|
||||
// token 刷新
|
||||
const refreshToken = async () => {
|
||||
if (!authState.refreshToken) {
|
||||
console.warn('No refresh token available');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await SSOApiService.refreshToken(authState.refreshToken);
|
||||
if (response?.access_token) {
|
||||
updateTokenAndSetTimer({
|
||||
access_token: response.access_token,
|
||||
refresh_token: response.refresh_token,
|
||||
expires_in: response.expires_in,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Token refresh failed:', error);
|
||||
// 刷新失败,重新登录
|
||||
logout();
|
||||
}
|
||||
};
|
||||
|
||||
// SSO登录
|
||||
const ssoLogin = async (code?: string, state?: string) => {
|
||||
try {
|
||||
console.log('🔐 [SSO] 开始SSO登录流程', { hasCode: !!code, hasState: !!state });
|
||||
setLoading(true);
|
||||
|
||||
if (code && state) {
|
||||
console.log('📞 [SSO] 处理SSO回调,调用 /api/sso/callback');
|
||||
// 有code和state,进行回调处理
|
||||
const response = await SSOApiService.callback(code, state);
|
||||
console.log('📞 [SSO] 回调响应:', {
|
||||
success: response?.success,
|
||||
hasToken: !!response?.access_token,
|
||||
hasUserInfo: !!response?.user_info
|
||||
});
|
||||
|
||||
if (response?.access_token) {
|
||||
console.log('🔑 [SSO] 获取到access_token,更新本地状态');
|
||||
updateTokenAndSetTimer({
|
||||
access_token: response.access_token,
|
||||
refresh_token: response.refresh_token,
|
||||
expires_in: response.expires_in,
|
||||
});
|
||||
|
||||
// 获取用户信息
|
||||
if (response.user_info) {
|
||||
console.log('👤 [SSO] 回调中包含用户信息,直接设置');
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
user: response.user_info || null,
|
||||
isAuthenticated: true,
|
||||
}));
|
||||
localStorage.setItem('user', JSON.stringify(response.user_info));
|
||||
// 角色判断:若非 sys_admin,跳转无权限
|
||||
try {
|
||||
const rolesRaw: any = (response as any)?.user_info?.roles || [];
|
||||
const roleNames: string[] = Array.isArray(rolesRaw)
|
||||
? rolesRaw.map((r: any) => (typeof r === 'string' ? r : r?.name)).filter(Boolean)
|
||||
: [];
|
||||
const isSysAdmin = roleNames.includes('sys_admin');
|
||||
console.log('🔎 [SSO] 角色检测:', { roleNames, isSysAdmin });
|
||||
if (!isSysAdmin) {
|
||||
console.warn('🚫 [Permission] 非 sys_admin,跳转 /no-permission');
|
||||
navigate('/no-permission', { replace: true });
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('⚠️ [SSO] 角色解析失败:', e);
|
||||
}
|
||||
} else {
|
||||
console.log('👤 [SSO] 回调中无用户信息,调用 /api/sso/userinfo');
|
||||
await getCurrentUser();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('🚀 [SSO] 获取登录URL,调用 /api/sso/login');
|
||||
// 无code和state,获取登录URL
|
||||
const response = await SSOApiService.initiateLogin();
|
||||
console.log('🚀 [SSO] 登录URL响应:', {
|
||||
success: response?.success,
|
||||
hasAuthUrl: !!response?.auth_url,
|
||||
authUrl: response?.auth_url
|
||||
});
|
||||
|
||||
if (response?.auth_url) {
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
loginUrl: response.auth_url,
|
||||
}));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ [SSO] SSO登录失败:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取当前用户信息
|
||||
const getCurrentUser = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await SSOApiService.userInfo();
|
||||
if (response?.user_info) {
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
user: response.user_info,
|
||||
isAuthenticated: true,
|
||||
}));
|
||||
localStorage.setItem('user', JSON.stringify(response.user_info));
|
||||
// 角色判断:若非 sys_admin,跳转无权限
|
||||
try {
|
||||
const rolesRaw: any = (response as any)?.user_info?.roles || [];
|
||||
const roleNames: string[] = Array.isArray(rolesRaw)
|
||||
? rolesRaw.map((r: any) => (typeof r === 'string' ? r : r?.name)).filter(Boolean)
|
||||
: [];
|
||||
const isSysAdmin = roleNames.includes('sys_admin');
|
||||
console.log('🔎 [Auth] 角色检测:', { roleNames, isSysAdmin });
|
||||
if (!isSysAdmin) {
|
||||
console.warn('🚫 [Permission] 非 sys_admin,跳转 /no-permission');
|
||||
navigate('/no-permission', { replace: true });
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('⚠️ [Auth] 角色解析失败:', e);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Get current user failed:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化认证状态
|
||||
const initAuth = async () => {
|
||||
try {
|
||||
console.log('🔐 [Auth] 开始初始化认证状态');
|
||||
setLoading(true);
|
||||
|
||||
// 检查URL中的code参数
|
||||
const code = searchParams.get('code');
|
||||
const state = searchParams.get('state');
|
||||
console.log('🔍 [Auth] 检查URL参数:', { code: !!code, state: !!state });
|
||||
|
||||
if (code && state) {
|
||||
console.log('📞 [Auth] 检测到SSO回调参数,开始处理回调');
|
||||
// 如果有code参数,进行单点登录
|
||||
try {
|
||||
await ssoLogin(code, state);
|
||||
setSearchParams({});
|
||||
console.log('✅ [Auth] SSO回调处理完成');
|
||||
} catch (error) {
|
||||
console.error('❌ [Auth] SSO回调处理失败:', error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查本地存储的token
|
||||
const token = localStorage.getItem('token');
|
||||
const refreshToken = localStorage.getItem('refreshToken');
|
||||
const userStr = localStorage.getItem('user');
|
||||
console.log('💾 [Auth] 检查本地存储:', {
|
||||
hasToken: !!token,
|
||||
hasRefreshToken: !!refreshToken,
|
||||
hasUser: !!userStr
|
||||
});
|
||||
|
||||
if (token && userStr) {
|
||||
console.log('🔑 [Auth] 发现本地token,开始验证有效性');
|
||||
try {
|
||||
const user = JSON.parse(userStr);
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
user,
|
||||
token,
|
||||
refreshToken,
|
||||
isAuthenticated: true, // 先标记为已认证,避免触发SSO登录
|
||||
}));
|
||||
|
||||
// 在后台验证token是否有效(不阻塞用户使用)
|
||||
getCurrentUser().then(() => {
|
||||
console.log('✅ [Auth] 后台Token验证成功');
|
||||
}).catch(error => {
|
||||
console.warn('⚠️ [Auth] 后台Token验证失败,但不影响用户使用:', error);
|
||||
// 可以选择是否要处理验证失败的情况
|
||||
});
|
||||
|
||||
console.log('✅ [Auth] 本地token验证成功,用户已认证');
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error('❌ [Auth] 用户数据解析失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有code也没有有效token,请求登录URL
|
||||
console.log('🚀 [Auth] 无有效认证信息,开始获取登录URL');
|
||||
await ssoLogin();
|
||||
} catch (error) {
|
||||
console.error('❌ [Auth] 初始化认证失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 登出
|
||||
const logout = async () => {
|
||||
try {
|
||||
console.log('🚪 [Logout] 开始登出流程');
|
||||
|
||||
// 调用后端登出接口获取登出URL
|
||||
let logoutUrl = null;
|
||||
if (authState.token) {
|
||||
try {
|
||||
console.log('📞 [Logout] 调用后端登出接口 /api/sso/logout');
|
||||
const res = await SSOApiService.logout();
|
||||
console.log('📞 [Logout] 后端登出响应:', {
|
||||
success: res?.success,
|
||||
message: res?.message
|
||||
});
|
||||
|
||||
// 检查返回的message是否为有效URL
|
||||
if (res?.message && res.message.startsWith('http')) {
|
||||
logoutUrl = res.message;
|
||||
console.log('✅ [Logout] 使用后端返回的登出URL:', logoutUrl);
|
||||
} else {
|
||||
console.warn('⚠️ [Logout] 后端返回的message不是有效URL:', res.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ [Logout] 后端登出失败,但继续清理本地状态:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理本地状态(必须)
|
||||
console.log('🧹 [Logout] 清理本地状态');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('refreshToken');
|
||||
localStorage.removeItem('user');
|
||||
|
||||
// 清理定时器
|
||||
if (refreshTimerRef.current) {
|
||||
clearTimeout(refreshTimerRef.current);
|
||||
}
|
||||
|
||||
// 使用后端返回的登出URL,如果没有则使用默认的
|
||||
if (logoutUrl) {
|
||||
console.log('✅ [Logout] 登出完成,跳转到SSO登出页面');
|
||||
console.log('🔄 [Logout] 登出URL:', logoutUrl);
|
||||
window.location.href = logoutUrl;
|
||||
} else {
|
||||
console.log('✅ [Logout] 登出完成,跳转到当前域名');
|
||||
// 如果没有有效的登出URL,跳转到当前域名让应用重新走认证流程
|
||||
window.location.href = window.location.origin;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ [Logout] 登出失败:', error);
|
||||
// 即使登出失败,也要清理本地状态
|
||||
console.log('🧹 [Logout] 清理本地状态(异常情况)');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('refreshToken');
|
||||
localStorage.removeItem('user');
|
||||
|
||||
setAuthState({
|
||||
user: null,
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
isAuthenticated: false,
|
||||
loginUrl: null,
|
||||
});
|
||||
|
||||
// 跳转到登录页面
|
||||
console.log('🔄 [Logout] 异常情况,跳转到登录页面');
|
||||
await redirectToLogin();
|
||||
}
|
||||
};
|
||||
|
||||
// 处理业务接口被动触发的token刷新和重新登录问题
|
||||
useEffect(() => {
|
||||
const handleTokenRefreshed = (event: CustomEvent) => {
|
||||
const { access_token, refresh_token, expires_in } = event.detail;
|
||||
updateTokenAndSetTimer({
|
||||
access_token,
|
||||
refresh_token,
|
||||
expires_in,
|
||||
});
|
||||
};
|
||||
|
||||
const handleRelogin = () => {
|
||||
logout();
|
||||
};
|
||||
|
||||
window.addEventListener('auth:tokenRefreshed', handleTokenRefreshed as EventListener);
|
||||
window.addEventListener('auth:relogin', handleRelogin);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('auth:tokenRefreshed', handleTokenRefreshed as EventListener);
|
||||
window.removeEventListener('auth:relogin', handleRelogin);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (refreshTimerRef.current) {
|
||||
clearTimeout(refreshTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...authState,
|
||||
loading,
|
||||
initAuth,
|
||||
redirectToLogin,
|
||||
refreshToken,
|
||||
getCurrentUser,
|
||||
logout,
|
||||
};
|
||||
};
|
||||
74
src/hooks/usePagePermissions.ts
Normal file
74
src/hooks/usePagePermissions.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { useAtom } from 'jotai';
|
||||
import { authStateAtom } from '../atoms/auth';
|
||||
|
||||
export const usePagePermissions = () => {
|
||||
const [authState] = useAtom(authStateAtom);
|
||||
|
||||
const hasPagePermission = (pagePath: string, action: string = 'read'): boolean => {
|
||||
console.log(`🔐 [usePagePermissions] 检查页面权限 "${pagePath}:${action}"`);
|
||||
console.log(`🔐 [usePagePermissions] 用户数据:`, authState.user);
|
||||
|
||||
// 检查用户是否有页面数据
|
||||
if (!authState.user?.pages) {
|
||||
console.log(`🔐 [usePagePermissions] 用户页面数据为空:`, authState.user);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 根据页面路径和激活状态检查权限
|
||||
const hasAccess = authState.user.pages.some(page =>
|
||||
page.path === pagePath && page.is_active !== false
|
||||
);
|
||||
|
||||
console.log(`🔐 [usePagePermissions] 页面权限检查结果: ${hasAccess}, 用户可访问页面:`,
|
||||
authState.user.pages.map(p => ({ path: p.path, isActive: p.is_active })));
|
||||
return hasAccess;
|
||||
};
|
||||
|
||||
const hasPageAction = (pagePath: string, action: string): boolean => {
|
||||
return hasPagePermission(pagePath, action);
|
||||
};
|
||||
|
||||
const getPageActions = (pagePath: string): string[] => {
|
||||
console.log(`🔐 [usePagePermissions] 获取页面操作权限 "${pagePath}"`);
|
||||
|
||||
if (!authState.user?.pages) {
|
||||
console.log(`🔐 [usePagePermissions] 用户页面数据为空`);
|
||||
return [];
|
||||
}
|
||||
|
||||
// 检查用户是否有该页面的访问权限
|
||||
const hasAccess = authState.user.pages.some(page => page.path === pagePath);
|
||||
if (!hasAccess) {
|
||||
console.log(`🔐 [usePagePermissions] 用户无该页面访问权限`);
|
||||
return [];
|
||||
}
|
||||
|
||||
// 简化版本:如果有页面访问权限,返回基本操作
|
||||
return ['read', 'create', 'update', 'delete'];
|
||||
};
|
||||
|
||||
const getAccessiblePages = (): string[] => {
|
||||
console.log(`🔐 [usePagePermissions] 获取用户可访问页面`);
|
||||
console.log(`🔐 [usePagePermissions] 用户数据:`, authState.user);
|
||||
|
||||
if (!authState.user?.pages) {
|
||||
console.log(`🔐 [usePagePermissions] 用户页面数据为空`);
|
||||
return [];
|
||||
}
|
||||
|
||||
// 只返回激活状态的页面路径
|
||||
const pages = authState.user.pages
|
||||
.filter(page => page.is_active !== false)
|
||||
.map(page => page.path);
|
||||
console.log(`🔐 [usePagePermissions] 用户可访问页面:`, pages);
|
||||
|
||||
return pages;
|
||||
};
|
||||
|
||||
return {
|
||||
hasPagePermission,
|
||||
hasPageAction,
|
||||
getPageActions,
|
||||
getAccessiblePages
|
||||
};
|
||||
};
|
||||
46
src/hooks/usePermissions.ts
Normal file
46
src/hooks/usePermissions.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useAtom } from 'jotai';
|
||||
import { authStateAtom } from '../atoms/auth';
|
||||
|
||||
export const usePermissions = () => {
|
||||
const [authState] = useAtom(authStateAtom);
|
||||
|
||||
const hasPermission = (permission: string): boolean => {
|
||||
if (!authState.user?.permissions) {
|
||||
console.log(`🔐 [usePermissions] 用户权限数据为空:`, authState.user);
|
||||
return false;
|
||||
}
|
||||
const hasAccess = authState.user.permissions.includes(permission);
|
||||
console.log(`🔐 [usePermissions] 检查权限 "${permission}": ${hasAccess}, 用户权限:`, authState.user.permissions);
|
||||
return hasAccess;
|
||||
};
|
||||
|
||||
const hasAnyPermission = (permissions: string[]): boolean => {
|
||||
return permissions.some(permission => hasPermission(permission));
|
||||
};
|
||||
|
||||
const hasAllPermissions = (permissions: string[]): boolean => {
|
||||
return permissions.every(permission => hasPermission(permission));
|
||||
};
|
||||
|
||||
const hasRole = (roleName: string): boolean => {
|
||||
if (!authState.user?.roles) return false;
|
||||
return authState.user.roles.includes(roleName);
|
||||
};
|
||||
|
||||
const hasAnyRole = (roleNames: string[]): boolean => {
|
||||
return roleNames.some(roleName => hasRole(roleName));
|
||||
};
|
||||
|
||||
const hasAllRoles = (roleNames: string[]): boolean => {
|
||||
return roleNames.every(roleName => hasRole(roleName));
|
||||
};
|
||||
|
||||
return {
|
||||
hasPermission,
|
||||
hasAnyPermission,
|
||||
hasAllPermissions,
|
||||
hasRole,
|
||||
hasAnyRole,
|
||||
hasAllRoles
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user