Files
lingxi/apps/web/src/routes.ts
T
Stev_Wang 61894df401 feat(identity): 完成 A2 角色权限与数据范围全栈落地
背景:A2 开卡前完成 H1 权限门,确认权限点目录、特殊权限、SoD 三对、六类数据范围与超管隐式全集口径(ADR-001)。

改动:
- shared:30 权限点 / 10 模块目录与元数据、特殊权限 6 编码、SoD 三对、角色与权限树契约;/me 改 dataScopes 多角色并集。
- prisma:expand 迁移新增 permissions / data_scopes;种子按原型口径重写角色权限、数据范围与 8 项特殊授予。
- api:角色 CRUD / 复制 / 停用 / 删除、权限点启动同步、SoD 40304、系统角色保护、六类范围 ScopeContext 与 CustomerDirectory 端口、变更审计与 outbox。
- web:方案 B 主从分栏三角色页面(列表 / 编辑器 / 新建),权限树、范围选择、特殊权限二次确认、保存影响、SoD 冲突、只读态与模板预填。
- 测试:角色集成 13 条 + A2 e2e 1 条;A1 全量回归。

验证:format:check / lint / typecheck / test(306)/ test:int(27)/ A2 e2e(1,复跑通过)/ openapi:export 全绿。
2026-11-03 12:26:51 +08:00

80 lines
2.5 KiB
TypeScript

/**
* 前端路由常量(工程规范 04 / 11:路由集中管理,禁止散落字符串)
*
* 约定:`/<模块>/<功能>`;模块 key、基础路径与页面目录一一对应。
* 例如:管理中心(system)→ /system/users → pages/system/users。
*/
export const ROUTES = {
home: '/',
login: '/login',
system: {
root: '/system',
users: '/system/users',
userNew: '/system/users/new',
userDetail: '/system/users/:id',
roles: '/system/roles',
roleNew: '/system/roles/new',
roleDetail: '/system/roles/:id',
},
unauthorized: '/401',
forbidden: '/403',
notFound: '/404',
serverError: '/500',
} as const;
export type RoutePath =
| typeof ROUTES.home
| typeof ROUTES.login
| typeof ROUTES.system.root
| typeof ROUTES.system.users
| typeof ROUTES.system.userNew
| typeof ROUTES.system.userDetail
| typeof ROUTES.system.roles
| typeof ROUTES.system.roleNew
| typeof ROUTES.system.roleDetail
| typeof ROUTES.unauthorized
| typeof ROUTES.forbidden
| typeof ROUTES.notFound
| typeof ROUTES.serverError;
/** 用户详情深链:`/system/users/{id}` */
export function userDetailPath(userId: string): string {
return `${ROUTES.system.users}/${encodeURIComponent(userId)}`;
}
/** 角色详情 / 编辑器深链:`/system/roles/{id}` */
export function roleDetailPath(roleId: string): string {
return `${ROUTES.system.roles}/${encodeURIComponent(roleId)}`;
}
/** 新建角色深链;带来源角色时作为复制模板预填(特殊权限不随复制带入) */
export function roleNewPath(sourceRoleId?: string): string {
return sourceRoleId === undefined
? ROUTES.system.roleNew
: `${ROUTES.system.roleNew}?from=${encodeURIComponent(sourceRoleId)}`;
}
/**
* 校验回跳地址:只允许站内绝对路径(以单个 `/` 开头),
* 拒绝协议相对地址(`//evil.com`)与外部 URL,避免开放重定向。
*/
export function sanitizeRedirect(value: string | null | undefined): string | null {
if (value === null || value === undefined) {
return null;
}
const trimmed = value.trim();
if (!trimmed.startsWith('/') || trimmed.startsWith('//')) {
return null;
}
if (trimmed.startsWith(ROUTES.login)) {
return null;
}
return trimmed;
}
/** 登录页地址;带 redirect 时回跳原路径(仅接受站内相对路径) */
export function loginPath(redirect?: string): string {
const target = sanitizeRedirect(redirect);
return target === null ? ROUTES.login : `${ROUTES.login}?redirect=${encodeURIComponent(target)}`;
}