fix: enforce expired trial subscription menu access
Build & Deploy / Build & Push Docker Image (push) Successful in 2m48s
Test / Type Check (all packages) (push) Successful in 52s
Build & Deploy / Deploy to VPS (push) Successful in 4s
Test / API Unit Tests (push) Failing after 1m9s
Test / Homepage Unit Tests (push) Successful in 43s
Test / Storefront Unit Tests (push) Successful in 42s
Test / Admin Unit Tests (push) Successful in 47s
Test / Dashboard Unit Tests (push) Successful in 41s
Test / API Integration Tests (push) Successful in 1m0s

This commit is contained in:
root
2026-07-02 02:32:37 -04:00
parent 0dcbe18c54
commit 75a1d39050
6 changed files with 156 additions and 22 deletions
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { APPROVED_BASELINE_MENU_KEYS, hasRenderableMenuItems } from './Sidebar'
import { APPROVED_BASELINE_MENU_KEYS, hasRenderableMenuItems, shouldUseGeneratedMenu } from './Sidebar'
describe('Sidebar menu rendering helpers', () => {
it('keeps the safe fallback menu to the approved seven baseline items in order', () => {
@@ -17,6 +17,9 @@ describe('Sidebar menu rendering helpers', () => {
it('treats an empty employee menu as non-renderable so fallback navigation remains available', () => {
expect(hasRenderableMenuItems(null)).toBe(false)
expect(hasRenderableMenuItems([])).toBe(false)
expect(shouldUseGeneratedMenu('loaded', [])).toBe(false)
expect(shouldUseGeneratedMenu('loaded', [], 'full')).toBe(false)
expect(shouldUseGeneratedMenu('loaded', [], 'none')).toBe(true)
})
it('ignores structural-only menu responses without clickable entries', () => {
@@ -49,12 +52,12 @@ describe('Sidebar menu rendering helpers', () => {
})
it('detects nested clickable menu entries', () => {
expect(hasRenderableMenuItems([
const menuItems = [
{
id: 'parent',
systemKey: null,
label: 'Operations',
itemType: 'PARENT_MENU',
itemType: 'PARENT_MENU' as const,
routeOrUrl: null,
icon: null,
parentId: null,
@@ -65,7 +68,7 @@ describe('Sidebar menu rendering helpers', () => {
id: 'fleet',
systemKey: 'fleet',
label: 'Fleet',
itemType: 'INTERNAL_PAGE',
itemType: 'INTERNAL_PAGE' as const,
routeOrUrl: '/fleet',
icon: 'Car',
parentId: 'parent',
@@ -75,6 +78,10 @@ describe('Sidebar menu rendering helpers', () => {
},
],
},
])).toBe(true)
]
expect(hasRenderableMenuItems(menuItems)).toBe(true)
expect(shouldUseGeneratedMenu('loaded', menuItems)).toBe(true)
expect(shouldUseGeneratedMenu('failed', menuItems)).toBe(false)
})
})
@@ -68,6 +68,8 @@ interface GeneratedMenuItem {
children: GeneratedMenuItem[]
}
type SubscriptionAccessLevel = 'full' | 'limited' | 'read_only' | 'none'
function computeInitials(name: string): string {
const parts = name.trim().split(/\s+/).filter(Boolean)
if (parts.length === 0) return 'U'
@@ -136,6 +138,16 @@ export function hasRenderableMenuItems(items: GeneratedMenuItem[] | null): boole
})
}
export function shouldUseGeneratedMenu(
menuLoadState: 'loading' | 'loaded' | 'failed',
items: GeneratedMenuItem[] | null,
subscriptionAccessLevel: SubscriptionAccessLevel | null = null,
): boolean {
if (menuLoadState !== 'loaded') return false
if (subscriptionAccessLevel === 'none') return true
return hasRenderableMenuItems(items)
}
function notifyParent(message: Record<string, unknown>) {
if (typeof window === 'undefined' || window.parent === window) return
window.parent.postMessage(message, '*')
@@ -154,6 +166,7 @@ export default function Sidebar() {
})
const [role, setRole] = useState<string>('AGENT')
const [menuItems, setMenuItems] = useState<GeneratedMenuItem[] | null>(null)
const [menuAccessLevel, setMenuAccessLevel] = useState<SubscriptionAccessLevel | null>(null)
const [menuLoadState, setMenuLoadState] = useState<'loading' | 'loaded' | 'failed'>('loading')
const [open, setOpen] = useState(false)
const [mounted, setMounted] = useState(false)
@@ -221,7 +234,7 @@ export default function Sidebar() {
Promise.all([
apiFetch<{ employee: EmployeeProfile }>('/auth/employee/me'),
apiFetch<{ items: GeneratedMenuItem[] }>('/auth/employee/menu').catch(() => null),
apiFetch<{ items: GeneratedMenuItem[]; subscriptionAccessLevel?: SubscriptionAccessLevel }>('/auth/employee/menu').catch(() => null),
])
.then(([{ employee }, menu]) => {
if (cancelled) return
@@ -230,8 +243,10 @@ export default function Sidebar() {
if (employee.role) setRole(employee.role)
if (menu) {
setMenuItems(menu.items)
setMenuAccessLevel(menu.subscriptionAccessLevel ?? null)
setMenuLoadState('loaded')
} else {
setMenuAccessLevel(null)
setMenuLoadState('failed')
}
if (employee.preferredLanguage === 'en' || employee.preferredLanguage === 'fr' || employee.preferredLanguage === 'ar') {
@@ -247,6 +262,7 @@ export default function Sidebar() {
.catch(() => {
if (cancelled) return
if (!cached) setUser(toSidebarUser({}, dict.workspaceUser, dict.localAuth))
setMenuAccessLevel(null)
setMenuLoadState('failed')
})
@@ -283,7 +299,7 @@ export default function Sidebar() {
children: [],
}))
const useGeneratedMenu = menuLoadState === 'loaded'
const useGeneratedMenu = shouldUseGeneratedMenu(menuLoadState, menuItems, menuAccessLevel)
const ownerSystemMenuItems = OWNER_SYSTEM_NAV_ITEMS
.filter((item) => !mounted || hasMinRole(role, item.minRole))
.map((item) => ({