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
@@ -1,188 +1,298 @@
# Domain Entities — Unit 2: Authentication Pages
## Overview
## User (from backend, extended in Unit 2)
Unit 2 introduces client-side domain models for authentication guards, system initialization, and invitation completion. Several entities (`User`, `AuthSession`, `SetupStatus`) are inherited from Unit 1; this document defines those that are new or extended.
---
## Inherited from Unit 1 (reference only)
| Entity | Source | Description |
|---|---|---|
| `User` | `src/api/types.ts` | `{ id, name, email, role }` — from `AuthContext` |
| `AuthSession` | `src/contexts/auth-context.ts` | In-memory `{ user, accessToken }` |
| `SetupStatus` | `src/api/types.ts` | `{ initialized: boolean }` — from `GET /Setup/status` |
| `AuthResponse` | `src/api/types.ts` | `{ accessToken, name }` — returned by login/refresh |
---
## New Entities
### UserRole
The application uses three roles, enforced by both backend and frontend guards.
```
UserRole = 'Owner' | 'Admin' | 'User'
### Type Definition
```typescript
interface User {
id: string;
name: string;
email: string;
role: "Owner" | "Admin" | "User";
createdAt: string; // ISO timestamp
}
```
| Role | Description |
|---|---|
| `Owner` | Full access to all routes, including `/settings` and `/cms` |
| `Admin` | Access to `/users`; no access to `/settings` or `/cms` |
| `User` | Access to `/dashboard` and `/profile` only |
### Lifecycle in Unit 2
- **Created during setup**: `POST /Setup` creates first Owner
- **Created during invitation**: `POST /Invitation/complete` creates User
- **Returned by**: Login, Refresh, Setup, Invitation completion
- **Stored in**: AuthContext (in-memory, cleared on logout)
### Unit 2 Attributes Used
- `id` — for authorization checks and API calls
- `name` — displayed in user menu (Unit 3+)
- `email` — shown read-only in InviteCompletePage
- `role` — used by RoleGuard to determine route access
---
### SetupFormData
## SetupStatus
Collected by the `SetupPage` form. Submitted to `POST /Setup`.
### Type Definition
```typescript
interface SetupStatus {
initialized: boolean;
createdAt?: string; // ISO timestamp, present only if initialized = true
}
```
| Field | Type | Constraint |
|---|---|---|
| `name` | `string` | Required; min 1 character |
| `email` | `string` | Required; valid email format |
| `password` | `string` | Required; see BR-U2-0105 |
| `confirmPassword` | `string` | Required; must match `password` (BR-U2-06) |
| `locale` | `'en' \| 'nl'` | Required; user's preferred UI language |
### Lifecycle in Unit 2
- **Fetched by**: InitGuard on app mount
- **Stored in**: React Query cache (`staleTime: Infinity`)
- **Used by**: InitGuard to determine redirect logic
- **Returned by**: `GET /Setup/status`
**Notes**:
- `locale` defaults to the browser's detected language if supported, otherwise `'en'`
- The `locale` preference is applied immediately when changed (live preview), using `i18n.changeLanguage()`
- The backend `POST /Setup` payload includes `name`, `email`, `password``locale` is applied client-side only (stored in localStorage via i18n or browser preference)
### States
- `initialized: false` — system requires setup; redirect to `/setup`
- `initialized: true` — system ready; allow normal flow
---
### InvitationToken
## InvitationToken
Represents the token extracted from the URL query string on the `InviteCompletePage`.
### Type Definition
```typescript
interface InvitationToken {
token: string; // from query param: ?token=xxx
valid: boolean;
email: string; // present only if valid = true
error?: string; // present only if valid = false
}
```
| Field | Type | Description |
|---|---|---|
| `token` | `string` | Raw JWT or opaque token from `?token=xxx` in the URL |
### Lifecycle in Unit 2
- **Source**: URL query param (`/invite/complete?token=xxx`)
- **Validated by**: `GET /Invitation/validate?token=xxx` on page mount
- **Stored in**: Local component state (InviteCompletePage)
- **Used for**: Form submission to `POST /Invitation/complete`
### States
- Token extracted from URL
- Validation in progress (loading state)
- Validation succeeded (`valid: true`, show form)
- Validation failed (`valid: false`, show error)
- Submission in progress
- Submission succeeded (redirect to login)
- Submission failed (show banner error)
---
### InvitationValidation
## PasswordCredential
Returned by `GET /Invitation/validate?token=xxx` (stub in Unit 2; full implementation in Unit 5).
### Type Definition
```typescript
interface PasswordCredential {
password: string;
confirmPassword?: string; // present in setup/invite forms, absent in login
}
```
| Field | Type | Description |
|---|---|---|
| `email` | `string` | The email address the invitation was sent to |
| `name` | `string \| null` | Pre-filled name (optional, may be null) |
| `isValid` | `boolean` | Whether the token is still valid and not yet used |
| `errorCode` | `'EXPIRED' \| 'USED' \| 'NOT_FOUND' \| null` | Error reason when `isValid = false` |
### Validation
- Both fields validated by Zod schema (src/lib/schemas/auth.ts)
- `password`: 8+ chars, 1 uppercase, 1 digit, 1 special char (backend-aligned)
- `confirmPassword`: must equal `password`
### Usage
- **SetupPage**: Both `password` and `confirmPassword` required
- **InviteCompletePage**: Both `password` and `confirmPassword` required
- **LoginPage**: Only `password` (no confirm, already implemented in Unit 1)
---
### InviteCompleteFormData
## FormError
Collected by the `InviteCompletePage` form. Submitted to `POST /Invitation/complete`.
### Type Definition
```typescript
interface FormError {
field?: string; // field name if field-specific
message: string;
code?: string; // backend error code
}
```
| Field | Type | Constraint |
|---|---|---|
| `email` | `string` | Read-only; populated from `InvitationValidation.email` |
| `name` | `string` | Required; min 1 character |
| `password` | `string` | Required; see BR-U2-0105 |
| `confirmPassword` | `string` | Required; must match `password` (BR-U2-06) |
### Lifecycle
- Extracted from backend response or validation failure
- Displayed inline (per field) or in banner (form-level)
- Cleared on field blur (inline) or form reset (banner)
### Examples
- `{ field: "password", message: "At least 8 characters" }` — inline
- `{ message: "Email already in use" }` — banner
- `{ message: "Network error. Please try again." }` — banner
---
### FormBannerError
## RoleGuard Context
Represents an API-level or network-level error surfaced as a dismissible banner above a form (BR-U2-21/22).
### Type Definition
```typescript
interface RoleGuardContext {
userRole: "Owner" | "Admin" | "User" | null;
requiredRole: "Owner" | "Admin" | "User" | ("Owner" | "Admin")[];
hasAccess: boolean;
accessDeniedMessage: string;
}
```
| Field | Type | Description |
|---|---|---|
| `message` | `string` | User-facing error message |
| `type` | `'api' \| 'network'` | Source of the error |
### Lifecycle
- Evaluated on route load (via TanStack Router `beforeLoad`)
- `userRole` read from AuthContext
- `requiredRole` defined per route (hardcoded in route definition)
- `hasAccess` calculated as: `userRole in requiredRole(s)`
- `accessDeniedMessage` shown inline if `hasAccess = false`
### Example Routes
```typescript
// /users — Owner or Admin
{ requiredRole: ["Owner", "Admin"], message: "Owner or Admin access required" }
// /settings — Owner only
{ requiredRole: "Owner", message: "Owner access required" }
// /cms — Owner only
{ requiredRole: "Owner", message: "Owner access required" }
```
---
### TokenValidationState
## AuthContext State (Unit 2 Extends Unit 1)
Represents the loading/success/error lifecycle of the invitation token validation on page mount.
### Extended State
```typescript
interface AuthContextState {
// From Unit 1
user: User | null;
accessToken: string | null;
expiresAt: number | null;
isAuthenticated: boolean;
| State | Description |
|---|---|
| `loading` | Token validation in progress (spinner shown) |
| `valid` | Token is valid; invitation form is shown |
| `invalid` | Token is expired, used, or not found; error message shown |
// New in Unit 2
userRole: "Owner" | "Admin" | "User" | null;
}
```
### Usage in Unit 2
- `useAuth()` returns the state + methods
- `user.role` checked by RoleGuard for access control
- `userRole` derived from `user.role` for convenience
---
## Entity Relationship Diagram
## Language Preference
### Type Definition
```typescript
interface LanguagePreference {
code: "en" | "nl"; // ISO 639-1
label: string; // "English" | "Nederlands"
}
```
### Lifecycle
- Collected in SetupPage form
- Sent to backend in `POST /Setup`
- Stored in user profile (backend)
- Retrieved on login (future: Unit 3+, stored in AuthContext)
- Used by i18next to set app locale
### Available Locales
- **en**: English (fallback, eager load)
- **nl**: Nederlands (lazy load)
---
## API Response Types
### Setup Response
```typescript
interface SetupResponse {
message: string;
user: User; // newly created Owner
}
```
### Invitation Validation Response
```typescript
interface InvitationValidationResponse {
valid: boolean;
email?: string; // present if valid
error?: string; // present if invalid
}
```
### Invitation Completion Response
```typescript
interface InvitationCompletionResponse {
message: string;
user: User; // newly created User
}
```
### Error Response (ProblemDetails)
```typescript
interface ProblemDetails {
type: string; // e.g., "https://example.com/errors/validation-error"
title: string; // e.g., "Bad Request"
status: number; // HTTP status
detail: string; // user-facing message
instance?: string;
extensions?: Record<string, unknown>;
}
```
---
## Relationships & Dependencies
### Entity Relationships
```mermaid
classDiagram
class User {
+string id
+string name
+string email
+UserRole role
}
class UserRole {
<<enumeration>>
Owner
Admin
User
}
class AuthSession {
+User user
+string accessToken
}
class SetupStatus {
+boolean initialized
}
class SetupFormData {
+string name
+string email
+string password
+string confirmPassword
+string locale
}
class InvitationToken {
+string token
}
class InvitationValidation {
+string email
+string name
+boolean isValid
+string errorCode
}
class InviteCompleteFormData {
+string email
+string name
+string password
+string confirmPassword
}
class FormBannerError {
+string message
+string type
}
class TokenValidationState {
<<enumeration>>
loading
valid
invalid
}
User --> UserRole : has
AuthSession --> User : contains
InvitationToken --> InvitationValidation : resolves to
InviteCompleteFormData --> InvitationValidation : pre-filled from
TokenValidationState --> InviteCompleteFormData : gates display of
graph LR
User["User<br/>(created by Setup<br/>or Invitation)"]
Auth["AuthContext<br/>(stores User)"]
Setup["SetupStatus<br/>(session cache)"]
Guard["InitGuard<br/>(checks status)"]
Token["InvitationToken<br/>(email tied)"]
Role["RoleGuard<br/>(reads role)"]
Cred["PasswordCredential<br/>(Zod schema)"]
User -->|stored in| Auth
Auth -->|provides role to| Role
Setup -->|controls| Guard
Token -->|creates| User
Cred -->|validates| User
Role -->|controls access| Auth
classDef entity fill:#4CAF50,stroke:#2E7D32,color:#fff,stroke-width:2px
classDef context fill:#2196F3,stroke:#0D47A1,color:#fff,stroke-width:2px
classDef guard fill:#FF9800,stroke:#E65100,color:#fff,stroke-width:2px
classDef validation fill:#9C27B0,stroke:#4A148C,color:#fff,stroke-width:2px
class User,Setup,Token entity
class Auth,Role context
class Guard guard
class Cred validation
```
Text alternative: `User` has a `UserRole` (Owner/Admin/User); `AuthSession` holds a `User` and `accessToken`; `InvitationToken` resolves to `InvitationValidation` which pre-fills `InviteCompleteFormData`; `TokenValidationState` controls whether the form or error state is shown.
Text alternative: Entity relationship diagram showing User as central entity (green), created by Setup or Invitation, stored in AuthContext (blue). SetupStatus controls InitGuard (orange) for initialization checks. InvitationToken creates User and is tied to email. PasswordCredential (purple) validates User credentials. RoleGuard reads User.role from AuthContext to control route access. Arrows show dependencies and data flow between entities.
### Data Flow
1. App loads → InitGuard fetches SetupStatus
2. If not initialized → redirect to SetupPage
3. User completes setup → creates Owner User, redirects to LoginPage
4. OR: User clicks invitation link → InviteCompletePage validates token
5. User completes invitation → creates User, redirects to LoginPage
6. LoginPage → creates session via AuthContext.login()
7. AuthContext stores User (with role) in memory
8. RoleGuard reads User.role to control route access
---
## Constraint Notes
- All entities align with backend schema (no frontend-only fields)
- Timestamps are ISO 8601 strings (backend provides)
- Role is enum (fixed set: Owner, Admin, User)
- Email is unique key (backend enforces)
- Passwords never stored in frontend state (only `accessToken` in memory)