2025-07-28 07:50:50 +08:00
|
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
|
|
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
|
|
|
|
|
import { oidcClient } from '@/lib/auth/oidc-client';
|
|
|
|
|
|
import { TokenManager } from '@/lib/auth/token-manager';
|
|
|
|
|
|
import { useTRPC } from '@fenghuo/client';
|
|
|
|
|
|
import { useQuery } from '@tanstack/react-query';
|
|
|
|
|
|
import {
|
|
|
|
|
|
SystemPermission,
|
|
|
|
|
|
PermissionUtils,
|
|
|
|
|
|
SystemRole,
|
|
|
|
|
|
SystemRoleConfig,
|
|
|
|
|
|
type PermissionMeta,
|
|
|
|
|
|
UserWithRelations,
|
|
|
|
|
|
userWithRelationsSelect
|
|
|
|
|
|
} from '@fenghuo/common';
|
|
|
|
|
|
import { useToken } from '@/components/providers/token-provider';
|
|
|
|
|
|
import { Prisma, Role } from '@fenghuo/db/index';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 导出权限相关类型和工具
|
|
|
|
|
|
export {
|
|
|
|
|
|
SystemPermission,
|
|
|
|
|
|
PermissionUtils,
|
|
|
|
|
|
SystemRole,
|
|
|
|
|
|
SystemRoleConfig,
|
|
|
|
|
|
type PermissionMeta
|
|
|
|
|
|
} from '@fenghuo/common';
|
|
|
|
|
|
|
|
|
|
|
|
// 扩展用户接口
|
|
|
|
|
|
export interface User extends Omit<UserWithRelations, 'roles'> {
|
|
|
|
|
|
roles: string[]; // 角色名称数组
|
|
|
|
|
|
permissions?: SystemPermission[]; // 使用新的权限系统
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface AuthProviderProps {
|
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface AuthContextType {
|
|
|
|
|
|
// 用户状态
|
|
|
|
|
|
user: User | null;
|
|
|
|
|
|
isLoading: boolean;
|
|
|
|
|
|
isAuthenticated: boolean;
|
|
|
|
|
|
|
|
|
|
|
|
// 权限检查方法
|
|
|
|
|
|
hasRole: (role: string) => boolean;
|
|
|
|
|
|
hasPermission: (permission: SystemPermission) => boolean;
|
|
|
|
|
|
hasAnyRole: (roles: string[]) => boolean;
|
|
|
|
|
|
hasAnyPermission: (permissions: SystemPermission[]) => boolean;
|
|
|
|
|
|
hasAllPermissions: (permissions: SystemPermission[]) => boolean;
|
|
|
|
|
|
isSuperAdmin: () => boolean;
|
|
|
|
|
|
isAdmin: () => boolean;
|
|
|
|
|
|
|
|
|
|
|
|
// 认证操作
|
|
|
|
|
|
login: (username: string, password: string) => Promise<void>;
|
|
|
|
|
|
register: (userData: Prisma.UserCreateArgs) => Promise<void>;
|
|
|
|
|
|
logout: () => Promise<void>;
|
|
|
|
|
|
getAccessToken: () => Promise<string | null>;
|
|
|
|
|
|
loadUser: () => Promise<void>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const AuthContext = createContext<AuthContextType | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
export default function AuthProvider({ children }: AuthProviderProps) {
|
|
|
|
|
|
const { refreshToken, clearTokens } = useToken();
|
|
|
|
|
|
const [userId, setUserId] = useState<string | null>(null);
|
|
|
|
|
|
const [user, setUser] = useState<User | null>(null);
|
|
|
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
|
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
const trpc = useTRPC();
|
|
|
|
|
|
|
|
|
|
|
|
// 修改 tRPC 查询,包含角色权限
|
|
|
|
|
|
const { data: backendUser, error: userError, isLoading: isUserLoading } = useQuery({
|
|
|
|
|
|
...trpc.user.findFirst.queryOptions({
|
|
|
|
|
|
where: { id: userId! },
|
|
|
|
|
|
select: userWithRelationsSelect
|
|
|
|
|
|
}),
|
|
|
|
|
|
enabled: !!userId,
|
|
|
|
|
|
retry: false,
|
|
|
|
|
|
}) as { data: UserWithRelations | null, error: Error | null, isLoading: boolean };
|
|
|
|
|
|
|
|
|
|
|
|
// 更新用户状态当后端用户数据加载完成时
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (backendUser) {
|
|
|
|
|
|
const userRoles = backendUser.roles?.map(r => r.slug) || [];
|
|
|
|
|
|
// 传递完整的角色数据给权限计算函数
|
|
|
|
|
|
const userPermissions = getUserPermissions(backendUser.roles || []);
|
|
|
|
|
|
const appUser: User = {
|
|
|
|
|
|
...backendUser,
|
|
|
|
|
|
roles: userRoles,
|
|
|
|
|
|
permissions: userPermissions,
|
|
|
|
|
|
};
|
|
|
|
|
|
setUser(appUser);
|
|
|
|
|
|
} else if (userError) {
|
|
|
|
|
|
console.error('获取用户数据失败:', userError);
|
|
|
|
|
|
setUser(null);
|
|
|
|
|
|
setIsAuthenticated(false);
|
|
|
|
|
|
TokenManager.clearTokens();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [backendUser, userError]);
|
|
|
|
|
|
|
|
|
|
|
|
// 加载用户信息
|
|
|
|
|
|
const loadUser = useCallback(async () => {
|
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const accessToken = await TokenManager.getValidAccessToken();
|
|
|
|
|
|
if (accessToken) {
|
|
|
|
|
|
// 只获取 OIDC 用户信息来获得用户 ID
|
|
|
|
|
|
const oidcUser = await oidcClient.getUserInfo(accessToken);
|
|
|
|
|
|
setUserId(oidcUser.sub);
|
|
|
|
|
|
setIsAuthenticated(true);
|
|
|
|
|
|
|
|
|
|
|
|
// 触发 QueryProvider 刷新 token
|
|
|
|
|
|
refreshToken();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setUserId(null);
|
|
|
|
|
|
setUser(null);
|
|
|
|
|
|
setIsAuthenticated(false);
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载用户信息失败:', error);
|
|
|
|
|
|
setUserId(null);
|
|
|
|
|
|
setUser(null);
|
|
|
|
|
|
setIsAuthenticated(false);
|
|
|
|
|
|
TokenManager.clearTokens();
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [refreshToken]);
|
|
|
|
|
|
|
|
|
|
|
|
// 当 tRPC 查询完成时更新 loading 状态
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (userId && !isUserLoading) {
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [userId, isUserLoading]);
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化时加载用户信息
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
loadUser();
|
|
|
|
|
|
}, [loadUser]);
|
|
|
|
|
|
|
|
|
|
|
|
// 登录功能
|
|
|
|
|
|
const login = useCallback(async (username: string, password: string): Promise<void> => {
|
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 调用 OIDC 服务登录
|
|
|
|
|
|
const tokenResponse = await oidcClient.loginWithPassword(username, password);
|
|
|
|
|
|
|
|
|
|
|
|
// 保存令牌
|
|
|
|
|
|
TokenManager.saveTokens(tokenResponse);
|
|
|
|
|
|
|
|
|
|
|
|
// 获取用户信息并设置 userId,让 tRPC 查询处理剩余逻辑
|
|
|
|
|
|
const oidcUser = await oidcClient.getUserInfo(tokenResponse.access_token);
|
|
|
|
|
|
setUserId(oidcUser.sub);
|
|
|
|
|
|
setIsAuthenticated(true);
|
|
|
|
|
|
|
|
|
|
|
|
// 登录成功后直接刷新token context
|
|
|
|
|
|
await refreshToken();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('登录失败:', error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [refreshToken]);
|
|
|
|
|
|
|
|
|
|
|
|
// 注册功能
|
|
|
|
|
|
const register = useCallback(async (userData: Prisma.UserCreateArgs): Promise<void> => {
|
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
await oidcClient.register(userData);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('注册失败:', error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 退出登录功能
|
|
|
|
|
|
const logout = useCallback(async () => {
|
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const accessToken = TokenManager.getAccessToken();
|
|
|
|
|
|
if (accessToken) {
|
|
|
|
|
|
await oidcClient.logout(accessToken);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('退出登录失败:', error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
TokenManager.clearTokens();
|
|
|
|
|
|
setUserId(null);
|
|
|
|
|
|
setUser(null);
|
|
|
|
|
|
setIsAuthenticated(false);
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
|
|
|
|
|
|
// 退出时清除token
|
|
|
|
|
|
clearTokens();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [clearTokens]);
|
|
|
|
|
|
|
|
|
|
|
|
// 获取访问令牌
|
|
|
|
|
|
const getAccessToken = useCallback(async () => {
|
|
|
|
|
|
return await TokenManager.getValidAccessToken();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 权限检查方法 - 使用新的权限系统
|
|
|
|
|
|
const hasRole = useCallback((role: string): boolean => {
|
|
|
|
|
|
return user?.roles.includes(role) || false;
|
|
|
|
|
|
}, [user]);
|
|
|
|
|
|
|
|
|
|
|
|
const hasPermission = useCallback((permission: SystemPermission): boolean => {
|
|
|
|
|
|
if (!user?.permissions) return false;
|
|
|
|
|
|
return PermissionUtils.hasPermission(user.permissions, permission);
|
|
|
|
|
|
}, [user]);
|
|
|
|
|
|
|
|
|
|
|
|
const hasAnyRole = useCallback((roles: string[]): boolean => {
|
|
|
|
|
|
return roles.some(role => hasRole(role));
|
|
|
|
|
|
}, [hasRole]);
|
|
|
|
|
|
|
|
|
|
|
|
const hasAnyPermission = useCallback((permissions: SystemPermission[]): boolean => {
|
|
|
|
|
|
if (!user?.permissions) return false;
|
|
|
|
|
|
return PermissionUtils.hasAnyPermission(user.permissions, permissions);
|
|
|
|
|
|
}, [user]);
|
|
|
|
|
|
|
|
|
|
|
|
const hasAllPermissions = useCallback((permissions: SystemPermission[]): boolean => {
|
|
|
|
|
|
if (!user?.permissions) return false;
|
|
|
|
|
|
return PermissionUtils.hasAllPermissions(user.permissions, permissions);
|
|
|
|
|
|
}, [user]);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否为超级管理员
|
|
|
|
|
|
const isSuperAdmin = useCallback((): boolean => {
|
|
|
|
|
|
return hasPermission(SystemPermission.SUPER_ADMIN);
|
|
|
|
|
|
}, [hasPermission]);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否为管理员级别
|
|
|
|
|
|
const isAdmin = useCallback((): boolean => {
|
|
|
|
|
|
return hasRole(SystemRole.SUPER_ADMIN) || hasRole(SystemRole.ADMIN);
|
|
|
|
|
|
}, [hasRole]);
|
|
|
|
|
|
|
|
|
|
|
|
const contextValue: AuthContextType = {
|
|
|
|
|
|
user,
|
|
|
|
|
|
isLoading,
|
|
|
|
|
|
isAuthenticated,
|
|
|
|
|
|
hasRole,
|
|
|
|
|
|
hasPermission,
|
|
|
|
|
|
hasAnyRole,
|
|
|
|
|
|
hasAnyPermission,
|
|
|
|
|
|
hasAllPermissions,
|
|
|
|
|
|
isSuperAdmin,
|
|
|
|
|
|
isAdmin,
|
|
|
|
|
|
login,
|
|
|
|
|
|
register,
|
|
|
|
|
|
logout,
|
|
|
|
|
|
getAccessToken,
|
|
|
|
|
|
loadUser,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<AuthContext.Provider value={contextValue}>
|
|
|
|
|
|
{children}
|
|
|
|
|
|
</AuthContext.Provider>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function useAuth() {
|
|
|
|
|
|
const context = useContext(AuthContext);
|
|
|
|
|
|
if (!context) {
|
|
|
|
|
|
throw new Error('useAuth must be used within an AuthProvider');
|
|
|
|
|
|
}
|
|
|
|
|
|
return context;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 计算用户权限 - 基于数据库角色权限
|
|
|
|
|
|
const getUserPermissions = (roles: Role[]): SystemPermission[] => {
|
|
|
|
|
|
const permissions = new Set<SystemPermission>();
|
|
|
|
|
|
|
|
|
|
|
|
roles.forEach((role) => {
|
|
|
|
|
|
if (!role.permissions || !Array.isArray(role.permissions)) {
|
|
|
|
|
|
console.warn('角色权限数据格式错误:', role);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
role.permissions.forEach(permission => {
|
|
|
|
|
|
if (PermissionUtils.isValidPermission(permission)) {
|
|
|
|
|
|
permissions.add(permission as SystemPermission);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn('无效的权限代码:', permission);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
return Array.from(permissions);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 权限检查工具函数 - 更新为使用新的权限系统
|
|
|
|
|
|
export const checkPermission = (userPermissions: SystemPermission[], requiredPermissions?: SystemPermission[]): boolean => {
|
|
|
|
|
|
if (!requiredPermissions || requiredPermissions.length === 0) return true;
|
|
|
|
|
|
return PermissionUtils.hasAnyPermission(userPermissions, requiredPermissions);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 检查角色权限的工具函数
|
|
|
|
|
|
export const checkRole = (userRoles: string[], requiredRoles?: string[]): boolean => {
|
|
|
|
|
|
if (!requiredRoles || requiredRoles.length === 0) return true;
|
|
|
|
|
|
return requiredRoles.some(role => userRoles.includes(role));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 过滤菜单项的工具函数 - 支持角色和权限检查
|
|
|
|
|
|
export const filterMenuItems = <T extends { roles?: string[]; permissions?: SystemPermission[] }>(
|
|
|
|
|
|
items: T[],
|
|
|
|
|
|
user: User | null
|
|
|
|
|
|
): T[] => {
|
2025-07-28 10:32:25 +08:00
|
|
|
|
return items
|
2025-07-28 07:50:50 +08:00
|
|
|
|
if (!user) return [];
|
|
|
|
|
|
|
|
|
|
|
|
return items.filter(item => {
|
|
|
|
|
|
// 检查角色要求
|
|
|
|
|
|
if (item.roles && !checkRole(user.roles, item.roles)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查权限要求
|
|
|
|
|
|
if (item.permissions && user.permissions && !checkPermission(user.permissions, item.permissions)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|