Files
leaudit-platform-frontend/app/api/auth/check-route-permission.server.ts
T

194 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 服务端路由权限检查工具
* 用于在 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 });
}
}