fix(router): check system permissions against target project

This commit is contained in:
Cheng Zhou
2026-06-08 11:08:23 +08:00
parent 8cd8c38900
commit 30e2398400
9 changed files with 283 additions and 82 deletions
+26 -6
View File
@@ -47,8 +47,8 @@ describe("project management access", () => {
expect(source).toContain("suppressErrorMessage: true");
expect(source).toContain("const canProject = (row: Study, module: string, action: \"read\" | \"write\")");
expect(source).toContain("canProject(scope.row, 'project_members', 'read')");
expect(source).toContain("canProject(scope.row, 'project_members', 'write')");
expect(source).toContain("canProject(scope.row, 'sites', 'read')");
expect(source).toContain("canConfigureProject(scope.row)");
expect(source).toContain("canManageProjectBackend(scope.row)");
expect(source).toContain("canProject(scope.row, 'audit_export', 'read')");
});
@@ -74,13 +74,33 @@ describe("project management access", () => {
expect(source).not.toContain('fixed="right"');
});
it("opens project names in management detail when the user has setup config read permission", () => {
it("renders project names as plain text and moves setup configuration to a gear action", () => {
const source = readProjects();
expect(source).toContain("v-if=\"canProject(scope.row, 'setup_config', 'read')\"");
expect(source).toContain('@click="goProject(scope.row)" class="project-name"');
expect(source).not.toContain("<el-link");
expect(source).not.toContain('@click="goProject(scope.row)" class="project-name"');
expect(source).toContain('<span class="project-name">{{ scope.row.name }}</span>');
expect(source).toContain('v-if="canProject(scope.row, \'setup_config\', \'write\')" content="项目配置"');
expect(source).toContain(':icon="Setting"');
expect(source).toContain('@click="goProject(scope.row)"');
expect(source).toContain('@click="enterStudy(scope.row)"');
expect(source).toContain('if (!canProject(row, "setup_config", "read"))');
expect(source).toContain('if (!canProject(row, "setup_config", "write"))');
});
it("shows project permission configuration only for admins and target project PMs", () => {
const source = readProjects();
expect(source).toContain('v-if="canConfigureProject(scope.row)" content="权限管理"');
expect(source).toContain("const canConfigureProject = (row: Study) => canManageProjectBackend(row);");
expect(source).not.toContain("canProject(scope.row, 'project_members', 'write')");
});
it("shows center management only for admins and target project PMs", () => {
const source = readProjects();
expect(source).toContain('v-if="canManageProjectBackend(scope.row)" :content="TEXT.modules.adminProjects.sites"');
expect(source).toContain("const canManageProjectBackend = (row: Study) => isAdmin.value || row.role_in_study === \"PM\";");
expect(source).not.toContain("canProject(scope.row, 'sites', 'read')");
});
it("checks project management actions against the current row role instead of the first permission role", () => {
+20 -15
View File
@@ -74,8 +74,7 @@
</svg>
</div>
<div class="project-info">
<el-link v-if="canProject(scope.row, 'setup_config', 'read')" type="primary" @click="goProject(scope.row)" class="project-name">{{ scope.row.name }}</el-link>
<span v-else class="project-name project-name--disabled">{{ scope.row.name }}</span>
<span class="project-name">{{ scope.row.name }}</span>
<span class="project-code">{{ scope.row.code || '-' }}</span>
</div>
</div>
@@ -123,10 +122,13 @@
<el-tooltip v-if="canProject(scope.row, 'project_members', 'read')" :content="TEXT.modules.adminProjects.members" placement="top">
<el-button link type="primary" :icon="User" class="action-btn" @click="goMembers(scope.row)" />
</el-tooltip>
<el-tooltip v-if="canProject(scope.row, 'project_members', 'write')" content="权限管理" placement="top">
<el-tooltip v-if="canProject(scope.row, 'setup_config', 'write')" content="项目配置" placement="top">
<el-button link type="primary" :icon="Setting" class="action-btn" @click="goProject(scope.row)" />
</el-tooltip>
<el-tooltip v-if="canConfigureProject(scope.row)" content="权限管理" placement="top">
<el-button link type="primary" :icon="Key" class="action-btn" @click="goPermissions(scope.row)" />
</el-tooltip>
<el-tooltip v-if="canProject(scope.row, 'sites', 'read')" :content="TEXT.modules.adminProjects.sites" placement="top">
<el-tooltip v-if="canManageProjectBackend(scope.row)" :content="TEXT.modules.adminProjects.sites" placement="top">
<el-button link type="primary" :icon="OfficeBuilding" class="action-btn" @click="goSites(scope.row)" />
</el-tooltip>
<el-tooltip v-if="canProject(scope.row, 'audit_export', 'read')" :content="TEXT.menu.auditLogs" placement="top">
@@ -155,7 +157,7 @@
import { computed, onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus";
import { User, OfficeBuilding, ArrowRight, Delete, Lock, Key, Document, Plus } from "@element-plus/icons-vue";
import { User, OfficeBuilding, ArrowRight, Delete, Lock, Key, Document, Plus, Setting } from "@element-plus/icons-vue";
import { fetchStudies, deleteStudy, lockStudy } from "../../api/studies";
import { fetchMyApiEndpointPermissions } from "../../api/projectPermissions";
import type { ApiEndpointPermissionsResponse, Study } from "../../types/api";
@@ -232,15 +234,26 @@ const canProject = (row: Study, module: string, action: "read" | "write") => {
return isApiPermissionAllowed(rolePerms[operationKey]);
};
const canManageProjectBackend = (row: Study) => isAdmin.value || row.role_in_study === "PM";
const canConfigureProject = (row: Study) => canManageProjectBackend(row);
const goMembers = (row: Study) => {
router.push(`/admin/permissions/project?projectId=${row.id}&sub=members`);
};
const goSites = (row: Study) => {
if (!canManageProjectBackend(row)) {
ElMessage.warning("仅项目 PM 可管理该项目中心");
return;
}
router.push(`/admin/projects/${row.id}/sites`);
};
const goPermissions = (row: Study) => {
if (!canConfigureProject(row)) {
ElMessage.warning("仅项目 PM 可配置该项目权限");
return;
}
router.push(`/admin/permissions/project?projectId=${row.id}`);
};
@@ -342,8 +355,8 @@ const goDetail = (row: Study) => {
};
const goProject = (row: Study) => {
if (!canProject(row, "setup_config", "read")) {
ElMessage.warning("当前角色无权访问该项目的立项配置");
if (!canProject(row, "setup_config", "write")) {
ElMessage.warning("当前角色无权编辑该项目的立项配置");
return;
}
goDetail(row);
@@ -364,7 +377,6 @@ onMounted(() => {
grid-template-columns: repeat(4, 1fr);
gap: 12px;
padding: 16px;
border-bottom: 1px solid var(--unified-shell-divider);
}
.stat-card {
@@ -462,13 +474,6 @@ onMounted(() => {
font-size: 13px;
}
.project-name--disabled {
font-weight: 600;
font-size: 13px;
color: var(--ctms-text-secondary);
cursor: default;
}
.project-code {
font-size: 12px;
color: var(--ctms-text-secondary);
+7 -7
View File
@@ -9,7 +9,7 @@
</div>
<div class="unified-section site-table-section section--flush-x section--flush-top section--flush-bottom">
<el-table :data="displaySites" v-loading="loading" stripe :row-class-name="siteRowClass" style="width: 100%" class="site-table">
<el-table-column prop="name" :label="TEXT.common.fields.siteName" min-width="220">
<el-table-column prop="name" :label="TEXT.common.fields.siteName" min-width="140">
<template #default="scope">
<div class="site-name-cell">
<span>{{ scope.row.name }}</span>
@@ -19,25 +19,25 @@
</div>
</template>
</el-table-column>
<el-table-column prop="city" :label="TEXT.common.fields.city" width="100" />
<el-table-column prop="pi_name" :label="TEXT.modules.adminSites.piLabel" width="120" />
<el-table-column :label="TEXT.common.fields.phone" width="140">
<el-table-column prop="city" :label="TEXT.common.fields.city" min-width="80" />
<el-table-column prop="pi_name" :label="TEXT.modules.adminSites.piLabel" min-width="100" />
<el-table-column :label="TEXT.common.fields.phone" min-width="110">
<template #default="scope">
{{ phoneText(scope.row) }}
</template>
</el-table-column>
<el-table-column prop="contact" :label="TEXT.common.fields.contact" min-width="160">
<el-table-column prop="contact" :label="TEXT.common.fields.contact" min-width="120">
<template #default="scope">
{{ contactLabel(scope.row) }}
</template>
</el-table-column>
<el-table-column :label="TEXT.common.fields.status" width="100">
<el-table-column :label="TEXT.common.fields.status" min-width="80">
<template #default="scope">
<el-tag type="success" v-if="scope.row.is_active">已解锁</el-tag>
<el-tag type="danger" v-else>已锁定</el-tag>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="200" align="center" class-name="action-column">
<el-table-column :label="TEXT.common.labels.actions" min-width="160" align="center" class-name="action-column">
<template #default="scope">
<el-button link type="primary" size="small" @click="openEdit(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
<el-button