# 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. 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**. --- ## 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. - Minimal 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. ### 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). --- ## 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**. - As of RHL-020, protected routes are guarded by a session check. ### 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` | 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` - 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`. --- ## 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. --- ## 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` must be `.jsx` because it renders a JSX Provider. --- ## 8. Tests ### 8.1 Unit tests - `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) ### 8.2 Running tests From the repo root: ```bash npx vitest run ``` Optional build check: ```bash npm run build ``` --- ## 9. Manual verification checklist (RHL-020) ### 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` ### 9.2 Server (direct HTTP) The current server deployment is accessed via **direct HTTP**: - `http://:3000` 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` --- ## 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