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,124 +1,234 @@
# Business Rules — Unit 2: Authentication Pages
## Password Validation (Shared Schema)
## Setup Form Validation Rules
These rules apply to every password field in the application. The Zod schema lives in `src/lib/schemas/auth.ts` and is imported by `SetupPage`, `InviteCompletePage`, and `LoginPage` (BR-U2-24).
### Field-Level Rules
| ID | Rule | Zod constraint |
|---|---|---|
| BR-U2-01 | Minimum 8 characters | `.min(8)` |
| BR-U2-02 | At least 1 uppercase letter (AZ) | `.regex(/[A-Z]/)` |
| BR-U2-03 | At least 1 lowercase letter (az) | `.regex(/[a-z]/)` |
| BR-U2-04 | At least 1 digit (09) | `.regex(/[0-9]/)` |
| BR-U2-05 | At least 1 non-alphanumeric character (e.g. `!@#$%^&*`) | `.regex(/[^a-zA-Z0-9]/)` |
| Field | Type | Rules | Error Message |
|-------|------|-------|---------------|
| **Name** | string | Required, 1255 chars | "Name is required" |
| **Email** | string | Required, valid email format | "Enter a valid email address" |
| **Password** | string | Zod schema matching backend (8+ chars, 1 uppercase, 1 digit, 1 special) | Backend-specific error (e.g., "Password must contain uppercase") |
| **Confirm Password** | string | Required, must match Password field | "Passwords do not match" |
| **Language** | enum | "en" or "nl" | (dropdown, always valid) |
These rules mirror the backend `IdentityOptions.Password` configuration exactly. Any change to backend password rules must also update this schema.
### Cross-Field Rules
- `Confirm Password` must equal `Password` (checked on blur and form validation)
---
## Confirm Password
## Invitation Completion Form Validation Rules
| ID | Rule |
|---|---|
| BR-U2-06 | `confirmPassword` must be identical to `password`. Validated via Zod `.refine()` at the schema root level — not as an individual field constraint. Error is attached to the `confirmPassword` field. |
### Field-Level Rules
| Field | Type | Rules | Error Message |
|-------|------|-------|---------------|
| **Email** | string | Read-only (from token validation) | N/A |
| **Name** | string | Required, 1255 chars | "Name is required" |
| **Password** | string | Zod schema matching backend | Backend-specific error |
| **Confirm Password** | string | Required, must match Password field | "Passwords do not match" |
### Token Validation Rules
- Token comes from query param (`?token=xxx`)
- Validated on page mount via `GET /Invitation/validate?token=xxx`
- If valid: backend returns `{ valid: true, email: "..." }`
- If invalid/expired: backend returns `{ valid: false, error: "..." }`
- **Loading state** shown while validating
- **Error state** shown if validation fails (no form rendered)
---
## Email Validation
## Initialization Status Rules
| ID | Rule |
|---|---|
| BR-U2-07 | `email` must pass Zod `.email()` (RFC-compliant format). Validated client-side before submission. |
### InitGuard Behavior
| Scenario | Action |
|----------|--------|
| App loads, `GET /Setup/status` returns `{ initialized: true }` | Allow all routes; proceed normally |
| App loads, `GET /Setup/status` returns `{ initialized: false }` | Redirect to `/setup` (InitGuard blocks other routes) |
| User navigates to `/setup` directly | Show setup form (accessible regardless of init status) |
| User navigates to any other route while not initialized | Redirect to `/setup` (InitGuard blocks) |
| User completes setup (`POST /Setup` succeeds) | App redirects to `/login`, session resets, init status re-fetched on next app load |
| Network error while checking init status | Treat as "not initialized" and redirect to `/setup` (fail-safe) |
### Caching Strategy
- `staleTime: Infinity` (session-level cache, never re-fetch after first successful load)
- Rationale: Setup operation ends with redirect to `/login`, which reloads the app
- No manual re-fetching needed during the session
---
## System Initialization Guard (InitGuard)
## Role-Based Access Control Rules
| ID | Rule |
|---|---|
| BR-U2-08 | Setup status (`GET /Setup/status`) is fetched exactly once per browser session. The result is held in React state at the root route level. It is never re-fetched unless the user reloads the page. |
| BR-U2-09 | When `initialized: false`, all routes redirect to `/setup` — including `/login`. The only route that bypasses this redirect is `/setup` itself. |
| BR-U2-10 | When `initialized: true`, navigating to `/setup` redirects to `/login`. The `/setup` route is only accessible when the system is uninitialized. |
### Role Definition
- **Owner**: Created during system initialization (`POST /Setup`)
- **Admin**: Reserved for future use (not used in Unit 26)
- **User**: Created via user invitations (`POST /Invitation/complete`)
### Route Access Rules
| Route | Required Role(s) | Behavior if Denied | Guard Component |
|-------|------------------|--------------------|-----------------|
| `/setup` | None (public, no auth required) | N/A | None |
| `/login` | None (public, no auth required) | N/A | None |
| `/invite/complete` | None (public, token-authenticated) | N/A | None |
| `/dashboard` | Any authenticated | N/A | ProtectedRoute (Unit 1) |
| `/profile` | Any authenticated | N/A | ProtectedRoute (Unit 1) |
| `/users` | Owner **or** Admin | Inline "Access Denied" message | RoleGuard (this unit) |
| `/settings` | Owner only | Inline "Access Denied" message | RoleGuard (this unit) |
| `/cms` | Owner only | Inline "Access Denied" message | RoleGuard (this unit) |
### Access Denied Behavior
- When user lacks required role for a route:
- **DO**: Show inline "Access Denied" message within the page (e.g., in a styled container)
- **DO NOT**: Use toast notifications
- **DO NOT**: Redirect to separate `/403` route
- **DO**: Make it clear the page exists but access is denied
- **Example**: "You do not have permission to access this page. Contact your administrator."
### Guard Implementation
- `RoleGuard` implemented as TanStack Router `beforeLoad` hook in route definitions
- Evaluates `user.role` from AuthContext at route load time
- Returns redirect-to-self if denied (allows inline error display via page component state)
- OR: page component checks role and displays error inline
---
## Authentication Guard (ProtectedRoute)
## Error Handling Rules
| ID | Rule |
|---|---|
| BR-U2-11 | Any route inside the authenticated layout requires a valid `AuthSession` (non-null `user` in `AuthContext`). |
| BR-U2-12 | When there is no authenticated session, the router redirects to `/login`. The originally intended URL is preserved as a `redirect` search parameter (e.g. `/login?redirect=%2Fusers`). |
| BR-U2-13 | No protected page content is rendered, even transiently, before the guard check resolves. |
### Form Submission Error Display
Two-part error handling (combining inline validation + banner):
#### Part 1: Real-Time Inline Validation
- Zod schema validates as user types/blurs on field
- Show error message below the field in red text
- Example: `<span class="text-red-600 text-sm">{error}</span>`
- Clears when user fixes the error
#### Part 2: Form-Level Banner (After Submit)
- After form submission, if backend returns error:
- Show dismissible banner at the top of the form with full error message
- Banner includes close button
- Banner persists until user dismisses or form is reset
- Consistent with LoginPage pattern (Unit 1)
- If network error (no response):
- Banner: "Network error. Please try again."
### Specific Error Scenarios
| Scenario | Banner Message | Inline Errors |
|----------|----------------|---------------|
| Email already exists on setup | "Setup failed: This email is already registered" | None (only from backend) |
| Password too weak | "Setup failed: [backend message about password rules]" | Pre-validated by Zod |
| Network timeout on setup | "Network error. Please try again." | None |
| Invalid invitation token on load | Full-page error (InitGuard redirect to setup OR error page) | N/A |
| Token expired during form fill | Error shown after submit: "Invitation link expired" | N/A |
| Invalid form data before submit | Inline errors only (Zod validation) | Below each field |
---
## Role Guard (RoleGuard)
## Translation Key Rules
| ID | Rule |
|---|---|
| BR-U2-14 | Route `/` (dashboard) — accessible to: `Owner`, `Admin`, `User` |
| BR-U2-15 | Route `/profile` — accessible to: `Owner`, `Admin`, `User` |
| BR-U2-16 | Route `/users` — accessible to: `Owner`, `Admin` |
| BR-U2-17 | Route `/settings` — accessible to: `Owner` only |
| BR-U2-18 | Route `/cms` — accessible to: `Owner` only |
| BR-U2-19 | When a user's role is insufficient for the requested route, the page renders an inline "Access Denied" message within the normal page shell. No redirect to `/403` and no separate route is created in Unit 2. |
| BR-U2-20 | The inline "Access Denied" state must be rendered by `RoleGuard` as a wrapper/HOC, not embedded in individual page components. |
### Locales
- **English** (en) — eager load (fallback language)
- **Nederlands** (nl) — lazy load (per-route chunk)
### Translation Keys Scope
- All Unit 2 keys added to existing `translation.json` files (not separate namespace files)
- Organized by feature: `setup`, `inviteComplete`, `errors` sections
- Used with `i18next` pattern: `t('setup.title')`, `t('errors.accessDenied')`
### Language Preference Storage
- SetupPage collects language preference at account creation
- Stored in backend user profile (for future use)
- Frontend respects this preference on login (future: read from user data)
---
## SetupPage Rules
## Password Schema Rules
| ID | Rule |
|---|---|
| BR-U2-21 | The SetupPage form collects: `name`, `email`, `password`, `confirmPassword`, `locale`. |
| BR-U2-22 | `locale` defaults to the browser's detected language (`navigator.language`), falling back to `'en'` if the detected language is not supported. |
| BR-U2-23 | Changing `locale` immediately applies `i18n.changeLanguage()` so the page re-renders in the selected language as a preview. |
| BR-U2-24 | After a successful `POST /Setup` response, the user is NOT automatically logged in. A success message is shown and the user is redirected to `/login` after a short delay (12 seconds) or immediately on a "Go to login" action. |
| BR-U2-25 | The `POST /Setup` payload contains `{ name, email, password }`. The `locale` field is not sent to the backend. |
### Location
`src/lib/schemas/auth.ts` — shared by LoginPage, SetupPage, InviteCompletePage
### Validation Rules (Backend-Aligned)
```typescript
const passwordSchema = z.string()
.min(8, "At least 8 characters")
.regex(/[A-Z]/, "At least one uppercase letter")
.regex(/[0-9]/, "At least one digit")
.regex(/[!@#$%^&*\(\)\-_=+\[\]{}|;:,.<>?]/, "At least one special character");
const confirmPasswordSchema = z.string()
.min(1, "Confirm password is required");
const setupFormSchema = z.object({
name: z.string().min(1).max(255),
email: z.string().email(),
password: passwordSchema,
confirmPassword: confirmPasswordSchema,
language: z.enum(['en', 'nl'])
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"]
});
```
### Shared Usage
- `passwordSchema` — used by LoginPage, SetupPage, InviteCompletePage
- `confirmPasswordSchema` — used only when form collects both password fields (setup, invite)
---
## InviteCompletePage Rules
## MSW Mock Handler Rules
| ID | Rule |
|---|---|
| BR-U2-26 | On page mount, the token from `?token=xxx` is extracted from the URL and sent to `GET /Invitation/validate?token=xxx`. |
| BR-U2-27 | While the validation request is in flight, a loading spinner is shown and the form is not rendered. |
| BR-U2-28 | If the token is valid, the form is shown with the `email` field pre-filled from the validation response and set to read-only. |
| BR-U2-29 | If the token is invalid or expired, an error state is shown. No form is rendered. The error message explains the reason (expired / already used / not found). A link to `/login` is provided. |
| BR-U2-30 | If no `token` query parameter is present in the URL, this is treated as an invalid token (show error state immediately, no validation request). |
| BR-U2-31 | After a successful `POST /Invitation/complete` response, the user is NOT automatically logged in. A success message is shown and the user is redirected to `/login`. |
### Handler Organization
- **Setup handlers**: Extend existing `src/mocks/setup/` → add `POST /Setup` handler
- **Invitation handlers**: New `src/mocks/invitation/` → add `GET /Invitation/validate` + `POST /Invitation/complete` handlers
### Setup Handler Behaviors
#### `GET /Setup/status`
- Always returns `{ initialized: true/false }`
- Default: `{ initialized: false }` on app first load
- After `POST /Setup` succeeds: automatically return `{ initialized: true }` for subsequent requests
#### `POST /Setup`
- Request: `{ name, email, password, language }`
- Validates email not already used (mock: always accepts if password valid)
- Returns `{ status: 201, message: "System initialized", user: { id, name, email, role: "Owner" } }`
- Error: `{ status: 400, detail: "Email already in use" }` (ProblemDetails format)
### Invitation Handler Behaviors
#### `GET /Invitation/validate?token=xxx`
- Mock tokens: `valid-token-123`, `expired-token-456`
- Valid token: returns `{ valid: true, email: "invited@example.com" }`
- Invalid/expired: returns `{ valid: false, error: "Invalid or expired token" }` (ProblemDetails format)
#### `POST /Invitation/complete`
- Request: `{ token, name, password }`
- Validates token and password
- Returns `{ status: 201, user: { id, name, email, role: "User" }, message: "Account created" }`
- Error: `{ status: 400, detail: "Invalid token" }` (ProblemDetails format)
---
## Form Error Handling (Application-Wide Standard)
## Consistency Rules
These rules define the error handling pattern that applies to ALL forms in the application (SetupPage, InviteCompletePage, LoginPage).
### Form Pattern Consistency
- All forms use `react-hook-form` + `zod` (LoginPage pattern from Unit 1)
- Error display: inline (on blur) + banner (on submit)
- Submission feedback: disable submit button during request
- Success: redirect or success message
| ID | Rule |
|---|---|
| BR-U2-32 | Field validation errors from Zod are shown **inline**, directly below the field. Inline errors are triggered **on blur** (when the user leaves a field), not on every keystroke. |
| BR-U2-33 | Once a field has been touched (blurred), validation re-runs **on change** so the error clears as soon as the user corrects the input. |
| BR-U2-34 | API-level errors (e.g. `400 Bad Request`, `409 Conflict`) returned after form submission are shown in a **dismissible banner** above the form. |
| BR-U2-35 | Network errors (no response received) are shown in the **dismissible banner** with the message: "Unable to connect. Please try again." |
| BR-U2-36 | The banner is dismissed when the user submits the form again or clicks the dismiss button. |
| BR-U2-37 | `LoginPage` (from Unit 1) must be updated in Unit 2 to align with the inline validation pattern (BR-U2-32/33). The banner pattern is already in place. |
### Navigation Consistency
- After setup: redirect to `/login` (user must log in)
- After invitation completion: redirect to `/login` (user must log in)
- After login: redirect to `/dashboard` (existing pattern from Unit 1)
---
## i18n Rules
| ID | Rule |
|---|---|
| BR-U2-38 | All Unit 2 strings (setup, invite complete, error messages, guard messages) are added to the existing `translation.json` files under `setup` and `inviteComplete` namespaces. No separate namespace files are created. |
| BR-U2-39 | The supported locales are `en` and `nl`. All keys must be present in both locale files. |
---
## MSW Handler Rules
| ID | Rule |
|---|---|
| BR-U2-40 | `POST /Setup` is added to the existing `src/mocks/setup/handlers.ts` file. |
| BR-U2-41 | `GET /Invitation/validate` and `POST /Invitation/complete` are added to a new file `src/mocks/invitation/handlers.ts`. |
| BR-U2-42 | MSW handlers for invitation endpoints are stubs: they return hard-coded success/error scenarios sufficient to test Unit 2 UI states. Full dynamic behaviour is implemented in Unit 5. |
### Component API Consistency
- All hooks follow React conventions (use* prefix)
- All components accept standard React props (className, etc.)
- All pages rendered in `src/pages/` directory (consistent with LoginPage)
- All routes defined in `src/router.tsx` (code-based routing)