task-33(会话/菜单/权限): 路由菜单权限守卫统一准入判定
新增纯函数 isRouteAllowed(无 menuKey 开放页放行/super_admin 全放行/其余按 key 命中);beforeEach 越权回退首个可见页、全无则回根路由空态。8 用例全过。
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
|
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
|
||||||
import type { AdminMenuNode } from '@/types/admin'
|
|
||||||
import AdminLayout from '@/layout/AdminLayout.vue'
|
import AdminLayout from '@/layout/AdminLayout.vue'
|
||||||
import NotFoundPage from '@/pages/error/NotFoundPage.vue'
|
import NotFoundPage from '@/pages/error/NotFoundPage.vue'
|
||||||
import { useAdminSessionStore } from '@/stores/admin-session'
|
import { useAdminSessionStore } from '@/stores/admin-session'
|
||||||
import { APP_BASE_PATH } from '@/config/app'
|
import { APP_BASE_PATH } from '@/config/app'
|
||||||
import { adminRouteRecords } from './routes'
|
import { adminRouteRecords } from './routes'
|
||||||
import { defineLazyPage } from './lazy-page'
|
import { defineLazyPage } from './lazy-page'
|
||||||
|
import { isRouteAllowed } from './helpers'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(APP_BASE_PATH),
|
history: createWebHistory(APP_BASE_PATH),
|
||||||
@@ -37,16 +37,11 @@ router.beforeEach(async (to) => {
|
|||||||
if (to.path === '/') {
|
if (to.path === '/') {
|
||||||
return session.firstVisible || true
|
return session.firstVisible || true
|
||||||
}
|
}
|
||||||
const menuKey = to.meta.menuKey
|
// 页面级权限:无 menuKey 开放页放行;越权页回退首个可见页,全无则回根空态。
|
||||||
if (menuKey && !session.isSuperAdmin && !hasMenu(session.menuTree, menuKey)) {
|
if (!isRouteAllowed(session.isSuperAdmin, to.meta.menuKey, session.menuTree)) {
|
||||||
// 无权限页面:回退到首个可见页面;一个都没有则回根路由空态。
|
|
||||||
return session.firstVisible || '/'
|
return session.firstVisible || '/'
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
function hasMenu(nodes: AdminMenuNode[], key: string): boolean {
|
|
||||||
return nodes.some((node) => node.key === key || hasMenu(node.children || [], key))
|
|
||||||
}
|
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { readSource } from './helpers.ts'
|
||||||
|
import { isRouteAllowed } from '../src/router/helpers.ts'
|
||||||
|
|
||||||
|
test('test_task_033_route_permission_guard_normal_primary_path', () => {
|
||||||
|
// 正常主路径:页面带 menuKey 且普通用户拥有该菜单 key 时放行。
|
||||||
|
const tree = [{ key: 'admin_users', name: '用户管理', route: '/account/users' }]
|
||||||
|
assert.equal(isRouteAllowed(false, 'admin_users', tree), true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_task_033_route_permission_guard_normal_variant_input', () => {
|
||||||
|
// 正常变体:超级管理员即便不带该 key 也放行;无 menuKey 页面不受限。
|
||||||
|
assert.equal(isRouteAllowed(true, 'whatever', []), true)
|
||||||
|
assert.equal(isRouteAllowed(false, undefined, []), true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_task_033_route_permission_guard_normal_repeated_operation_is_idempotent', () => {
|
||||||
|
// 正常重复:判定纯函数,输入不变结果稳定。
|
||||||
|
const tree = [{ key: 'g', name: '组', children: [{ key: 'users', name: '用户', route: '/users' }] }]
|
||||||
|
assert.equal(isRouteAllowed(false, 'users', tree), isRouteAllowed(false, 'users', tree))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_task_033_route_permission_guard_boundary_empty_input', () => {
|
||||||
|
// 边界空值:空菜单树对普通用户视为无权限(除无 menuKey 页面外)。
|
||||||
|
assert.equal(isRouteAllowed(false, 'admin_users', []), false)
|
||||||
|
assert.equal(isRouteAllowed(false, undefined, []), true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_task_033_route_permission_guard_boundary_single_item', () => {
|
||||||
|
// 边界单元素:只有单个可路由页面时的命中与未命中。
|
||||||
|
const tree = [{ key: 'solo', name: '单页', route: '/solo' }]
|
||||||
|
assert.equal(isRouteAllowed(false, 'solo', tree), true)
|
||||||
|
assert.equal(isRouteAllowed(false, 'other', tree), false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_task_033_route_permission_guard_boundary_limit_or_missing_field', () => {
|
||||||
|
// 边界上限/缺字段:深层叶子菜单 key 也能命中。
|
||||||
|
const tree = [{ key: 'g', name: '组', children: [{ key: 'inner', name: '内', route: '/inner' }] }]
|
||||||
|
assert.equal(isRouteAllowed(false, 'inner', tree), true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_task_033_route_permission_guard_invalid_input_rejected', () => {
|
||||||
|
// 异常输入:null/空 key 视为无菜单门槛页面一律放行(不误拦开放页)。
|
||||||
|
assert.equal(isRouteAllowed(false, null as unknown as string, []), true)
|
||||||
|
assert.equal(isRouteAllowed(false, '', []), true)
|
||||||
|
assert.equal(isRouteAllowed(false, 'x', []), false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_task_033_route_permission_guard_dependency_failure_returns_actionable_message', () => {
|
||||||
|
// 依赖失败:守卫把越权路由回退给首个可见页/根路由,复用统一判定。
|
||||||
|
const router = readSource('src/router/index.ts')
|
||||||
|
assert.match(router, /isRouteAllowed/, '路由守卫使用共享权限判定')
|
||||||
|
assert.match(router, /firstVisible/, '越权回退首个可见页')
|
||||||
|
const helpers = readSource('src/router/helpers.ts')
|
||||||
|
assert.match(helpers, /export function isRouteAllowed/)
|
||||||
|
assert.match(helpers, /hasMenuKey/)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user