fix subscription page
Build & Deploy / Build & Push Docker Image (push) Successful in 7m57s
Test / Type Check (all packages) (push) Successful in 4m27s
Build & Deploy / Deploy to VPS (push) Successful in 7s
Test / API Unit Tests (push) Failing after 3m2s
Test / Homepage Unit Tests (push) Successful in 4m3s
Test / Storefront Unit Tests (push) Successful in 3m32s
Test / Admin Unit Tests (push) Successful in 3m27s
Test / Dashboard Unit Tests (push) Successful in 3m3s
Test / API Integration Tests (push) Failing after 3m53s

This commit is contained in:
root
2026-06-29 23:15:55 -04:00
parent a752a399c2
commit f22e0d45e1
22 changed files with 842 additions and 72 deletions
@@ -4,6 +4,11 @@ import { usePathname, useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import { apiFetch } from '@/lib/api'
import { toDashboardAppPath } from '@/lib/dashboardPaths'
import {
getDashboardFallbackRoute,
resolveDashboardRoutePolicy,
roleCanAccessPolicy,
} from '@/lib/dashboardRoutePolicies'
type GeneratedMenuItem = {
id: string
@@ -16,6 +21,12 @@ type EmployeeMenuResponse = {
items: GeneratedMenuItem[]
}
type EmployeeProfileResponse = {
employee: {
role: string
}
}
export function flattenInternalRoutes(items: GeneratedMenuItem[]): string[] {
const routes: string[] = []
@@ -45,40 +56,75 @@ export function resolveAccessRedirect(currentPath: string, allowedRoutes: string
return fallbackRoute === currentPath ? null : fallbackRoute
}
function buildSignInRedirect(currentPath: string) {
const params = new URLSearchParams()
params.set('redirect', currentPath)
return `/sign-in?${params.toString()}`
}
export default function DashboardAccessGuard({ children }: { children: React.ReactNode }) {
const pathname = usePathname()
const router = useRouter()
const [ready, setReady] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let cancelled = false
async function enforceAccess() {
const currentPath = toDashboardAppPath(pathname)
const policy = resolveDashboardRoutePolicy(currentPath)
try {
setReady(false)
const currentPath = toDashboardAppPath(pathname)
const menu = await apiFetch<EmployeeMenuResponse>('/auth/employee/menu')
setError(null)
const { employee } = await apiFetch<EmployeeProfileResponse>('/auth/employee/me')
if (cancelled) return
const allowedRoutes = flattenInternalRoutes(menu.items)
const redirectPath = resolveAccessRedirect(currentPath, allowedRoutes)
if (!roleCanAccessPolicy(employee.role, policy)) {
const fallbackRoute = getDashboardFallbackRoute(employee.role)
if (fallbackRoute !== currentPath) {
router.replace(fallbackRoute)
return
}
}
if (redirectPath) {
router.replace(redirectPath)
return
if (policy.menuRegistrationRequired) {
const menu = await apiFetch<EmployeeMenuResponse>('/auth/employee/menu')
if (cancelled) return
const allowedRoutes = flattenInternalRoutes(menu.items)
const redirectPath = resolveAccessRedirect(currentPath, allowedRoutes)
if (redirectPath) {
router.replace(redirectPath)
return
}
}
setReady(true)
} catch (error: any) {
if (cancelled) return
if (error?.statusCode === 401 || error?.statusCode === 403 || error?.statusCode === 402) {
router.replace('/')
if (error?.statusCode === 401) {
const target = buildSignInRedirect(currentPath)
if (target !== currentPath) router.replace(target)
return
}
// Do not deadlock the dashboard on transient network failures.
setReady(true)
if (error?.statusCode === 402) {
if (policy.billingRecoveryRoute) setReady(true)
else router.replace('/subscription')
return
}
if (error?.statusCode === 403) {
if (currentPath !== '/') router.replace('/')
else setReady(true)
return
}
setError(error?.message ?? 'Unable to verify dashboard access. Please try again.')
}
}
@@ -89,6 +135,16 @@ export default function DashboardAccessGuard({ children }: { children: React.Rea
}
}, [pathname, router])
if (error) {
return (
<div className="flex min-h-[40vh] items-center justify-center px-6">
<div className="max-w-md rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-700">
{error}
</div>
</div>
)
}
if (!ready) {
return (
<div className="flex min-h-[40vh] items-center justify-center">