|
|
@@ -8,50 +8,93 @@
|
|
|
|
|
|
<!-- --------------------------------------------------------------------------- -->
|
|
|
|
|
|
-# Frontend UI: App Shell, Routing, Login Flow, and UI RBAC (RHL-019 / RHL-020 / RHL-021)
|
|
|
+# Frontend UI: App Shell, Routing, Auth/RBAC, and Explorer (RHL-019 / RHL-020 / RHL-021 / RHL-022)
|
|
|
|
|
|
-This document describes the **frontend routing scaffold** and the **application shell layout** for the RHL Lieferscheine app.
|
|
|
+This document describes the **frontend routing scaffold**, the **application shell layout**, and the **core navigation UI (Explorer)** 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**.
|
|
|
+Timeline:
|
|
|
+
|
|
|
+- **RHL-019**: Public vs protected route scaffold + AppShell + placeholder pages.
|
|
|
+- **RHL-020**: Real login flow + session handling and redirects.
|
|
|
+- **RHL-021**: UI-side RBAC guard + consistent Forbidden / NotFound UX.
|
|
|
+- **RHL-022**: Explorer v2 (Year → Month → Day → Files) + shadcn Breadcrumbs with dropdowns.
|
|
|
+
|
|
|
+> **Language policy**
|
|
|
+>
|
|
|
+> - Conversation and developer coordination can be German.
|
|
|
+> - **Code, comments, tests, and documentation are English.**
|
|
|
+> - **All user-facing UI strings are German** (labels, button text, alerts, hints).
|
|
|
|
|
|
---
|
|
|
|
|
|
## 1. Scope
|
|
|
|
|
|
-### 1.1 Implemented (as of RHL-021)
|
|
|
+### 1.1 Implemented (as of RHL-022)
|
|
|
+
|
|
|
+- **Public** `/login` route with a functional login form (shadcn/ui primitives).
|
|
|
+
|
|
|
+- **Protected** application shell for all authenticated routes:
|
|
|
+
|
|
|
+ - Top navigation (brand, status, logout)
|
|
|
+ - Sidebar placeholder area
|
|
|
+ - Main content area
|
|
|
+
|
|
|
+- **Session guard** for protected routes:
|
|
|
+
|
|
|
+ - Session identity is checked via `GET /api/auth/me`.
|
|
|
+ - When unauthenticated: redirect to `/login?reason=expired&next=<original-url>`.
|
|
|
+
|
|
|
+- **In-shell auth gating (UX improvement)**:
|
|
|
+
|
|
|
+ - Auth loading/error/redirect states render **inside** the AppShell main content.
|
|
|
+ - AppShell (TopNav + sidebar) remains stable; no “blank spinner screens”.
|
|
|
+
|
|
|
+- **Logout**:
|
|
|
+
|
|
|
+ - Logout button calls `GET /api/auth/logout` and redirects to `/login?reason=logged-out`.
|
|
|
+
|
|
|
+- **UI RBAC (branch-level)**:
|
|
|
|
|
|
-- Public `/login` route with a functional login form (shadcn/ui primitives).
|
|
|
+ - `BranchGuard` prevents branch users from accessing other branches’ URLs.
|
|
|
+ - Admin/dev can access multiple branches.
|
|
|
+ - Admin/dev branch existence validation uses `GET /api/branches`.
|
|
|
+ - Fail-open policy on validation failures (do not lock the UI on temporary API errors).
|
|
|
|
|
|
-- Protected application shell for all other routes.
|
|
|
+- **Route param validation** (syntactic):
|
|
|
|
|
|
-- Session guard for the protected area:
|
|
|
+ - `year`: `YYYY`
|
|
|
+ - `month`: `01–12`
|
|
|
+ - `day`: `01–31`
|
|
|
+ - Invalid params trigger `notFound()` early in layouts.
|
|
|
|
|
|
- - checks session via `GET /api/auth/me`
|
|
|
- - redirects to `/login?reason=expired&next=<original-url>` when unauthenticated
|
|
|
+- **Explorer v2** (Branch → Year → Month → Day → Files):
|
|
|
|
|
|
-- Logout button wired to `GET /api/auth/logout`.
|
|
|
+ - `/:branch` → years
|
|
|
+ - `/:branch/:year` → months
|
|
|
+ - `/:branch/:year/:month` → days
|
|
|
+ - `/:branch/:year/:month/:day` → files
|
|
|
|
|
|
-- Minimal `UserStatus` that displays session state (role + branch).
|
|
|
+- **Breadcrumb navigation**:
|
|
|
|
|
|
-- Centralized helper utilities for auth redirect behavior (`reason` / `next`) and error-to-message mapping.
|
|
|
+ - shadcn/ui `Breadcrumb` + dropdowns for year/month/day when options are available.
|
|
|
+ - Dropdown options are derived from real API results (only show segments that exist).
|
|
|
|
|
|
-**RHL-021 additions:**
|
|
|
+- **Consistent states** across Explorer levels:
|
|
|
|
|
|
-- 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`.
|
|
|
+ - Loading states (Skeleton)
|
|
|
+ - Empty states
|
|
|
+ - Error states with retry
|
|
|
+ - FS_NOT_FOUND mapped to an Explorer “path no longer exists” card
|
|
|
|
|
|
### 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).
|
|
|
+- Search UI (route exists as placeholder: `/:branch/search`).
|
|
|
+- PDF viewer / streaming endpoint integration (planned as RHL-023). File “Open” stays disabled for now.
|
|
|
+- Admin/dev branch selector in the sidebar.
|
|
|
+- Performance polish:
|
|
|
|
|
|
-> Note: Prior to RHL-021, “Full RBAC UI guard” was listed as out of scope. It is now implemented for branch routes.
|
|
|
+ - smoother navigation via client-side caching / prefetching
|
|
|
+ - skeleton/layout shift reduction
|
|
|
|
|
|
---
|
|
|
|
|
|
@@ -63,29 +106,29 @@ The app uses Next.js App Router **Route Groups** to separate public and protecte
|
|
|
|
|
|
- **Public**: `app/(public)`
|
|
|
|
|
|
- - Routes that do **not** show the authenticated app shell.
|
|
|
+ - Routes that do **not** show the authenticated AppShell.
|
|
|
- Current route: `/login`
|
|
|
|
|
|
- **Protected**: `app/(protected)`
|
|
|
|
|
|
- - Routes that render inside the **AppShell**.
|
|
|
+ - Routes that render inside the AppShell.
|
|
|
- Protected routes are guarded by:
|
|
|
|
|
|
- 1. session check (AuthProvider)
|
|
|
+ 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 |
|
|
|
+| URL | Purpose | Notes |
|
|
|
+| ---------------------------- | --------------------------- | ---------------------------------------------------------- |
|
|
|
+| `/login` | Login page | Supports `reason` and `next` query params |
|
|
|
+| `/` | Protected entry placeholder | Rendered only when authenticated |
|
|
|
+| `/:branch` | Explorer: years | Example: `/NL01` |
|
|
|
+| `/:branch/:year` | Explorer: months | Example: `/NL01/2025` |
|
|
|
+| `/:branch/:year/:month` | Explorer: days | Example: `/NL01/2025/12` |
|
|
|
+| `/:branch/:year/:month/:day` | Explorer: files | Example: `/NL01/2025/12/31` |
|
|
|
+| `/:branch/search` | Search placeholder | Explicit segment so `search` is not interpreted as `:year` |
|
|
|
+| `/forbidden` | Forbidden page wrapper | Optional wrapper; Forbidden is typically rendered inline |
|
|
|
|
|
|
Important:
|
|
|
|
|
|
@@ -114,21 +157,22 @@ Responsibilities:
|
|
|
- Minimal centered layout for public routes.
|
|
|
- Intended for `/login` (and potential future public routes).
|
|
|
|
|
|
-### 3.3 Protected layout (with session guard)
|
|
|
+### 3.3 Protected layout
|
|
|
|
|
|
File: `app/(protected)/layout.jsx`
|
|
|
|
|
|
Responsibilities:
|
|
|
|
|
|
-- Wraps all protected pages with the **AppShell**.
|
|
|
-- Wraps the auth provider in a **`<Suspense>` boundary**.
|
|
|
-- Adds the **session guard** via `components/auth/AuthProvider.jsx`.
|
|
|
+- Wrap all protected pages with:
|
|
|
|
|
|
-Why the Suspense boundary is required:
|
|
|
+ - `AuthProvider` (session check + redirect)
|
|
|
+ - `AppShell` (stable frame)
|
|
|
+ - `AuthGate` (renders auth loading/error/redirect UI inside the shell)
|
|
|
|
|
|
-- 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.
|
|
|
+UX rationale:
|
|
|
+
|
|
|
+- We keep the AppShell frame visible while auth/session checks run.
|
|
|
+- This avoids full-screen “blank spinners” on slow connections.
|
|
|
|
|
|
---
|
|
|
|
|
|
@@ -141,11 +185,10 @@ File: `components/auth/AuthProvider.jsx`
|
|
|
Behavior:
|
|
|
|
|
|
1. On mount, call `apiClient.getMe()`.
|
|
|
-
|
|
|
2. If `{ user: { ... } }`:
|
|
|
|
|
|
- set auth state to `authenticated`
|
|
|
- - render the protected UI
|
|
|
+ - render protected UI
|
|
|
|
|
|
3. If `{ user: null }`:
|
|
|
|
|
|
@@ -156,7 +199,17 @@ 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)
|
|
|
+### 4.2 AuthGate (in-shell gating)
|
|
|
+
|
|
|
+File: `components/auth/AuthGate.jsx`
|
|
|
+
|
|
|
+Behavior:
|
|
|
+
|
|
|
+- While session is loading: show an in-shell loading card.
|
|
|
+- On auth errors: show an in-shell error card + retry.
|
|
|
+- On unauthenticated: show an in-shell “redirecting” message while redirect happens.
|
|
|
+
|
|
|
+### 4.3 Login page (reason / next)
|
|
|
|
|
|
Files:
|
|
|
|
|
|
@@ -166,14 +219,12 @@ 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
|
|
|
+ - `expired` → show “session expired” banner (German)
|
|
|
+ - `logged-out` → show “logged out” banner (German)
|
|
|
|
|
|
3. On submit, the form calls `apiClient.login({ username, password })`.
|
|
|
-
|
|
|
4. On success:
|
|
|
|
|
|
- redirect to `next` if present
|
|
|
@@ -181,17 +232,17 @@ Flow:
|
|
|
|
|
|
5. On failure:
|
|
|
|
|
|
- - show a safe, user-friendly error message
|
|
|
+ - show a safe, user-friendly error message (German)
|
|
|
|
|
|
Username policy:
|
|
|
|
|
|
-- The backend stores usernames in lowercase and performs normalization during login.
|
|
|
-- The UI enforces this policy as well:
|
|
|
+- Backend stores usernames in lowercase and performs normalization during login.
|
|
|
+- UI enforces this policy as well:
|
|
|
|
|
|
- username input is normalized to lowercase
|
|
|
- `autoCapitalize="none"` to prevent mobile auto-caps
|
|
|
|
|
|
-### 4.3 Logout
|
|
|
+### 4.4 Logout
|
|
|
|
|
|
File: `components/auth/LogoutButton.jsx`
|
|
|
|
|
|
@@ -200,7 +251,7 @@ Flow:
|
|
|
1. Calls `apiClient.logout()`.
|
|
|
2. Redirects to `/login?reason=logged-out`.
|
|
|
|
|
|
-### 4.4 User status
|
|
|
+### 4.5 User status
|
|
|
|
|
|
Files:
|
|
|
|
|
|
@@ -209,365 +260,343 @@ Files:
|
|
|
|
|
|
Behavior:
|
|
|
|
|
|
-- AuthProvider provides a minimal auth context (`status`, `user`).
|
|
|
-- `UserStatus` renders a short indicator:
|
|
|
+- AuthProvider provides a minimal auth context (`status`, `user`, `error`).
|
|
|
+- `UserStatus` renders a short indicator in the TopNav:
|
|
|
|
|
|
- - loading → `Loading...`
|
|
|
- - authenticated → `<role> (<branchId>)` when available
|
|
|
+ - loading → `Lädt…`
|
|
|
+ - authenticated → role + optional branchId
|
|
|
- unauthenticated/error → fallback text
|
|
|
|
|
|
---
|
|
|
|
|
|
-## 5. Frontend helper modules
|
|
|
+## 5. UI RBAC, Forbidden, and NotFound (RHL-021)
|
|
|
|
|
|
-### 5.1 API client
|
|
|
+### 5.1 Goals
|
|
|
|
|
|
-File:
|
|
|
-
|
|
|
-- `lib/frontend/apiClient.js`
|
|
|
-
|
|
|
-Rules:
|
|
|
-
|
|
|
-- All UI code must call the backend through this client.
|
|
|
-
|
|
|
-- Defaults:
|
|
|
-
|
|
|
- - `credentials: "include"`
|
|
|
- - `cache: "no-store"`
|
|
|
+RHL-021 adds a friendly UI layer on top of backend RBAC:
|
|
|
|
|
|
-- Throws `ApiClientError` for standardized backend errors.
|
|
|
+- 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.
|
|
|
|
|
|
-- RHL-020 uses:
|
|
|
+Backend RBAC remains the source of truth. UI RBAC exists to:
|
|
|
|
|
|
- - `login({ username, password })`
|
|
|
- - `logout()`
|
|
|
- - `getMe()`
|
|
|
+- prevent “obviously forbidden” navigation in the frontend
|
|
|
+- provide clear and consistent UX for end users
|
|
|
|
|
|
-### 5.2 Auth redirect helpers (`reason` / `next`)
|
|
|
+### 5.2 BranchGuard (UI-side RBAC)
|
|
|
|
|
|
-File: `lib/frontend/authRedirect.js`
|
|
|
+Files:
|
|
|
|
|
|
-Provides:
|
|
|
+- `components/auth/BranchGuard.jsx`
|
|
|
+- Pure logic:
|
|
|
|
|
|
-- `sanitizeNext(next)` to prevent open redirects.
|
|
|
-- `buildLoginUrl({ reason, next })`.
|
|
|
-- `parseLoginParams(searchParams)`.
|
|
|
+ - `lib/frontend/rbac/branchAccess.js`
|
|
|
+ - `lib/frontend/rbac/branchUiDecision.js`
|
|
|
|
|
|
-### 5.3 Auth message mapping
|
|
|
+Responsibilities:
|
|
|
|
|
|
-File: `lib/frontend/authMessages.js`
|
|
|
+- Read `user` and `status` from AuthContext.
|
|
|
+- Enforce branch rules:
|
|
|
|
|
|
-- Centralized mapping from error codes to user-facing strings.
|
|
|
-- Centralized banner copy for `reason=expired` and `reason=logged-out`.
|
|
|
+ - role `branch` → allowed only when `:branch === user.branchId`
|
|
|
+ - role `admin` / `dev` → allowed for any branch that exists
|
|
|
|
|
|
-### 5.4 Frontend route helpers
|
|
|
+Admin/dev branch existence validation:
|
|
|
|
|
|
-File: `lib/frontend/routes.js`
|
|
|
+- `BranchGuard` fetches `GET /api/branches` and verifies the route branch exists.
|
|
|
+- Fail-open policy:
|
|
|
|
|
|
-- Centralizes URL building.
|
|
|
-- Prevents scattered stringly-typed URLs.
|
|
|
-- Encodes dynamic segments defensively.
|
|
|
+ - If fetching the list fails, do not block rendering.
|
|
|
+ - Backend RBAC and subsequent API calls remain authoritative.
|
|
|
|
|
|
----
|
|
|
+### 5.3 Param validation (year/month/day)
|
|
|
|
|
|
-## 6. UI primitives (shadcn/ui)
|
|
|
+Files:
|
|
|
|
|
|
-The login UI uses shadcn/ui primitives from `components/ui/*`.
|
|
|
+- `lib/frontend/params.js`
|
|
|
|
|
|
-Required components for the current scope:
|
|
|
+Layout enforcement (server-side `notFound()`):
|
|
|
|
|
|
-- `card`
|
|
|
-- `input`
|
|
|
-- `label`
|
|
|
-- `alert`
|
|
|
+- `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`
|
|
|
|
|
|
-These are added to the repository via the shadcn CLI.
|
|
|
+### 5.4 Forbidden UX
|
|
|
|
|
|
----
|
|
|
+Files:
|
|
|
|
|
|
-## 7. File Naming Convention (.js vs .jsx)
|
|
|
+- `components/system/ForbiddenView.jsx`
|
|
|
+- Optional wrapper route: `app/(protected)/forbidden/page.jsx`
|
|
|
|
|
|
-To keep the project consistent and avoid tooling issues:
|
|
|
+Where Forbidden is shown:
|
|
|
|
|
|
-- Use **`.jsx`** for files that contain JSX:
|
|
|
+- BranchGuard renders ForbiddenView inline for branch mismatch.
|
|
|
|
|
|
- - `app/**/page.jsx`, `app/**/layout.jsx`
|
|
|
- - React components in `components/**`
|
|
|
+### 5.5 NotFound UX
|
|
|
|
|
|
-- Use **`.js`** for non-JSX files:
|
|
|
+Files:
|
|
|
|
|
|
- - `lib/**` utilities and helpers
|
|
|
- - `app/api/**/route.js`
|
|
|
- - `models/**`
|
|
|
- - tests that do not contain JSX
|
|
|
+- `components/system/NotFoundView.jsx`
|
|
|
+- Protected not-found entry: `app/(protected)/not-found.jsx`
|
|
|
|
|
|
-Note:
|
|
|
+Where NotFound is shown:
|
|
|
|
|
|
-- `components/auth/authContext.jsx` must be `.jsx` because it renders a JSX Provider.
|
|
|
+- invalid params in layouts via `notFound()`
|
|
|
+- admin/dev: non-existing branches via BranchGuard existence validation
|
|
|
|
|
|
---
|
|
|
|
|
|
-## 8. Tests
|
|
|
+## 6. Explorer v2 (RHL-022)
|
|
|
|
|
|
-### 8.1 Unit tests
|
|
|
+### 6.1 UI goal
|
|
|
|
|
|
-Existing (RHL-019 / RHL-020):
|
|
|
+Provide a simple “file explorer” drill-down:
|
|
|
|
|
|
-- `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)
|
|
|
+- Year → Month → Day → Files
|
|
|
|
|
|
-RHL-021 additions:
|
|
|
+### 6.2 Explorer pages
|
|
|
|
|
|
-- `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)
|
|
|
+Routes and components:
|
|
|
|
|
|
-### 8.2 Running tests
|
|
|
+- `/:branch` → `components/explorer/levels/YearsExplorer.jsx`
|
|
|
+- `/:branch/:year` → `components/explorer/levels/MonthsExplorer.jsx`
|
|
|
+- `/:branch/:year/:month` → `components/explorer/levels/DaysExplorer.jsx`
|
|
|
+- `/:branch/:year/:month/:day` → `components/explorer/levels/FilesExplorer.jsx`
|
|
|
|
|
|
-From the repo root:
|
|
|
+### 6.3 Data fetching strategy
|
|
|
|
|
|
-```bash
|
|
|
-npx vitest run
|
|
|
-```
|
|
|
+- All Explorer pages are **Client Components**.
|
|
|
+- All API calls go through `lib/frontend/apiClient.js`.
|
|
|
+- A small hook provides consistent query state:
|
|
|
|
|
|
-Optional build check:
|
|
|
+ - `lib/frontend/hooks/useExplorerQuery.js`
|
|
|
|
|
|
-```bash
|
|
|
-npm run build
|
|
|
-```
|
|
|
-
|
|
|
----
|
|
|
-
|
|
|
-## 9. Manual verification checklist
|
|
|
+Design:
|
|
|
|
|
|
-### 9.1 Local (Docker)
|
|
|
+- predictable states: `loading | success | error`
|
|
|
+- retry mechanism exposed to the UI
|
|
|
+- no routing side effects inside the hook (routing remains in the page components)
|
|
|
|
|
|
-Start:
|
|
|
-
|
|
|
-```bash
|
|
|
-docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build
|
|
|
-```
|
|
|
+### 6.4 Breadcrumbs (with dropdowns)
|
|
|
|
|
|
-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
|
|
|
+Files:
|
|
|
|
|
|
-- Logout
|
|
|
+- UI component:
|
|
|
|
|
|
- - Expect redirect to `/login?reason=logged-out`
|
|
|
+ - `components/explorer/breadcrumbs/ExplorerBreadcrumbs.jsx`
|
|
|
+ - `components/explorer/breadcrumbs/SegmentDropdown.jsx`
|
|
|
|
|
|
-RHL-021 checks:
|
|
|
+- Pure helpers:
|
|
|
|
|
|
-- Branch user:
|
|
|
+ - `lib/frontend/explorer/breadcrumbDropdowns.js`
|
|
|
+ - `lib/frontend/explorer/formatters.js` (German month labels)
|
|
|
+ - `lib/frontend/explorer/sorters.js`
|
|
|
|
|
|
- - `/NL01/...` works (own branch)
|
|
|
- - `/NL02/...` shows Forbidden (UI guard)
|
|
|
- - Invalid params (e.g. `/NL01/abcd`, `/NL01/2024/99/01`) show NotFound
|
|
|
+Rules:
|
|
|
|
|
|
-- Admin/dev:
|
|
|
+- Breadcrumb shows the current path: branch → year → month → day.
|
|
|
+- Dropdowns appear only when options are available:
|
|
|
|
|
|
- - Existing branches render
|
|
|
- - Non-existing branch (e.g. `/NL9999`) shows NotFound (branch existence validation)
|
|
|
+ - years dropdown on month/day/files levels
|
|
|
+ - months dropdown on day/files levels
|
|
|
+ - days dropdown on files level
|
|
|
|
|
|
-> 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.
|
|
|
+Fail-open behavior:
|
|
|
|
|
|
-### 9.2 Server
|
|
|
+- If dropdown option queries fail, the breadcrumb still renders the current path.
|
|
|
|
|
|
-Deploy and verify on the server URL.
|
|
|
+### 6.5 Loading / empty / error states
|
|
|
|
|
|
-Important cookie note:
|
|
|
+Shared Explorer UI building blocks:
|
|
|
|
|
|
-- Browsers reject `Secure` cookies over HTTP.
|
|
|
-- Therefore the server `.env.server` must set:
|
|
|
+- `components/explorer/ExplorerPageShell.jsx`
|
|
|
+- `components/explorer/ExplorerSectionCard.jsx`
|
|
|
+- `components/explorer/states/*`
|
|
|
|
|
|
-```env
|
|
|
-SESSION_COOKIE_SECURE=false
|
|
|
-```
|
|
|
+Error mapping:
|
|
|
|
|
|
-Verify flows on the server URL:
|
|
|
+- `lib/frontend/explorer/errorMapping.js` maps API client errors to UX outcomes:
|
|
|
|
|
|
-- Unauthenticated redirect + `next`
|
|
|
-- Valid login sets cookie and redirects back to `next`
|
|
|
-- Logout clears session and shows `reason=logged-out`
|
|
|
+ - `AUTH_UNAUTHENTICATED` → redirect to login (expired)
|
|
|
+ - `AUTH_FORBIDDEN_BRANCH` → ForbiddenView
|
|
|
+ - `FS_NOT_FOUND` → ExplorerNotFound
|
|
|
+ - other errors → ExplorerError + retry
|
|
|
|
|
|
-RHL-021 checks on server:
|
|
|
+### 6.6 Files list
|
|
|
|
|
|
-- Branch-user forbidden routes show Forbidden UI.
|
|
|
-- Invalid params show NotFound.
|
|
|
-- Admin/dev branch existence validation matches real NAS branch folders.
|
|
|
+- Uses shadcn/ui `Table`.
|
|
|
+- Shows:
|
|
|
|
|
|
----
|
|
|
+ - file name
|
|
|
+ - relative path (desktop column + mobile secondary line)
|
|
|
|
|
|
-## 10. Planned follow-ups
|
|
|
+- Primary action:
|
|
|
|
|
|
-- 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)
|
|
|
+ - “Öffnen” button remains disabled until the PDF endpoint/viewer ticket (RHL-023).
|
|
|
|
|
|
---
|
|
|
|
|
|
-## 11. UI RBAC Guard & Forbidden/NotFound UX (RHL-021)
|
|
|
-
|
|
|
-### 11.1 Goals
|
|
|
+## 7. UI primitives (shadcn/ui)
|
|
|
|
|
|
-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:
|
|
|
+The Explorer + auth UI uses shadcn/ui primitives from `components/ui/*`.
|
|
|
|
|
|
- - Forbidden page for RBAC mismatches
|
|
|
- - Consistent NotFound for invalid params and unknown branches
|
|
|
+Required components for the current scope:
|
|
|
|
|
|
-Backend RBAC remains the source of truth. UI RBAC exists to:
|
|
|
+- `card`
|
|
|
+- `input`
|
|
|
+- `label`
|
|
|
+- `alert`
|
|
|
+- `button`
|
|
|
+- `breadcrumb`
|
|
|
+- `dropdown-menu`
|
|
|
+- `skeleton`
|
|
|
+- `table`
|
|
|
|
|
|
-- prevent “obviously forbidden” navigation in the frontend
|
|
|
-- provide clearer, consistent UX for end users
|
|
|
+---
|
|
|
|
|
|
-### 11.2 BranchGuard (UI-side RBAC)
|
|
|
+## 8. File Naming Convention (.js vs .jsx)
|
|
|
|
|
|
-Files:
|
|
|
+To keep the project consistent:
|
|
|
|
|
|
-- `components/auth/BranchGuard.jsx`
|
|
|
-- Pure logic:
|
|
|
+- Use **`.jsx`** for files that contain JSX:
|
|
|
|
|
|
- - `lib/frontend/rbac/branchAccess.js`
|
|
|
- - `lib/frontend/rbac/branchUiDecision.js`
|
|
|
+ - `app/**/page.jsx`, `app/**/layout.jsx`
|
|
|
+ - React components in `components/**`
|
|
|
|
|
|
-Responsibilities:
|
|
|
+- Use **`.js`** for non-JSX files:
|
|
|
|
|
|
-- Read `user` and `status` from `AuthContext`.
|
|
|
-- Enforce branch rules:
|
|
|
+ - `lib/**` utilities and helpers
|
|
|
+ - `app/api/**/route.js`
|
|
|
+ - `models/**`
|
|
|
+ - tests that do not contain JSX
|
|
|
|
|
|
- - role `branch` → allowed only when `:branch === user.branchId`
|
|
|
- - role `admin` / `dev` → allowed for any branch that exists
|
|
|
+---
|
|
|
|
|
|
-Guard ordering:
|
|
|
+## 9. Tests
|
|
|
|
|
|
-1. **AuthProvider** runs first and ensures we have a valid session (or redirects to login).
|
|
|
-2. **BranchGuard** runs for all routes under `/:branch/...`.
|
|
|
+### 9.1 Unit tests
|
|
|
|
|
|
-### 11.3 Admin/dev branch existence validation
|
|
|
+Core tests:
|
|
|
|
|
|
-Problem:
|
|
|
+- `lib/frontend/routes.test.js`
|
|
|
+- `lib/frontend/apiClient.test.js`
|
|
|
+- `lib/frontend/authRedirect.test.js`
|
|
|
+- `lib/frontend/authMessages.test.js`
|
|
|
|
|
|
-- 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.
|
|
|
+RBAC tests:
|
|
|
|
|
|
-Solution:
|
|
|
+- `lib/frontend/rbac/branchAccess.test.js`
|
|
|
+- `lib/frontend/rbac/branchUiDecision.test.js`
|
|
|
|
|
|
-- For `admin` and `dev` users, BranchGuard validates that the route branch exists by calling:
|
|
|
+Explorer helper tests:
|
|
|
|
|
|
- - `GET /api/branches` via `apiClient.getBranches()`
|
|
|
+- `lib/frontend/explorer/breadcrumbDropdowns.test.js`
|
|
|
+- `lib/frontend/explorer/errorMapping.test.js`
|
|
|
+- `lib/frontend/explorer/formatters.test.js`
|
|
|
+- `lib/frontend/explorer/sorters.test.js`
|
|
|
|
|
|
-Behavior:
|
|
|
+Component SSR smoke test:
|
|
|
|
|
|
-- While the branch list is being fetched, BranchGuard shows a small loader:
|
|
|
+- `components/app-shell/AppShell.test.js`
|
|
|
|
|
|
- - “Validating branch…”
|
|
|
+### 9.2 Running tests
|
|
|
|
|
|
-- If the requested `:branch` is not in the returned list:
|
|
|
+From the repo root:
|
|
|
|
|
|
- - show **NotFound** UX
|
|
|
+```bash
|
|
|
+npx vitest run
|
|
|
+```
|
|
|
|
|
|
-Fail-open policy:
|
|
|
+Optional build check:
|
|
|
|
|
|
-- If fetching the branch list fails (network issues, temporary backend failure):
|
|
|
+```bash
|
|
|
+npm run build
|
|
|
+```
|
|
|
|
|
|
- - 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:
|
|
|
+## 10. Manual verification checklist
|
|
|
|
|
|
-- 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.
|
|
|
+### 10.1 Local (Docker)
|
|
|
|
|
|
-### 11.4 Param validation (year/month/day)
|
|
|
+Start:
|
|
|
|
|
|
-Files:
|
|
|
+```bash
|
|
|
+docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build
|
|
|
+```
|
|
|
|
|
|
-- `lib/frontend/params.js`
|
|
|
-- Layout enforcement (server-side `notFound()`):
|
|
|
+Verify flows in the browser:
|
|
|
|
|
|
- - `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`
|
|
|
+- Open a protected route while logged out (e.g. `/NL01/2025/12`)
|
|
|
|
|
|
-Rules (syntactic validation only):
|
|
|
+ - Expect redirect to `/login?reason=expired&next=/NL01/2025/12`
|
|
|
|
|
|
-- `year`: `YYYY` (4 digits)
|
|
|
-- `month`: `MM` (01–12)
|
|
|
-- `day`: `DD` (01–31)
|
|
|
+- Invalid login
|
|
|
|
|
|
-If a param is invalid:
|
|
|
+ - Expect a German error message (e.g. “Benutzername oder Passwort ist falsch.”)
|
|
|
|
|
|
-- Next’s `notFound()` is triggered immediately
|
|
|
-- The protected NotFound UI is shown
|
|
|
+- Valid login
|
|
|
|
|
|
-Notes:
|
|
|
+ - Expect redirect into the protected route
|
|
|
|
|
|
-- 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.
|
|
|
+- Logout
|
|
|
|
|
|
-### 11.5 Forbidden UX
|
|
|
+ - Expect redirect to `/login?reason=logged-out`
|
|
|
|
|
|
-Files:
|
|
|
+RBAC checks:
|
|
|
|
|
|
-- Reusable UI: `components/system/ForbiddenView.jsx`
|
|
|
-- Optional wrapper route: `app/(protected)/forbidden/page.jsx`
|
|
|
+- Branch user:
|
|
|
|
|
|
-Where Forbidden is shown:
|
|
|
+ - `/NL01/...` works (own branch)
|
|
|
+ - `/NL02/...` shows Forbidden
|
|
|
+ - invalid params (e.g. `/NL01/abcd`, `/NL01/2024/99/01`) show NotFound
|
|
|
|
|
|
-- BranchGuard renders ForbiddenView inline for branch mismatch.
|
|
|
+Explorer checks:
|
|
|
|
|
|
-CTAs:
|
|
|
+- `/:branch` shows years
|
|
|
+- `/:branch/:year` shows months
|
|
|
+- `/:branch/:year/:month` shows days
|
|
|
+- `/:branch/:year/:month/:day` shows files
|
|
|
+- Breadcrumb dropdowns:
|
|
|
|
|
|
-- Branch users: “Go to my branch” (links to `/${user.branchId}`)
|
|
|
-- Admin/dev: “Go to home” (until a branch list/selector is available)
|
|
|
+ - year dropdown exists on month/day/files levels
|
|
|
+ - month dropdown exists on day/files levels
|
|
|
+ - day dropdown exists on files level
|
|
|
|
|
|
-### 11.6 NotFound UX
|
|
|
+### 10.2 Server
|
|
|
|
|
|
-Files:
|
|
|
+Deploy and verify on the server URL.
|
|
|
|
|
|
-- Reusable UI: `components/system/NotFoundView.jsx`
|
|
|
-- Protected not-found entry: `app/(protected)/not-found.jsx`
|
|
|
+Important cookie note:
|
|
|
|
|
|
-Where NotFound is shown:
|
|
|
+- Browsers reject `Secure` cookies over HTTP.
|
|
|
+- Therefore the server `.env.server` must set:
|
|
|
|
|
|
-- Invalid params in layouts via `notFound()`.
|
|
|
-- Admin/dev: non-existing branch codes via BranchGuard existence validation.
|
|
|
+```env
|
|
|
+SESSION_COOKIE_SECURE=false
|
|
|
+```
|
|
|
|
|
|
-### 11.7 Interaction with backend RBAC errors
|
|
|
+Verify flows:
|
|
|
|
|
|
-Even with UI-side RBAC, the backend remains authoritative.
|
|
|
+- Unauthenticated redirect + `next`
|
|
|
+- Valid login sets cookie and redirects back to `next`
|
|
|
+- Logout clears session and shows `reason=logged-out`
|
|
|
|
|
|
-Recommended policy for later UI tickets (Explorer/Search):
|
|
|
+Admin/dev checks:
|
|
|
|
|
|
-- `AUTH_UNAUTHENTICATED`:
|
|
|
+- existing branches render
|
|
|
+- non-existing branch (e.g. `/NL9999`) shows NotFound (existence validation)
|
|
|
|
|
|
- - let the existing session flow handle it (redirect to `/login?reason=expired`)
|
|
|
+---
|
|
|
|
|
|
-- `AUTH_FORBIDDEN_BRANCH`:
|
|
|
+## 11. Planned follow-ups
|
|
|
|
|
|
- - render ForbiddenView for that branch route
|
|
|
+- Search UI and filters (`/:branch/search`).
|
|
|
+- PDF open/view experience (RHL-023).
|
|
|
+- Admin/dev branch selector in the sidebar.
|
|
|
+- Smooth navigation / perceived performance improvements:
|
|
|
|
|
|
-This policy can be implemented as a small helper when Explorer/Search UI begins consuming the navigation endpoints.
|
|
|
+ - reduce skeleton/layout shift
|
|
|
+ - client-side caching / prefetching for Explorer drill-down
|