fix(web): 按权限隐藏管理工作台入口
基于 /api/v1/me 返回的当前用户角色控制主导航,仅向具备 power 或 manager 权限的 operator、manager、admin 展示管理工作台。所有已登录页面统一加载当前用户身份,避免停留首页时无法正确判断权限。\n\n新增角色映射与导航渲染回归测试。已验证前端 152 项测试、前端构建、pnpm lint 和 git diff --cached --check。
This commit is contained in:
@@ -117,6 +117,7 @@ import {
|
|||||||
} from './api';
|
} from './api';
|
||||||
import type { ConsoleData, StatItem } from './app-state';
|
import type { ConsoleData, StatItem } from './app-state';
|
||||||
import { AppShell } from './components/layout/AppShell';
|
import { AppShell } from './components/layout/AppShell';
|
||||||
|
import { canAccessAdminWorkspace } from './auth-permissions';
|
||||||
import { LoginRequiredPanel } from './components/LoginRequiredPanel';
|
import { LoginRequiredPanel } from './components/LoginRequiredPanel';
|
||||||
import { OIDCCallbackNotice } from './components/OIDCCallbackNotice';
|
import { OIDCCallbackNotice } from './components/OIDCCallbackNotice';
|
||||||
import { useCatalogOperations } from './hooks/useCatalogOperations';
|
import { useCatalogOperations } from './hooks/useCatalogOperations';
|
||||||
@@ -483,7 +484,8 @@ export function App() {
|
|||||||
loadedDataKeysRef.current.delete('walletTransactions');
|
loadedDataKeysRef.current.delete('walletTransactions');
|
||||||
loadingDataKeysRef.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) {
|
async function ensureData(keys: DataKey[], nextToken = token, force = false) {
|
||||||
@@ -1410,6 +1412,7 @@ export function App() {
|
|||||||
return (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
activePage={activePage}
|
activePage={activePage}
|
||||||
|
canAccessAdmin={canAccessAdminWorkspace(currentUser)}
|
||||||
health={health}
|
health={health}
|
||||||
isAuthenticated={isAuthenticated}
|
isAuthenticated={isAuthenticated}
|
||||||
state={state}
|
state={state}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import type { AuthUser } from '@easyai-ai-gateway/contracts';
|
||||||
|
import { canAccessAdminWorkspace } from './auth-permissions';
|
||||||
|
|
||||||
|
function userWithRoles(role?: string[]): AuthUser {
|
||||||
|
return { sub: 'user-1', username: 'test-user', role };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('canAccessAdminWorkspace', () => {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
|
<AppShell
|
||||||
|
activePage="home"
|
||||||
|
canAccessAdmin={canAccessAdmin}
|
||||||
|
health={null}
|
||||||
|
isAuthenticated
|
||||||
|
state="ready"
|
||||||
|
onLogin={handler}
|
||||||
|
onNavigate={handler}
|
||||||
|
onRefresh={handler}
|
||||||
|
onSignOut={handler}
|
||||||
|
>
|
||||||
|
<div>content</div>
|
||||||
|
</AppShell>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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('管理工作台');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -15,6 +15,7 @@ const navItems: Array<{ key: PageKey; label: string; icon: ReactNode }> = [
|
|||||||
|
|
||||||
export function AppShell(props: {
|
export function AppShell(props: {
|
||||||
activePage: PageKey;
|
activePage: PageKey;
|
||||||
|
canAccessAdmin: boolean;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
health: HealthResponse | null;
|
health: HealthResponse | null;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
@@ -35,7 +36,7 @@ export function AppShell(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<nav className="topNav" aria-label="主导航">
|
<nav className="topNav" aria-label="主导航">
|
||||||
{navItems.map((item) => (
|
{navItems.filter((item) => item.key !== 'admin' || props.canAccessAdmin).map((item) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="topNavItem"
|
className="topNavItem"
|
||||||
|
|||||||
Reference in New Issue
Block a user