Repository navigation
RouteGuards
Access control in the frontend is enforced by pathless layout routes (React components that wrap a subtree and redirect before rendering) and beforeLoad hooks (router-level checks that run before any component renders). The previous guard system — higher-order components applied by applyGuards() to a hand-built route registry — was removed with the registry itself in the file-based router migration.
There is no applyGuards, no RouteDescription.guards array, and no src/routes/guards/ directory. A route's access rules are now decided by where its file sits in src/routes/ and what that route declares in beforeLoad.
| Old HOC guard | New mechanism | Where |
|---|---|---|
withProtected |
_authenticated pathless layout: auth + status gate |
src/routes/_authenticated.tsx |
withPublicOnly |
Public-only landing behavior in the / route component |
src/routes/index.tsx |
Route-level roles on RouteDescription
|
Role-tier layouts: _admin (ADMIN), _client-access (any-of VIEWER/SUBMITTER) |
src/routes/_authenticated/_admin.tsx, src/routes/_authenticated/_client-access.tsx
|
withFeatureFlag |
beforeLoad that throws notFound() when the flag is off |
each gated route file (see table below) |
withPersistentRedirect |
DashboardRedirect component on /dashboard
|
src/routes/dashboard.tsx |
withOfflineSupport |
Removed as a route gate. offlineOnly / offlineReady survive as staticData consumed by the nav projection only (no route is blocked by connectivity anymore) |
src/routing/nav/useNavEntries.ts |
navigateInTree / InTreePath
|
Removed. Navigation is typed directly through the single Register on the router — navigate({ to, params }) and Link to accept only real route paths |
src/router.tsx |
File: frontend/src/routes/_authenticated.tsx
A pathless layout (no URL segment) wrapping every protected URL. It is a React layout rather than a beforeLoad hook because authentication state lives in React context (useAuth) — a hook outside React would need a duplicated, non-React snapshot of the same state, and the two could disagree.
Behavior, in decision order (pure logic in computeAuthRedirect, src/routing/auth.ts):
- Auth resolving → Carbon
Loadingoverlay; no decisions whileisLoading - No user → persist
pathname + searchStrtosessionStorage(redirectAfterLogin), navigate to/login(external Cognito URL) — except when the match is the unknown-URL catch-all, or the pathname is/loginitself - No user on the catch-all (
routeId === '/_authenticated/$') → navigate to/without persisting (no real destination to restore) - Status
no-role→/no-role - Status
role-error→/unauthorized?reason=<CODE>, except when already on/unauthorized(loop guard) - All checks pass → one
<Layout><Outlet /></Layout>— page components never wrap themselves inLayout
The redirect fires from useLayoutEffect so it commits before the browser paints (no flash of gated content).
Effect-dependency rule: the redirect effect depends on primitives only. Depending on a fresh per-render object (like the whole { href } result) re-fires the effect each transition and each new navigate() cancels the pending one — an infinite cancel/restart loop that never commits the hop. The routing access-matrix tests caught exactly this before release.
Files: frontend/src/routes/_authenticated/_admin.tsx, frontend/src/routes/_authenticated/_client-access.tsx
| Tier | REQUIRED_ROLES |
Covers |
|---|---|---|
_admin |
ADMIN |
/configuration subtree |
_client-access |
any-of VIEWER, SUBMITTER
|
/clients |
Shared pattern (both files are structurally identical apart from the roles const):
- one
REQUIRED_ROLESconst — the single declaration point for the tier; -
hasAnyRole(user.roles, REQUIRED_ROLES)— any-of comparison onFamRole.role(assignments areFamRoleobjects, not strings); -
useLayoutEffectnavigates denied users to/unauthorizedbefore first paint (no-flash); -
Loadingoverlay while pending or denied-but-not-yet-redirected; -
staticData.rolesmirrors the list as nav metadata only — the layout component is the gate. The nav projection readsstaticDataso it can filter menu entries without importing route components; - routes inherit the tier by living in the tier's folder:
_admin/configuration.tsxand its children are ADMIN-gated by placement, with no per-route role declaration.
Feature flags are checked in the route's beforeLoad by throwing a router-level 404:
beforeLoad: () => {
if (!featureFlags['reporting-unit-details-enabled']) throw notFound();
},| Flag | Gate location | Covers |
|---|---|---|
reporting-unit-create-enabled |
src/routes/_authenticated/reporting-units/create.tsx |
/reporting-units/create |
reporting-unit-details-enabled |
src/routes/_authenticated/reporting-units/$ruId/index.tsx |
/reporting-units/$ruId |
reporting-unit-block-details-enabled |
src/routes/_authenticated/reporting-units/$ruId/$blockId.tsx |
/reporting-units/$ruId/$blockId |
configuration-enabled |
src/routes/_authenticated/_admin/configuration.tsx |
entire /configuration subtree — one check; children never repeat it |
Why beforeLoad for flags but a layout for auth: flags are synchronous environment values readable anywhere; auth is React context, invisible to beforeLoad. Each check lives where its data lives.
Consequence — flags run first: beforeLoad executes before the React auth gate renders. An unauthenticated user on a disabled route gets the auth-aware 404 (redirect to / to sign in), not a login prompt for a nonexistent page: disabled means absent, for everyone. And an authenticated non-admin on a disabled /configuration sees a 404 rather than /unauthorized — the flag check precedes the role tier. Both outcomes are the accepted fail-closed trade-off of putting flags in beforeLoad.
Each gated route also declares staticData.featureFlag so useNavEntries hides the side-nav entry while the flag is off.
File: frontend/src/routes/index.tsx
The / landing route keeps public-only semantics in its component (PublicOnlyLanding): while auth resolves it renders a labelled Loading overlay, authenticated users are redirected to /dashboard in a useLayoutEffect (no landing flash), unauthenticated users see LandingPage. No wrapper HOC — the behavior is the route component itself.
File: frontend/src/routes/dashboard.tsx
DashboardRedirect on /dashboard (top-level, outside _authenticated) resolves where a freshly authenticated user lands, once per mount via a useRef:
- Wait for auth to resolve.
- Stored safe target in
sessionStorage→ clear it, navigate there. - OAuth callback (
code+statequery params) →/search. - Otherwise →
/search, passing non-OAuth query params through.
getSafeRedirectTarget (src/routing/safeRedirect.ts) rejects empty values, non-path URLs (no leading /, or //), cross-origin absolute URLs (open-redirect protection), and /dashboard itself (self-target loop). See Routing for the sequence diagram.
| Rule | Rationale |
|---|---|
Auth/status/role decisions in layout components, flag decisions in beforeLoad
|
Each check runs where its data lives: React context vs synchronous env values |
Redirect from useLayoutEffect, never useEffect
|
The hop commits before paint — no flash of gated content |
| Effect deps must be primitives | A fresh object dep re-fires the effect every render; each new navigate() cancels the pending one → infinite loop |
staticData.roles / featureFlag mirror the gate for nav only |
The nav projection filters entries without importing components; the layout/beforeLoad remains the single enforcement point |
One <Layout> shell, from _authenticated
|
Page components never wrap themselves in Layout — errors and 404s render in exactly one shell |
| Subtree gates declared once at the section layout | Children inherit by placement; repeating a parent's check per child invites drift |
| Never gate loaders | Loaders resolve data only; flag checks live in beforeLoad, auth in the layouts — one concern, one place |
- Routing — full routing architecture: request flow, 404 split, search state, loaders, nav generation
- Routes — catalog of route files and URLs
-
frontend/src/routing/auth.ts—computeAuthRedirect/hasAnyRolepure logic -
frontend/src/routing/redirectStorage.ts—sessionStoragekeyredirectAfterLogin -
frontend/src/routing/safeRedirect.ts—getSafeRedirectTarget/isOAuthCallback
This wiki documents NR Waste Plus for users and developers. Verify security and deployment guidance against the current application configuration before applying it elsewhere.
- Report an issue or ask a question in the GitHub repository.
- Follow the Contributing Guidelines to propose an update.
- Read the Code of Conduct.
Collection indexes support human browsing and machine retrieval. Keep both current when adding or moving wiki pages.
NR Waste Plus Wiki | GitHub Repository