13 KiB
Business Logic Model — Unit 2: Authentication Pages
Overview
Unit 2 implements three core business logic flows:
- System Initialization — First Owner setup
- User Invitation Completion — New users complete their account
- 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/statusreturnsinitialized: false InitGuardin__root.tsxredirects 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 /Setuppayload:{ 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
sequenceDiagram
participant User
participant SetupPage as SetupPage<br/>(Frontend)
participant Backend as Backend API<br/>POST /Setup
participant LoginPage as LoginPage<br/>(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 InviteCompletePagevalidates 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 emailPOST /Invitation/completepayload:{ token, name, password }— complete invitation- Both endpoints return user data if successful
User Invitation Completion Flow Diagram
sequenceDiagram
participant User
participant InvitePage as InviteCompletePage<br/>(Frontend)
participant ValidateAPI as Backend API<br/>GET /Invitation/validate
participant CompleteAPI as Backend API<br/>POST /Invitation/complete
participant LoginPage as LoginPage<br/>(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) RoleGuardchecksuser.rolefrom 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
beforeLoadhook (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
_authenticatedlayout require authentication (ProtectedRoute guard already enforces this)
Role-Based Access Control Decision Flow Diagram
graph TD
user["Authenticated User<br/>Navigates to Route"]
route["Route Requires Role?"]
check["RoleGuard Checks<br/>user.role from AuthContext"]
match["User Role Matches<br/>Required Role(s)?"]
allow["✓ Access Allowed<br/>Render Page"]
deny["✗ Access Denied<br/>Show Inline Message"]
msg["Message: You do not have<br/>permission to access this page"]
user --> route
route -->|No role required| allow
route -->|Role required<br/>e.g., /users, /settings| check
check --> match
match -->|Yes<br/>Owner or Admin| allow
match -->|No<br/>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.tsxroute, runs before all routes - Calls
GET /Setup/statuson 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
/loginanyway, which reloads the app staleTime: Infinity(cache for entire session)
- Rationale: setup operations redirect back to
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
/setupifinitialized: 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)
- Real-time inline validation — Show errors next to fields as user types (via Zod schema)
- 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)
{
"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 currentuser(for role checks) andaccessToken- RoleGuard reads
user.roleto 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/)
// 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/)
// 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.