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