From 4fa1981bb8d2d10c590bb227252dae7e4cdc257f Mon Sep 17 00:00:00 2001 From: wangbo Date: Mon, 3 Aug 2026 09:38:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E6=8C=89=E6=9D=83=E9=99=90?= =?UTF-8?q?=E9=9A=90=E8=97=8F=E7=AE=A1=E7=90=86=E5=B7=A5=E4=BD=9C=E5=8F=B0?= =?UTF-8?q?=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 基于 /api/v1/me 返回的当前用户角色控制主导航,仅向具备 power 或 manager 权限的 operator、manager、admin 展示管理工作台。所有已登录页面统一加载当前用户身份,避免停留首页时无法正确判断权限。\n\n新增角色映射与导航渲染回归测试。已验证前端 152 项测试、前端构建、pnpm lint 和 git diff --cached --check。 --- apps/web/src/App.tsx | 5 ++- apps/web/src/auth-permissions.test.ts | 21 ++++++++++++ apps/web/src/auth-permissions.ts | 9 ++++++ .../src/components/layout/AppShell.test.tsx | 32 +++++++++++++++++++ apps/web/src/components/layout/AppShell.tsx | 3 +- 5 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/auth-permissions.test.ts create mode 100644 apps/web/src/auth-permissions.ts create mode 100644 apps/web/src/components/layout/AppShell.test.tsx diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index fc2cd52..73378bc 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -117,6 +117,7 @@ import { } from './api'; import type { ConsoleData, StatItem } from './app-state'; import { AppShell } from './components/layout/AppShell'; +import { canAccessAdminWorkspace } from './auth-permissions'; import { LoginRequiredPanel } from './components/LoginRequiredPanel'; import { OIDCCallbackNotice } from './components/OIDCCallbackNotice'; import { useCatalogOperations } from './hooks/useCatalogOperations'; @@ -483,7 +484,8 @@ export function App() { loadedDataKeysRef.current.delete('walletTransactions'); loadingDataKeysRef.current.delete('walletTransactions'); } - await ensureData(dataKeysForRoute(activePage, adminSection, workspaceSection, Boolean(nextToken)), nextToken, force); + const routeKeys = dataKeysForRoute(activePage, adminSection, workspaceSection, Boolean(nextToken)); + await ensureData(nextToken ? ['currentUser', ...routeKeys] : routeKeys, nextToken, force); } async function ensureData(keys: DataKey[], nextToken = token, force = false) { @@ -1410,6 +1412,7 @@ export function App() { return ( { + it.each(['admin', 'manager', 'operator'])('allows the %s role', (role) => { + expect(canAccessAdminWorkspace(userWithRoles([role]))).toBe(true); + }); + + it.each([undefined, [], ['user'], ['creator']])('rejects non-admin roles: %j', (roles) => { + expect(canAccessAdminWorkspace(userWithRoles(roles))).toBe(false); + }); + + it('rejects an unresolved user', () => { + expect(canAccessAdminWorkspace(null)).toBe(false); + }); +}); diff --git a/apps/web/src/auth-permissions.ts b/apps/web/src/auth-permissions.ts new file mode 100644 index 0000000..50cccc7 --- /dev/null +++ b/apps/web/src/auth-permissions.ts @@ -0,0 +1,9 @@ +import type { AuthUser } from '@easyai-ai-gateway/contracts'; + +// Keep this aligned with backend roles that grant PermissionPower for read-only +// admin APIs; manager/admin additionally receive write access. +const adminWorkspaceRoles = new Set(['admin', 'manager', 'operator']); + +export function canAccessAdminWorkspace(user: AuthUser | null | undefined) { + return user?.role?.some((role) => adminWorkspaceRoles.has(role)) ?? false; +} diff --git a/apps/web/src/components/layout/AppShell.test.tsx b/apps/web/src/components/layout/AppShell.test.tsx new file mode 100644 index 0000000..7e287db --- /dev/null +++ b/apps/web/src/components/layout/AppShell.test.tsx @@ -0,0 +1,32 @@ +import { renderToStaticMarkup } from 'react-dom/server'; +import { describe, expect, it, vi } from 'vitest'; +import { AppShell } from './AppShell'; + +function renderShell(canAccessAdmin: boolean) { + const handler = vi.fn(); + return renderToStaticMarkup( + +
content
+
, + ); +} + +describe('AppShell', () => { + it('hides the admin workspace navigation without admin access', () => { + expect(renderShell(false)).not.toContain('管理工作台'); + }); + + it('shows the admin workspace navigation with admin access', () => { + expect(renderShell(true)).toContain('管理工作台'); + }); +}); diff --git a/apps/web/src/components/layout/AppShell.tsx b/apps/web/src/components/layout/AppShell.tsx index 2b84bcd..f70835b 100644 --- a/apps/web/src/components/layout/AppShell.tsx +++ b/apps/web/src/components/layout/AppShell.tsx @@ -15,6 +15,7 @@ const navItems: Array<{ key: PageKey; label: string; icon: ReactNode }> = [ export function AppShell(props: { activePage: PageKey; + canAccessAdmin: boolean; children: ReactNode; health: HealthResponse | null; isAuthenticated: boolean; @@ -35,7 +36,7 @@ export function AppShell(props: {