feat():learning后台管理前端页面初始化

This commit is contained in:
yuj
2025-12-04 17:51:24 +08:00
commit 83a614bd75
97 changed files with 23324 additions and 0 deletions

415
src/hooks/useAuth.ts Normal file
View 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,
};
};

View 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
};
};

View 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
};
};