49 lines
1.3 KiB
TypeScript
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
|
|
}
|