# Business Rules — Unit 2: Authentication Pages ## Setup Form Validation Rules ### Field-Level Rules | 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) | ### Cross-Field Rules - `Confirm Password` must equal `Password` (checked on blur and form validation) --- ## Invitation Completion Form Validation Rules ### 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) --- ## Initialization Status Rules ### 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 --- ## Role-Based Access Control Rules ### 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 --- ## Error Handling Rules ### 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: `{error}` - 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 | --- ## Translation Key Rules ### 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) --- ## Password Schema Rules ### 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) --- ## MSW Mock Handler Rules ### 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) --- ## Consistency Rules ### 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 ### 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) ### 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)