优化构架

This commit is contained in:
2026-08-03 18:49:18 +08:00
parent 1241d11c01
commit 44c33a6280
133 changed files with 651 additions and 603 deletions
+109
View File
@@ -0,0 +1,109 @@
import { describe, expect, it } from 'vitest'
import {
decideRouteAccess,
resolvePreviewTarget,
sanitizeInternalPath,
} from './accessDecision'
describe('sanitizeInternalPath', () => {
it.each([
[undefined, '/'],
[null, '/'],
[42, '/'],
['', '/'],
['customers', '/'],
['//example.com', '/'],
['/\\example.com', '/'],
['/\n/example.com', '/'],
['/\r/example.com', '/'],
['/\t/example.com', '/'],
['/\u0000/example.com', '/'],
['/\u007f/example.com', '/'],
['https://example.com', '/'],
])('sanitizes %s to %s', (value, expected) => {
expect(sanitizeInternalPath(value)).toBe(expected)
})
it('accepts an internal path with its query string', () => {
expect(sanitizeInternalPath('/customers?status=pending')).toBe('/customers?status=pending')
})
})
describe('decideRouteAccess', () => {
it('checks authentication before route permission', () => {
expect(decideRouteAccess({
requiresAuth: true,
isAuthenticated: false,
fullPath: '/system/roles?tab=enabled',
permission: 'system:role:list',
hasPermission: false,
})).toEqual({ type: 'login', redirect: '/system/roles?tab=enabled' })
})
it('sanitizes an unsafe login redirect', () => {
expect(decideRouteAccess({
requiresAuth: true,
isAuthenticated: false,
fullPath: '//example.com',
permission: 'system:role:list',
hasPermission: false,
})).toEqual({ type: 'login', redirect: '/' })
})
it('sends an authenticated user without permission to forbidden and preserves the internal source', () => {
expect(decideRouteAccess({
requiresAuth: true,
isAuthenticated: true,
fullPath: '/customers?status=pending',
permission: 'customer:list',
hasPermission: false,
})).toEqual({ type: 'forbidden', from: '/customers?status=pending' })
})
it('allows the authenticated forbidden route because it has no permission metadata', () => {
expect(decideRouteAccess({
requiresAuth: true,
isAuthenticated: true,
fullPath: '/403?from=%2Fcustomers',
hasPermission: false,
})).toEqual({ type: 'allow' })
})
})
describe('resolvePreviewTarget', () => {
it('recovers from forbidden to the first accessible route', () => {
expect(resolvePreviewTarget({
currentPath: '/403',
currentPermission: undefined,
candidateCodes: new Set(['customer:list']),
firstPath: '/customers',
})).toBe('/customers')
})
it('stays on forbidden when the candidate has no accessible route', () => {
expect(resolvePreviewTarget({
currentPath: '/403',
currentPermission: undefined,
candidateCodes: new Set<string>(),
firstPath: null,
})).toBe('/403')
})
it('falls back when the current route permission is lost', () => {
expect(resolvePreviewTarget({
currentPath: '/system/roles',
currentPermission: 'system:role:list',
candidateCodes: new Set(['customer:list']),
firstPath: '/customers',
})).toBe('/customers')
})
it('keeps the current route when its permission remains available', () => {
expect(resolvePreviewTarget({
currentPath: '/customers',
currentPermission: 'customer:list',
candidateCodes: new Set(['customer:list']),
firstPath: '/customers',
})).toBeNull()
})
})
+48
View File
@@ -0,0 +1,48 @@
export type RouteAccessDecision =
| { type: 'allow' }
| { type: 'login'; redirect: string }
| { type: 'forbidden'; from: string }
export interface RouteAccessInput {
requiresAuth?: boolean
isAuthenticated: boolean
fullPath: string
permission?: string
hasPermission: boolean
}
export interface PreviewTargetInput {
currentPath: string
currentPermission?: string
candidateCodes: ReadonlySet<string>
firstPath: string | null
}
export function sanitizeInternalPath(value: unknown): string {
if (
typeof value !== 'string'
|| !value.startsWith('/')
|| value[1] === '/'
|| value[1] === '\\'
|| /[\u0000-\u001F\u007F]/.test(value)
) return '/'
return value
}
export function decideRouteAccess(input: RouteAccessInput): RouteAccessDecision {
if (input.requiresAuth && !input.isAuthenticated) {
return { type: 'login', redirect: sanitizeInternalPath(input.fullPath) }
}
if (input.permission && !input.hasPermission) {
return { type: 'forbidden', from: sanitizeInternalPath(input.fullPath) }
}
return { type: 'allow' }
}
export function resolvePreviewTarget(input: PreviewTargetInput): string | null {
if (input.currentPath === '/403') return input.firstPath ?? '/403'
if (input.currentPermission && !input.candidateCodes.has(input.currentPermission)) {
return input.firstPath ?? '/403'
}
return null
}