完善项目级权限与项目管理入口

- 新增项目成员候选用户接口,避免 PM 读取全局用户列表
- 按项目角色和权限矩阵控制项目管理、成员、中心与审计入口
- 合并侧边栏管理后台入口,统一从项目管理进入成员等模块
- 加强成员管理安全约束,禁止 PM 修改自己、系统管理员或更高权限角色
- 修复多项目角色缓存串用问题,并补充前后端回归测试
This commit is contained in:
Cheng Zhou
2026-05-13 14:16:15 +08:00
parent ab1975d3c6
commit a17f4cc522
21 changed files with 452 additions and 134 deletions
+3 -32
View File
@@ -427,7 +427,7 @@ const routes: RouteRecordRaw[] = [
path: "projects",
name: "AdminProjects",
component: AdminProjects,
meta: { title: TEXT.menu.projectManagement, requiresAdmin: true },
meta: { title: TEXT.menu.projectManagement },
},
{
path: "projects/:projectId",
@@ -445,7 +445,7 @@ const routes: RouteRecordRaw[] = [
path: "projects/:projectId/permissions",
name: "AdminProjectPermissions",
component: ProjectPermissions,
meta: { title: "权限管理", requiresAdmin: true },
meta: { title: "权限管理", adminProjectPermission: { module: "project_members", action: "write" } },
},
{
path: "projects/:projectId/sites",
@@ -472,25 +472,6 @@ const router = createRouter({
routes,
});
const pmAdminLandingModules: Array<{ module: string; path: (studyId: string) => string }> = [
{ module: "project_members", path: (studyId) => `/admin/projects/${studyId}/members` },
{ module: "sites", path: (studyId) => `/admin/projects/${studyId}/sites` },
{ module: "audit_export", path: () => "/admin/audit-logs" },
];
const findPmAdminLandingPath = async () => {
const studyStore = useStudyStore();
const studyId = studyStore.currentStudy?.id;
if (!studyId) return null;
if (!studyStore.currentPermissions) {
await studyStore.loadCurrentStudyPermissions();
}
const role = studyStore.currentStudyRole || (studyStore.currentStudy as any)?.role_in_study || "";
if (role !== "PM") return null;
const matched = pmAdminLandingModules.find((item) => !!studyStore.currentPermissions?.roles?.PM?.[item.module]?.read);
return matched ? matched.path(studyId) : null;
};
const ensureAdminProjectAccess = async (to: any, isAdmin: boolean) => {
if (isAdmin) return true;
const studyStore = useStudyStore();
@@ -511,7 +492,7 @@ const ensureAdminProjectAccess = async (to: any, isAdmin: boolean) => {
const role = studyStore.currentStudyRole || (studyStore.currentStudy as any)?.role_in_study || "";
if (role !== "PM") return false;
return !!studyStore.currentPermissions?.roles?.PM?.[permission.module]?.[permission.action];
return !!studyStore.currentPermissions?.roles?.[role]?.[permission.module]?.[permission.action];
};
router.beforeEach(async (to, _from, next) => {
@@ -549,23 +530,13 @@ router.beforeEach(async (to, _from, next) => {
next({ path: studyStore.currentStudy ? "/project/overview" : "/admin/users" });
return;
}
if (!isAdmin && token && (to.path === "/" || to.path === "/workbench")) {
const pmAdminLanding = await findPmAdminLandingPath().catch(() => null);
if (pmAdminLanding) {
next({ path: pmAdminLanding });
return;
}
}
if ((to.path === "/login" || to.path === "/register") && token) {
if (!auth.forceLogin) {
const pmAdminLanding = isAdmin ? null : await findPmAdminLandingPath().catch(() => null);
next({
path: isAdmin
? studyStore.currentStudy
? "/project/overview"
: "/admin/users"
: pmAdminLanding
? pmAdminLanding
: studyStore.currentStudy
? "/project/overview"
: "/workbench",