fix(platform): restore child admin menu access

This commit is contained in:
GuoQing Zhang
2026-07-16 20:06:19 +08:00
parent 94e2446b1d
commit 57a7585c34
7 changed files with 93 additions and 41 deletions
+1 -1
View File
@@ -170,7 +170,7 @@ export default function App() {
const noAuthPages = ['chat', 'resouce']
const path = location.pathname.replace(__APP_ENV__.BASE_URL, '').split('/')?.[1] || ''
// 动态路由根据权限:部门管理员补 create_app、子租户管理员补 sys(详见 resolveRoutePermissions
// Resolve route permissions with fallbacks for department and child tenant admins.
const router = useMemo(() => {
if (user && user.role === 'admin') return getAdminRouter()
if (!user?.user_id) return null
@@ -15,6 +15,7 @@ export default function HeaderMenu({ }) {
return user.role === 'admin'
}, [user])
const canOpenBuild = isAdmin
|| Boolean(user.is_child_admin)
|| Boolean(user.web_menu?.includes('build'))
|| Boolean(user.web_menu?.includes('create_app'))
const canManageWorkbench = canManageWorkbenchConfig(user)
@@ -69,16 +69,16 @@ export default function MainLayout() {
navigator('/reset')
}
// 系统超管(租户超管仍走自定义角色 web_menu;部门管理员由服务端合并全量菜单)
// Super admins and child tenant admins receive the full admin menu.
// Tenant management remains super-admin-only; department admins receive menus from the backend.
const isSuperAdmin = useMemo(() => user.role === "admin", [user])
const isDeptAdmin = Boolean(user.is_department_admin)
const isChildAdmin = Boolean(user.is_child_admin)
const canManageWorkbenchConfig = isSuperAdmin || isChildAdmin
// 侧栏:数据集 / 日志 — 超管与部门管理员
const isFullAdminShell = isSuperAdmin || isDeptAdmin
// 侧栏:系统管理 — 超管 / 部门管理员 / Child Admin。SystemPage 内部按
// PRD §3.3 已为 Child Admin 分了 Tab 视角(组织 + 角色)
const showSystemNav = isFullAdminShell || isChildAdmin
// Covers admin entries such as datasets that have no independent web_menu route key.
const isFullAdminShell = isSuperAdmin || isDeptAdmin || isChildAdmin
// SystemPage limits each admin type to the tabs it can manage.
const showSystemNav = isFullAdminShell
// 审批管理 — 仅超管 / Child Admin(部门管理员不可见)
const showApprovalNav = isSuperAdmin || isChildAdmin
// Admin-area approval scope (falls back to the legacy global flag for
@@ -96,7 +96,7 @@ export default function MainLayout() {
|| user.web_menu?.includes('frontend')
|| canManageWorkbenchConfig
}
return user.web_menu?.includes(menu) || isSuperAdmin
return user.web_menu?.includes(menu) || isSuperAdmin || isChildAdmin
}
const u = user as User
+20 -13
View File
@@ -173,13 +173,25 @@ function hasRoutePermission(permissions: string[], key: string) {
}
/**
* 把后端下发的 web_menu 转成路由层用的 permissions 数组。
* Convert the backend web_menu value into route permissions.
*
* - 部门管理员补 `create_app`(后端来不及下发时的兜底,原有行为)。
* - 子租户管理员(Child Admin)补 `sys` / `model`:后端 web_menu 不下发 sys/system_config 给非
* 超管/非部门管理员,且默认不下发 `model` / `workstation` 资源。子租户管理员需要在自己
* 租户内管理模型和工作台配置,路由层放行后由后端各自的 tenant admin 校验实际权限。
* - Department admins receive a create_app fallback when the backend omits it.
* - Child tenant admins receive every admin route except tenant management. Their backend web_menu
* still comes from a regular role, while business APIs retain tenant and resource authorization.
*/
const CHILD_ADMIN_ROUTE_PERMISSIONS = [
"board",
"build",
"create_app",
"knowledge",
"model",
"evaluation",
"mark_task",
"log",
"sys",
"workstation",
] as const
export function resolveRoutePermissions(user: {
web_menu?: string[]
is_department_admin?: boolean | null
@@ -189,14 +201,9 @@ export function resolveRoutePermissions(user: {
if (user.is_department_admin && !perms.includes("create_app")) {
perms = [...perms, "create_app"]
}
if (user.is_child_admin && !perms.includes("sys")) {
perms = [...perms, "sys"]
}
if (user.is_child_admin && !perms.includes("model")) {
perms = [...perms, "model"]
}
if (user.is_child_admin && !perms.includes("workstation")) {
perms = [...perms, "workstation"]
if (user.is_child_admin) {
const missing = CHILD_ADMIN_ROUTE_PERMISSIONS.filter((permission) => !perms.includes(permission))
if (missing.length) perms = [...perms, ...missing]
}
return perms
}
@@ -0,0 +1,38 @@
import { render, screen } from "@testing-library/react"
import { MemoryRouter } from "react-router-dom"
import { describe, expect, it, vi } from "vitest"
import { userContext } from "@/contexts/userContext"
import HeaderMenu from "@/layout/HeaderMenu"
vi.hoisted(() => {
;(globalThis as any).__APP_ENV__ = { BASE_URL: "" }
})
vi.mock("@/components/bs-icons", () => ({
TabIcon: () => <span aria-hidden="true" />,
}))
describe("HeaderMenu for Child Admin", () => {
it("shows application, tool, and workbench tabs without role web_menu grants", () => {
const user = {
user_id: 1,
user_name: "child-admin",
role: "user",
web_menu: [],
is_child_admin: true,
}
render(
<MemoryRouter initialEntries={["/build/apps"]}>
<userContext.Provider value={{ user } as any}>
<HeaderMenu />
</userContext.Provider>
</MemoryRouter>,
)
expect(screen.getByText("build.app")).toBeInTheDocument()
expect(screen.getByText("build.tools")).toBeInTheDocument()
expect(screen.getByText("build.workbench")).toBeInTheDocument()
})
})
@@ -145,7 +145,7 @@ function renderLayout(userOverrides: Record<string, unknown> = {}) {
);
}
describe("MainLayout system / tenant nav for Child Admin (PRD §3.3)", () => {
describe("MainLayout admin nav for Child Admin", () => {
beforeEach(() => {
vi.clearAllMocks();
Object.defineProperty(window, "localStorage", {
@@ -158,26 +158,25 @@ describe("MainLayout system / tenant nav for Child Admin (PRD §3.3)", () => {
});
});
it("Child Admin sees the system menu entry", () => {
renderLayout({ is_child_admin: true });
expect(screen.getByText("menu.system")).toBeInTheDocument();
});
it("Child Admin sees the build entry for workstation config", () => {
renderLayout({ is_child_admin: true });
expect(screen.getByText("menu.skills")).toBeInTheDocument();
});
it("Child Admin does NOT see the tenant management entry (super-only until backend is opened up)", () => {
it("Child Admin sees every admin menu except tenant management", () => {
renderLayout({ is_child_admin: true });
for (const label of [
"menu.dashboard",
"menu.skills",
"menu.knowledge",
"menu.dataset",
"menu.models",
"menu.evaluation",
"menu.annotation",
"menu.log",
"menu.approval",
"menu.system",
]) {
expect(screen.getByText(label)).toBeInTheDocument();
}
expect(screen.queryByText("tenant.management")).toBeNull();
});
it("Child Admin does NOT see the dataset entry (super / dept admin only)", () => {
renderLayout({ is_child_admin: true });
expect(screen.queryByText("menu.dataset")).toBeNull();
});
it("plain user without any admin flag sees neither system nor tenant management", () => {
renderLayout();
expect(screen.queryByText("menu.system")).toBeNull();
@@ -96,11 +96,18 @@ describe("resolveRoutePermissions", () => {
expect(perms.filter((p) => p === "model")).toHaveLength(1)
})
it("handles missing web_menu gracefully", () => {
it("grants Child Admin every admin route permission except tenant management", () => {
expect(resolveRoutePermissions({})).toEqual([])
expect(resolveRoutePermissions({ is_child_admin: true })).toEqual([
"sys",
"board",
"build",
"create_app",
"knowledge",
"model",
"evaluation",
"mark_task",
"log",
"sys",
"workstation",
])
})
@@ -116,6 +123,6 @@ describe("resolveRoutePermissions", () => {
})
expect(perms).toContain("sys")
expect(perms).toContain("workstation")
expect(perms).not.toContain("create_app")
expect(perms).toContain("create_app")
})
})