implement permissions

This commit is contained in:
2026-04-03 15:11:51 +08:00
parent 5f2f9f2e96
commit 089041c7f1
3 changed files with 80 additions and 1 deletions
@@ -0,0 +1,47 @@
import { describe, it, expect } from 'vitest'
import { ADMIN_ONLY_PATH_PREFIXES, isAdminOnlyPath } from '../permissions'
describe('ADMIN_ONLY_PATH_PREFIXES', () => {
it('contains all 7 admin-only paths', () => {
expect(ADMIN_ONLY_PATH_PREFIXES).toHaveLength(7)
expect(ADMIN_ONLY_PATH_PREFIXES).toContain('/users')
expect(ADMIN_ONLY_PATH_PREFIXES).toContain('/roles')
expect(ADMIN_ONLY_PATH_PREFIXES).toContain('/route-groups')
expect(ADMIN_ONLY_PATH_PREFIXES).toContain('/mq-status')
expect(ADMIN_ONLY_PATH_PREFIXES).toContain('/failed-messages')
expect(ADMIN_ONLY_PATH_PREFIXES).toContain('/logs/requests')
expect(ADMIN_ONLY_PATH_PREFIXES).toContain('/logs/operations')
})
})
describe('isAdminOnlyPath', () => {
it('matches exact admin-only paths', () => {
for (const path of ADMIN_ONLY_PATH_PREFIXES) {
expect(isAdminOnlyPath(path)).toBe(true)
}
})
it('matches sub-paths via prefix matching', () => {
expect(isAdminOnlyPath('/users/123')).toBe(true)
expect(isAdminOnlyPath('/users/123/edit')).toBe(true)
expect(isAdminOnlyPath('/roles/5')).toBe(true)
expect(isAdminOnlyPath('/logs/requests/detail')).toBe(true)
expect(isAdminOnlyPath('/logs/operations/42')).toBe(true)
expect(isAdminOnlyPath('/failed-messages/retry')).toBe(true)
})
it('does not match non-admin paths', () => {
expect(isAdminOnlyPath('/')).toBe(false)
expect(isAdminOnlyPath('/login')).toBe(false)
expect(isAdminOnlyPath('/products')).toBe(false)
expect(isAdminOnlyPath('/orders')).toBe(false)
expect(isAdminOnlyPath('/dashboard')).toBe(false)
})
it('does not match prefix-similar but distinct paths', () => {
expect(isAdminOnlyPath('/users-export')).toBe(false)
expect(isAdminOnlyPath('/roles-backup')).toBe(false)
expect(isAdminOnlyPath('/route-groups-old')).toBe(false)
expect(isAdminOnlyPath('/mq-status-history')).toBe(false)
})
})
+25
View File
@@ -0,0 +1,25 @@
/**
* 仅管理员可访问的路径前缀列表。
* 路由守卫使用前缀匹配:以列表中任意项开头的路径都会被拦截。
*
* 同步要求:此列表必须与 MainLayout.vue 中 adminOnly: true 的菜单项保持一致。
*/
export const ADMIN_ONLY_PATH_PREFIXES: readonly string[] = [
'/users',
'/roles',
'/route-groups',
'/mq-status',
'/failed-messages',
'/logs/requests',
'/logs/operations',
] as const
/**
* 判断给定路径是否属于 admin-only 路径(前缀匹配)。
* 例如 '/users/123' 会匹配 '/users' 前缀。
*/
export function isAdminOnlyPath(path: string): boolean {
return ADMIN_ONLY_PATH_PREFIXES.some(
(prefix) => path === prefix || path.startsWith(prefix + '/'),
)
}
@@ -1,6 +1,7 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '../user'
import { fakeJwtWithRole } from '@/test/helpers'
describe('Permission: user store 权限判断', () => {
beforeEach(() => {
@@ -11,6 +12,7 @@ describe('Permission: user store 权限判断', () => {
describe('isAdmin', () => {
it('administrator 角色返回 true', () => {
const store = useUserStore()
store.setToken(fakeJwtWithRole('administrator'), 'rt')
store.setUser({ id: 1, username: 'admin', email: 'a@a.com', role: 'administrator', status: 1 })
expect(store.isAdmin).toBe(true)
@@ -40,6 +42,7 @@ describe('Permission: user store 权限判断', () => {
describe('hasPermission', () => {
it('admin 对 write 操作返回 true', () => {
const store = useUserStore()
store.setToken(fakeJwtWithRole('administrator'), 'rt')
store.setUser({ id: 1, username: 'admin', email: 'a@a.com', role: 'administrator', status: 1 })
expect(store.hasPermission('write')).toBe(true)
@@ -47,6 +50,7 @@ describe('Permission: user store 权限判断', () => {
it('admin 对 read 操作返回 true', () => {
const store = useUserStore()
store.setToken(fakeJwtWithRole('administrator'), 'rt')
store.setUser({ id: 1, username: 'admin', email: 'a@a.com', role: 'administrator', status: 1 })
expect(store.hasPermission('read')).toBe(true)
@@ -80,17 +84,20 @@ describe('Permission: user store 权限判断', () => {
})
describe('角色切换响应', () => {
it('用户角色从 accessor 切换为 administrator 后 isAdmin 变为 true', () => {
it('用户 token 从 accessor 切换为 administrator 后 isAdmin 变为 true', () => {
const store = useUserStore()
store.setToken(fakeJwtWithRole('accessor'), 'rt')
store.setUser({ id: 1, username: 'u', email: 'u@u.com', role: 'accessor', status: 1 })
expect(store.isAdmin).toBe(false)
store.setToken(fakeJwtWithRole('administrator'), 'rt')
store.setUser({ id: 1, username: 'u', email: 'u@u.com', role: 'administrator', status: 1 })
expect(store.isAdmin).toBe(true)
})
it('logout 后 isAdmin 恢复为 false', () => {
const store = useUserStore()
store.setToken(fakeJwtWithRole('administrator'), 'rt')
store.setUser({ id: 1, username: 'a', email: 'a@a.com', role: 'administrator', status: 1 })
expect(store.isAdmin).toBe(true)