拆分系统监测模块并统一运维化文案

This commit is contained in:
Cheng Zhou
2026-06-11 11:06:59 +08:00
parent b7484c8e01
commit 1886765db8
21 changed files with 222 additions and 143 deletions
+2 -5
View File
@@ -14,7 +14,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import func, select, desc from sqlalchemy import func, select, desc
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.deps import get_current_user, get_db_session, is_system_admin, list_active_pm_study_ids from app.core.deps import get_current_user, get_db_session, is_system_admin
from app.core.permission_monitor import get_permission_monitor from app.core.permission_monitor import get_permission_monitor
from app.models.permission_access_log import PermissionAccessLog from app.models.permission_access_log import PermissionAccessLog
from app.models.permission_metric_snapshot import PermissionMetricSnapshot from app.models.permission_metric_snapshot import PermissionMetricSnapshot
@@ -39,10 +39,7 @@ class MonitoringScope:
async def resolve_monitoring_scope(db: AsyncSession, current_user) -> MonitoringScope: async def resolve_monitoring_scope(db: AsyncSession, current_user) -> MonitoringScope:
if is_system_admin(current_user): if is_system_admin(current_user):
return MonitoringScope(is_admin=True, study_ids=set()) return MonitoringScope(is_admin=True, study_ids=set())
return MonitoringScope( raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
is_admin=False,
study_ids=await list_active_pm_study_ids(db, current_user.id),
)
def _apply_monitoring_scope_to_log_query(query, scope: MonitoringScope): def _apply_monitoring_scope_to_log_query(query, scope: MonitoringScope):
+12 -12
View File
@@ -969,54 +969,54 @@ SYSTEM_PERMISSIONS: dict[str, dict] = {
"description": "管理权限模板", "description": "管理权限模板",
"roles": ["ADMIN"], "roles": ["ADMIN"],
}, },
# 权限监控 # 系统监测
"system:monitoring:metrics": { "system:monitoring:metrics": {
"module": "system_monitoring", "module": "system_monitoring",
"action": "read", "action": "read",
"description": "查看权限监控指标", "description": "查看系统监测指标",
"roles": ["ADMIN", "PM"], "roles": ["ADMIN"],
}, },
"system:monitoring:cache_stats": { "system:monitoring:cache_stats": {
"module": "system_monitoring", "module": "system_monitoring",
"action": "read", "action": "read",
"description": "查看缓存统计", "description": "查看缓存统计",
"roles": ["ADMIN", "PM"], "roles": ["ADMIN"],
}, },
"system:monitoring:alerts": { "system:monitoring:alerts": {
"module": "system_monitoring", "module": "system_monitoring",
"action": "read", "action": "read",
"description": "查看权限告警", "description": "查看权限告警",
"roles": ["ADMIN", "PM"], "roles": ["ADMIN"],
}, },
"system:monitoring:health": { "system:monitoring:health": {
"module": "system_monitoring", "module": "system_monitoring",
"action": "read", "action": "read",
"description": "查看权限系统健康状态", "description": "查看权限系统健康状态",
"roles": ["ADMIN", "PM"], "roles": ["ADMIN"],
}, },
"system:monitoring:access_logs": { "system:monitoring:access_logs": {
"module": "system_monitoring", "module": "system_monitoring",
"action": "read", "action": "read",
"description": "查看权限访问日志", "description": "查看权限访问日志",
"roles": ["ADMIN", "PM"], "roles": ["ADMIN"],
}, },
"system:monitoring:trends": { "system:monitoring:trends": {
"module": "system_monitoring", "module": "system_monitoring",
"action": "read", "action": "read",
"description": "查看权限趋势数据", "description": "查看权限趋势数据",
"roles": ["ADMIN", "PM"], "roles": ["ADMIN"],
}, },
"system:monitoring:reset_metrics": { "system:monitoring:reset_metrics": {
"module": "system_monitoring", "module": "system_monitoring",
"action": "update", "action": "update",
"description": "重置监控指标", "description": "重置系统监测指标",
"roles": ["ADMIN", "PM"], "roles": ["ADMIN"],
}, },
"system:monitoring:clear_alerts": { "system:monitoring:clear_alerts": {
"module": "system_monitoring", "module": "system_monitoring",
"action": "update", "action": "update",
"description": "清除告警", "description": "清除告警",
"roles": ["ADMIN", "PM"], "roles": ["ADMIN"],
}, },
"system:monitoring:security_logs": { "system:monitoring:security_logs": {
"module": "system_monitoring", "module": "system_monitoring",
@@ -1044,6 +1044,6 @@ SYSTEM_MODULE_LABELS: dict[str, str] = {
"system_users": "账号管理", "system_users": "账号管理",
"system_projects": "项目管理", "system_projects": "项目管理",
"system_permissions": "权限管理", "system_permissions": "权限管理",
"system_monitoring": "权限监控", "system_monitoring": "系统监测",
"system_audit": "审计日志", "system_audit": "审计日志",
} }
@@ -3,10 +3,12 @@
import uuid import uuid
import pytest import pytest
from fastapi import HTTPException
from sqlalchemy import text from sqlalchemy import text
from app.core.permission_monitor import set_permission_monitor, PermissionMonitor from app.core.permission_monitor import set_permission_monitor, PermissionMonitor
from app.api.v1 import permission_monitoring from app.api.v1 import permission_monitoring
from app.api.v1.system_permissions import list_system_permissions
class FakeIpInfo: class FakeIpInfo:
@@ -22,6 +24,35 @@ class AdminUserStub:
is_admin = True is_admin = True
class ProjectPmUserStub:
id = uuid.uuid4()
is_admin = False
@pytest.mark.asyncio
async def test_resolve_monitoring_scope_rejects_project_pm(db_session):
"""系统监测模块仅允许系统管理员访问,项目 PM 不再具备监测范围。"""
with pytest.raises(HTTPException) as exc_info:
await permission_monitoring.resolve_monitoring_scope(db_session, ProjectPmUserStub())
assert exc_info.value.status_code == 403
@pytest.mark.asyncio
async def test_system_permission_monitoring_definitions_are_admin_only():
"""系统级权限定义中的系统监测模块不应再标注 PM 角色。"""
data = await list_system_permissions()
monitoring_items = [
item for item in data["permissions"]
if item["module"] == "system_monitoring"
]
assert monitoring_items
assert data["module_labels"]["system_monitoring"] == "系统监测"
assert all(item["roles"] == ["ADMIN"] for item in monitoring_items)
assert all("PM" not in item["roles"] for item in monitoring_items)
async def _seed_permission_log(db_session, study_id: uuid.UUID, user_id: uuid.UUID, *, allowed: bool, elapsed_ms: float) -> None: async def _seed_permission_log(db_session, study_id: uuid.UUID, user_id: uuid.UUID, *, allowed: bool, elapsed_ms: float) -> None:
study_exists = ( study_exists = (
await db_session.execute(text("SELECT id FROM studies WHERE id = :id"), {"id": str(study_id)}) await db_session.execute(text("SELECT id FROM studies WHERE id = :id"), {"id": str(study_id)})
+2 -2
View File
@@ -34,7 +34,7 @@ export const updateApiEndpointPermissions = (
export const fetchApiOperations = () => export const fetchApiOperations = () =>
apiGet<{ operations: ApiOperation[] }>(`/api/v1/api-permissions/operations`); apiGet<{ operations: ApiOperation[] }>(`/api/v1/api-permissions/operations`);
// 权限系统监API // 系统监API
export const fetchPermissionMetrics = () => export const fetchPermissionMetrics = () =>
apiGet<PermissionMetricsResponse>(`/api/v1/permission-monitoring/metrics`); apiGet<PermissionMetricsResponse>(`/api/v1/permission-monitoring/metrics`);
@@ -55,7 +55,7 @@ export const resetPermissionMetrics = () =>
export const clearPermissionAlerts = () => export const clearPermissionAlerts = () =>
apiPost<void>(`/api/v1/permission-monitoring/clear-alerts`); apiPost<void>(`/api/v1/permission-monitoring/clear-alerts`);
// 权限监控 - 新增API // 系统监测 - 新增API
export const fetchAccessLogs = (params: { export const fetchAccessLogs = (params: {
study_id?: string; study_id?: string;
user_id?: string; user_id?: string;
+6 -2
View File
@@ -28,6 +28,10 @@
<el-icon><Document /></el-icon> <el-icon><Document /></el-icon>
<span>{{ TEXT.menu.auditLogs }}</span> <span>{{ TEXT.menu.auditLogs }}</span>
</el-menu-item> </el-menu-item>
<el-menu-item v-if="isAdmin" index="/admin/system-monitoring">
<el-icon><DataAnalysis /></el-icon>
<span>{{ TEXT.menu.systemMonitoring }}</span>
</el-menu-item>
<el-sub-menu v-if="canAccessAdminPermissions" index="permissions"> <el-sub-menu v-if="canAccessAdminPermissions" index="permissions">
<template #title> <template #title>
<el-icon><Key /></el-icon> <el-icon><Key /></el-icon>
@@ -35,7 +39,6 @@
</template> </template>
<el-menu-item index="/admin/permissions/system">系统级权限</el-menu-item> <el-menu-item index="/admin/permissions/system">系统级权限</el-menu-item>
<el-menu-item index="/admin/permissions/project">项目权限配置</el-menu-item> <el-menu-item index="/admin/permissions/project">项目权限配置</el-menu-item>
<el-menu-item index="/admin/permissions/monitoring">权限监控</el-menu-item>
</el-sub-menu> </el-sub-menu>
</el-menu-item-group> </el-menu-item-group>
@@ -235,7 +238,7 @@ import { fetchSites } from "../api/sites";
import { TEXT } from "../locales"; import { TEXT } from "../locales";
import { import {
User, Suitcase, House, Calendar, Flag, User, Suitcase, House, Calendar, Flag,
CircleCheck, Box, Coin, Notebook, Document, ArrowDown, SwitchButton, Files, Key CircleCheck, Box, Coin, Notebook, Document, ArrowDown, SwitchButton, Files, Key, DataAnalysis
} from "@element-plus/icons-vue"; } from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import { getProjectRoutePermission, hasProjectPermission, projectRouteLandingPaths } from "../utils/projectRoutePermissions"; import { getProjectRoutePermission, hasProjectPermission, projectRouteLandingPaths } from "../utils/projectRoutePermissions";
@@ -301,6 +304,7 @@ const activeMenu = computed(() => {
if (path.startsWith("/knowledge/instruction-files")) return "/knowledge/instruction-files"; if (path.startsWith("/knowledge/instruction-files")) return "/knowledge/instruction-files";
if (path.startsWith("/projects/")) return "/admin/projects"; if (path.startsWith("/projects/")) return "/admin/projects";
if (path.startsWith("/admin/projects/")) return "/admin/projects"; if (path.startsWith("/admin/projects/")) return "/admin/projects";
if (path.startsWith("/admin/system-monitoring") || path.startsWith("/admin/permission-monitoring")) return "/admin/system-monitoring";
if (path.startsWith("/admin/permissions/")) return path; if (path.startsWith("/admin/permissions/")) return path;
return path; return path;
}); });
@@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
const readSource = () => readFileSync(resolve(__dirname, "./Layout.vue"), "utf8");
describe("layout system monitoring navigation", () => {
it("shows system monitoring as an admin-only peer module outside permission management", () => {
const source = readSource();
const permissionMenuStart = source.indexOf('<el-sub-menu v-if="canAccessAdminPermissions" index="permissions">');
const permissionMenuEnd = source.indexOf("</el-sub-menu>", permissionMenuStart);
const permissionMenu = source.slice(permissionMenuStart, permissionMenuEnd);
expect(source).toContain('v-if="isAdmin" index="/admin/system-monitoring"');
expect(source).toContain("{{ TEXT.menu.systemMonitoring }}");
expect(permissionMenu).not.toContain("/admin/system-monitoring");
expect(permissionMenu).not.toContain("/admin/permission-monitoring");
expect(permissionMenu).not.toContain("/admin/permissions/monitoring");
expect(source).not.toContain('index="/admin/permissions/monitoring"');
expect(source).toContain('return "/admin/system-monitoring";');
});
});
@@ -21,13 +21,25 @@ describe("PermissionIpLocations", () => {
expect(source).toContain("VChart"); expect(source).toContain("VChart");
expect(source).toContain("chinaMapOption"); expect(source).toContain("chinaMapOption");
expect(source).toContain("用户分布"); expect(source).toContain("访问来源分布");
expect(source).toContain("访问次数"); expect(source).toContain("访问次数");
expect(source).toContain("访问用户数"); expect(source).toContain("访问用户数");
expect(source).toContain("来源 IP 数"); expect(source).toContain("来源 IP 数");
expect(source).toContain("summary"); expect(source).toContain("summary");
}); });
it("uses source-analysis copy instead of IP-location tab wording", () => {
const source = readSource();
expect(source).toContain("访问来源分析");
expect(source).toContain("来源分布");
expect(source).toContain("热点来源地");
expect(source).not.toContain("IP 属地分析");
expect(source).not.toContain("中国地图");
expect(source).not.toContain("热点属地");
expect(source).not.toContain("用户分布图");
});
it("uses the standard geojson.cn China map data instead of generated rectangles", () => { it("uses the standard geojson.cn China map data instead of generated rectangles", () => {
const source = readSource(); const source = readSource();
const mapSource = readMapSource(); const mapSource = readMapSource();
@@ -3,7 +3,7 @@
<section class="ip-hero"> <section class="ip-hero">
<div class="ip-locations-toolbar"> <div class="ip-locations-toolbar">
<div class="toolbar-left"> <div class="toolbar-left">
<span class="toolbar-title">IP 属地分析</span> <span class="toolbar-title">访问来源分析</span>
<span class="toolbar-desc">查看访问来源的地理分布</span> <span class="toolbar-desc">查看访问来源的地理分布</span>
</div> </div>
<div class="toolbar-actions"> <div class="toolbar-actions">
@@ -37,7 +37,7 @@
<div class="map-panel"> <div class="map-panel">
<div class="section-head"> <div class="section-head">
<div class="section-title-group"> <div class="section-title-group">
<h4>中国地图</h4> <h4>来源分布</h4>
<p>访问来源地理分布热力图</p> <p>访问来源地理分布热力图</p>
</div> </div>
<el-tag type="info" effect="plain" round size="small">{{ currentPeriodLabel }}</el-tag> <el-tag type="info" effect="plain" round size="small">{{ currentPeriodLabel }}</el-tag>
@@ -48,7 +48,7 @@
<aside class="rank-panel"> <aside class="rank-panel">
<div class="section-head compact"> <div class="section-head compact">
<div class="section-title-group"> <div class="section-title-group">
<h4>热点</h4> <h4>热点来源</h4>
<p>按访问次数排序</p> <p>按访问次数排序</p>
</div> </div>
</div> </div>
@@ -219,7 +219,7 @@ const chinaMapOption = computed(() => ({
}, },
series: [ series: [
{ {
name: "用户分布", name: "访问来源分布",
type: "map", type: "map",
map: "ctms-china", map: "ctms-china",
roam: false, roam: false,
@@ -77,4 +77,28 @@ describe("PermissionMonitoring.vue", () => {
expect(source).toContain("isAdmin?: boolean"); expect(source).toContain("isAdmin?: boolean");
expect(source).toContain(':show-security-log="props.isAdmin"'); expect(source).toContain(':show-security-log="props.isAdmin"');
}); });
it("uses system monitoring tab names and operation-oriented overview copy", () => {
const source = readFileSync(resolve(__dirname, "./PermissionMonitoring.vue"), "utf8");
expect(source).toContain('label="运行概览"');
expect(source).toContain('label="性能趋势"');
expect(source).toContain('label="访问审计"');
expect(source).toContain('label="来源分析"');
expect(source).toContain('label: "今日监测事件"');
expect(source).toContain('label: "每分钟事件"');
expect(source).toContain('label: "今日通过率"');
expect(source).toContain('label: "历史事件总数"');
expect(source).toContain("异常拒绝排行");
expect(source).not.toContain('label="实时概览"');
expect(source).not.toContain('label="趋势分析"');
expect(source).not.toContain('label="访问日志"');
expect(source).not.toContain('label="IP属地"');
expect(source).not.toContain('label: "今日总检查"');
expect(source).not.toContain('label: "每分钟请求"');
expect(source).not.toContain('label: "今日允许率"');
expect(source).not.toContain('label: "历史总记录"');
expect(source).not.toContain("被拒绝最多的权限");
});
}); });
@@ -1,7 +1,7 @@
<template> <template>
<div class="permission-monitoring"> <div class="permission-monitoring">
<el-tabs v-model="activeTab" @tab-change="onTabChange"> <el-tabs v-model="activeTab" @tab-change="onTabChange">
<el-tab-pane label="实时概览" name="overview"> <el-tab-pane label="运行概览" name="overview">
<div v-if="statsSummary" class="stats-summary"> <div v-if="statsSummary" class="stats-summary">
<div v-for="card in summaryCards" :key="card.label" class="summary-card" :class="card.tone"> <div v-for="card in summaryCards" :key="card.label" class="summary-card" :class="card.tone">
<div class="summary-icon"> <div class="summary-icon">
@@ -83,7 +83,7 @@
<div v-if="topDenied && topDenied.length > 0" class="denied-card"> <div v-if="topDenied && topDenied.length > 0" class="denied-card">
<div class="card-header"> <div class="card-header">
<h3>被拒绝最多的权限</h3> <h3>异常拒绝排行</h3>
<el-tag effect="plain" size="small" type="info"> 7 </el-tag> <el-tag effect="plain" size="small" type="info"> 7 </el-tag>
</div> </div>
<div class="denied-list"> <div class="denied-list">
@@ -121,15 +121,15 @@
</div> </div>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="趋势分析" name="trends"> <el-tab-pane label="性能趋势" name="trends">
<PermissionTrendCharts ref="trendChartsRef" /> <PermissionTrendCharts ref="trendChartsRef" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="访问日志" name="logs"> <el-tab-pane label="访问审计" name="logs">
<PermissionAccessLogs ref="accessLogsRef" :show-security-log="props.isAdmin" /> <PermissionAccessLogs ref="accessLogsRef" :show-security-log="props.isAdmin" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="IP属地" name="ip-locations"> <el-tab-pane label="来源分析" name="ip-locations">
<PermissionIpLocations ref="ipLocationsRef" /> <PermissionIpLocations ref="ipLocationsRef" />
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
@@ -226,19 +226,19 @@ const summaryCards = computed(() => {
if (!statsSummary.value) return []; if (!statsSummary.value) return [];
return [ return [
{ {
label: "今日总检查", label: "今日监测事件",
value: statsSummary.value.today.total_checks.toLocaleString(), value: statsSummary.value.today.total_checks.toLocaleString(),
tone: "blue", tone: "blue",
icon: IconCheck, icon: IconCheck,
}, },
{ {
label: "每分钟请求", label: "每分钟事件",
value: statsSummary.value.last_hour.requests_per_minute, value: statsSummary.value.last_hour.requests_per_minute,
tone: "cyan", tone: "cyan",
icon: IconActivity, icon: IconActivity,
}, },
{ {
label: "今日允许率", label: "今日通过率",
value: `${statsSummary.value.today.allow_rate}%`, value: `${statsSummary.value.today.allow_rate}%`,
tone: statsSummary.value.today.allow_rate >= 80 ? "green" : "warning", tone: statsSummary.value.today.allow_rate >= 80 ? "green" : "warning",
icon: IconPercent, icon: IconPercent,
@@ -250,7 +250,7 @@ const summaryCards = computed(() => {
icon: IconClock, icon: IconClock,
}, },
{ {
label: "历史总记录", label: "历史事件总数",
value: statsSummary.value.total_logs.toLocaleString(), value: statsSummary.value.total_logs.toLocaleString(),
tone: "violet", tone: "violet",
icon: IconDatabase, icon: IconDatabase,
@@ -0,0 +1,25 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
const readSource = () => readFileSync(resolve(__dirname, "./PermissionTrendCharts.vue"), "utf8");
describe("PermissionTrendCharts", () => {
it("uses operation-oriented trend copy", () => {
const source = readSource();
expect(source).toContain("系统性能趋势");
expect(source).toContain("查看系统运行监测指标的变化趋势");
expect(source).toContain("监测事件趋势");
expect(source).toContain("响应时间趋势");
expect(source).toContain("缓存命中率");
expect(source).toContain("异常拒绝率趋势");
expect(source).toContain('name: "异常拒绝率"');
expect(source).not.toContain("数据趋势");
expect(source).not.toContain("查看权限系统各项指标的变化趋势");
expect(source).not.toContain("检查量趋势");
expect(source).not.toContain("<h4>拒绝率趋势</h4>");
expect(source).not.toContain('name: "拒绝率"');
});
});
@@ -2,8 +2,8 @@
<div class="trend-charts"> <div class="trend-charts">
<div class="trend-toolbar"> <div class="trend-toolbar">
<div class="toolbar-left"> <div class="toolbar-left">
<span class="toolbar-title">数据趋势</span> <span class="toolbar-title">系统性能趋势</span>
<span class="toolbar-desc">查看权限系统各项指标的变化趋势</span> <span class="toolbar-desc">查看系统运行监测指标的变化趋势</span>
</div> </div>
<el-radio-group v-model="period" size="small" @change="loadData"> <el-radio-group v-model="period" size="small" @change="loadData">
<el-radio-button value="24h">24小时</el-radio-button> <el-radio-button value="24h">24小时</el-radio-button>
@@ -18,7 +18,7 @@
<div class="chart-icon blue"> <div class="chart-icon blue">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
</div> </div>
<h4>检查量趋势</h4> <h4>监测事件趋势</h4>
</div> </div>
<v-chart :option="checksChartOption" autoresize class="chart" /> <v-chart :option="checksChartOption" autoresize class="chart" />
</div> </div>
@@ -45,7 +45,7 @@
<div class="chart-icon danger"> <div class="chart-icon danger">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg>
</div> </div>
<h4>拒绝率趋势</h4> <h4>异常拒绝率趋势</h4>
</div> </div>
<v-chart :option="denyRateOption" autoresize class="chart" /> <v-chart :option="denyRateOption" autoresize class="chart" />
</div> </div>
@@ -181,7 +181,7 @@ const denyRateOption = computed(() => ({
yAxis: { type: "value", max: 100, name: "%", splitLine: { lineStyle: { color: "#f1f5f9" } }, axisLabel: { color: "#64748b" } }, yAxis: { type: "value", max: 100, name: "%", splitLine: { lineStyle: { color: "#f1f5f9" } }, axisLabel: { color: "#64748b" } },
series: [ series: [
{ {
name: "拒绝率", name: "异常拒绝率",
type: "line", type: "line",
smooth: true, smooth: true,
symbol: "circle", symbol: "circle",
+2 -1
View File
@@ -262,6 +262,7 @@ export const TEXT = {
projectManagement: "项目管理", projectManagement: "项目管理",
auditLogs: "审计日志", auditLogs: "审计日志",
permissionManagement: "权限管理", permissionManagement: "权限管理",
systemMonitoring: "系统监测",
currentProject: "当前项目", currentProject: "当前项目",
projectOverview: "项目总览", projectOverview: "项目总览",
projectMilestones: "项目里程碑", projectMilestones: "项目里程碑",
@@ -1135,7 +1136,7 @@ export const TEXT = {
title: "权限管理", title: "权限管理",
moduleLevel: "模块级权限", moduleLevel: "模块级权限",
apiLevel: "角色权限", apiLevel: "角色权限",
monitoring: "权限监控", monitoring: "系统监测",
refreshMetrics: "刷新指标", refreshMetrics: "刷新指标",
save: "保存", save: "保存",
search: "搜索端点", search: "搜索端点",
+18 -1
View File
@@ -45,7 +45,7 @@ describe("admin project route permissions", () => {
it("guards project permission configuration with the system-level project config permission", () => { it("guards project permission configuration with the system-level project config permission", () => {
const source = readRouter(); const source = readRouter();
const projectPermissionRouteStart = source.indexOf('name: "AdminPermissionsProject"'); const projectPermissionRouteStart = source.indexOf('name: "AdminPermissionsProject"');
const projectPermissionRouteEnd = source.indexOf('name: "AdminPermissionsMonitoring"', projectPermissionRouteStart); const projectPermissionRouteEnd = source.indexOf('path: "system-monitoring"', projectPermissionRouteStart);
const projectPermissionRoute = source.slice(projectPermissionRouteStart, projectPermissionRouteEnd); const projectPermissionRoute = source.slice(projectPermissionRouteStart, projectPermissionRouteEnd);
expect(source).toContain('const SYSTEM_PERMISSION_PROJECT_CONFIG = "system:permissions:project_config";'); expect(source).toContain('const SYSTEM_PERMISSION_PROJECT_CONFIG = "system:permissions:project_config";');
@@ -61,6 +61,23 @@ describe("admin project route permissions", () => {
expect(targetProjectBranch).not.toContain("ensureDefaultPmStudy"); expect(targetProjectBranch).not.toContain("ensureDefaultPmStudy");
}); });
it("registers system monitoring as an admin-only peer admin module with legacy redirects", () => {
const source = readRouter();
const monitoringRouteStart = source.indexOf('name: "AdminSystemMonitoring"');
const monitoringRouteEnd = source.indexOf("],", monitoringRouteStart);
const monitoringRoute = source.slice(monitoringRouteStart, monitoringRouteEnd);
expect(source).toContain('import SystemMonitoringPage from "../views/admin/SystemMonitoringPage.vue";');
expect(source).toContain('path: "system-monitoring"');
expect(monitoringRoute).toContain("component: SystemMonitoringPage");
expect(monitoringRoute).toContain("requiresAdmin: true");
expect(source).toContain('path: "permission-monitoring"');
expect(source).toContain('path: "permissions/monitoring"');
expect(source).toContain('redirect: "/admin/system-monitoring"');
expect(source).not.toContain('const SYSTEM_PERMISSION_MONITORING_METRICS = "system:monitoring:metrics";');
expect(source).not.toContain("[SYSTEM_PERMISSION_MONITORING_METRICS]");
});
it("keeps login landing independent from PM management backend access", () => { it("keeps login landing independent from PM management backend access", () => {
const source = readRouter(); const source = readRouter();
+15 -8
View File
@@ -18,6 +18,7 @@ import AdminUserApproval from "../views/admin/AdminUserApproval.vue";
import AdminProjects from "../views/admin/Projects.vue"; import AdminProjects from "../views/admin/Projects.vue";
import AdminSites from "../views/admin/Sites.vue"; import AdminSites from "../views/admin/Sites.vue";
import PermissionManagement from "../views/admin/PermissionManagement.vue"; import PermissionManagement from "../views/admin/PermissionManagement.vue";
import SystemMonitoringPage from "../views/admin/SystemMonitoringPage.vue";
import ProjectDetail from "../views/admin/ProjectDetail.vue"; import ProjectDetail from "../views/admin/ProjectDetail.vue";
import ProfileSettings from "../views/ProfileSettings.vue"; import ProfileSettings from "../views/ProfileSettings.vue";
import ProjectOverview from "../views/ia/ProjectOverview.vue"; import ProjectOverview from "../views/ia/ProjectOverview.vue";
@@ -57,7 +58,6 @@ import { TEXT } from "../locales";
const SYSTEM_PERMISSION_READ = "system:permissions:read"; const SYSTEM_PERMISSION_READ = "system:permissions:read";
const SYSTEM_PERMISSION_PROJECT_CONFIG = "system:permissions:project_config"; const SYSTEM_PERMISSION_PROJECT_CONFIG = "system:permissions:project_config";
const SYSTEM_PERMISSION_MONITORING_METRICS = "system:monitoring:metrics";
const routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
{ {
@@ -372,6 +372,20 @@ const routes: RouteRecordRaw[] = [
component: AuditLogs, component: AuditLogs,
meta: { title: TEXT.menu.auditLogs, adminProjectPermission: { module: "audit_export", action: "read" } }, meta: { title: TEXT.menu.auditLogs, adminProjectPermission: { module: "audit_export", action: "read" } },
}, },
{
path: "system-monitoring",
name: "AdminSystemMonitoring",
component: SystemMonitoringPage,
meta: { title: TEXT.menu.systemMonitoring, requiresAdmin: true },
},
{
path: "permission-monitoring",
redirect: "/admin/system-monitoring",
},
{
path: "permissions/monitoring",
redirect: "/admin/system-monitoring",
},
{ {
path: "permissions", path: "permissions",
redirect: "/admin/permissions/system", redirect: "/admin/permissions/system",
@@ -388,12 +402,6 @@ const routes: RouteRecordRaw[] = [
component: PermissionManagement, component: PermissionManagement,
meta: { title: "项目权限配置", systemPermission: SYSTEM_PERMISSION_PROJECT_CONFIG }, meta: { title: "项目权限配置", systemPermission: SYSTEM_PERMISSION_PROJECT_CONFIG },
}, },
{
path: "permissions/monitoring",
name: "AdminPermissionsMonitoring",
component: PermissionManagement,
meta: { title: "权限监控", systemPermission: SYSTEM_PERMISSION_MONITORING_METRICS },
},
], ],
}, },
{ {
@@ -416,7 +424,6 @@ const ADMIN_PROJECT_OPERATION_KEYS: Record<string, Record<"read" | "write", stri
const SYSTEM_PERMISSION_ROLES: Record<string, string[]> = { const SYSTEM_PERMISSION_ROLES: Record<string, string[]> = {
[SYSTEM_PERMISSION_READ]: ["ADMIN", "PM"], [SYSTEM_PERMISSION_READ]: ["ADMIN", "PM"],
[SYSTEM_PERMISSION_PROJECT_CONFIG]: ["ADMIN", "PM"], [SYSTEM_PERMISSION_PROJECT_CONFIG]: ["ADMIN", "PM"],
[SYSTEM_PERMISSION_MONITORING_METRICS]: ["ADMIN", "PM"],
}; };
const getTargetProjectId = (to: any) => { const getTargetProjectId = (to: any) => {
+4 -4
View File
@@ -218,7 +218,7 @@ export interface HealthResponse {
cache_stats: CacheStatsResponse; cache_stats: CacheStatsResponse;
} }
// 权限监控 - 访问日志 // 系统监测 - 访问日志
export interface AccessLogItem { export interface AccessLogItem {
id: string; id: string;
study_id: string; study_id: string;
@@ -293,7 +293,7 @@ export interface SecurityAccessLogsResponse {
items: SecurityAccessLogItem[]; items: SecurityAccessLogItem[];
} }
// 权限监控 - 趋势数据 // 系统监测 - 趋势数据
export interface TrendDataPoint { export interface TrendDataPoint {
bucket_time: string; bucket_time: string;
total_checks: number; total_checks: number;
@@ -312,7 +312,7 @@ export interface TrendsResponse {
data_points: TrendDataPoint[]; data_points: TrendDataPoint[];
} }
// 权限监控 - 被拒绝最多 // 系统监测 - 被拒绝最多
export interface TopDeniedItem { export interface TopDeniedItem {
endpoint_key: string; endpoint_key: string;
role: string; role: string;
@@ -349,7 +349,7 @@ export interface IpLocationsResponse {
items: IpLocationStatItem[]; items: IpLocationStatItem[];
} }
// 权限监控 - 统计摘要 // 系统监测 - 统计摘要
export interface StatsSummaryResponse { export interface StatsSummaryResponse {
total_logs: number; total_logs: number;
today: { today: {
@@ -59,7 +59,8 @@ describe("ApiPermissions.vue", () => {
const source = readFileSync(resolve(__dirname, "./ApiPermissions.vue"), "utf8"); const source = readFileSync(resolve(__dirname, "./ApiPermissions.vue"), "utf8");
expect(source).toContain('label="角色权限"'); expect(source).toContain('label="角色权限"');
expect(source).toContain('label="权限监控"'); expect(source).not.toContain('label="权限监控"');
expect(source).not.toContain("<PermissionMonitoring");
}); });
it("has save button disabled when not dirty", () => { it("has save button disabled when not dirty", () => {
+2 -70
View File
@@ -19,10 +19,6 @@
</span> </span>
</div> </div>
<div class="permission-actions"> <div class="permission-actions">
<el-button @click="resetMetrics" :loading="resetting">
<el-icon><RefreshRight /></el-icon>
刷新指标
</el-button>
<el-button type="primary" :loading="saving" :disabled="!dirty" @click="save"> <el-button type="primary" :loading="saving" :disabled="!dirty" @click="save">
<el-icon><Check /></el-icon> <el-icon><Check /></el-icon>
保存 保存
@@ -46,17 +42,6 @@
@update="onApiMatrixUpdate" @update="onApiMatrixUpdate"
/> />
</el-tab-pane> </el-tab-pane>
<!-- 权限监控 -->
<el-tab-pane label="权限监控" name="monitoring">
<PermissionMonitoring
:is-admin="isAdmin"
:metrics="metrics"
:health="health"
:alerts="alerts"
@refresh="loadMonitoringData"
/>
</el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
</div> </div>
@@ -66,39 +51,24 @@
import { ref, computed, onMounted } from "vue"; import { ref, computed, onMounted } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { Key, RefreshRight, Check } from "@element-plus/icons-vue"; import { Key, Check } from "@element-plus/icons-vue";
import type { import type {
ApiEndpointPermissionsResponse, ApiEndpointPermissionsResponse,
PermissionMetricsResponse,
CacheStatsResponse,
AlertsResponse,
HealthResponse,
} from "@/types/api"; } from "@/types/api";
import { import {
fetchApiEndpointPermissions, fetchApiEndpointPermissions,
updateApiEndpointPermissions, updateApiEndpointPermissions,
fetchPermissionMetrics,
fetchCacheStats,
fetchPermissionAlerts,
fetchPermissionHealth,
resetPermissionMetrics,
} from "@/api/projectPermissions"; } from "@/api/projectPermissions";
import { useStudyStore } from "@/store/study"; import { useStudyStore } from "@/store/study";
import { useAuthStore } from "@/store/auth";
import { isSystemAdmin } from "@/utils/roles";
import ApiEndpointPermissions from "@/components/ApiEndpointPermissions.vue"; import ApiEndpointPermissions from "@/components/ApiEndpointPermissions.vue";
import PermissionMonitoring from "@/components/PermissionMonitoring.vue";
import PermissionTemplateSelector from "@/components/PermissionTemplateSelector.vue"; import PermissionTemplateSelector from "@/components/PermissionTemplateSelector.vue";
const route = useRoute(); const route = useRoute();
const studyStore = useStudyStore(); const studyStore = useStudyStore();
const auth = useAuthStore();
const isAdmin = computed(() => isSystemAdmin(auth.user));
const activeTab = ref<"api" | "monitoring">("api"); const activeTab = ref<"api">("api");
const loading = ref(false); const loading = ref(false);
const saving = ref(false); const saving = ref(false);
const resetting = ref(false);
const project = computed(() => studyStore.currentStudy); const project = computed(() => studyStore.currentStudy);
const studyId = computed(() => { const studyId = computed(() => {
@@ -109,11 +79,6 @@ const studyId = computed(() => {
// //
const apiMatrix = ref<ApiEndpointPermissionsResponse | null>(null); const apiMatrix = ref<ApiEndpointPermissionsResponse | null>(null);
//
const metrics = ref<PermissionMetricsResponse | null>(null);
const health = ref<HealthResponse | null>(null);
const alerts = ref<AlertsResponse | null>(null);
// //
const dirty = ref(false); const dirty = ref(false);
@@ -133,25 +98,6 @@ const loadPermissionData = async () => {
} }
}; };
const loadMonitoringData = async () => {
if (!studyId.value) return;
try {
const [metricsRes, healthRes, alertsRes] = await Promise.all([
fetchPermissionMetrics(),
fetchPermissionHealth(),
fetchPermissionAlerts(undefined, 20),
]);
metrics.value = metricsRes.data;
health.value = healthRes.data;
alerts.value = alertsRes.data;
} catch (error) {
ElMessage.error("加载监控数据失败");
console.error(error);
}
};
const onApiMatrixUpdate = (newMatrix: ApiEndpointPermissionsResponse) => { const onApiMatrixUpdate = (newMatrix: ApiEndpointPermissionsResponse) => {
apiMatrix.value = newMatrix; apiMatrix.value = newMatrix;
dirty.value = true; dirty.value = true;
@@ -209,20 +155,6 @@ const save = async () => {
} }
}; };
const resetMetrics = async () => {
resetting.value = true;
try {
await resetPermissionMetrics();
await loadMonitoringData();
ElMessage.success("指标已重置");
} catch (error) {
ElMessage.error("重置指标失败");
console.error(error);
} finally {
resetting.value = false;
}
};
onMounted(() => { onMounted(() => {
loadPermissionData(); loadPermissionData();
}); });
@@ -351,7 +351,9 @@ describe("permission management custom roles", () => {
it("does not render the duplicated monitoring header", () => { it("does not render the duplicated monitoring header", () => {
const source = readSource(); const source = readSource();
expect(source).toContain('<PermissionMonitoring :is-admin="isAdmin" />'); expect(source).not.toContain("<PermissionMonitoring");
expect(source).not.toContain('activeTab === "monitoring"');
expect(source).not.toContain("/admin/permissions/monitoring");
expect(source).not.toContain("实时监控权限使用情况、趋势分析与异常告警"); expect(source).not.toContain("实时监控权限使用情况、趋势分析与异常告警");
expect(source).not.toContain("重置指标"); expect(source).not.toContain("重置指标");
expect(source).not.toContain("refreshMonitoring"); expect(source).not.toContain("refreshMonitoring");
@@ -215,18 +215,6 @@
</div> </div>
</template> </template>
<!--
权限监控
-->
<template v-else-if="activeTab === 'monitoring'">
<div class="perm-body">
<div class="perm-body-padded">
<PermissionMonitoring :is-admin="isAdmin" />
</div>
</div>
</template>
<!-- <!--
角色管理抽屉 角色管理抽屉
--> -->
@@ -612,7 +600,6 @@ import {
} from "@/utils/projectPermissionModules"; } from "@/utils/projectPermissionModules";
import { useRoleTemplateMeta } from "@/composables/useRoleTemplateMeta"; import { useRoleTemplateMeta } from "@/composables/useRoleTemplateMeta";
import ApiEndpointPermissions from "@/components/ApiEndpointPermissions.vue"; import ApiEndpointPermissions from "@/components/ApiEndpointPermissions.vue";
import PermissionMonitoring from "@/components/PermissionMonitoring.vue";
import PermissionTemplateSelector from "@/components/PermissionTemplateSelector.vue"; import PermissionTemplateSelector from "@/components/PermissionTemplateSelector.vue";
const route = useRoute(); const route = useRoute();
@@ -657,16 +644,15 @@ const memberRoleLabels = (row: MemberRoleRow) => {
}; };
// //
const tabFromPath = (): "project" | "system" | "monitoring" => { const tabFromPath = (): "project" | "system" => {
const p = route.path; const p = route.path;
if (p.endsWith("/system")) return "system"; if (p.endsWith("/system")) return "system";
if (p.endsWith("/monitoring")) return "monitoring";
return "project"; return "project";
}; };
const activeTab = computed({ const activeTab = computed({
get: () => tabFromPath(), get: () => tabFromPath(),
set: (tab: "project" | "system" | "monitoring") => { set: (tab: "project" | "system") => {
const pathMap = { system: "/admin/permissions/system", project: "/admin/permissions/project", monitoring: "/admin/permissions/monitoring" }; const pathMap = { system: "/admin/permissions/system", project: "/admin/permissions/project" };
router.push({ path: pathMap[tab], query: route.query }); router.push({ path: pathMap[tab], query: route.query });
}, },
}); });
@@ -0,0 +1,18 @@
<template>
<div class="system-monitoring-page">
<PermissionMonitoring :is-admin="true" />
</div>
</template>
<script setup lang="ts">
import PermissionMonitoring from "@/components/PermissionMonitoring.vue";
</script>
<style scoped>
.system-monitoring-page {
margin: -6px -8px;
min-height: calc(100vh - 52px);
min-height: calc(100dvh - 52px);
background: #f5f7fa;
}
</style>