权限管理:接入项目级权限矩阵
新增项目级角色权限矩阵,覆盖 PM、CRA、PV、IMP、QA 等项目角色,并通过迁移表持久化各业务模块的读写权限。 后端将参与者、合同费用、物资、启动、里程碑、风险问题、监查稽查、FAQ、共享库、文件版本等接口接入矩阵校验,修复审计日志导出权限和登录 502 的依赖导入问题。 前端新增权限管理页面和项目路由权限映射,按矩阵控制导航显示、直接访问拦截、操作按钮启停,并限制管理后台仅 admin 和授权 PM 可进入。 补充后端权限归一化与接口静态测试、前端路由/权限工具/权限管理页面/工作台等回归测试。
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import { useStudyStore } from "../store/study";
|
||||
import { getProjectRole, isSystemAdmin } from "../utils/roles";
|
||||
import { isSystemAdmin } from "../utils/roles";
|
||||
import { findFirstAccessibleProjectPath, getProjectRoutePermission, hasProjectPermission } from "../utils/projectRoutePermissions";
|
||||
import { fetchStudyDetail } from "../api/studies";
|
||||
import { fetchProjectRolePermissions } from "../api/projectPermissions";
|
||||
import Layout from "../components/Layout.vue";
|
||||
import Login from "../views/Login.vue";
|
||||
import Register from "../views/Register.vue";
|
||||
@@ -16,6 +19,7 @@ import AdminProjects from "../views/admin/Projects.vue";
|
||||
import ProjectMembers from "../views/admin/ProjectMembers.vue";
|
||||
import AdminSites from "../views/admin/Sites.vue";
|
||||
import ProjectDetail from "../views/admin/ProjectDetail.vue";
|
||||
import ProjectPermissions from "../views/admin/ProjectPermissions.vue";
|
||||
import ProfileSettings from "../views/ProfileSettings.vue";
|
||||
import ProjectOverview from "../views/ia/ProjectOverview.vue";
|
||||
import ProjectMilestones from "../views/ia/ProjectMilestones.vue";
|
||||
@@ -435,19 +439,25 @@ const routes: RouteRecordRaw[] = [
|
||||
path: "projects/:projectId/members",
|
||||
name: "AdminProjectMembers",
|
||||
component: ProjectMembers,
|
||||
meta: { title: TEXT.modules.adminProjectMembers.memberLabel, requiresAdmin: true },
|
||||
meta: { title: TEXT.modules.adminProjectMembers.memberLabel, adminProjectPermission: { module: "project_members", action: "read" } },
|
||||
},
|
||||
{
|
||||
path: "projects/:projectId/permissions",
|
||||
name: "AdminProjectPermissions",
|
||||
component: ProjectPermissions,
|
||||
meta: { title: "权限管理", requiresAdmin: true },
|
||||
},
|
||||
{
|
||||
path: "projects/:projectId/sites",
|
||||
name: "AdminSites",
|
||||
component: AdminSites,
|
||||
meta: { title: TEXT.modules.adminSites.title, requiresAdmin: true },
|
||||
meta: { title: TEXT.modules.adminSites.title, adminProjectPermission: { module: "sites", action: "read" } },
|
||||
},
|
||||
{
|
||||
path: "audit-logs",
|
||||
name: "AdminAuditLogs",
|
||||
component: AuditLogs,
|
||||
meta: { title: TEXT.menu.auditLogs, requiresAdmin: true },
|
||||
meta: { title: TEXT.menu.auditLogs, adminProjectPermission: { module: "audit_export", action: "read" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -462,6 +472,29 @@ const router = createRouter({
|
||||
routes,
|
||||
});
|
||||
|
||||
const ensureAdminProjectAccess = async (to: any, isAdmin: boolean) => {
|
||||
if (isAdmin) return true;
|
||||
const studyStore = useStudyStore();
|
||||
const permission = to.meta.adminProjectPermission as { module: string; action: "read" | "write" } | undefined;
|
||||
if (!permission) return true;
|
||||
|
||||
const routeProjectId = typeof to.params.projectId === "string" ? to.params.projectId : "";
|
||||
if (routeProjectId && studyStore.currentStudy?.id !== routeProjectId) {
|
||||
const [{ data: project }, { data: permissions }] = await Promise.all([
|
||||
fetchStudyDetail(routeProjectId),
|
||||
fetchProjectRolePermissions(routeProjectId),
|
||||
]);
|
||||
studyStore.setCurrentStudy(project);
|
||||
studyStore.currentPermissions = permissions;
|
||||
} else if (!studyStore.currentPermissions) {
|
||||
await studyStore.loadCurrentStudyPermissions();
|
||||
}
|
||||
|
||||
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];
|
||||
};
|
||||
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
const auth = useAuthStore();
|
||||
const studyStore = useStudyStore();
|
||||
@@ -511,13 +544,24 @@ router.beforeEach(async (to, _from, next) => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (to.meta.adminProjectPermission) {
|
||||
const allowed = await ensureAdminProjectAccess(to, isAdmin).catch(() => false);
|
||||
if (!allowed) {
|
||||
next({ path: isAdmin ? "/admin/users" : "/workbench" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (to.meta.requiresAdmin && !isAdmin) {
|
||||
next({ path: "/workbench" });
|
||||
return;
|
||||
}
|
||||
if (to.name === "AuditLogs") {
|
||||
const projectRole = getProjectRole(studyStore.currentStudy, studyStore.currentStudyRole);
|
||||
if (!(isAdmin || projectRole === "PM")) {
|
||||
if (to.name === "AdminAuditLogs") {
|
||||
if (studyStore.currentStudy && !studyStore.currentPermissions) {
|
||||
await studyStore.loadCurrentStudyPermissions().catch(() => {});
|
||||
}
|
||||
const role = studyStore.currentStudyRole || (studyStore.currentStudy as any)?.role_in_study || "";
|
||||
const canReadAudit = !!studyStore.currentPermissions?.roles?.[role]?.audit_export?.read;
|
||||
if (!(isAdmin || (role === "PM" && canReadAudit))) {
|
||||
next({ path: "/project/overview" });
|
||||
return;
|
||||
}
|
||||
@@ -526,6 +570,19 @@ router.beforeEach(async (to, _from, next) => {
|
||||
next({ path: isAdmin ? "/admin/users" : "/workbench" });
|
||||
return;
|
||||
}
|
||||
if (to.meta.requiresStudy && studyStore.currentStudy) {
|
||||
if (!studyStore.currentPermissions) {
|
||||
await studyStore.loadCurrentStudyPermissions().catch(() => {});
|
||||
}
|
||||
const projectRole = studyStore.currentStudyRole || (studyStore.currentStudy as any)?.role_in_study || "";
|
||||
const permission = getProjectRoutePermission(to.path);
|
||||
const canAccessProjectRoute = hasProjectPermission(studyStore.currentPermissions?.roles, projectRole, permission, isAdmin);
|
||||
if (!canAccessProjectRoute) {
|
||||
const fallback = findFirstAccessibleProjectPath(studyStore.currentPermissions?.roles, projectRole, isAdmin);
|
||||
next({ path: fallback || "/workbench" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user