From e95eeed90e1775e42817c9fa93ee8a194e1de9ac Mon Sep 17 00:00:00 2001 From: Cheng Zhou Date: Tue, 19 May 2026 16:03:27 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=9D=83=E9=99=90=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E4=B8=8E=E7=9B=91=E6=8E=A7=E6=8C=87=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/v1/permission_monitoring.py | 37 +- backend/app/core/permission_cache.py | 29 + backend/app/core/permission_monitor.py | 39 ++ backend/app/core/project_permissions.py | 62 +- backend/tests/conftest.py | 5 +- backend/tests/test_api_permissions.py | 75 +++ .../tests/test_api_permissions_endpoints.py | 88 +-- backend/tests/test_permission_monitoring.py | 26 + frontend/src/components/Layout.vue | 14 +- frontend/src/locales/zh-CN.ts | 2 +- frontend/src/router/index.ts | 24 +- frontend/src/views/admin/ApiPermissions.vue | 2 +- .../src/views/admin/PermissionManagement.vue | 531 ++++++++++-------- frontend/src/views/admin/Projects.vue | 4 +- 14 files changed, 603 insertions(+), 335 deletions(-) diff --git a/backend/app/api/v1/permission_monitoring.py b/backend/app/api/v1/permission_monitoring.py index 725b271e..bf1b59fd 100644 --- a/backend/app/api/v1/permission_monitoring.py +++ b/backend/app/api/v1/permission_monitoring.py @@ -8,7 +8,7 @@ from typing import Annotated from fastapi import APIRouter, Depends, status from app.core.deps import get_current_user -from app.core.permission_monitor import get_permission_monitor +from app.core.permission_monitor import evaluate_permission_system_health, get_permission_monitor router = APIRouter(prefix="/permission-monitoring", tags=["permission-monitoring"]) @@ -98,37 +98,4 @@ async def permission_system_health( metrics = monitor.get_metrics() cache_stats = monitor.get_cache_stats() - # 计算健康分数 - check_metrics = metrics["check_metrics"] - cache_metrics = metrics["cache_metrics"] - - health_score = 100 - issues = [] - - # 检查错误率 - if check_metrics["error_rate"] > 1: - health_score -= 20 - issues.append("权限检查错误率过高") - - # 检查缓存命中率 - if cache_metrics["hit_rate"] < 50: - health_score -= 10 - issues.append("缓存命中率过低") - - # 检查平均响应时间 - if check_metrics["avg_time"] > 0.01: # 10ms - health_score -= 10 - issues.append("权限检查响应时间过长") - - # 检查拒绝率 - if check_metrics["deny_rate"] > 50: - health_score -= 5 - issues.append("权限拒绝率过高") - - return { - "status": "healthy" if health_score >= 80 else "degraded" if health_score >= 50 else "unhealthy", - "health_score": max(0, health_score), - "issues": issues, - "metrics": metrics, - "cache_stats": cache_stats, - } + return evaluate_permission_system_health(metrics, cache_stats) diff --git a/backend/app/core/permission_cache.py b/backend/app/core/permission_cache.py index a20047d3..9e5445e7 100644 --- a/backend/app/core/permission_cache.py +++ b/backend/app/core/permission_cache.py @@ -52,6 +52,35 @@ class PermissionCache: self._member_role_cache[cache_key] = (role, time.time()) return role + def get_project_permissions( + self, + study_id: uuid.UUID, + ttl: int | None = None, + ) -> dict[str, dict[str, bool]] | None: + """获取项目接口权限覆盖表缓存""" + if ttl is None: + ttl = self.default_ttl + + cache_key = self._make_project_cache_key(study_id) + if cache_key not in self._project_permissions_cache: + return None + + permissions, timestamp = self._project_permissions_cache[cache_key] + if self._is_expired(timestamp, ttl): + self._project_permissions_cache.pop(cache_key, None) + return None + + return permissions + + def set_project_permissions( + self, + study_id: uuid.UUID, + permissions: dict[str, dict[str, bool]], + ) -> None: + """缓存项目接口权限覆盖表""" + cache_key = self._make_project_cache_key(study_id) + self._project_permissions_cache[cache_key] = (permissions, time.time()) + def invalidate_project_permissions(self, study_id: uuid.UUID) -> None: cache_key = self._make_project_cache_key(study_id) self._project_permissions_cache.pop(cache_key, None) diff --git a/backend/app/core/permission_monitor.py b/backend/app/core/permission_monitor.py index 7e837dba..b6e8f9be 100644 --- a/backend/app/core/permission_monitor.py +++ b/backend/app/core/permission_monitor.py @@ -17,6 +17,9 @@ from typing import Any from app.core.permission_cache import get_permission_cache +CACHE_HIT_RATE_HEALTH_MIN_ACCESSES = 10 + + @dataclass class PermissionCheckMetrics: """权限检查指标""" @@ -293,3 +296,39 @@ def set_permission_monitor(monitor: PermissionMonitor) -> None: """设置全局权限监控器实例(用于测试)""" global _permission_monitor _permission_monitor = monitor + + +def evaluate_permission_system_health(metrics: dict[str, Any], cache_stats: dict[str, Any]) -> dict[str, Any]: + """根据权限系统指标评估健康状态""" + check_metrics = metrics["check_metrics"] + cache_metrics = metrics["cache_metrics"] + + health_score = 100 + issues = [] + + if check_metrics["error_rate"] > 1: + health_score -= 20 + issues.append("权限检查错误率过高") + + if ( + cache_metrics["total_accesses"] >= CACHE_HIT_RATE_HEALTH_MIN_ACCESSES + and cache_metrics["hit_rate"] < 50 + ): + health_score -= 10 + issues.append("缓存命中率过低") + + if check_metrics["avg_time"] > 0.01: + health_score -= 10 + issues.append("权限检查响应时间过长") + + if check_metrics["deny_rate"] > 50: + health_score -= 5 + issues.append("权限拒绝率过高") + + return { + "status": "healthy" if health_score >= 80 else "degraded" if health_score >= 50 else "unhealthy", + "health_score": max(0, health_score), + "issues": issues, + "metrics": metrics, + "cache_stats": cache_stats, + } diff --git a/backend/app/core/project_permissions.py b/backend/app/core/project_permissions.py index edcfab9b..7b481260 100644 --- a/backend/app/core/project_permissions.py +++ b/backend/app/core/project_permissions.py @@ -10,6 +10,35 @@ from app.core.api_permissions import API_ENDPOINT_PERMISSIONS, OPERATION_PREREQU from app.core.permission_cache import get_permission_cache +async def _get_project_permission_overrides( + db: AsyncSession, + study_id: uuid.UUID, +) -> dict[str, dict[str, bool]]: + cache = get_permission_cache() + cached_permissions = cache.get_project_permissions(study_id) + from app.core.permission_monitor import get_permission_monitor + + monitor = get_permission_monitor() + if cached_permissions is not None: + monitor.record_cache_hit() + return cached_permissions + + monitor.record_cache_miss() + result = await db.execute( + select(ApiEndpointPermission).where( + ApiEndpointPermission.study_id == study_id, + ) + ) + rows = result.scalars().all() + + permissions: dict[str, dict[str, bool]] = {} + for row in rows: + permissions.setdefault(row.role, {})[row.endpoint_key] = row.allowed + + cache.set_project_permissions(study_id, permissions) + return permissions + + async def role_has_api_permission( db: AsyncSession, study_id: uuid.UUID, @@ -21,18 +50,12 @@ async def role_has_api_permission( if role == "ADMIN": return True - result = await db.execute( - select(ApiEndpointPermission).where( - ApiEndpointPermission.study_id == study_id, - ApiEndpointPermission.role == role, - ApiEndpointPermission.endpoint_key == endpoint_key, - ) - ) - perm = result.scalar_one_or_none() - if perm is None: + permissions = await _get_project_permission_overrides(db, study_id) + allowed = permissions.get(role or "", {}).get(endpoint_key) + if allowed is None: return False - if not perm.allowed: + if not allowed: return False if check_prerequisites: @@ -77,12 +100,7 @@ async def get_api_endpoint_permissions( 返回格式: {role: {endpoint_key: {allowed: bool}}} """ - result = await db.execute( - select(ApiEndpointPermission).where( - ApiEndpointPermission.study_id == study_id, - ) - ) - rows = result.scalars().all() + overrides = await _get_project_permission_overrides(db, study_id) roles = ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"] matrix: dict[str, dict[str, dict[str, bool]]] = {} @@ -92,10 +110,11 @@ async def get_api_endpoint_permissions( default_allowed = role in config.get("default_roles", []) matrix[role][endpoint_key] = {"allowed": default_allowed} - for row in rows: - if row.role not in matrix: - matrix[row.role] = {} - matrix[row.role][row.endpoint_key] = {"allowed": row.allowed} + for role, endpoints in overrides.items(): + if role not in matrix: + matrix[role] = {} + for endpoint_key, allowed in endpoints.items(): + matrix[role][endpoint_key] = {"allowed": allowed} return matrix @@ -132,5 +151,8 @@ async def replace_api_endpoint_permissions( cache = get_permission_cache() cache.invalidate_project_permissions(study_id) cache.invalidate_all_member_roles(study_id) + from app.core.permission_monitor import get_permission_monitor + + get_permission_monitor().record_cache_invalidation() return await get_api_endpoint_permissions(db, study_id) diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 656eb61f..2c12cdc3 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -111,8 +111,8 @@ async def db_session(test_engine) -> AsyncGenerator[AsyncSession, None]: study_code = f"TEST-STUDY-{uuid.uuid4().hex[:8]}" await session.execute( text(""" - INSERT INTO studies (id, code, name, status, is_locked, visit_schedule) - VALUES (:id, :code, :name, :status, :is_locked, :visit_schedule) + INSERT INTO studies (id, code, name, status, is_locked, visit_schedule, active_roles) + VALUES (:id, :code, :name, :status, :is_locked, :visit_schedule, :active_roles) """), { "id": str(uuid.uuid4()), @@ -121,6 +121,7 @@ async def db_session(test_engine) -> AsyncGenerator[AsyncSession, None]: "status": "ACTIVE", "is_locked": False, "visit_schedule": "[]", + "active_roles": "[]", } ) await session.commit() diff --git a/backend/tests/test_api_permissions.py b/backend/tests/test_api_permissions.py index 7f82a7da..c4258806 100644 --- a/backend/tests/test_api_permissions.py +++ b/backend/tests/test_api_permissions.py @@ -2,9 +2,12 @@ import pytest import uuid +from sqlalchemy import delete from sqlalchemy.ext.asyncio import AsyncSession from app.core.project_permissions import role_has_api_permission +from app.core.permission_cache import PermissionCache, set_permission_cache +from app.core.permission_monitor import PermissionMonitor, set_permission_monitor from app.models.api_endpoint_permission import ApiEndpointPermission @@ -208,3 +211,75 @@ async def test_api_permission_unknown_endpoint(db_session: AsyncSession): db_session, study_id, "CRA", "unknown:endpoint", check_prerequisites=False ) assert result is False + + +@pytest.mark.asyncio +async def test_api_permission_check_uses_project_permission_cache(db_session: AsyncSession): + """测试接口权限检查会复用项目权限缓存并记录命中指标""" + cache = PermissionCache() + monitor = PermissionMonitor() + set_permission_cache(cache) + set_permission_monitor(monitor) + study_id = uuid.uuid4() + + db_session.add( + ApiEndpointPermission( + study_id=study_id, + role="CRA", + endpoint_key="subjects:create", + allowed=True, + ) + ) + await db_session.commit() + + first_result = await role_has_api_permission( + db_session, study_id, "CRA", "subjects:create", check_prerequisites=False + ) + await db_session.execute( + delete(ApiEndpointPermission).where(ApiEndpointPermission.study_id == study_id) + ) + await db_session.commit() + second_result = await role_has_api_permission( + db_session, study_id, "CRA", "subjects:create", check_prerequisites=False + ) + + cache_metrics = monitor.get_metrics()["cache_metrics"] + assert first_result is True + assert second_result is True + assert cache.get_cache_stats()["project_permissions_count"] == 1 + assert cache_metrics["cache_misses"] == 1 + assert cache_metrics["cache_hits"] == 1 + + +@pytest.mark.asyncio +async def test_replace_api_endpoint_permissions_invalidates_project_permission_cache( + db_session: AsyncSession, +): + """测试替换权限后会失效旧缓存并记录失效指标""" + from app.core.project_permissions import replace_api_endpoint_permissions + + cache = PermissionCache() + monitor = PermissionMonitor() + set_permission_cache(cache) + set_permission_monitor(monitor) + study_id = uuid.uuid4() + + await replace_api_endpoint_permissions( + db_session, + study_id, + {"CRA": {"subjects:create": True}}, + ) + assert cache.get_cache_stats()["project_permissions_count"] == 1 + + await replace_api_endpoint_permissions( + db_session, + study_id, + {"CRA": {"subjects:create": False}}, + ) + + result = await role_has_api_permission( + db_session, study_id, "CRA", "subjects:create", check_prerequisites=False + ) + cache_metrics = monitor.get_metrics()["cache_metrics"] + assert result is False + assert cache_metrics["cache_invalidations"] == 2 diff --git a/backend/tests/test_api_permissions_endpoints.py b/backend/tests/test_api_permissions_endpoints.py index 2ea161d0..5523e693 100644 --- a/backend/tests/test_api_permissions_endpoints.py +++ b/backend/tests/test_api_permissions_endpoints.py @@ -30,7 +30,7 @@ async def test_get_api_endpoint_permissions_with_custom(db_session: AsyncSession perm = ApiEndpointPermission( study_id=study_id, role="CRA", - endpoint_key="POST:/subjects", + endpoint_key="subjects:create", allowed=True, ) db_session.add(perm) @@ -40,8 +40,8 @@ async def test_get_api_endpoint_permissions_with_custom(db_session: AsyncSession assert isinstance(result, dict) assert "CRA" in result - assert "POST:/subjects" in result["CRA"] - assert result["CRA"]["POST:/subjects"]["allowed"] is True + assert "subjects:create" in result["CRA"] + assert result["CRA"]["subjects:create"]["allowed"] is True @pytest.mark.asyncio @@ -51,9 +51,9 @@ async def test_replace_api_endpoint_permissions_single_role(db_session: AsyncSes payload = { "CRA": { - "POST:/subjects": True, - "GET:/subjects": True, - "PATCH:/subjects/{id}": True, + "subjects:create": True, + "subjects:list": True, + "subjects:update": True, } } @@ -61,9 +61,9 @@ async def test_replace_api_endpoint_permissions_single_role(db_session: AsyncSes assert isinstance(result, dict) assert "CRA" in result - assert result["CRA"]["POST:/subjects"]["allowed"] is True - assert result["CRA"]["GET:/subjects"]["allowed"] is True - assert result["CRA"]["PATCH:/subjects/{id}"]["allowed"] is True + assert result["CRA"]["subjects:create"]["allowed"] is True + assert result["CRA"]["subjects:list"]["allowed"] is True + assert result["CRA"]["subjects:update"]["allowed"] is True @pytest.mark.asyncio @@ -73,21 +73,21 @@ async def test_replace_api_endpoint_permissions_multiple_roles(db_session: Async payload = { "CRA": { - "POST:/subjects": True, - "GET:/subjects": True, + "subjects:create": True, + "subjects:list": True, }, "PV": { - "GET:/subjects": True, - "GET:/subjects/{id}": True, + "subjects:list": True, + "subjects:read": True, } } result = await replace_api_endpoint_permissions(db_session, study_id, payload) - assert result["CRA"]["POST:/subjects"]["allowed"] is True - assert result["CRA"]["GET:/subjects"]["allowed"] is True - assert result["PV"]["GET:/subjects"]["allowed"] is True - assert result["PV"]["GET:/subjects/{id}"]["allowed"] is True + assert result["CRA"]["subjects:create"]["allowed"] is True + assert result["CRA"]["subjects:list"]["allowed"] is True + assert result["PV"]["subjects:list"]["allowed"] is True + assert result["PV"]["subjects:read"]["allowed"] is True @pytest.mark.asyncio @@ -97,13 +97,13 @@ async def test_replace_api_endpoint_permissions_deny(db_session: AsyncSession): payload = { "PV": { - "POST:/subjects": False, + "subjects:create": False, } } result = await replace_api_endpoint_permissions(db_session, study_id, payload) - assert result["PV"]["POST:/subjects"]["allowed"] is False + assert result["PV"]["subjects:create"]["allowed"] is False @pytest.mark.asyncio @@ -115,7 +115,7 @@ async def test_replace_api_endpoint_permissions_overwrites_existing(db_session: perm = ApiEndpointPermission( study_id=study_id, role="CRA", - endpoint_key="POST:/subjects", + endpoint_key="subjects:create", allowed=True, ) db_session.add(perm) @@ -124,13 +124,13 @@ async def test_replace_api_endpoint_permissions_overwrites_existing(db_session: # 替换权限 payload = { "CRA": { - "POST:/subjects": False, + "subjects:create": False, } } result = await replace_api_endpoint_permissions(db_session, study_id, payload) - assert result["CRA"]["POST:/subjects"]["allowed"] is False + assert result["CRA"]["subjects:create"]["allowed"] is False @pytest.mark.asyncio @@ -140,23 +140,23 @@ async def test_replace_api_endpoint_permissions_multiple_endpoints(db_session: A payload = { "CRA": { - "POST:/subjects": True, - "GET:/subjects": True, - "PATCH:/subjects/{id}": True, - "DELETE:/subjects/{id}": False, - "POST:/risk-issues": True, - "GET:/risk-issues": True, + "subjects:create": True, + "subjects:list": True, + "subjects:update": True, + "subjects:delete": False, + "risk_issues:create": True, + "risk_issues:list": True, } } result = await replace_api_endpoint_permissions(db_session, study_id, payload) - assert result["CRA"]["POST:/subjects"]["allowed"] is True - assert result["CRA"]["GET:/subjects"]["allowed"] is True - assert result["CRA"]["PATCH:/subjects/{id}"]["allowed"] is True - assert result["CRA"]["DELETE:/subjects/{id}"]["allowed"] is False - assert result["CRA"]["POST:/risk-issues"]["allowed"] is True - assert result["CRA"]["GET:/risk-issues"]["allowed"] is True + assert result["CRA"]["subjects:create"]["allowed"] is True + assert result["CRA"]["subjects:list"]["allowed"] is True + assert result["CRA"]["subjects:update"]["allowed"] is True + assert result["CRA"]["subjects:delete"]["allowed"] is False + assert result["CRA"]["risk_issues:create"]["allowed"] is True + assert result["CRA"]["risk_issues:list"]["allowed"] is True @pytest.mark.asyncio @@ -167,13 +167,13 @@ async def test_replace_api_endpoint_permissions_different_studies(db_session: As payload_1 = { "CRA": { - "POST:/subjects": True, + "subjects:create": True, } } payload_2 = { "CRA": { - "POST:/subjects": False, + "subjects:create": False, } } @@ -183,8 +183,8 @@ async def test_replace_api_endpoint_permissions_different_studies(db_session: As result_1 = await get_api_endpoint_permissions(db_session, study_id_1) result_2 = await get_api_endpoint_permissions(db_session, study_id_2) - assert result_1["CRA"]["POST:/subjects"]["allowed"] is True - assert result_2["CRA"]["POST:/subjects"]["allowed"] is False + assert result_1["CRA"]["subjects:create"]["allowed"] is True + assert result_2["CRA"]["subjects:create"]["allowed"] is False @pytest.mark.asyncio @@ -209,13 +209,13 @@ async def test_replace_api_endpoint_permissions_partial_update(db_session: Async perm1 = ApiEndpointPermission( study_id=study_id, role="CRA", - endpoint_key="POST:/subjects", + endpoint_key="subjects:create", allowed=True, ) perm2 = ApiEndpointPermission( study_id=study_id, role="CRA", - endpoint_key="GET:/subjects", + endpoint_key="subjects:list", allowed=True, ) db_session.add(perm1) @@ -225,16 +225,16 @@ async def test_replace_api_endpoint_permissions_partial_update(db_session: Async # 只更新一个权限 payload = { "CRA": { - "POST:/subjects": False, + "subjects:create": False, } } result = await replace_api_endpoint_permissions(db_session, study_id, payload) # POST权限应该被更新 - assert result["CRA"]["POST:/subjects"]["allowed"] is False + assert result["CRA"]["subjects:create"]["allowed"] is False # GET权限应该保持不变 - assert result["CRA"]["GET:/subjects"]["allowed"] is True + assert result["CRA"]["subjects:list"]["allowed"] is True @pytest.mark.asyncio @@ -246,7 +246,7 @@ async def test_get_api_endpoint_permissions_structure(db_session: AsyncSession): perm = ApiEndpointPermission( study_id=study_id, role="CRA", - endpoint_key="POST:/subjects", + endpoint_key="subjects:create", allowed=True, ) db_session.add(perm) diff --git a/backend/tests/test_permission_monitoring.py b/backend/tests/test_permission_monitoring.py index 1643df67..3bc01726 100644 --- a/backend/tests/test_permission_monitoring.py +++ b/backend/tests/test_permission_monitoring.py @@ -17,6 +17,7 @@ from app.core.permission_monitor import ( CacheMetrics, get_permission_monitor, set_permission_monitor, + evaluate_permission_system_health, ) @@ -312,3 +313,28 @@ async def test_cache_metrics_dataclass(): assert metrics.hit_rate == pytest.approx(80.0, 0.1) assert metrics.miss_rate == pytest.approx(20.0, 0.1) + + +@pytest.mark.asyncio +async def test_health_check_ignores_cache_hit_rate_without_samples(): + """没有缓存访问样本时,不应判定缓存命中率过低""" + monitor = PermissionMonitor() + metrics = monitor.get_metrics() + cache_stats = monitor.get_cache_stats() + + health = evaluate_permission_system_health(metrics, cache_stats) + + assert metrics["cache_metrics"]["total_accesses"] == 0 + assert "缓存命中率过低" not in health["issues"] + + +@pytest.mark.asyncio +async def test_health_check_ignores_cache_hit_rate_with_too_few_samples(): + """缓存访问样本过少时,不应判定缓存命中率过低""" + monitor = PermissionMonitor() + for _ in range(3): + monitor.record_cache_miss() + + health = evaluate_permission_system_health(monitor.get_metrics(), monitor.get_cache_stats()) + + assert "缓存命中率过低" not in health["issues"] diff --git a/frontend/src/components/Layout.vue b/frontend/src/components/Layout.vue index 2effd991..8aa27cc5 100644 --- a/frontend/src/components/Layout.vue +++ b/frontend/src/components/Layout.vue @@ -33,10 +33,15 @@ {{ TEXT.menu.auditLogs }} - - - {{ TEXT.menu.permissionManagement }} - + + + 系统级权限 + 项目权限配置 + 权限监控 + @@ -280,6 +285,7 @@ const activeMenu = computed(() => { if (path.startsWith("/knowledge/instruction-files")) return "/knowledge/instruction-files"; if (path.startsWith("/projects/")) return "/admin/projects"; if (path.startsWith("/admin/projects/")) return "/admin/projects"; + if (path.startsWith("/admin/permissions/")) return path; return path; }); diff --git a/frontend/src/locales/zh-CN.ts b/frontend/src/locales/zh-CN.ts index 6693101e..64d7d50a 100644 --- a/frontend/src/locales/zh-CN.ts +++ b/frontend/src/locales/zh-CN.ts @@ -1154,7 +1154,7 @@ export const TEXT = { permissionManagement: { title: "权限管理", moduleLevel: "模块级权限", - apiLevel: "接口级权限", + apiLevel: "角色权限", monitoring: "权限监控", refreshMetrics: "刷新指标", save: "保存", diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index bf109a3d..25cd75c1 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -436,11 +436,11 @@ const routes: RouteRecordRaw[] = [ }, { path: "projects/:projectId/members", - redirect: (to) => ({ path: "/admin/permissions", query: { projectId: to.params.projectId as string, sub: "members" } }), + redirect: (to) => ({ path: "/admin/permissions/project", query: { projectId: to.params.projectId as string, sub: "members" } }), }, { path: "projects/:id/api-permissions", - redirect: (to) => ({ path: "/admin/permissions", query: { projectId: to.params.id as string } }), + redirect: (to) => ({ path: "/admin/permissions/project", query: { projectId: to.params.id as string } }), }, { path: "projects/:projectId/sites", @@ -456,9 +456,25 @@ const routes: RouteRecordRaw[] = [ }, { path: "permissions", - name: "AdminPermissions", + redirect: "/admin/permissions/system", + }, + { + path: "permissions/system", + name: "AdminPermissionsSystem", component: PermissionManagement, - meta: { title: TEXT.menu.permissionManagement, requiresAdmin: true }, + meta: { title: "系统级权限", requiresAdmin: true }, + }, + { + path: "permissions/project", + name: "AdminPermissionsProject", + component: PermissionManagement, + meta: { title: "项目权限配置", requiresAdmin: true }, + }, + { + path: "permissions/monitoring", + name: "AdminPermissionsMonitoring", + component: PermissionManagement, + meta: { title: "权限监控", requiresAdmin: true }, }, ], }, diff --git a/frontend/src/views/admin/ApiPermissions.vue b/frontend/src/views/admin/ApiPermissions.vue index 622094ca..5fc698f2 100644 --- a/frontend/src/views/admin/ApiPermissions.vue +++ b/frontend/src/views/admin/ApiPermissions.vue @@ -33,7 +33,7 @@ - + -
-
- +
- - -
- -
-
- 选择项目 - - - -
- - -
-
- - -
- -
- - - -
- - - - -
- + - - -
-
-
-
- - - 刷新 - - 重置指标 -
+ + + + + - -