# 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 ; if (!initialized) { return ; } return ; } }); ``` **Logic**: - If `isLoading`: show spinner - If not initialized: redirect to `/setup` (blocks all other routes) - If initialized: proceed normally **Bypass Routes**: - `/setup` — always accessible (InitGuard not checked) - `/login` — always accessible (public route) - `/invite/complete` — always accessible (token-authenticated, public) **Cache Duration**: Infinite (session-scoped) --- ### RoleGuard Hook/Component **Path**: `src/auth/RoleGuard.tsx` or inline in route `beforeLoad` **Purpose**: Enforce role-based access to protected routes **Implementation**: TanStack Router `beforeLoad` hook in each protected route **Logic**: ```typescript const authenticatedRoute = createRoute({ getParentRoute: () => _authenticatedRoute, path: '/users', beforeLoad: ({ context }) => { const { user } = context; // from AuthContext if (!user || !['Owner', 'Admin'].includes(user.role)) { // Denied — return error or redirect with flag throw new Error('Access denied'); // or set flag in context } }, component: UsersPage }); ``` **Alternative: Page-Level Guard**: - Route allows access, component checks role - If denied: show inline error message instead of redirect - **User's preference (Q10-C)**: Inline message (not separate /403 route) **Routes & Requirements**: | Route | Required Role(s) | If Denied | |-------|------------------|-----------| | `/users` | Owner or Admin | Inline message | | `/settings` | Owner | Inline message | | `/cms` | Owner | Inline message | **Page-Level Implementation** (if using page component): ```typescript function UsersPage() { const { user } = useAuth(); const hasAccess = user?.role === 'Owner' || user?.role === 'Admin'; if (!hasAccess) { return (

{t('errors.accessDenied')}

); } return ; } ``` **Data Attributes**: - `data-testid="access-denied-message"` — error message container **i18n Keys**: - `errors.accessDenied` --- ## Shared Components & Hooks (Unit 2 Creates or Extends) ### useSetup Hook **Path**: `src/api/useSetup.ts` **Purpose**: Handle POST /Setup API call **Signature**: ```typescript function useSetup() { return { mutate: (data: SetupFormData) => Promise, isLoading: boolean, error: ProblemDetails | null }; } ``` **Implementation**: - Uses `api.post('/Setup', data)` - Returns ProblemDetails error if backend rejects --- ### useValidateInvitation Hook **Path**: `src/api/useInvitation.ts` **Purpose**: Validate invitation token **Signature**: ```typescript function useValidateInvitation(token: string) { return { data: { valid: boolean, email?: string } | null, isLoading: boolean, error: ProblemDetails | null }; } ``` **Implementation**: - Calls `api.get('/Invitation/validate', { params: { token } })` - Auto-runs on mount if token provided --- ### useCompleteInvitation Hook **Path**: `src/api/useInvitation.ts` **Purpose**: Complete invitation and create user **Signature**: ```typescript function useCompleteInvitation() { return { mutate: (data: InvitationCompletionData) => Promise, isLoading: boolean, error: ProblemDetails | null }; } ``` **Implementation**: - Uses `api.post('/Invitation/complete', data)` - Returns ProblemDetails error if backend rejects --- ## Error Handling Components ### FormErrorBanner **Path**: `src/components/ui/FormErrorBanner.tsx` **Purpose**: Display dismissible form-level error message **Props**: ```typescript interface FormErrorBannerProps { error: FormError | null; onDismiss: () => void; } ``` **Layout**: - Red/warning background - Error message text - Close (×) button - Fade animation on dismiss **Used by**: SetupPage, InviteCompletePage --- ### FieldError **Path**: `src/components/ui/FieldError.tsx` **Purpose**: Display inline field-level error **Props**: ```typescript interface FieldErrorProps { message?: string; } ``` **Layout**: - Small red text below input - Only shown if message exists **Used by**: All form fields (SetupPage, InviteCompletePage, LoginPage) --- ## Consistency with Unit 1 ### Reused from Unit 1: - shadcn Input, Button, Label, Card, DropdownMenu (from Tailwind v4 + primitives) - LoginPage form pattern (react-hook-form + zod, error handling) - AuthContext and useAuth hook - ApiClient and error handling (ProblemDetails) - MSW mock setup and handlers - i18n (react-i18next with lazy locale loading) - TanStack Router and beforeLoad guards - Test setup and testing utilities ### New in Unit 2: - SetupPage (form + setup flow) - InviteCompletePage (form + invitation flow) - InitGuard (route guard hook) - RoleGuard (route guard hook) - useSetup, useValidateInvitation, useCompleteInvitation (API hooks) - FormErrorBanner component - FieldError component - New i18n keys (setup, inviteComplete, errors sections) - New MSW handlers (setup, invitation) ### Pattern Consistency: - All forms use react-hook-form + zod - All API calls use api client - All errors follow ProblemDetails format - All components use shadcn primitives - All routes defined in src/router.tsx - All pages in src/pages/ - All tests use Vitest + RTL + MSW