# Business Logic Model — Unit 2: Authentication Pages ## Overview 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). --- ## System Initialization Flow ### 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 sequenceDiagram participant User participant SetupPage as SetupPage
(Frontend) participant Backend as Backend API
POST /Setup participant LoginPage as LoginPage
(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: 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. --- ## User Invitation Completion Flow ### 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 sequenceDiagram participant User participant InvitePage as InviteCompletePage
(Frontend) participant ValidateAPI as Backend API
GET /Invitation/validate participant CompleteAPI as Backend API
POST /Invitation/complete participant LoginPage as LoginPage
(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: 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. --- ## Role-Based Access Control Flow ### Trigger - Authenticated user navigates to a protected route (e.g., `/users`, `/settings`) - `RoleGuard` checks `user.role` from AuthContext ### 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 ### 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 | ### 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) ### Role-Based Access Control Decision Flow Diagram ```mermaid graph TD user["Authenticated User
Navigates to Route"] route["Route Requires Role?"] check["RoleGuard Checks
user.role from AuthContext"] match["User Role Matches
Required Role(s)?"] allow["✓ Access Allowed
Render Page"] deny["✗ Access Denied
Show Inline Message"] msg["Message: You do not have
permission to access this page"] user --> route route -->|No role required| allow route -->|Role required
e.g., /users, /settings| check check --> match match -->|Yes
Owner or Admin| allow match -->|No
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: 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.