Adds unit 2 functional design and code generation plan and gap 4 report
This commit is contained in:
+195
-85
@@ -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 (A–Z) | `.regex(/[A-Z]/)` |
|
||||
| BR-U2-03 | At least 1 lowercase letter (a–z) | `.regex(/[a-z]/)` |
|
||||
| BR-U2-04 | At least 1 digit (0–9) | `.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, 1–255 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, 1–255 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 2–6)
|
||||
- **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 (1–2 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)
|
||||
|
||||
Reference in New Issue
Block a user