Problem
frontend/app/(auth)/layout.tsx and frontend/app/(auth)/admin/layout.tsx are empty wrappers with no authentication check. ProtectedRoute only runs client-side JavaScript. A user who directly navigates to /admin/users receives and renders the full protected page before the client-side auth check can redirect them. Additionally, RoleProtectedPage returns null on a role mismatch instead of redirecting - the user sees a blank screen.
Proposed Solution
- Create
frontend/middleware.ts intercepting all requests matching /admin/*
- Read the JWT from the
accessToken cookie, verify it using jose (jwtVerify with JWT_SECRET)
- Redirect unauthenticated requests to
/login?redirect={originalPath}
- Redirect role-violation requests to
/admin/dashboard
- Change
RoleProtectedPage to call router.replace() instead of returning null
Acceptance Criteria
Problem
frontend/app/(auth)/layout.tsxandfrontend/app/(auth)/admin/layout.tsxare empty wrappers with no authentication check.ProtectedRouteonly runs client-side JavaScript. A user who directly navigates to/admin/usersreceives and renders the full protected page before the client-side auth check can redirect them. Additionally,RoleProtectedPagereturnsnullon a role mismatch instead of redirecting - the user sees a blank screen.Proposed Solution
frontend/middleware.tsintercepting all requests matching/admin/*accessTokencookie, verify it usingjose(jwtVerifywithJWT_SECRET)/login?redirect={originalPath}/admin/dashboardRoleProtectedPageto callrouter.replace()instead of returningnullAcceptance Criteria
/admin/userswith no session redirects to/loginexpclaim)staffrole token navigating to asuper_adminpage redirects to/admin/dashboardmatcherconfig excludes public routes (/api,/_next,/menu, etc.)