# Frontend Components — Unit 2: Authentication Pages
## Component Hierarchy
```mermaid
graph TD
root["__root.tsx
(Root Layout)"]
initguard["InitGuard
(wrapper/hook)"]
errorboundary["ErrorBoundary
(from Unit 1)"]
auth["_authenticated.tsx
(Protected Layout)"]
protroute["ProtectedRoute
(from Unit 1)"]
roleguard["RoleGuard
(NEW)"]
setup["SetupPage
NEW /setup"]
invite["InviteCompletePage
NEW /invite/complete"]
login["LoginPage
from Unit 1 /login"]
dashboard["DashboardPage
/dashboard
(all roles)"]
profile["ProfilePage
/profile
(all roles)"]
users["UsersPage
/users
(Owner/Admin)"]
settings["SettingsPage
/settings
(Owner)"]
cms["CmsPage
/cms
(Owner)"]
root --> initguard
root --> errorboundary
errorboundary --> auth
auth --> protroute
auth --> roleguard
root --> setup
root --> invite
root --> login
protroute --> dashboard
protroute --> profile
roleguard --> users
roleguard --> settings
roleguard --> cms
classDef rootComponent fill:#4CAF50,stroke:#2E7D32,color:#fff,stroke-width:2px
classDef guardComponent fill:#FF9800,stroke:#E65100,color:#fff,stroke-width:2px
classDef publicPage fill:#FFC107,stroke:#F57F17,color:#000,stroke-width:2px
classDef protectedPage fill:#2196F3,stroke:#0D47A1,color:#fff,stroke-width:2px
classDef restrictedPage fill:#E91E63,stroke:#880E4F,color:#fff,stroke-width:2px
class root rootComponent
class initguard,errorboundary,protroute,roleguard guardComponent
class setup,invite,login publicPage
class dashboard,profile protectedPage
class users,settings,cms restrictedPage
```
Text alternative: Component hierarchy showing root layout at top, with InitGuard and ErrorBoundary branching down to the _authenticated layout containing ProtectedRoute and RoleGuard. Unprotected routes (Setup, Invite, Login) branch directly from root; protected routes branch from ProtectedRoute (Dashboard, Profile) or RoleGuard (Users, Settings, CMS), with color indicating access level (green=root, orange=guard, yellow=public, blue=protected, pink=owner-only).
---
## Component Specifications
### SetupPage
**Path**: `src/pages/SetupPage.tsx`
**Purpose**: Collect first Owner account credentials and system initialization
**Props**: None (route component)
**State**:
- `formData`: { name, email, password, confirmPassword, language }
- `isLoading`: boolean (during submit)
- `error`: FormError | null (banner error)
**Form Fields**:
- Text input: **Name** (required, 1–255 chars)
- Email input: **Email** (required, valid email)
- Password input: **Password** (required, 8+ chars, upper, digit, special)
- Password input: **Confirm Password** (required, must match password)
- Dropdown select: **Language Preference** (options: "English", "Nederlands")
**Validation**:
- Real-time: Zod schema on blur/change
- Submit: Full form validation, show errors inline + banner
**Submission**:
- POST `/Setup` with form data
- Success: Show success message, redirect to `/login` after delay (2–3 seconds)
- Error: Show banner with backend error message
- Network error: Show "Network error. Please try again."
**Data Attributes**:
- `data-testid="setup-form"` — form element
- `data-testid="setup-name-input"` — name field
- `data-testid="setup-email-input"` — email field
- `data-testid="setup-password-input"` — password field
- `data-testid="setup-confirm-password-input"` — confirm password field
- `data-testid="setup-language-select"` — language dropdown
- `data-testid="setup-submit-button"` — submit button
**Layout**:
- Card container (from Unit 1 shadcn primitives)
- Heading: "Initialize System" (translated: `t('setup.title')`)
- Form fields in vertical stack
- Submit button: "Create Owner Account" (translated: `t('setup.submitButton')`)
- Error banner (if error exists): dismissible, red background
- Success message (after submit): inline, success styling
**API Hooks**:
- Custom hook `useSetup()` — POST /Setup
- Returns: `{ mutate, isLoading, error }`
- Error format: backend ProblemDetails
**i18n Keys**:
- `setup.title`, `setup.nameLabel`, `setup.emailLabel`, `setup.passwordLabel`, `setup.confirmPasswordLabel`, `setup.languageLabel`, `setup.submitButton`, `setup.successMessage`
---
### InviteCompletePage
**Path**: `src/pages/InviteCompletePage.tsx`
**Purpose**: Complete user invitation and create non-Owner user account
**Props**: None (route component; token from URL query param)
**State**:
- `token`: string (from URL query param)
- `invitationEmail`: string | null (from validation)
- `loadingState`: "loading" | "ready" | "error" | "submitting" | "success"
- `formData`: { name, password, confirmPassword }
- `error`: FormError | null (banner error)
**Lifecycle**:
1. Mount: Extract token from URL
2. Validation: GET `/Invitation/validate?token=xxx`
- Show loading state
- If valid: show form with email read-only
- If invalid: show error state with "Request new invitation" link
**Form Fields** (shown only if token valid):
- Text input: **Email** (read-only, pre-filled from validation)
- Text input: **Name** (required, 1–255 chars)
- Password input: **Password** (required, 8+ chars, upper, digit, special)
- Password input: **Confirm Password** (required, must match password)
**Validation**:
- Real-time: Zod schema on blur/change
- Submit: Full form validation, show errors inline + banner
**Submission**:
- POST `/Invitation/complete` with { token, name, password }
- Success: Show success message, redirect to `/login` after delay (2–3 seconds)
- Error: Show banner with backend error message
- Token expired during form fill: Show "Token expired. Request a new invitation."
**Data Attributes**:
- `data-testid="invite-complete-form"` — form element
- `data-testid="invite-email-input"` — email field (read-only)
- `data-testid="invite-name-input"` — name field
- `data-testid="invite-password-input"` — password field
- `data-testid="invite-confirm-password-input"` — confirm password field
- `data-testid="invite-submit-button"` — submit button
- `data-testid="invite-loading-spinner"` — loading indicator
**States & Layout**:
- **Loading**: Spinner + "Validating invitation..." (translated: `t('inviteComplete.loadingMessage')`)
- **Ready** (token valid): Form with all fields visible
- **Error** (token invalid): Full-page error message + link to request new invitation
- **Success**: Success message + redirect message
**Headings & Labels**:
- Heading: "Complete Your Account" (translated: `t('inviteComplete.title')`)
- Email label: "Email" (read-only)
- Form fields, error messages, button text all translated
**API Hooks**:
- Custom hook `useValidateInvitation(token)` — GET /Invitation/validate
- Returns: `{ data: { valid, email }, isLoading, error }`
- Custom hook `useCompleteInvitation()` — POST /Invitation/complete
- Returns: `{ mutate, isLoading, error }`
**i18n Keys**:
- `inviteComplete.title`, `inviteComplete.emailLabel`, `inviteComplete.nameLabel`, `inviteComplete.passwordLabel`, `inviteComplete.confirmPasswordLabel`, `inviteComplete.submitButton`, `inviteComplete.loadingMessage`, `inviteComplete.invalidTokenMessage`, `inviteComplete.requestNewInvitationLink`, `inviteComplete.successMessage`
---
### InitGuard Hook
**Path**: `src/auth/InitGuard.tsx` or `src/contexts/useInitGuard.ts`
**Purpose**: Ensure system initialization before accessing protected routes
**Hook Signature**:
```typescript
function useInitGuard(): {
initialized: boolean | null;
isLoading: boolean;
error: Error | null;
}
```
**Implementation**:
- Runs in `__root.tsx` route `beforeLoad`
- Calls `GET /Setup/status` on app mount
- Caches result for entire session (`staleTime: Infinity`)
- Returns `{ initialized, isLoading, error }`
**Router Integration** (in `src/router.tsx`):
```typescript
export const rootRoute = createRootRoute({
component: () => {
const { initialized, isLoading } = useInitGuard();
if (isLoading) return
{t('errors.accessDenied')}