frontend-ui.md 15 KB

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), 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-021)

  • Public /login route with a functional login form (shadcn/ui primitives).

  • Protected application shell for all other routes.

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

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

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

Note: Prior to RHL-021, “Full RBAC UI guard” was listed as out of scope. It is now implemented for branch routes.


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.
    • 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
/forbidden Forbidden page Optional wrapper route; UI typically renders Forbidden inline

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

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

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 the current scope:

  • 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.jsx must be .jsx because it renders a JSX Provider.

8. Tests

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:

npx vitest run

Optional build check:

npm run build

9. Manual verification checklist

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

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)

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.

9.2 Server

Deploy and verify on the server URL.

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

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