Quellcode durchsuchen

RHL-021 refactor(docs): update frontend-ui documentation to reflect RHL-021 additions including UI RBAC guard and improved error handling

Code_Uwe vor 1 Monat
Ursprung
Commit
b2e238b5a8
1 geänderte Dateien mit 240 neuen und 25 gelöschten Zeilen
  1. 240 25
      Docs/frontend-ui.md

+ 240 - 25
Docs/frontend-ui.md

@@ -8,36 +8,50 @@
 
 <!-- --------------------------------------------------------------------------- -->
 
-# Frontend UI: App Shell, Routing, and Login Flow (RHL-019 / RHL-020)
+# 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) and was extended in **RHL-020** with a **real login flow**, a **session guard** for protected routes, and a minimal **logout + user status UX**.
+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-020)
+### 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.
-- Minimal session guard for the protected area:
+
+- Session guard for the protected area:
 
   - checks session via `GET /api/auth/me`
   - redirects to `/login?reason=expired&next=<original-url>` 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.
 
-### 1.2 Still out of scope (planned)
+**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
 
-- Full RBAC UI guard (branch users should be prevented from navigating to other branches in the UI).
 - Explorer navigation UI (years/months/days lists in sidebar).
 - Search UI and filters.
 - PDF viewer / file open.
-- HTTPS / reverse proxy (handled in a separate ticket).
+- 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.
 
 ---
 
@@ -55,19 +69,23 @@ The app uses Next.js App Router **Route Groups** to separate public and protecte
 - **Protected**: `app/(protected)`
 
   - Routes that render inside the **AppShell**.
-  - As of RHL-020, protected routes are guarded by a session check.
+  - 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` |
+| 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:
 
@@ -123,6 +141,7 @@ File: `components/auth/AuthProvider.jsx`
 Behavior:
 
 1. On mount, call `apiClient.getMe()`.
+
 2. If `{ user: { ... } }`:
 
    - set auth state to `authenticated`
@@ -147,12 +166,14 @@ Files:
 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
@@ -201,15 +222,21 @@ Behavior:
 
 ### 5.1 API client
 
-File: `lib/frontend/apiClient.js`
+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 })`
@@ -233,13 +260,21 @@ 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 RHL-020:
+Required components for the current scope:
 
 - `card`
 - `input`
@@ -268,7 +303,7 @@ To keep the project consistent and avoid tooling issues:
 
 Note:
 
-- `components/auth/authContext` must be `.jsx` because it renders a JSX Provider.
+- `components/auth/authContext.jsx` must be `.jsx` because it renders a JSX Provider.
 
 ---
 
@@ -276,12 +311,20 @@ Note:
 
 ### 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:
@@ -298,7 +341,7 @@ npm run build
 
 ---
 
-## 9. Manual verification checklist (RHL-020)
+## 9. Manual verification checklist
 
 ### 9.1 Local (Docker)
 
@@ -326,11 +369,24 @@ Verify flows in the browser:
 
   - Expect redirect to `/login?reason=logged-out`
 
-### 9.2 Server (direct HTTP)
+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)
 
-The current server deployment is accessed via **direct HTTP**:
+> 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.
 
-- `http://<server-ip>:3000`
+### 9.2 Server
+
+Deploy and verify on the server URL.
 
 Important cookie note:
 
@@ -347,12 +403,171 @@ Verify flows on the server URL:
 - 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)
-- UI-level RBAC guards (branch users cannot navigate to other branches)
 - 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.