Files
admin-web/src/core/routing/accessDecision.ts
T
2026-08-03 18:49:18 +08:00

49 lines
1.3 KiB
TypeScript

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
}