统一项目角色与接口权限配置
This commit is contained in:
@@ -13,6 +13,8 @@ describe("permission project role model", () => {
|
||||
expect(source).toContain("study.currentStudyRole");
|
||||
expect(source).toContain("study.currentPermissions");
|
||||
expect(source).toContain('"faq.edit": "faq:update"');
|
||||
expect(source).toContain('"ae.create": "subject_aes:create"');
|
||||
expect(source).toContain('"ae.close": "subject_aes:update"');
|
||||
expect(source).toContain('"fees.contract.create": "fees_contracts:create"');
|
||||
expect(source).toContain('"fees.contract.update": "fees_contracts:update"');
|
||||
expect(source).toContain('"fees.contract.delete": "fees_contracts:delete"');
|
||||
|
||||
@@ -59,15 +59,14 @@ export const usePermission = () => {
|
||||
"subject.enroll": "subjects:update",
|
||||
"subject.complete": "subjects:update",
|
||||
"subject.drop": "subjects:update",
|
||||
"ae.create": "risk_issues:create",
|
||||
"ae.close": "risk_issues:update",
|
||||
"ae.create": "subject_aes:create",
|
||||
"ae.close": "subject_aes:update",
|
||||
"project.overview.read": "project_overview:read",
|
||||
"faq.edit": "faq:update",
|
||||
"faq.create": "faq:create",
|
||||
"faq.reply": "faq_reply:create",
|
||||
"knowledge.notes.write": "knowledge_notes:create",
|
||||
"shared.library.read": "knowledge_notes:read",
|
||||
"shared.library.write": "knowledge_notes:create",
|
||||
"precautions.read": "precautions:read",
|
||||
"precautions.write": "precautions:create",
|
||||
"project.members.manage": "project_members:update",
|
||||
"site.manage": "sites:update",
|
||||
"site.cra.bind": "sites:update",
|
||||
@@ -76,8 +75,6 @@ export const usePermission = () => {
|
||||
"fees.contract.delete": "fees_contracts:delete",
|
||||
"documents.create": "documents:create",
|
||||
"documents.delete": "documents:delete",
|
||||
"fees.attachment.delete": "fees_attachments:delete",
|
||||
"file.attachment.delete": "attachments:delete",
|
||||
"audit.export.read": "audit_logs:read",
|
||||
};
|
||||
const operationKey = permissionMap[action];
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
export const PROJECT_PERMISSION_MODULE_LABELS: Record<string, string> = {
|
||||
subjects: "参与者管理",
|
||||
sites: "中心管理",
|
||||
risk_issues: "风险问题",
|
||||
project_members: "项目成员",
|
||||
project_milestones: "项目里程碑",
|
||||
project_overview: "项目总览",
|
||||
setup_config: "立项配置",
|
||||
fees: "合同费用",
|
||||
materials: "物资管理",
|
||||
documents: "文件版本",
|
||||
startup_ethics: "立项与伦理",
|
||||
startup_auth: "启动与授权",
|
||||
audit_export: "审计日志",
|
||||
shared_library: "共享库",
|
||||
subject_histories: "病史记录",
|
||||
};
|
||||
|
||||
const COMMON_PROJECT_PERMISSION_MODULES = new Set([
|
||||
"setup_config",
|
||||
"project_members",
|
||||
"sites",
|
||||
"audit_export",
|
||||
]);
|
||||
|
||||
export const PROJECT_PERMISSION_CATEGORY_LABELS: Record<"common" | "business", string> = {
|
||||
common: "通用权限",
|
||||
business: "业务权限",
|
||||
};
|
||||
|
||||
const PROJECT_MENU_MODULE_ORDER = [
|
||||
"setup_config",
|
||||
"project_members",
|
||||
"sites",
|
||||
"audit_export",
|
||||
"project_overview",
|
||||
"project_milestones",
|
||||
"fees",
|
||||
"materials",
|
||||
"documents",
|
||||
"startup_ethics",
|
||||
"startup_auth",
|
||||
"subjects",
|
||||
"subject_histories",
|
||||
"risk_issues",
|
||||
"shared_library",
|
||||
];
|
||||
|
||||
export const projectPermissionModuleLabel = (module: string): string =>
|
||||
PROJECT_PERMISSION_MODULE_LABELS[module] || module;
|
||||
|
||||
export const projectPermissionCategory = (module: string): "common" | "business" =>
|
||||
COMMON_PROJECT_PERMISSION_MODULES.has(module) ? "common" : "business";
|
||||
|
||||
export const projectPermissionCategoryLabel = (module: string): string =>
|
||||
PROJECT_PERMISSION_CATEGORY_LABELS[projectPermissionCategory(module)];
|
||||
|
||||
export const getProjectPermissionModuleOrder = (module: string): number => {
|
||||
const index = PROJECT_MENU_MODULE_ORDER.indexOf(module);
|
||||
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
||||
};
|
||||
|
||||
export const compareProjectPermissionModules = (a: string, b: string): number => {
|
||||
const categoryDiff = Number(projectPermissionCategory(a) === "business") - Number(projectPermissionCategory(b) === "business");
|
||||
if (categoryDiff !== 0) return categoryDiff;
|
||||
const orderDiff = getProjectPermissionModuleOrder(a) - getProjectPermissionModuleOrder(b);
|
||||
if (orderDiff !== 0) return orderDiff;
|
||||
return projectPermissionModuleLabel(a).localeCompare(projectPermissionModuleLabel(b), "zh-CN");
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
const PROJECT_ROLE_RANK: Record<string, number> = {
|
||||
ADMIN: 100,
|
||||
PM: 80,
|
||||
QA: 60,
|
||||
PV: 50,
|
||||
CRA: 40,
|
||||
CTA: 40,
|
||||
};
|
||||
|
||||
export const compareProjectPermissionRoles = (a: string, b: string): number =>
|
||||
(PROJECT_ROLE_RANK[b] ?? 0) - (PROJECT_ROLE_RANK[a] ?? 0) || a.localeCompare(b);
|
||||
@@ -5,33 +5,41 @@ describe("project route permissions", () => {
|
||||
it("maps project routes to backend operation keys", () => {
|
||||
expect(getProjectRoutePermission("/project/overview")).toEqual({ operationKey: "project_overview:read" });
|
||||
expect(getProjectRoutePermission("/project/milestones")).toEqual({ operationKey: "project_milestones:read" });
|
||||
expect(getProjectRoutePermission("/finance/contracts")).toEqual({ operationKey: "finance_contracts:read" });
|
||||
expect(getProjectRoutePermission("/finance/contracts/new")).toEqual({ operationKey: "finance_contracts:create" });
|
||||
expect(getProjectRoutePermission("/finance/contracts/abc/edit")).toEqual({ operationKey: "finance_contracts:update" });
|
||||
expect(getProjectRoutePermission("/finance/contracts")).toBeNull();
|
||||
expect(getProjectRoutePermission("/finance/contracts/new")).toBeNull();
|
||||
expect(getProjectRoutePermission("/finance/contracts/abc/edit")).toBeNull();
|
||||
expect(getProjectRoutePermission("/drug/shipments")).toEqual({ operationKey: "drug_shipments:read" });
|
||||
expect(getProjectRoutePermission("/drug/shipments/new")).toEqual({ operationKey: "drug_shipments:create" });
|
||||
expect(getProjectRoutePermission("/drug/shipments/abc/edit")).toEqual({ operationKey: "drug_shipments:update" });
|
||||
expect(getProjectRoutePermission("/subjects/new")).toEqual({ operationKey: "subjects:create" });
|
||||
expect(getProjectRoutePermission("/subjects/abc/edit")).toEqual({ operationKey: "subjects:update" });
|
||||
expect(getProjectRoutePermission("/startup/feasibility/new")).toEqual({ operationKey: "ethics:create" });
|
||||
expect(getProjectRoutePermission("/startup/feasibility/abc/edit")).toEqual({ operationKey: "ethics:update" });
|
||||
expect(getProjectRoutePermission("/risk-issues")).toEqual({ operationKey: "subject_aes:list" });
|
||||
expect(getProjectRoutePermission("/risk-issues/sae")).toEqual({ operationKey: "subject_aes:list" });
|
||||
expect(getProjectRoutePermission("/risk-issues/pd")).toEqual({ operationKey: "subject_pds:list" });
|
||||
expect(getProjectRoutePermission("/risk-issues/monitoring-visits")).toEqual({ operationKey: "monitoring_issues:list" });
|
||||
expect(getProjectRoutePermission("/etmf")).toEqual({ operationKey: "documents:read" });
|
||||
expect(getProjectRoutePermission("/startup/feasibility/new")).toEqual({ operationKey: "startup_initiation:create" });
|
||||
expect(getProjectRoutePermission("/startup/feasibility/abc/edit")).toEqual({ operationKey: "startup_initiation:update" });
|
||||
expect(getProjectRoutePermission("/startup/ethics/new")).toEqual({ operationKey: "startup_ethics:create" });
|
||||
expect(getProjectRoutePermission("/startup/ethics/abc/edit")).toEqual({ operationKey: "startup_ethics:update" });
|
||||
expect(getProjectRoutePermission("/startup/kickoff/new")).toEqual({ operationKey: "startup_auth:create" });
|
||||
expect(getProjectRoutePermission("/startup/kickoff/abc/edit")).toEqual({ operationKey: "startup_auth:update" });
|
||||
expect(getProjectRoutePermission("/knowledge/medical-consult/abc")).toEqual({ operationKey: "faq:read" });
|
||||
expect(getProjectRoutePermission("/knowledge/notes/new")).toEqual({ operationKey: "knowledge_notes:create" });
|
||||
expect(getProjectRoutePermission("/knowledge/notes/abc/edit")).toEqual({ operationKey: "knowledge_notes:update" });
|
||||
expect(getProjectRoutePermission("/knowledge/precautions/new")).toEqual({ operationKey: "precautions:create" });
|
||||
expect(getProjectRoutePermission("/knowledge/precautions/abc/edit")).toEqual({ operationKey: "precautions:update" });
|
||||
expect(getProjectRoutePermission("/knowledge/notes/new")).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects project routes when the role lacks operation permission", () => {
|
||||
const matrix = {
|
||||
CRA: {
|
||||
"project_overview:read": { allowed: false },
|
||||
"knowledge_notes:read": { allowed: true },
|
||||
"precautions:read": { allowed: true },
|
||||
},
|
||||
} as any;
|
||||
|
||||
expect(hasProjectPermission(matrix, "CRA", getProjectRoutePermission("/project/overview"), false)).toBe(false);
|
||||
expect(hasProjectPermission(matrix, "CRA", getProjectRoutePermission("/knowledge/notes"), false)).toBe(true);
|
||||
expect(findFirstAccessibleProjectPath(matrix, "CRA", false)).toBe("/knowledge/notes");
|
||||
expect(hasProjectPermission(matrix, "CRA", getProjectRoutePermission("/knowledge/precautions"), false)).toBe(true);
|
||||
expect(findFirstAccessibleProjectPath(matrix, "CRA", false)).toBe("/knowledge/precautions");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,39 +7,39 @@ export type ProjectRoutePermission = {
|
||||
|
||||
const routePermissions: Array<{ prefixes: string[]; permission: ProjectRoutePermission }> = [
|
||||
{ prefixes: ["/fees/contracts/new"], permission: { operationKey: "fees_contracts:create" } },
|
||||
{ prefixes: ["/finance/contracts/new"], permission: { operationKey: "finance_contracts:create" } },
|
||||
{ prefixes: ["/drug/shipments/new"], permission: { operationKey: "drug_shipments:create" } },
|
||||
{ prefixes: ["/startup/feasibility/new", "/startup/ethics/new"], permission: { operationKey: "ethics:create" } },
|
||||
{ prefixes: ["/startup/feasibility/new"], permission: { operationKey: "startup_initiation:create" } },
|
||||
{ prefixes: ["/startup/ethics/new"], permission: { operationKey: "startup_ethics:create" } },
|
||||
{ prefixes: ["/startup/kickoff/new", "/startup/training/new"], permission: { operationKey: "startup_auth:create" } },
|
||||
{ prefixes: ["/subjects/new"], permission: { operationKey: "subjects:create" } },
|
||||
{ prefixes: ["/knowledge/notes/new"], permission: { operationKey: "knowledge_notes:create" } },
|
||||
{ prefixes: ["/knowledge/precautions/new"], permission: { operationKey: "precautions:create" } },
|
||||
{ prefixes: ["/project/overview"], permission: { operationKey: "project_overview:read" } },
|
||||
{ prefixes: ["/project/milestones"], permission: { operationKey: "project_milestones:read" } },
|
||||
{ prefixes: ["/fees/contracts"], permission: { operationKey: "fees_contracts:read" } },
|
||||
{ prefixes: ["/finance/contracts"], permission: { operationKey: "finance_contracts:read" } },
|
||||
{ prefixes: ["/drug/shipments"], permission: { operationKey: "drug_shipments:read" } },
|
||||
{ prefixes: ["/materials/equipment"], permission: { operationKey: "material_equipments:read" } },
|
||||
{ prefixes: ["/file-versions", "/trial/", "/documents/"], permission: { operationKey: "documents:read" } },
|
||||
{ prefixes: ["/startup/feasibility-ethics", "/startup/feasibility", "/startup/ethics"], permission: { operationKey: "ethics:read" } },
|
||||
{ prefixes: ["/startup/feasibility-ethics", "/startup/feasibility"], permission: { operationKey: "startup_initiation:read" } },
|
||||
{ prefixes: ["/startup/ethics"], permission: { operationKey: "startup_ethics:read" } },
|
||||
{ prefixes: ["/startup/meeting-auth", "/startup/kickoff", "/startup/training"], permission: { operationKey: "startup_auth:read" } },
|
||||
{ prefixes: ["/subjects"], permission: { operationKey: "subjects:read" } },
|
||||
{ prefixes: ["/risk-issues"], permission: { operationKey: "risk_issues:read" } },
|
||||
{ prefixes: ["/monitoring-audit", "/monitoring", "/audit"], permission: { operationKey: "monitoring_audit:read" } },
|
||||
{ prefixes: ["/risk-issues/monitoring-visits"], permission: { operationKey: "monitoring_issues:list" } },
|
||||
{ prefixes: ["/risk-issues/pd"], permission: { operationKey: "subject_pds:list" } },
|
||||
{ prefixes: ["/risk-issues"], permission: { operationKey: "subject_aes:list" } },
|
||||
{ prefixes: ["/etmf"], permission: { operationKey: "documents:read" } },
|
||||
{ prefixes: ["/knowledge/medical-consult"], permission: { operationKey: "faq:read" } },
|
||||
{ prefixes: ["/knowledge/notes", "/knowledge/support-files", "/knowledge/instruction-files"], permission: { operationKey: "knowledge_notes:read" } },
|
||||
{ prefixes: ["/knowledge/precautions", "/knowledge/support-files", "/knowledge/instruction-files"], permission: { operationKey: "precautions:read" } },
|
||||
];
|
||||
|
||||
const editRoutePermissions: Array<{ pattern: RegExp; permission: ProjectRoutePermission }> = [
|
||||
{ pattern: /^\/fees\/contracts\/[^/]+\/edit$/, permission: { operationKey: "fees_contracts:update" } },
|
||||
{ pattern: /^\/finance\/contracts\/[^/]+\/edit$/, permission: { operationKey: "finance_contracts:update" } },
|
||||
{ pattern: /^\/drug\/shipments\/[^/]+\/edit$/, permission: { operationKey: "drug_shipments:update" } },
|
||||
{ pattern: /^\/startup\/feasibility\/[^/]+\/edit$/, permission: { operationKey: "ethics:update" } },
|
||||
{ pattern: /^\/startup\/ethics\/[^/]+\/edit$/, permission: { operationKey: "ethics:update" } },
|
||||
{ pattern: /^\/startup\/feasibility\/[^/]+\/edit$/, permission: { operationKey: "startup_initiation:update" } },
|
||||
{ pattern: /^\/startup\/ethics\/[^/]+\/edit$/, permission: { operationKey: "startup_ethics:update" } },
|
||||
{ pattern: /^\/startup\/kickoff\/[^/]+\/edit$/, permission: { operationKey: "startup_auth:update" } },
|
||||
{ pattern: /^\/startup\/training\/[^/]+\/edit$/, permission: { operationKey: "startup_auth:update" } },
|
||||
{ pattern: /^\/subjects\/[^/]+\/edit$/, permission: { operationKey: "subjects:update" } },
|
||||
{ pattern: /^\/knowledge\/notes\/[^/]+\/edit$/, permission: { operationKey: "knowledge_notes:update" } },
|
||||
{ pattern: /^\/knowledge\/precautions\/[^/]+\/edit$/, permission: { operationKey: "precautions:update" } },
|
||||
];
|
||||
|
||||
export const projectRouteLandingPaths = [
|
||||
@@ -53,10 +53,9 @@ export const projectRouteLandingPaths = [
|
||||
"/startup/meeting-auth",
|
||||
"/subjects",
|
||||
"/risk-issues/sae",
|
||||
"/monitoring-audit",
|
||||
"/etmf",
|
||||
"/knowledge/medical-consult",
|
||||
"/knowledge/notes",
|
||||
"/knowledge/precautions",
|
||||
"/knowledge/support-files",
|
||||
"/knowledge/instruction-files",
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user