mirror of
https://github.com/dataelement/bisheng.git
synced 2026-09-01 15:32:50 +08:00
fix(platform): keep route permissions intact across router rebuilds
Opening a board and clicking 编辑 landed on /404 while the board list itself stayed reachable — the two routes disagreed about the same `board` permission. The route filter wrote its result back into the shared `privateRouter` (`cur.children = filterMenuItem(cur.children)`), and those children had already had their `permission` key destructured away. The router is rebuilt whenever `user` changes, so from the second build on nested routes carried no permission to check and survived unconditionally, while top-level routes — the board editor among them — were still filtered. A user without `board` therefore kept the list page and lost its editor. The filter now builds a fresh tree and never touches the shared table, so both routes make the same decision. The regression test fails on the old code with exactly the reported asymmetry: `dashboard` retained, `dashboard/:id` dropped.
This commit is contained in:
@@ -205,28 +205,29 @@ export const getPrivateRouter = (
|
||||
permissions: string[],
|
||||
opts?: { menuApprovalMode?: boolean },
|
||||
) => {
|
||||
const filterMenuItem = (_privateRouter) => {
|
||||
const result = _privateRouter.reduce((res, cur) => {
|
||||
// 递归
|
||||
if (cur.children?.length) {
|
||||
cur.children = filterMenuItem(cur.children)
|
||||
}
|
||||
// Builds a fresh route tree; `privateRouter` itself is never touched. It used
|
||||
// to write the filtered children back into the shared table, and those copies
|
||||
// had already lost their `permission` key — so from the second build on (the
|
||||
// router is rebuilt whenever `user` changes) nested routes were no longer
|
||||
// filtered at all, while top-level ones still were. That asymmetry let a user
|
||||
// reach a list page they should not see and then hit /404 on its top-level
|
||||
// editor route.
|
||||
const filterMenuItem = (routes) =>
|
||||
routes.reduce((res, cur) => {
|
||||
const { permission, children, ...other } = cur
|
||||
const next = children?.length ? { ...other, children: filterMenuItem(children) } : { ...other }
|
||||
|
||||
const { permission, ...other } = cur
|
||||
if (permission && !hasRoutePermission(permissions, permission)) {
|
||||
if (opts?.menuApprovalMode) {
|
||||
res.push({ ...other, element: <MenuPermissionPlaceholder /> })
|
||||
res.push({ ...next, element: <MenuPermissionPlaceholder /> })
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
res.push(other)
|
||||
res.push(next)
|
||||
return res
|
||||
}, [])
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
return createBrowserRouter(permissions ? filterMenuItem(privateRouter) : [],
|
||||
baseConfig)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* The route filter must not touch the shared route table.
|
||||
*
|
||||
* It used to write filtered children back into `privateRouter`, and those copies
|
||||
* had already lost their `permission` key — so every build after the first left
|
||||
* nested routes unfiltered while top-level ones were still filtered. A user then
|
||||
* reached a list page they should not see and hit /404 on its editor route.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { getPrivateRouter } from "@/routes"
|
||||
|
||||
type RouteNode = { path?: string; children?: RouteNode[] }
|
||||
|
||||
const pathsOf = (routes: RouteNode[]): string[] =>
|
||||
routes.flatMap((route) => [route.path, ...(route.children ? pathsOf(route.children) : [])]).filter(Boolean)
|
||||
|
||||
describe("getPrivateRouter", () => {
|
||||
it("filters the same way however many times it is called", () => {
|
||||
const withBoard = pathsOf(getPrivateRouter(["board"]).routes as RouteNode[])
|
||||
expect(withBoard).toContain("dashboard")
|
||||
expect(withBoard).toContain("dashboard/:id")
|
||||
|
||||
const withoutBoard = pathsOf(getPrivateRouter([]).routes as RouteNode[])
|
||||
expect(withoutBoard).not.toContain("dashboard")
|
||||
expect(withoutBoard).not.toContain("dashboard/:id")
|
||||
|
||||
const withBoardAgain = pathsOf(getPrivateRouter(["board"]).routes as RouteNode[])
|
||||
expect(withBoardAgain).toEqual(withBoard)
|
||||
})
|
||||
|
||||
it("keeps a denied route as a placeholder in menu-approval mode", () => {
|
||||
const paths = pathsOf(getPrivateRouter([], { menuApprovalMode: true }).routes as RouteNode[])
|
||||
|
||||
expect(paths).toContain("dashboard")
|
||||
expect(paths).toContain("dashboard/:id")
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user