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
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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user