194 lines
4.8 KiB
TypeScript
194 lines
4.8 KiB
TypeScript
/**
|
||
* 服务端路由权限检查工具
|
||
* 用于在 action 中检查用户是否有权限访问目标路由
|
||
*/
|
||
|
||
import { getUserRoutesByRole, type MenuItem } from './user-routes';
|
||
import { normalizeRoutePathForPermission } from '~/utils/route-alias';
|
||
|
||
/**
|
||
* 从 MenuItem 数组中提取所有路径(包括子路由)
|
||
*/
|
||
function extractAllPaths(menuItems: MenuItem[]): string[] {
|
||
const paths: string[] = [];
|
||
|
||
function traverse(items: MenuItem[]) {
|
||
for (const item of items) {
|
||
paths.push(item.path);
|
||
if (item.children && item.children.length > 0) {
|
||
traverse(item.children);
|
||
}
|
||
}
|
||
}
|
||
|
||
traverse(menuItems);
|
||
return paths;
|
||
}
|
||
|
||
/**
|
||
* 检查路径段是否看起来像动态ID
|
||
*/
|
||
function isDynamicIdSegment(segment: string): boolean {
|
||
// 纯数字
|
||
if (/^\d+$/.test(segment)) {
|
||
return true;
|
||
}
|
||
|
||
// UUID格式
|
||
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment)) {
|
||
return true;
|
||
}
|
||
|
||
// 包含数字的混合ID
|
||
if (/\d/.test(segment) && !/^[a-z]+(-[a-z]+)*$/i.test(segment)) {
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 检查路径是否在允许列表中
|
||
*/
|
||
function isPathAllowed(pathname: string, allowedPaths: string[]): boolean {
|
||
const checkPath = normalizeRoutePathForPermission(pathname);
|
||
|
||
// 精确匹配
|
||
if (allowedPaths.includes(checkPath)) {
|
||
return true;
|
||
}
|
||
|
||
// 动态路由匹配
|
||
for (const allowedPath of allowedPaths) {
|
||
if (checkPath.startsWith(allowedPath + '/')) {
|
||
const subPath = checkPath.substring(allowedPath.length + 1);
|
||
const segments = subPath.split('/');
|
||
const firstSegment = segments[0];
|
||
|
||
if (isDynamicIdSegment(firstSegment)) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 根路径
|
||
if (checkPath === '/') {
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
export interface CheckRoutePermissionResult {
|
||
/** 是否有权限访问 */
|
||
allowed: boolean;
|
||
/** 错误信息(当 allowed 为 false 时) */
|
||
error?: string;
|
||
/** 用户允许访问的所有路由 */
|
||
allowedPaths?: string[];
|
||
}
|
||
|
||
/**
|
||
* 检查用户是否有权限访问指定路由
|
||
*
|
||
* @param targetPath 目标路由路径(如 '/contract-draft/1')
|
||
* @param userRole 用户角色
|
||
* @param jwt JWT token
|
||
* @returns 权限检查结果
|
||
*
|
||
* @example
|
||
* ```ts
|
||
* // 在 action 中使用
|
||
* export async function action({ request }: ActionFunctionArgs) {
|
||
* const { userInfo, frontendJWT } = await getUserSession(request);
|
||
*
|
||
* // 检查用户是否有权限访问目标路由
|
||
* const permissionCheck = await checkRoutePermission(
|
||
* '/contract-draft/1',
|
||
* userInfo.role,
|
||
* frontendJWT
|
||
* );
|
||
*
|
||
* if (!permissionCheck.allowed) {
|
||
* return Response.json({ error: permissionCheck.error }, { status: 403 });
|
||
* }
|
||
*
|
||
* // 继续执行 action 逻辑...
|
||
* }
|
||
* ```
|
||
*/
|
||
export async function checkRoutePermission(
|
||
targetPath: string,
|
||
userRole: string,
|
||
jwt?: string
|
||
): Promise<CheckRoutePermissionResult> {
|
||
if (!jwt) {
|
||
return {
|
||
allowed: false,
|
||
error: '未提供认证信息'
|
||
};
|
||
}
|
||
|
||
try {
|
||
// 获取用户的路由权限(包含隐藏路由,用于权限校验)
|
||
const routesResult = await getUserRoutesByRole(userRole, jwt, true);
|
||
|
||
if (!routesResult.success || !routesResult.data) {
|
||
return {
|
||
allowed: false,
|
||
error: routesResult.error || '获取用户权限失败'
|
||
};
|
||
}
|
||
|
||
// 提取所有允许的路径
|
||
const allowedPaths = extractAllPaths(routesResult.data);
|
||
|
||
// 检查目标路径是否在允许列表中
|
||
const allowed = isPathAllowed(targetPath, allowedPaths);
|
||
|
||
if (!allowed) {
|
||
console.warn(`[checkRoutePermission] 用户无权访问路由: ${targetPath}`);
|
||
console.warn(`[checkRoutePermission] 用户允许的路由: ${allowedPaths.join(', ')}`);
|
||
}
|
||
|
||
return {
|
||
allowed,
|
||
error: allowed ? undefined : '您没有权限访问目标页面',
|
||
allowedPaths
|
||
};
|
||
} catch (error) {
|
||
console.error('[checkRoutePermission] 权限检查失败:', error);
|
||
return {
|
||
allowed: false,
|
||
error: '权限检查失败,请稍后重试'
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 简化版:直接检查是否有权限,抛出 403 Response
|
||
*
|
||
* @example
|
||
* ```ts
|
||
* export async function action({ request }: ActionFunctionArgs) {
|
||
* const { userInfo, frontendJWT } = await getUserSession(request);
|
||
*
|
||
* // 如果没有权限,会自动抛出 403 Response
|
||
* await requireRoutePermission('/contract-draft/1', userInfo.role, frontendJWT);
|
||
*
|
||
* // 继续执行 action 逻辑...
|
||
* }
|
||
* ```
|
||
*/
|
||
export async function requireRoutePermission(
|
||
targetPath: string,
|
||
userRole: string,
|
||
jwt?: string
|
||
): Promise<void> {
|
||
const result = await checkRoutePermission(targetPath, userRole, jwt);
|
||
|
||
if (!result.allowed) {
|
||
throw new Response(result.error || '无权访问', { status: 403 });
|
||
}
|
||
}
|