Adds unit 2 functional design and code generation plan and gap 4 report

This commit is contained in:
2026-06-21 23:28:45 +02:00
parent ab93a5c7d1
commit f9689d2091
9 changed files with 1567 additions and 1003 deletions
@@ -2,234 +2,334 @@
## Overview
Unit 2 implements five distinct business logic flows. Each is technology-agnostic; implementation details (TanStack Router APIs, React Query, etc.) are resolved in Code Generation.
Unit 2 implements three core business logic flows:
1. **System Initialization** — First Owner setup
2. **User Invitation Completion** — New users complete their account
3. **Role-Based Access Control** — Guard pages by user role
All flows depend on the authentication foundation (Unit 1: AuthContext, ApiClient, MSW).
---
## Flow 1: App Initialization — InitGuard
## System Initialization Flow
**Trigger**: Every page load / app mount (runs in the root route)
**Purpose**: Ensure the system is initialized before rendering any route
### Trigger
- User visits app when `GET /Setup/status` returns `initialized: false`
- `InitGuard` in `__root.tsx` redirects to `/setup` (blocking all other routes)
### SetupPage Form Collects
- **Name** (required string)
- **Email** (required, valid email format)
- **Password** (required, backend rules: min 8 chars, uppercase, digit, special char)
- **Confirm Password** (required, must match password)
- **Language Preference** (dropdown: English / Nederlands — determines i18n locale and stored for future use)
### Business Rules (Setup)
- Password must conform to backend validation rules (enforced via Zod schema matching backend)
- Email must be a valid email format
- Name can be any non-empty string
- Form is only shown when system is not initialized
- Success redirects to `/login` (user must log in after setup to verify email/password)
### Endpoint Integration
- `POST /Setup` payload: `{ name, email, password, language }` (language stored for future use)
- Backend creates first Owner user account
- Backend returns setup status (updated to `initialized: true`)
### System Initialization Flow Diagram
```mermaid
flowchart TD
A([App mounts]) --> B{"Setup status<br/>already cached?"}
B -- Yes --> E
B -- No --> C["Fetch GET /Setup/status"]
C --> D{"Request<br/>outcome"}
D -- Network error --> ERR["Unable to reach server"]
D -- Success --> E{"initialized?"}
E -- false --> F{"On /setup<br/>already?"}
F -- Yes --> G([Render /setup page])
F -- No --> H([Redirect to /setup])
E -- true --> I{"On /setup<br/>already?"}
I -- Yes --> J([Redirect to /login])
I -- No --> K([Continue to route])
classDef start fill:#c7f9e9,stroke:#065f46,stroke-width:2px,color:#1a1a1a
classDef decision fill:#fef3c7,stroke:#b45309,stroke-width:1px,color:#1a1a1a
classDef action fill:#dbeafe,stroke:#1d4ed8,stroke-width:1px,color:#1a1a1a
classDef error fill:#fee2e2,stroke:#b91c1c,stroke-width:1px,color:#1a1a1a
classDef terminal fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px,color:#1a1a1a
class A start
class B,D,E,F,I decision
class C action
class ERR error
class G,H,J,K terminal
sequenceDiagram
participant User
participant SetupPage as SetupPage<br/>(Frontend)
participant Backend as Backend API<br/>POST /Setup
participant LoginPage as LoginPage<br/>(Redirect)
User->>SetupPage: Opens /setup
SetupPage->>SetupPage: Render form (name, email, password, language)
User->>SetupPage: Fill form
User->>SetupPage: Submit
SetupPage->>Backend: POST /Setup {name, email, password, language}
alt Setup Success
Backend-->>SetupPage: 201 {user: Owner, status: initialized}
SetupPage->>SetupPage: Show success message
SetupPage->>LoginPage: Redirect to /login
LoginPage->>User: User logs in to verify credentials
else Setup Fails
Backend-->>SetupPage: 400 {detail: error message}
SetupPage->>SetupPage: Show error banner
User->>SetupPage: Fix and retry
end
```
Text alternative: On app mount, fetch setup status (once per session). If not initialized → redirect to /setup (except when already on /setup). If initialized → allow navigation (redirect away from /setup to /login).
**Caching rule**: The fetch result is held in React state at root level. Once loaded, it never re-fetches within the same session (BR-U2-08).
Text alternative: Sequence diagram showing user opening SetupPage, filling form with name/email/password/language, submitting to backend. On success: backend returns Owner user data and redirect to LoginPage. On failure: backend returns error, shown as banner, user can retry.
---
## Flow 2: Protected Route Access — ProtectedRoute
## User Invitation Completion Flow
**Trigger**: User navigates to any route inside the authenticated layout
**Purpose**: Prevent unauthenticated access to protected pages
### Trigger
- User clicks invitation link: `/invite/complete?token=xxx`
- `InviteCompletePage` validates token on mount (GET request to backend)
### InviteCompletePage Form Collects (if token valid)
- **Email** (read-only, shown from invitation data)
- **Name** (required string)
- **Password** (required, backend rules same as setup)
- **Confirm Password** (required, must match password)
### Business Rules (Invitation)
- Token validation happens on page mount with loading state
- If token invalid/expired: show error state directly (no form)
- If token valid: show form with email pre-filled (read-only)
- Password must conform to backend rules
- Name can be any non-empty string
- Success redirects to `/login` (user logs in to verify account)
### Endpoint Integration
- `GET /Invitation/validate?token=xxx` — validate token and retrieve email
- `POST /Invitation/complete` payload: `{ token, name, password }` — complete invitation
- Both endpoints return user data if successful
### User Invitation Completion Flow Diagram
```mermaid
flowchart TD
A([Route navigation]) --> B{"AuthContext:<br/>user present?"}
B -- Yes --> C([Render requested page])
B -- No --> D{"Restoring<br/>session?"}
D -- Yes --> E([Loading spinner])
E --> B
D -- No --> F["Redirect to /login<br/>with ?redirect=path"]
classDef start fill:#c7f9e9,stroke:#065f46,stroke-width:2px,color:#1a1a1a
classDef decision fill:#fef3c7,stroke:#b45309,stroke-width:1px,color:#1a1a1a
classDef action fill:#dbeafe,stroke:#1d4ed8,stroke-width:1px,color:#1a1a1a
classDef terminal fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px,color:#1a1a1a
classDef loading fill:#e0e7ff,stroke:#4338ca,stroke-width:1px,color:#1a1a1a
class A start
class B,D decision
class F action
class C terminal
class E loading
sequenceDiagram
participant User
participant InvitePage as InviteCompletePage<br/>(Frontend)
participant ValidateAPI as Backend API<br/>GET /Invitation/validate
participant CompleteAPI as Backend API<br/>POST /Invitation/complete
participant LoginPage as LoginPage<br/>(Redirect)
User->>InvitePage: Click invitation link /invite/complete?token=xxx
InvitePage->>InvitePage: Show loading spinner
InvitePage->>ValidateAPI: GET /Invitation/validate?token=xxx
alt Token Valid
ValidateAPI-->>InvitePage: {valid: true, email: user@example.com}
InvitePage->>InvitePage: Show form (email read-only, name, password)
User->>InvitePage: Fill name & password
User->>InvitePage: Submit
InvitePage->>CompleteAPI: POST /Invitation/complete {token, name, password}
alt Completion Success
CompleteAPI-->>InvitePage: 201 {user: User, message: success}
InvitePage->>InvitePage: Show success message
InvitePage->>LoginPage: Redirect to /login
else Completion Fails
CompleteAPI-->>InvitePage: 400 {detail: error}
InvitePage->>InvitePage: Show error banner
User->>InvitePage: Retry
end
else Token Invalid/Expired
ValidateAPI-->>InvitePage: {valid: false, error: Invalid token}
InvitePage->>InvitePage: Show error state (no form)
InvitePage->>User: Offer link to request new invitation
end
```
Text alternative: Check if user is in AuthContext. If restoring session, show spinner. If no user after restore, redirect to /login preserving the original path.
Text alternative: Sequence diagram showing user clicking invitation link, InviteCompletePage validating token with loading state. If valid: form appears with email read-only, user fills name/password and submits. On success: redirects to login. On failure: shows error banner. If token invalid: shows error state with option to request new invitation.
---
## Flow 3: Role-Restricted Route Access — RoleGuard
## Role-Based Access Control Flow
**Trigger**: Authenticated user navigates to a role-restricted route
**Purpose**: Enforce per-route role requirements
### Trigger
- Authenticated user navigates to a protected route (e.g., `/users`, `/settings`)
- `RoleGuard` checks `user.role` from AuthContext
```mermaid
flowchart TD
A([Authenticated navigation]) --> B{"Route has<br/>role restriction?"}
B -- No restriction --> C([Render page])
B -- Owner only --> D{"user.role<br/>= Owner?"}
B -- Owner or Admin --> E{"user.role<br/>= Owner or Admin?"}
D -- Yes --> C
D -- No --> F([Inline Access Denied])
E -- Yes --> C
E -- No --> F
### Role Model
- **Owner** — system administrator, can manage users, system settings, CMS
- **Admin** — (reserved for future use) may have limited permissions
- **User** — standard user, can only view dashboard and own profile
classDef start fill:#c7f9e9,stroke:#065f46,stroke-width:2px,color:#1a1a1a
classDef decision fill:#fef3c7,stroke:#b45309,stroke-width:1px,color:#1a1a1a
classDef terminal fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px,color:#1a1a1a
classDef denied fill:#fee2e2,stroke:#b91c1c,stroke-width:1px,color:#1a1a1a
### Route Access Rules
| Route | Required Role(s) | Behavior if Denied |
|-------|------------------|-------------------|
| `/setup` | None (public) | N/A |
| `/login` | None (public) | N/A |
| `/invite/complete` | None (public, token-authenticated) | N/A |
| `/dashboard` | Any authenticated | N/A |
| `/profile` | Any authenticated | N/A |
| `/users` | Owner or Admin | Show inline "Access Denied" message |
| `/settings` | Owner only | Show inline "Access Denied" message |
| `/cms` | Owner only | Show inline "Access Denied" message |
class A start
class B,D,E decision
class C terminal
class F denied
```
### Business Rules (Role Guard)
- Redirect logic happens in TanStack Router `beforeLoad` hook (not page-level)
- When access denied: show inline "Access Denied" message within the page component (not a separate route)
- Toast notifications are NOT used for access denied (inline message only)
- All routes under `_authenticated` layout require authentication (ProtectedRoute guard already enforces this)
Text alternative: If route has no restriction → render. If Owner-only route → check role, render page or show inline "Access Denied". If Owner/Admin route → same check.
**Inline Access Denied**: Rendered within the normal page shell (sidebar + layout remain visible). The message identifies the required role. No redirect occurs (BR-U2-19).
---
## Flow 4: System Setup — SetupPage
**Trigger**: User visits `/setup` and system is uninitialized (`initialized: false`)
**Purpose**: Create the first Owner account
```mermaid
flowchart TD
A([User opens /setup]) --> B["Render 5-field form<br/>Name · Email · Password<br/>Confirm Password · Locale"]
B --> C{"Locale<br/>changed?"}
C -- Yes --> D["Apply i18n.changeLanguage"]
D --> B
C -- No --> E["User submits form"]
E --> F{"Zod<br/>validation"}
F -- Invalid --> G["Show inline field errors"]
G --> B
F -- Valid --> H["Disable submit · loading"]
H --> I["POST /Setup"]
I --> J{"Response"}
J -- 200/201 --> K["Show success message"]
K --> L([Redirect to /login])
J -- 409 Conflict --> M["Banner: already initialized"]
J -- 4xx --> N["Banner: API error"]
J -- Network error --> O["Banner: Unable to connect"]
M --> B
N --> B
O --> B
classDef start fill:#c7f9e9,stroke:#065f46,stroke-width:2px,color:#1a1a1a
classDef decision fill:#fef3c7,stroke:#b45309,stroke-width:1px,color:#1a1a1a
classDef action fill:#dbeafe,stroke:#1d4ed8,stroke-width:1px,color:#1a1a1a
classDef error fill:#fee2e2,stroke:#b91c1c,stroke-width:1px,color:#1a1a1a
classDef terminal fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px,color:#1a1a1a
classDef success fill:#dcfce7,stroke:#15803d,stroke-width:1px,color:#1a1a1a
class A start
class C,F,J decision
class B,D,E,H,I action
class G,M,N,O error
class K success
class L terminal
```
Text alternative: Render 5-field form → live locale switching → submit → Zod validation → POST /Setup → success banner + redirect to /login, or error banner on API/network failure.
---
## Flow 5: Invitation Completion — InviteCompletePage
**Trigger**: User clicks an invitation link (`/invite/complete?token=xxx`)
**Purpose**: Complete account setup for an invited user
```mermaid
flowchart TD
A([User opens /invite/complete]) --> B{"token in URL?"}
B -- No --> C(["Error: invalid link"])
B -- Yes --> D(["Loading spinner"])
D --> E["GET /Invitation/validate"]
E --> F{"Validation<br/>result"}
F -- Network error --> G(["Error: unable to connect"])
F -- isValid=false --> H(["Error: expired / used / not found"])
F -- isValid=true --> I["Show form<br/>email read-only · Name · Password<br/>Confirm Password"]
I --> J["User submits form"]
J --> K{"Zod<br/>validation"}
K -- Invalid --> L["Inline field errors"]
L --> I
K -- Valid --> M(["Disable submit · loading"])
M --> N["POST /Invitation/complete"]
N --> O{"Response"}
O -- Success --> P["Show success message"]
P --> Q([Redirect to /login])
O -- 4xx --> R["Banner: API error"]
O -- Network error --> S["Banner: Unable to connect"]
R --> I
S --> I
classDef start fill:#c7f9e9,stroke:#065f46,stroke-width:2px,color:#1a1a1a
classDef decision fill:#fef3c7,stroke:#b45309,stroke-width:1px,color:#1a1a1a
classDef action fill:#dbeafe,stroke:#1d4ed8,stroke-width:1px,color:#1a1a1a
classDef error fill:#fee2e2,stroke:#b91c1c,stroke-width:1px,color:#1a1a1a
classDef terminal fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px,color:#1a1a1a
classDef success fill:#dcfce7,stroke:#15803d,stroke-width:1px,color:#1a1a1a
classDef loading fill:#e0e7ff,stroke:#4338ca,stroke-width:1px,color:#1a1a1a
class A start
class B,F,K,O decision
class E,I,J,N action
class C,G,H,R,S error
class P success
class Q terminal
class D,M loading
```
Text alternative: On mount → check token param → validate with API (loading spinner) → invalid token shows error state; valid token shows form → submit → success banner + redirect to /login, or error banner on failure.
---
## Flow Interaction Diagram
The five flows compose within the TanStack Router tree:
### Role-Based Access Control Decision Flow Diagram
```mermaid
graph TD
Root["__root · InitGuard"] --> PublicSetup["/setup · SetupPage"]
Root --> PublicLogin["/login · LoginPage"]
Root --> PublicInvite["/invite/complete · InviteCompletePage"]
Root --> AuthLayout["_authenticated · ProtectedRoute"]
AuthLayout --> Dashboard["/ · Dashboard"]
AuthLayout --> Profile["/profile · ProfilePage"]
AuthLayout --> Users["/users · UsersPage<br/>RoleGuard: Owner or Admin"]
AuthLayout --> Settings["/settings · SettingsPage<br/>RoleGuard: Owner"]
AuthLayout --> CMS["/cms · CmsPage<br/>RoleGuard: Owner"]
classDef guard fill:#fef3c7,stroke:#b45309,stroke-width:2px,color:#1a1a1a
classDef public fill:#dbeafe,stroke:#1d4ed8,stroke-width:1px,color:#1a1a1a
classDef protected fill:#dcfce7,stroke:#15803d,stroke-width:1px,color:#1a1a1a
classDef restricted fill:#fee2e2,stroke:#b91c1c,stroke-width:1px,color:#1a1a1a
class Root guard
class PublicSetup,PublicLogin,PublicInvite public
class Dashboard,Profile protected
class AuthLayout,Users,Settings,CMS restricted
user["Authenticated User<br/>Navigates to Route"]
route["Route Requires Role?"]
check["RoleGuard Checks<br/>user.role from AuthContext"]
match["User Role Matches<br/>Required Role(s)?"]
allow["✓ Access Allowed<br/>Render Page"]
deny["✗ Access Denied<br/>Show Inline Message"]
msg["Message: You do not have<br/>permission to access this page"]
user --> route
route -->|No role required| allow
route -->|Role required<br/>e.g., /users, /settings| check
check --> match
match -->|Yes<br/>Owner or Admin| allow
match -->|No<br/>Insufficient role| deny
deny --> msg
msg --> deny
classDef decision fill:#2196F3,stroke:#0D47A1,color:#fff,stroke-width:2px
classDef allowed fill:#4CAF50,stroke:#2E7D32,color:#fff,stroke-width:2px
classDef denied fill:#F44336,stroke:#C62828,color:#fff,stroke-width:2px
classDef message fill:#FF9800,stroke:#E65100,color:#fff,stroke-width:2px
class route,check,match decision
class allow allowed
class deny denied
class msg message
```
Text alternative: Root route runs InitGuard. Public routes (/setup, /login, /invite/complete) are accessible without authentication. The _authenticated layout wraps all protected routes and runs ProtectedRoute. Role-restricted routes (/users, /settings, /cms) additionally run RoleGuard.
Text alternative: Decision flow diagram for role-based access control. User navigates to route. If route requires no role: access allowed. If role required: RoleGuard checks user.role from AuthContext. If role matches required roles: access allowed and render page. If insufficient role: access denied, show inline message to user.
---
## InitGuard Logic
### Purpose
Ensure system initialization is complete before users access authenticated features.
### Implementation
- Placed in `__root.tsx` route, runs before all routes
- Calls `GET /Setup/status` on app mount (or when AuthContext is ready)
- Caching strategy: **session-level cache** (once loaded, never re-fetch during the session)
- Rationale: setup operations redirect back to `/login` anyway, which reloads the app
- `staleTime: Infinity` (cache for entire session)
### Routes That Bypass InitGuard
- `/setup` — setup page (accessible even if not initialized)
- `/login` — public login (not checked, public route)
- `/invite/complete` — public invitation completion (not checked, token-authenticated)
- All other routes redirect to `/setup` if `initialized: false`
---
## Error Handling Strategy
### Form Submission Errors
- Network errors (e.g., 500, connection failure)
- Backend validation errors (e.g., email already exists, password too weak)
- Token errors (invalid/expired invitation token)
### Error Display Pattern (Combination of A + B)
1. **Real-time inline validation** — Show errors next to fields as user types (via Zod schema)
2. **Form-level banner after submit** — After form submission, show a dismissible error banner at the top of the form with the full error message from the backend
- Consistent with LoginPage pattern (already implemented in Unit 1)
- Example: "Setup failed: Email already in use"
### Specific Error Cases
- **Invalid token on InviteCompletePage mount** — Show full-page error state with action (e.g., "Request a new invitation link")
- **Backend validation errors** — Combine inline (from Zod) + banner (from API response)
- **Network errors** — Banner only: "Network error. Please try again."
---
## Translation (i18n) Structure
All new pages use keys added to existing `src/i18n/locales/{en,nl}/translation.json` files.
### New Translation Keys (extend existing file)
```json
{
"setup": {
"title": "Initialize System",
"nameLabel": "Name",
"emailLabel": "Email",
"passwordLabel": "Password",
"confirmPasswordLabel": "Confirm Password",
"languageLabel": "Language Preference",
"submitButton": "Create Owner Account",
"successMessage": "Account created. Please log in."
},
"inviteComplete": {
"title": "Complete Your Account",
"emailLabel": "Email",
"nameLabel": "Name",
"passwordLabel": "Password",
"confirmPasswordLabel": "Confirm Password",
"submitButton": "Complete Setup",
"loadingMessage": "Validating invitation...",
"invalidTokenMessage": "This invitation link is invalid or has expired.",
"requestNewInvitationLink": "Request a new invitation link",
"successMessage": "Account created. Please log in."
},
"errors": {
"accessDenied": "You do not have permission to access this page.",
"setupRequired": "System setup required. Please initialize the system first.",
"invalidInvitationToken": "Invalid or expired invitation token."
}
}
```
---
## Authentication Context Integration
### AuthContext Usage in Unit 2
- `useAuth()` hook provides current `user` (for role checks) and `accessToken`
- RoleGuard reads `user.role` to determine route access
- SetupPage and InviteCompletePage do NOT call `AuthContext.login()` on success (user must log in manually)
- Login operations still go through LoginPage → `AuthContext.login()` (existing Unit 1 flow)
---
## MSW Mock Handlers
### New Mock Handlers for Unit 2
**Setup Handlers** (`src/mocks/setup/`)
```javascript
// POST /Setup — create first Owner account
// Request: { name, email, password, language }
// Response: { status: 201, message: "System initialized", user: {...} }
// GET /Setup/status — check initialization status
// Response: { initialized: true/false, created_at: ISO timestamp }
```
**Invitation Handlers** (`src/mocks/invitation/`)
```javascript
// GET /Invitation/validate?token=xxx — validate invitation token
// Response: { valid: true, email: "user@example.com" } or { valid: false, error: "Invalid token" }
// POST /Invitation/complete — complete invitation
// Request: { token, name, password }
// Response: { status: 201, user: { id, name, email, role }, message: "Account created" }
```
All handlers align with the real backend API (no extra `/me` endpoint; user data comes from setup/invitation responses).
---
## Password Validation Schema
**Location**: `src/lib/schemas/auth.ts`
The schema is shared by:
- LoginPage (Unit 1 — already exists, password only)
- SetupPage (Unit 2 — new, password + confirm)
- InviteCompletePage (Unit 2 — new, password + confirm)
Backend rules:
- Minimum 8 characters
- At least 1 uppercase letter
- At least 1 digit
- At least 1 special character (!@#$%^&*()-_=+[]{}|;:,.<>?)
Zod schema validates on both fields + cross-field `confirm password` match.