Files

13 KiB

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

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
  • 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

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)
  • 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

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

{
  "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/)

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