# Frontend UI: App Shell, Routing, Login Flow, and UI RBAC (RHL-019 / RHL-020 / RHL-021) This document describes the **frontend routing scaffold** and the **application shell layout** for the RHL Lieferscheine app. It started as a pure scaffold in **RHL-019** (public vs protected routes + AppShell + placeholder pages), was extended in **RHL-020** with a **real login flow** and a **session guard** for protected routes, and was extended again in **RHL-021** with a **UI-side RBAC guard** plus consistent **Forbidden / NotFound UX**. --- ## 1. Scope ### 1.1 Implemented (as of RHL-021) - Public `/login` route with a functional login form (shadcn/ui primitives). - Protected application shell for all other routes. - Session guard for the protected area: - checks session via `GET /api/auth/me` - redirects to `/login?reason=expired&next=` when unauthenticated - Logout button wired to `GET /api/auth/logout`. - Minimal `UserStatus` that displays session state (role + branch). - Centralized helper utilities for auth redirect behavior (`reason` / `next`) and error-to-message mapping. **RHL-021 additions:** - UI-side RBAC guard for branch routes (`BranchGuard`). - Consistent **Forbidden** UX for branch mismatches. - Consistent **NotFound** UX for invalid route params. - Server-side param validation via nested layouts (`notFound()` early). - Optional branch existence validation for admin/dev users via `GET /api/branches`. ### 1.2 Still out of scope / planned - Explorer navigation UI (years/months/days lists in sidebar). - Search UI and filters. - PDF viewer / file open. - A UI branch selector for admin/dev users (Sidebar placeholder will later host this). - Centralized UI error boundary mapping for API-level errors (e.g. mapping `AUTH_FORBIDDEN_BRANCH` to Forbidden UX inside explorer/search components). > Note: Prior to RHL-021, “Full RBAC UI guard” was listed as out of scope. It is now implemented for branch routes. --- ## 2. Route Groups & URL Structure The app uses Next.js App Router **Route Groups** to separate public and protected UI. ### 2.1 Route groups - **Public**: `app/(public)` - Routes that do **not** show the authenticated app shell. - Current route: `/login` - **Protected**: `app/(protected)` - Routes that render inside the **AppShell**. - Protected routes are guarded by: 1. session check (AuthProvider) 2. UI RBAC check for branch routes (BranchGuard) ### 2.2 URL map | URL | Purpose | Notes | | ---------------------------- | --------------------------- | ------------------------------------------------------------- | | `/login` | Login page | Supports `reason` and `next` query params | | `/` | Protected entry placeholder | Only rendered when authenticated | | `/:branch` | Branch placeholder | Example: `/NL01` | | `/:branch/:year` | Year placeholder | Example: `/NL01/2025` | | `/:branch/:year/:month` | Month placeholder | Example: `/NL01/2025/12` | | `/:branch/:year/:month/:day` | Day placeholder | Example: `/NL01/2025/12/31` | | `/:branch/search` | Search placeholder | Explicit segment so `search` is not interpreted as `:year` | | `/forbidden` | Forbidden page | Optional wrapper route; UI typically renders Forbidden inline | Important: - There is **no** standalone `/search` route. Visiting `/search` matches `/:branch` with `branch = "search"`. --- ## 3. Layouts ### 3.1 Root layout File: `app/layout.jsx` Responsibilities: - Global CSS imports (`app/globals.css`). - Theme provider setup (shadcn/ui + next-themes wrapper). - Base HTML/body structure. ### 3.2 Public layout File: `app/(public)/layout.jsx` Responsibilities: - Minimal centered layout for public routes. - Intended for `/login` (and potential future public routes). ### 3.3 Protected layout (with session guard) File: `app/(protected)/layout.jsx` Responsibilities: - Wraps all protected pages with the **AppShell**. - Wraps the auth provider in a **`` boundary**. - Adds the **session guard** via `components/auth/AuthProvider.jsx`. Why the Suspense boundary is required: - The session guard uses Next.js navigation hooks like `useSearchParams()`. - When a route is statically prerendered during production builds, `useSearchParams()` causes a CSR bailout unless wrapped by a Suspense boundary. - The Suspense fallback ensures the build stays valid while the client hydrates. --- ## 4. Authentication UX (RHL-020) ### 4.1 Session check for protected routes File: `components/auth/AuthProvider.jsx` Behavior: 1. On mount, call `apiClient.getMe()`. 2. If `{ user: { ... } }`: - set auth state to `authenticated` - render the protected UI 3. If `{ user: null }`: - redirect to `/login?reason=expired&next=` The `next` parameter: - includes the original `pathname` and query string - is sanitized to avoid open redirects (only internal paths are allowed) ### 4.2 Login page (reason / next) Files: - `app/(public)/login/page.jsx` (Server Component) - `components/auth/LoginForm.jsx` (Client Component) Flow: 1. Login page parses query params using `parseLoginParams(...)`. 2. If `reason` is present: - `expired` → show “Session expired” banner - `logged-out` → show “Logged out” banner 3. On submit, the form calls `apiClient.login({ username, password })`. 4. On success: - redirect to `next` if present - otherwise redirect to `/` 5. On failure: - show a safe, user-friendly error message Username policy: - The backend stores usernames in lowercase and performs normalization during login. - The UI enforces this policy as well: - username input is normalized to lowercase - `autoCapitalize="none"` to prevent mobile auto-caps ### 4.3 Logout File: `components/auth/LogoutButton.jsx` Flow: 1. Calls `apiClient.logout()`. 2. Redirects to `/login?reason=logged-out`. ### 4.4 User status Files: - `components/auth/authContext.jsx` - `components/app-shell/UserStatus.jsx` Behavior: - AuthProvider provides a minimal auth context (`status`, `user`). - `UserStatus` renders a short indicator: - loading → `Loading...` - authenticated → ` ()` when available - unauthenticated/error → fallback text --- ## 5. Frontend helper modules ### 5.1 API client File: - `lib/frontend/apiClient.js` Rules: - All UI code must call the backend through this client. - Defaults: - `credentials: "include"` - `cache: "no-store"` - Throws `ApiClientError` for standardized backend errors. - RHL-020 uses: - `login({ username, password })` - `logout()` - `getMe()` ### 5.2 Auth redirect helpers (`reason` / `next`) File: `lib/frontend/authRedirect.js` Provides: - `sanitizeNext(next)` to prevent open redirects. - `buildLoginUrl({ reason, next })`. - `parseLoginParams(searchParams)`. ### 5.3 Auth message mapping File: `lib/frontend/authMessages.js` - Centralized mapping from error codes to user-facing strings. - Centralized banner copy for `reason=expired` and `reason=logged-out`. ### 5.4 Frontend route helpers File: `lib/frontend/routes.js` - Centralizes URL building. - Prevents scattered stringly-typed URLs. - Encodes dynamic segments defensively. --- ## 6. UI primitives (shadcn/ui) The login UI uses shadcn/ui primitives from `components/ui/*`. Required components for the current scope: - `card` - `input` - `label` - `alert` These are added to the repository via the shadcn CLI. --- ## 7. File Naming Convention (.js vs .jsx) To keep the project consistent and avoid tooling issues: - Use **`.jsx`** for files that contain JSX: - `app/**/page.jsx`, `app/**/layout.jsx` - React components in `components/**` - Use **`.js`** for non-JSX files: - `lib/**` utilities and helpers - `app/api/**/route.js` - `models/**` - tests that do not contain JSX Note: - `components/auth/authContext.jsx` must be `.jsx` because it renders a JSX Provider. --- ## 8. Tests ### 8.1 Unit tests Existing (RHL-019 / RHL-020): - `lib/frontend/routes.test.js` (route builder) - `lib/frontend/apiClient.test.js` (client defaults + error mapping) - `lib/frontend/authRedirect.test.js` (reason/next parsing + sanitization) - `lib/frontend/authMessages.test.js` (UI message mappings) - `components/app-shell/AppShell.test.js` (SSR smoke test) RHL-021 additions: - `lib/frontend/params.test.js` (year/month/day/branch param validation) - `lib/frontend/rbac/branchAccess.test.js` (pure RBAC decision helper) - `lib/frontend/rbac/branchUiDecision.test.js` (UI decision helper: allowed/forbidden/not-found) ### 8.2 Running tests From the repo root: ```bash npx vitest run ``` Optional build check: ```bash npm run build ``` --- ## 9. Manual verification checklist ### 9.1 Local (Docker) Start: ```bash docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build ``` Verify flows in the browser: - Open a protected route while logged out (e.g. `/NL01/2025/12`) - Expect redirect to `/login?reason=expired&next=/NL01/2025/12` - Invalid login - Expect “Invalid username or password.” - Valid login - Expect redirect into the protected route - Logout - Expect redirect to `/login?reason=logged-out` RHL-021 checks: - Branch user: - `/NL01/...` works (own branch) - `/NL02/...` shows Forbidden (UI guard) - Invalid params (e.g. `/NL01/abcd`, `/NL01/2024/99/01`) show NotFound - Admin/dev: - Existing branches render - Non-existing branch (e.g. `/NL9999`) shows NotFound (branch existence validation) > Note: Branch existence validation for admin/dev uses `GET /api/branches`. The backend branch listing is subject to a 60s server-side TTL micro-cache (storage module). New branch folders may appear with up to ~60s delay. ### 9.2 Server Deploy and verify on the server URL. Important cookie note: - Browsers reject `Secure` cookies over HTTP. - Therefore the server `.env.server` must set: ```env SESSION_COOKIE_SECURE=false ``` Verify flows on the server URL: - Unauthenticated redirect + `next` - Valid login sets cookie and redirects back to `next` - Logout clears session and shows `reason=logged-out` RHL-021 checks on server: - Branch-user forbidden routes show Forbidden UI. - Invalid params show NotFound. - Admin/dev branch existence validation matches real NAS branch folders. --- ## 10. Planned follow-ups - HTTPS / reverse proxy deployment (separate ticket) - Replace placeholders with Explorer pages (years/months/days + files) - Add Search UI and filters - Add PDF open/view experience - Add admin/dev branch selector and navigation in the sidebar - Add centralized UI error mapping for API-level errors (Forbidden vs Session-expired) --- ## 11. UI RBAC Guard & Forbidden/NotFound UX (RHL-021) ### 11.1 Goals RHL-021 adds a friendly UI layer on top of backend RBAC: - Branch users must not access other branches’ URLs. - Admin/dev users may access any existing branch. - Invalid route parameters (year/month/day) should surface as NotFound. - Users should receive clear UX: - Forbidden page for RBAC mismatches - Consistent NotFound for invalid params and unknown branches Backend RBAC remains the source of truth. UI RBAC exists to: - prevent “obviously forbidden” navigation in the frontend - provide clearer, consistent UX for end users ### 11.2 BranchGuard (UI-side RBAC) Files: - `components/auth/BranchGuard.jsx` - Pure logic: - `lib/frontend/rbac/branchAccess.js` - `lib/frontend/rbac/branchUiDecision.js` Responsibilities: - Read `user` and `status` from `AuthContext`. - Enforce branch rules: - role `branch` → allowed only when `:branch === user.branchId` - role `admin` / `dev` → allowed for any branch that exists Guard ordering: 1. **AuthProvider** runs first and ensures we have a valid session (or redirects to login). 2. **BranchGuard** runs for all routes under `/:branch/...`. ### 11.3 Admin/dev branch existence validation Problem: - Without any existence check, an admin could navigate to a syntactically valid branch code that does not exist (e.g. `/NL200`) and still see a placeholder page. Solution: - For `admin` and `dev` users, BranchGuard validates that the route branch exists by calling: - `GET /api/branches` via `apiClient.getBranches()` Behavior: - While the branch list is being fetched, BranchGuard shows a small loader: - “Validating branch…” - If the requested `:branch` is not in the returned list: - show **NotFound** UX Fail-open policy: - If fetching the branch list fails (network issues, temporary backend failure): - BranchGuard does **not** block navigation permanently. - It falls back to allowing the route to render. - Backend RBAC and later API calls still enforce correctness. Security note: - Branch users do **not** perform existence checks for other branches. - If a branch user navigates to a different branch, they see Forbidden regardless of whether the branch exists. - This avoids leaking branch existence through UI behavior. ### 11.4 Param validation (year/month/day) Files: - `lib/frontend/params.js` - Layout enforcement (server-side `notFound()`): - `app/(protected)/[branch]/layout.jsx` (branch syntax) - `app/(protected)/[branch]/[year]/layout.jsx` - `app/(protected)/[branch]/[year]/[month]/layout.jsx` - `app/(protected)/[branch]/[year]/[month]/[day]/layout.jsx` Rules (syntactic validation only): - `year`: `YYYY` (4 digits) - `month`: `MM` (01–12) - `day`: `DD` (01–31) If a param is invalid: - Next’s `notFound()` is triggered immediately - The protected NotFound UI is shown Notes: - This ticket only covers obvious invalid params. - “Syntactically valid but missing on disk” (backend `FS_NOT_FOUND`) is handled later in Explorer/Search UI components. ### 11.5 Forbidden UX Files: - Reusable UI: `components/system/ForbiddenView.jsx` - Optional wrapper route: `app/(protected)/forbidden/page.jsx` Where Forbidden is shown: - BranchGuard renders ForbiddenView inline for branch mismatch. CTAs: - Branch users: “Go to my branch” (links to `/${user.branchId}`) - Admin/dev: “Go to home” (until a branch list/selector is available) ### 11.6 NotFound UX Files: - Reusable UI: `components/system/NotFoundView.jsx` - Protected not-found entry: `app/(protected)/not-found.jsx` Where NotFound is shown: - Invalid params in layouts via `notFound()`. - Admin/dev: non-existing branch codes via BranchGuard existence validation. ### 11.7 Interaction with backend RBAC errors Even with UI-side RBAC, the backend remains authoritative. Recommended policy for later UI tickets (Explorer/Search): - `AUTH_UNAUTHENTICATED`: - let the existing session flow handle it (redirect to `/login?reason=expired`) - `AUTH_FORBIDDEN_BRANCH`: - render ForbiddenView for that branch route This policy can be implemented as a small helper when Explorer/Search UI begins consuming the navigation endpoints.