# Frontend Components — Unit 2: Authentication Pages
## Component Hierarchy
```mermaid
graph TD
Root["__root · InitGuard"] --> AuthLayout["_authenticated · ProtectedRoute"]
Root --> LoginPage["LoginPage"]
Root --> SetupPage["SetupPage"]
Root --> InviteCompletePage["InviteCompletePage"]
AuthLayout --> Dashboard["DashboardPage"]
AuthLayout --> ProfilePage["ProfilePage"]
AuthLayout --> RoleGuardUsers["RoleGuard Owner|Admin
UsersPage"]
AuthLayout --> RoleGuardSettings["RoleGuard Owner
SettingsPage"]
AuthLayout --> RoleGuardCms["RoleGuard Owner
CmsPage"]
SetupPage --> useSetup["useSetup
useSetupStatus · useCreateOwner"]
InviteCompletePage --> useInvitation["useInvitation stub
useValidateInvitation · useCompleteSetup"]
SetupPage -.-> FormBanner["FormBannerError"]
SetupPage -.-> PasswordField["PasswordField"]
InviteCompletePage -.-> FormBanner
InviteCompletePage -.-> PasswordField
LoginPage -.-> FormBanner
classDef route fill:#dbeafe,stroke:#1d4ed8,stroke-width:1px,color:#1a1a1a
classDef page fill:#dcfce7,stroke:#15803d,stroke-width:1px,color:#1a1a1a
classDef guard fill:#fef3c7,stroke:#b45309,stroke-width:2px,color:#1a1a1a
classDef shared fill:#f3e8ff,stroke:#7c3aed,stroke-width:1px,color:#1a1a1a
classDef hook fill:#e0e7ff,stroke:#4338ca,stroke-width:1px,color:#1a1a1a
class Root,AuthLayout route
class LoginPage,SetupPage,InviteCompletePage,Dashboard,ProfilePage page
class RoleGuardUsers,RoleGuardSettings,RoleGuardCms guard
class FormBanner,PasswordField shared
class useSetup,useInvitation hook
```
Text alternative: Root route contains InitGuard logic; _authenticated layout wraps protected pages (ProtectedRoute in beforeLoad); public pages (Login, Setup, InviteComplete) are siblings at root level; RoleGuard wraps role-restricted pages as a rendering wrapper.
---
## Shared Schema — `src/lib/schemas/auth.ts`
**Purpose**: Single source of truth for password and auth form validation. Imported by all form pages.
```
Exports:
- passwordSchema Zod schema for a single password field (BR-U2-01–05)
- confirmPasswordSchema Zod object extension with .refine() for password match (BR-U2-06)
- loginSchema email + password (used by LoginPage)
- setupSchema name + email + password + confirmPassword + locale
- inviteCompleteSchema name + password + confirmPassword (email from API, not validated as input)
```
**File location**: `src/lib/schemas/auth.ts`
---
## Component Specifications
### 1. InitGuard (embedded in `__root` route)
Not a standalone component — implemented as logic within the `__root.tsx` route using TanStack Router's `beforeLoad` or as a React effect on mount.
| Aspect | Specification |
|---|---|
| **Trigger** | Runs on every navigation while app is mounted |
| **State** | `setupStatus: SetupStatus \| null`, `isLoadingStatus: boolean` |
| **Fetch** | Calls `useSetupStatus()` on mount; result cached in query cache with `staleTime: Infinity` (session-level caching, BR-U2-08) |
| **Loading state** | While `isLoadingStatus = true`, renders a full-screen loading spinner — no route content shown |
| **Redirect logic** | See business-logic-model.md Flow 1 |
| **API** | `GET /Setup/status` → `{ initialized: boolean }` |
---
### 2. ProtectedRoute (embedded in `_authenticated` layout route)
Implemented as a `beforeLoad` guard in the `_authenticated` TanStack Router layout route.
| Aspect | Specification |
|---|---|
| **Check** | `AuthContext.user !== null` |
| **Loading** | AuthProvider sets `isRestoring: boolean` while attempting silent refresh on mount. Guard waits for `isRestoring = false` before evaluating. |
| **Redirect** | On no user: redirect to `/login?redirect=` (BR-U2-12) |
| **No flash** | Guard blocks rendering of child routes until check resolves (BR-U2-13) |
---
### 3. RoleGuard
A React wrapper component that renders the page or an inline "Access Denied" state.
**Props**:
| Prop | Type | Description |
|---|---|---|
| `allowedRoles` | `UserRole[]` | Roles permitted to see the content |
| `children` | `ReactNode` | The page component to render if role matches |
**State**: None (reads `user.role` from `AuthContext`)
**Render logic**:
- If `user.role` is in `allowedRoles` → render `children`
- Otherwise → render inline `AccessDeniedMessage` (see below)
**Usage**:
```
// In the route component:
```
**AccessDeniedMessage** (inline component, no separate route):
- Heading: "Access Denied"
- Body: "You do not have permission to view this page. This section requires the [Role] role."
- Link: Back to Dashboard
---
### 4. SetupPage (`src/pages/SetupPage.tsx`)
**Purpose**: Collects first Owner account details and submits `POST /Setup`.
**Form fields**:
| Field | Input type | Validation | Notes |
|---|---|---|---|
| `name` | `text` | Required, min 1 char | Full name |
| `email` | `email` | Required, valid email (BR-U2-07) | |
| `password` | `password` | BR-U2-01–05 | PasswordField component (show/hide toggle) |
| `confirmPassword` | `password` | Must match `password` (BR-U2-06) | PasswordField component |
| `locale` | `select` | Required, one of `en \| nl` | Defaults to browser language; changes trigger `i18n.changeLanguage()` immediately |
**State**:
| State | Type | Description |
|---|---|---|
| `bannerError` | `FormBannerError \| null` | API/network error shown above form |
| `isSuccess` | `boolean` | True after successful submission (shows success state) |
**User interaction flow**:
1. Page renders with locale pre-selected based on browser language
2. User fills in fields; inline errors appear on blur (BR-U2-32)
3. User changes locale → immediate language switch (BR-U2-23)
4. On submit: Zod validates all fields; inline errors shown if invalid
5. If valid: submit button disabled + loading spinner; `POST /Setup` called
6. On success: success message displayed; redirect to `/login` after ~1.5s
7. On API error: banner shown; form re-enabled (BR-U2-34)
**API integration**: `useCreateOwner()` mutation from `src/api/useSetup.ts`
**i18n keys** (in `translation.json` under `setup`):
- `setup.title`, `setup.subtitle`
- `setup.fields.name`, `setup.fields.email`, `setup.fields.password`, `setup.fields.confirmPassword`, `setup.fields.locale`
- `setup.submit`, `setup.success`, `setup.errors.*`
---
### 5. InviteCompletePage (`src/pages/InviteCompletePage.tsx`)
**Purpose**: Completes account setup for an invited user via a tokenized URL.
**States / lifecycle**:
| State | UI shown |
|---|---|
| `loading` (token validation in progress) | Full-page loading spinner |
| `valid` (token validated successfully) | Form with email (read-only), name, password, confirmPassword |
| `invalid` (token expired/used/not found) | Error state with reason + link to /login |
| `no-token` (no `?token` in URL) | Error state: "Invalid invitation link" |
| `success` (form submitted successfully) | Success banner; redirect to /login |
**Form fields** (shown only when `state = valid`):
| Field | Input type | Validation | Notes |
|---|---|---|---|
| `email` | `text` | Read-only | Pre-filled from `InvitationValidation.email` |
| `name` | `text` | Required, min 1 char | |
| `password` | `password` | BR-U2-01–05 | PasswordField component |
| `confirmPassword` | `password` | Must match `password` (BR-U2-06) | PasswordField component |
**State**:
| State | Type | Description |
|---|---|---|
| `tokenValidationState` | `TokenValidationState` | `loading \| valid \| invalid` |
| `invitationValidation` | `InvitationValidation \| null` | Set when token is valid |
| `bannerError` | `FormBannerError \| null` | API/network error after form submit |
**On mount logic**:
1. Extract `token` from `useSearch()` (TanStack Router search params)
2. If no `token` → set state to `invalid` immediately (no API call)
3. If `token` present → call `useValidateInvitation(token)`, set state to `loading`
4. On validation success → set state to `valid`, store `InvitationValidation`
5. On validation failure → set state to `invalid` with `errorCode`
**API integration**:
- `useValidateInvitation(token)` → `GET /Invitation/validate?token=xxx` (stub in Unit 2)
- `useCompleteSetup()` → `POST /Invitation/complete` (stub in Unit 2)
**i18n keys** (in `translation.json` under `inviteComplete`):
- `inviteComplete.title`, `inviteComplete.loading`
- `inviteComplete.fields.*`
- `inviteComplete.errors.expired`, `inviteComplete.errors.used`, `inviteComplete.errors.notFound`, `inviteComplete.errors.noToken`
- `inviteComplete.submit`, `inviteComplete.success`
---
### 6. LoginPage (update — `src/pages/LoginPage.tsx`)
**Change from Unit 1**: Add inline field validation on blur (BR-U2-32/33). The API error banner is already in place.
**Specific changes**:
- Enable react-hook-form's `mode: 'onBlur'` (or `mode: 'onTouched'`) instead of submit-only validation
- After first blur, switch to `reValidateMode: 'onChange'` so errors clear immediately when corrected
- No structural changes to the component
---
### 7. Shared Component — PasswordField
A reusable wrapper around shadcn `Input` that adds a show/hide toggle.
**Props**:
| Prop | Type | Description |
|---|---|---|
| `id` | `string` | HTML id for label association |
| `placeholder` | `string` | Input placeholder text |
| `...register` | `UseFormRegisterReturn` | react-hook-form register props spread |
**Behaviour**:
- Internal `showPassword: boolean` state
- Renders ``
- Toggle button uses an eye / eye-off icon (lucide-react)
- `autocomplete` attribute set to `'new-password'` for setup/invite, `'current-password'` for login
**File location**: `src/components/ui/PasswordField.tsx`
---
### 8. Shared Component — FormBannerError
A dismissible alert banner rendered above form fields when an API or network error occurs.
**Props**:
| Prop | Type | Description |
|---|---|---|
| `error` | `FormBannerError \| null` | The error to display; `null` means hidden |
| `onDismiss` | `() => void` | Called when user dismisses the banner |
**Behaviour**:
- Renders nothing when `error = null`
- Uses shadcn `Alert` component with destructive variant
- Dismiss button (×) calls `onDismiss`
- Accessible: `role="alert"` attribute
**File location**: `src/components/ui/FormBannerError.tsx`
---
## API Hooks
### `src/api/useSetup.ts`
| Hook | Type | Description |
|---|---|---|
| `useSetupStatus()` | Query | `GET /Setup/status` → `SetupStatus`. `staleTime: Infinity` (session cache). |
| `useCreateOwner()` | Mutation | `POST /Setup` with `{ name, email, password }`. |
### `src/api/useInvitation.ts` (stub for Unit 2)
| Hook | Type | Description |
|---|---|---|
| `useValidateInvitation(token)` | Query | `GET /Invitation/validate?token=xxx` → `InvitationValidation`. Disabled when `token` is undefined. |
| `useCompleteSetup()` | Mutation | `POST /Invitation/complete` with `{ token, name, password }`. |
These hooks use MSW stubs in Unit 2. Full dynamic backend integration is deferred to Unit 5.
---
## MSW Handlers
### `src/mocks/setup/handlers.ts` (extend existing)
| Handler | Scenario |
|---|---|
| `POST /Setup` — success | Returns `201 Created` |
| `POST /Setup` — already initialized | Returns `409 Conflict` with `ProblemDetails` |
### `src/mocks/invitation/handlers.ts` (new file)
| Handler | Scenario |
|---|---|
| `GET /Invitation/validate?token=valid-token` | Returns `{ isValid: true, email: "test@example.com", name: null }` |
| `GET /Invitation/validate?token=expired-token` | Returns `{ isValid: false, errorCode: "EXPIRED" }` |
| `GET /Invitation/validate?token=used-token` | Returns `{ isValid: false, errorCode: "USED" }` |
| `POST /Invitation/complete` — success | Returns `200 OK` |
| `POST /Invitation/complete` — error | Returns `400 Bad Request` with `ProblemDetails` |
---
## File Location Summary
| File | Location | Status |
|---|---|---|
| Zod schemas | `src/lib/schemas/auth.ts` | New |
| InitGuard logic | `src/__root.tsx` (route) | New |
| ProtectedRoute logic | `src/routes/_authenticated.tsx` (route beforeLoad) | Extends Unit 1 |
| RoleGuard component | `src/components/auth/RoleGuard.tsx` | New |
| SetupPage | `src/pages/SetupPage.tsx` | Replaces Unit 1 stub |
| InviteCompletePage | `src/pages/InviteCompletePage.tsx` | New |
| LoginPage | `src/pages/LoginPage.tsx` | Update (inline validation) |
| PasswordField | `src/components/ui/PasswordField.tsx` | New |
| FormBannerError | `src/components/ui/FormBannerError.tsx` | New |
| useSetup hooks | `src/api/useSetup.ts` | New |
| useInvitation hooks | `src/api/useInvitation.ts` | New (stub) |
| Setup MSW handlers | `src/mocks/setup/handlers.ts` | Extend |
| Invitation MSW handlers | `src/mocks/invitation/handlers.ts` | New |
| EN translations | `src/i18n/locales/en/translation.json` | Extend |
| NL translations | `src/i18n/locales/nl/translation.json` | Extend |
> **Deviation note**: Unit-of-work.md specified `src/routes/` for page files. Per the established deviation from Unit 1, pages live in `src/pages/` and routing is in `src/router.tsx`. Route files (\_\_root.tsx, \_authenticated.tsx) follow TanStack Router conventions in `src/` root or `src/routes/` as needed by the router configuration.