fix: align dashboard guard with fallback menu access
Build & Deploy / Build & Push Docker Image (push) Successful in 2m56s
Test / Type Check (all packages) (push) Successful in 51s
Build & Deploy / Deploy to VPS (push) Successful in 4s
Test / API Unit Tests (push) Successful in 47s
Test / Homepage Unit Tests (push) Successful in 46s
Test / Storefront Unit Tests (push) Successful in 42s
Test / Admin Unit Tests (push) Successful in 42s
Test / Dashboard Unit Tests (push) Successful in 39s
Test / API Integration Tests (push) Successful in 1m1s
Build & Deploy / Build & Push Docker Image (push) Successful in 2m56s
Test / Type Check (all packages) (push) Successful in 51s
Build & Deploy / Deploy to VPS (push) Successful in 4s
Test / API Unit Tests (push) Successful in 47s
Test / Homepage Unit Tests (push) Successful in 46s
Test / Storefront Unit Tests (push) Successful in 42s
Test / Admin Unit Tests (push) Successful in 42s
Test / Dashboard Unit Tests (push) Successful in 39s
Test / API Integration Tests (push) Successful in 1m1s
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { resolveDashboardRoutePolicy, roleCanAccessPolicy } from '@/lib/dashboardRoutePolicies'
|
||||
import { flattenInternalRoutes, isAllowedRoute, resolveAccessRedirect } from './DashboardAccessGuard'
|
||||
import { flattenInternalRoutes, getBaselineInternalRoutes, isAllowedRoute, resolveAccessRedirect, resolveAllowedRoutes } from './DashboardAccessGuard'
|
||||
|
||||
describe('DashboardAccessGuard route helpers', () => {
|
||||
it('normalizes dashboard-prefixed menu routes before checking access', () => {
|
||||
@@ -35,6 +35,46 @@ describe('DashboardAccessGuard route helpers', () => {
|
||||
expect(resolveAccessRedirect('/fleet', [])).toBeNull()
|
||||
})
|
||||
|
||||
it('uses the approved baseline routes when active subscription menu data is empty or root-only', () => {
|
||||
expect(resolveAllowedRoutes({ items: [], subscriptionAccessLevel: 'full' }, 'OWNER')).toEqual([
|
||||
'/',
|
||||
'/reservations',
|
||||
'/fleet',
|
||||
'/customers',
|
||||
'/reports',
|
||||
'/billing',
|
||||
'/settings',
|
||||
])
|
||||
|
||||
expect(resolveAllowedRoutes({
|
||||
subscriptionAccessLevel: 'full',
|
||||
items: [
|
||||
{
|
||||
id: 'dashboard',
|
||||
itemType: 'INTERNAL_PAGE',
|
||||
routeOrUrl: '/',
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
}, 'MANAGER')).toEqual([
|
||||
'/',
|
||||
'/reservations',
|
||||
'/fleet',
|
||||
'/customers',
|
||||
'/reports',
|
||||
'/billing',
|
||||
])
|
||||
})
|
||||
|
||||
it('does not apply baseline fallback when subscription access is none', () => {
|
||||
expect(resolveAllowedRoutes({ items: [], subscriptionAccessLevel: 'none' }, 'OWNER')).toEqual([])
|
||||
})
|
||||
|
||||
it('filters baseline routes by role', () => {
|
||||
expect(getBaselineInternalRoutes('AGENT')).toEqual(['/', '/reservations', '/fleet', '/customers'])
|
||||
expect(getBaselineInternalRoutes('MANAGER')).toEqual(['/', '/reservations', '/fleet', '/customers', '/reports', '/billing'])
|
||||
})
|
||||
|
||||
it('redirects disallowed routes to the first visible internal route', () => {
|
||||
expect(resolveAccessRedirect('/settings', ['/', '/fleet'])).toBe('/')
|
||||
expect(resolveAccessRedirect('/fleet/123', ['/', '/fleet'])).toBeNull()
|
||||
|
||||
@@ -19,6 +19,7 @@ type GeneratedMenuItem = {
|
||||
|
||||
type EmployeeMenuResponse = {
|
||||
items: GeneratedMenuItem[]
|
||||
subscriptionAccessLevel?: 'full' | 'limited' | 'read_only' | 'none'
|
||||
}
|
||||
|
||||
type EmployeeProfileResponse = {
|
||||
@@ -27,6 +28,27 @@ type EmployeeProfileResponse = {
|
||||
}
|
||||
}
|
||||
|
||||
const ROLE_RANK: Record<string, number> = { OWNER: 3, MANAGER: 2, AGENT: 1 }
|
||||
const BASELINE_MENU_ROUTES = [
|
||||
{ route: '/', minRole: 'AGENT' },
|
||||
{ route: '/reservations', minRole: 'AGENT' },
|
||||
{ route: '/fleet', minRole: 'AGENT' },
|
||||
{ route: '/customers', minRole: 'AGENT' },
|
||||
{ route: '/reports', minRole: 'MANAGER' },
|
||||
{ route: '/billing', minRole: 'MANAGER' },
|
||||
{ route: '/settings', minRole: 'OWNER' },
|
||||
] as const
|
||||
|
||||
function hasMinRole(employeeRole: string, minRole: string): boolean {
|
||||
return (ROLE_RANK[employeeRole] ?? 0) >= (ROLE_RANK[minRole] ?? 0)
|
||||
}
|
||||
|
||||
export function getBaselineInternalRoutes(role: string): string[] {
|
||||
return BASELINE_MENU_ROUTES
|
||||
.filter((item) => hasMinRole(role, item.minRole))
|
||||
.map((item) => item.route)
|
||||
}
|
||||
|
||||
export function flattenInternalRoutes(items: GeneratedMenuItem[]): string[] {
|
||||
const routes: string[] = []
|
||||
|
||||
@@ -41,6 +63,15 @@ export function flattenInternalRoutes(items: GeneratedMenuItem[]): string[] {
|
||||
return Array.from(new Set(routes))
|
||||
}
|
||||
|
||||
export function resolveAllowedRoutes(menu: EmployeeMenuResponse, role: string): string[] {
|
||||
const routes = flattenInternalRoutes(menu.items)
|
||||
const shouldUseBaselineFallback =
|
||||
menu.subscriptionAccessLevel !== 'none' &&
|
||||
(routes.length === 0 || routes.every((route) => route === '/'))
|
||||
|
||||
return shouldUseBaselineFallback ? getBaselineInternalRoutes(role) : routes
|
||||
}
|
||||
|
||||
export function isAllowedRoute(currentPath: string, allowedRoutes: string[]) {
|
||||
return allowedRoutes.some((route) => {
|
||||
if (route === '/') return currentPath === '/'
|
||||
@@ -93,7 +124,12 @@ export default function DashboardAccessGuard({ children }: { children: React.Rea
|
||||
const menu = await apiFetch<EmployeeMenuResponse>('/auth/employee/menu')
|
||||
if (cancelled) return
|
||||
|
||||
const allowedRoutes = flattenInternalRoutes(menu.items)
|
||||
if (menu.subscriptionAccessLevel === 'none') {
|
||||
router.replace('/subscription')
|
||||
return
|
||||
}
|
||||
|
||||
const allowedRoutes = resolveAllowedRoutes(menu, employee.role)
|
||||
const redirectPath = resolveAccessRedirect(currentPath, allowedRoutes)
|
||||
|
||||
if (redirectPath) {
|
||||
|
||||
Reference in New Issue
Block a user