# 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.