frontend-ui.md 9.2 KB

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=<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)

  • 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 <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.

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 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 → <role> (<branchId>) 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:

npx vitest run

Optional build check:

npm run build

9. Manual verification checklist (RHL-020)

9.1 Local (Docker)

Start:

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://<server-ip>:3000

Important cookie note:

  • Browsers reject Secure cookies over HTTP.
  • Therefore the server .env.server must set:

    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