|
|
@@ -8,32 +8,44 @@
|
|
|
|
|
|
<!-- --------------------------------------------------------------------------- -->
|
|
|
|
|
|
-# Frontend UI: App Shell & Routing Scaffold (RHL-019)
|
|
|
+# Frontend UI: App Shell, Routing, and Login Flow (RHL-019 / RHL-020)
|
|
|
|
|
|
This document describes the **frontend routing scaffold** and the **application shell layout** for the RHL Lieferscheine app.
|
|
|
|
|
|
-Scope (RHL-019):
|
|
|
+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**.
|
|
|
|
|
|
-- Public `/login` route.
|
|
|
+---
|
|
|
+
|
|
|
+## 1. Scope
|
|
|
+
|
|
|
+### 1.1 Implemented (as of RHL-020)
|
|
|
+
|
|
|
+- Public `/login` route with a functional login form (shadcn/ui primitives).
|
|
|
- Protected application shell for all other routes.
|
|
|
-- Placeholder pages for branch/year/month/day routes and branch search.
|
|
|
-- Minimal session-awareness placeholders (no real auth guard yet).
|
|
|
-- Minimal tests to validate scaffold stability.
|
|
|
+- Minimal session guard for the protected area:
|
|
|
|
|
|
-Non-goals (out of scope for RHL-019):
|
|
|
+ - checks session via `GET /api/auth/me`
|
|
|
+ - redirects to `/login?reason=expired&next=<original-url>` when unauthenticated
|
|
|
|
|
|
-- Real login form, auth guard, and session-based redirects.
|
|
|
-- Explorer navigation UI (years/months/days list).
|
|
|
+- 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)
|
|
|
+
|
|
|
+- 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).
|
|
|
|
|
|
---
|
|
|
|
|
|
-## 1. Route Groups & URL Structure
|
|
|
+## 2. Route Groups & URL Structure
|
|
|
|
|
|
The app uses Next.js App Router **Route Groups** to separate public and protected UI.
|
|
|
|
|
|
-### 1.1 Route groups
|
|
|
+### 2.1 Route groups
|
|
|
|
|
|
- **Public**: `app/(public)`
|
|
|
|
|
|
@@ -43,19 +55,19 @@ The app uses Next.js App Router **Route Groups** to separate public and protecte
|
|
|
- **Protected**: `app/(protected)`
|
|
|
|
|
|
- Routes that render inside the **AppShell**.
|
|
|
- - RHL-019 intentionally does **not** enforce authentication yet.
|
|
|
+ - As of RHL-020, protected routes are guarded by a session check.
|
|
|
|
|
|
-### 1.2 URL map (scaffold)
|
|
|
+### 2.2 URL map
|
|
|
|
|
|
-| URL | Purpose | Notes |
|
|
|
-| ---------------------------- | --------------------------- | ------------------------------------------------------ |
|
|
|
-| `/login` | Login placeholder | Public layout (no AppShell) |
|
|
|
-| `/` | Protected entry placeholder | Later redirects based on session |
|
|
|
-| `/: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 treated 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` |
|
|
|
|
|
|
Important:
|
|
|
|
|
|
@@ -63,9 +75,9 @@ Important:
|
|
|
|
|
|
---
|
|
|
|
|
|
-## 2. Layouts
|
|
|
+## 3. Layouts
|
|
|
|
|
|
-### 2.1 Root layout
|
|
|
+### 3.1 Root layout
|
|
|
|
|
|
File: `app/layout.jsx`
|
|
|
|
|
|
@@ -75,7 +87,7 @@ Responsibilities:
|
|
|
- Theme provider setup (shadcn/ui + next-themes wrapper).
|
|
|
- Base HTML/body structure.
|
|
|
|
|
|
-### 2.2 Public layout
|
|
|
+### 3.2 Public layout
|
|
|
|
|
|
File: `app/(public)/layout.jsx`
|
|
|
|
|
|
@@ -84,80 +96,163 @@ Responsibilities:
|
|
|
- Minimal centered layout for public routes.
|
|
|
- Intended for `/login` (and potential future public routes).
|
|
|
|
|
|
-### 2.3 Protected layout
|
|
|
+### 3.3 Protected layout (with session guard)
|
|
|
|
|
|
File: `app/(protected)/layout.jsx`
|
|
|
|
|
|
Responsibilities:
|
|
|
|
|
|
- Wraps all protected pages with the **AppShell**.
|
|
|
-- Intentionally contains **no auth guard** in RHL-019.
|
|
|
+- Wraps the auth provider in a **`<Suspense>` 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.
|
|
|
|
|
|
---
|
|
|
|
|
|
-## 3. AppShell
|
|
|
+## 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=<current-url>`
|
|
|
|
|
|
-The AppShell is the stable frame for authenticated UI.
|
|
|
+The `next` parameter:
|
|
|
|
|
|
-Folder: `components/app-shell/*`
|
|
|
+- includes the original `pathname` and query string
|
|
|
+- is sanitized to avoid open redirects (only internal paths are allowed)
|
|
|
|
|
|
-- `AppShell.jsx`
|
|
|
+### 4.2 Login page (reason / next)
|
|
|
|
|
|
- - Overall layout container: **TopNav** + content area.
|
|
|
- - Uses `min-h-screen flex flex-col` so the content area can fill remaining height.
|
|
|
+Files:
|
|
|
|
|
|
-- `TopNav.jsx`
|
|
|
+- `app/(public)/login/page.jsx` (Server Component)
|
|
|
+- `components/auth/LoginForm.jsx` (Client Component)
|
|
|
|
|
|
- - Branding.
|
|
|
- - User status placeholder.
|
|
|
- - Placeholder buttons for theme and logout (not wired yet).
|
|
|
+Flow:
|
|
|
|
|
|
-- `SidebarPlaceholder.jsx`
|
|
|
+1. Login page parses query params using `parseLoginParams(...)`.
|
|
|
+2. If `reason` is present:
|
|
|
|
|
|
- - Reserved space for future navigation/filter UI.
|
|
|
- - Later additions:
|
|
|
+ - `expired` → show “Session expired” banner
|
|
|
+ - `logged-out` → show “Logged out” banner
|
|
|
|
|
|
- - Admin/dev branch selector
|
|
|
- - Explorer navigation (year/month/day)
|
|
|
- - Search filters and shortcuts
|
|
|
+3. On submit, the form calls `apiClient.login({ username, password })`.
|
|
|
+4. On success:
|
|
|
|
|
|
-- `UserStatus.jsx`
|
|
|
+ - redirect to `next` if present
|
|
|
+ - otherwise redirect to `/`
|
|
|
|
|
|
- - Placeholder only.
|
|
|
- - Later reads session state (via `apiClient.getMe()`).
|
|
|
+5. On failure:
|
|
|
|
|
|
-### 3.1 Responsive behavior
|
|
|
+ - show a safe, user-friendly error message
|
|
|
|
|
|
-- Sidebar is hidden on small viewports (`md:block`).
|
|
|
-- Main content remains usable on mobile sizes.
|
|
|
+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 → `<role> (<branchId>)` when available
|
|
|
+ - unauthenticated/error → fallback text
|
|
|
|
|
|
---
|
|
|
|
|
|
-## 4. Placeholder Pages
|
|
|
+## 5. Frontend helper modules
|
|
|
+
|
|
|
+### 5.1 API client
|
|
|
+
|
|
|
+File: `lib/frontend/apiClient.js`
|
|
|
+
|
|
|
+- All UI code must call the backend through this client.
|
|
|
+- Defaults:
|
|
|
|
|
|
-Placeholder pages validate:
|
|
|
+ - `credentials: "include"`
|
|
|
+ - `cache: "no-store"`
|
|
|
|
|
|
-- URL structure
|
|
|
-- Layout composition
|
|
|
-- Dynamic route parameter handling
|
|
|
+- Throws `ApiClientError` for standardized backend errors.
|
|
|
+- RHL-020 uses:
|
|
|
|
|
|
-They render via:
|
|
|
+ - `login({ username, password })`
|
|
|
+ - `logout()`
|
|
|
+ - `getMe()`
|
|
|
|
|
|
-- `components/placeholders/PlaceholderPage.jsx`
|
|
|
+### 5.2 Auth redirect helpers (`reason` / `next`)
|
|
|
|
|
|
-### 4.1 Dynamic route params (Next.js App Router)
|
|
|
+File: `lib/frontend/authRedirect.js`
|
|
|
|
|
|
-In this project setup, dynamic route `params` can behave like an async value.
|
|
|
+Provides:
|
|
|
|
|
|
-Rule of thumb:
|
|
|
+- `sanitizeNext(next)` to prevent open redirects.
|
|
|
+- `buildLoginUrl({ reason, next })`.
|
|
|
+- `parseLoginParams(searchParams)`.
|
|
|
|
|
|
-- In dynamic routes (`[branch]`, `[year]`, `[month]`, `[day]`), unwrap `params` before using properties.
|
|
|
+### 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. File Naming Convention (.js vs .jsx)
|
|
|
+## 6. UI primitives (shadcn/ui)
|
|
|
+
|
|
|
+The login UI uses shadcn/ui primitives from `components/ui/*`.
|
|
|
+
|
|
|
+Required components for RHL-020:
|
|
|
+
|
|
|
+- `card`
|
|
|
+- `input`
|
|
|
+- `label`
|
|
|
+- `alert`
|
|
|
+
|
|
|
+These are added to the repository via the shadcn CLI.
|
|
|
+
|
|
|
+---
|
|
|
|
|
|
-To keep the project consistent and avoid test/tooling issues:
|
|
|
+## 7. File Naming Convention (.js vs .jsx)
|
|
|
+
|
|
|
+To keep the project consistent and avoid tooling issues:
|
|
|
|
|
|
- Use **`.jsx`** for files that contain JSX:
|
|
|
|
|
|
@@ -171,44 +266,23 @@ To keep the project consistent and avoid test/tooling issues:
|
|
|
- `models/**`
|
|
|
- tests that do not contain JSX
|
|
|
|
|
|
----
|
|
|
-
|
|
|
-## 6. Frontend Route Helpers
|
|
|
-
|
|
|
-File: `lib/frontend/routes.js`
|
|
|
-
|
|
|
-Purpose:
|
|
|
-
|
|
|
-- Centralize URL building so UI code does not scatter hardcoded strings.
|
|
|
-- Encode dynamic segments defensively.
|
|
|
-- Keep navigation consistent across components.
|
|
|
-
|
|
|
-Provided helpers:
|
|
|
+Note:
|
|
|
|
|
|
-- `homePath()` → `/`
|
|
|
-- `loginPath()` → `/login`
|
|
|
-- `branchPath(branch)` → `/:branch`
|
|
|
-- `yearPath(branch, year)` → `/:branch/:year`
|
|
|
-- `monthPath(branch, year, month)` → `/:branch/:year/:month`
|
|
|
-- `dayPath(branch, year, month, day)` → `/:branch/:year/:month/:day`
|
|
|
-- `searchPath(branch)` → `/:branch/search`
|
|
|
+- `components/auth/authContext` must be `.jsx` because it renders a JSX Provider.
|
|
|
|
|
|
---
|
|
|
|
|
|
-## 7. Tests
|
|
|
+## 8. Tests
|
|
|
|
|
|
-### 7.1 Unit tests
|
|
|
+### 8.1 Unit tests
|
|
|
|
|
|
-- `lib/frontend/routes.test.js`
|
|
|
+- `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)
|
|
|
|
|
|
- - Validates URL builder behavior.
|
|
|
-
|
|
|
-- `components/app-shell/AppShell.test.js`
|
|
|
-
|
|
|
- - Smoke test: server-renders AppShell and asserts key text is present.
|
|
|
- - `next/link` is mocked to avoid Next runtime dependency.
|
|
|
-
|
|
|
-### 7.2 Running tests
|
|
|
+### 8.2 Running tests
|
|
|
|
|
|
From the repo root:
|
|
|
|
|
|
@@ -216,7 +290,7 @@ From the repo root:
|
|
|
npx vitest run
|
|
|
```
|
|
|
|
|
|
-Optional (recommended) build check:
|
|
|
+Optional build check:
|
|
|
|
|
|
```bash
|
|
|
npm run build
|
|
|
@@ -224,43 +298,61 @@ npm run build
|
|
|
|
|
|
---
|
|
|
|
|
|
-## 8. Manual Verification Checklist
|
|
|
-
|
|
|
-### 8.1 Local (Docker)
|
|
|
+## 9. Manual verification checklist (RHL-020)
|
|
|
|
|
|
-Follow `Docs/runbook.md`.
|
|
|
+### 9.1 Local (Docker)
|
|
|
|
|
|
-Typical command:
|
|
|
+Start:
|
|
|
|
|
|
```bash
|
|
|
docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build
|
|
|
```
|
|
|
|
|
|
-Verify routes:
|
|
|
+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.”
|
|
|
|
|
|
-- `/login` (public layout)
|
|
|
-- `/` (protected app shell)
|
|
|
-- `/:branch` (e.g. `/NL01`)
|
|
|
-- `/:branch/:year/:month/:day` (placeholder)
|
|
|
-- `/:branch/search`
|
|
|
+- Valid login
|
|
|
|
|
|
-### 8.2 Server
|
|
|
+ - Expect redirect into the protected route
|
|
|
|
|
|
-Follow `Docs/runbook.md`.
|
|
|
+- Logout
|
|
|
|
|
|
-Verify:
|
|
|
+ - Expect redirect to `/login?reason=logged-out`
|
|
|
+
|
|
|
+### 9.2 Server (direct HTTP)
|
|
|
+
|
|
|
+The current server deployment is accessed via **direct HTTP**:
|
|
|
+
|
|
|
+- `http://<server-ip>:3000`
|
|
|
+
|
|
|
+Important cookie note:
|
|
|
+
|
|
|
+- Browsers reject `Secure` cookies over HTTP.
|
|
|
+- Therefore the server `.env.server` must set:
|
|
|
+
|
|
|
+```env
|
|
|
+SESSION_COOKIE_SECURE=false
|
|
|
+```
|
|
|
|
|
|
-- `curl -s http://127.0.0.1:3000/api/health`
|
|
|
-- Browser:
|
|
|
+Verify flows on the server URL:
|
|
|
|
|
|
- - `http(s)://<server>:3000/login`
|
|
|
- - `http(s)://<server>:3000/NL01/...` (or a real branch)
|
|
|
+- Unauthenticated redirect + `next`
|
|
|
+- Valid login sets cookie and redirects back to `next`
|
|
|
+- Logout clears session and shows `reason=logged-out`
|
|
|
|
|
|
---
|
|
|
|
|
|
-## 9. Planned Follow-ups
|
|
|
+## 10. Planned follow-ups
|
|
|
|
|
|
-- Add real **auth guard** and session-based redirects (`/` → `/login` or `/:branch`).
|
|
|
-- Replace placeholders with Explorer pages (years/months/days + files).
|
|
|
-- Add Search UI and filters.
|
|
|
-- Add PDF open/view experience.
|
|
|
+- 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
|