Files
slp-modular-cms/aidlc-docs/features/cms-frontend/construction/unit-2/functional-design/frontend-components.md
T
2026-06-21 00:15:28 +02:00

13 KiB
Raw Blame History

Frontend Components — Unit 2: Authentication Pages

Component Hierarchy

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<br/>UsersPage"]
    AuthLayout --> RoleGuardSettings["RoleGuard Owner<br/>SettingsPage"]
    AuthLayout --> RoleGuardCms["RoleGuard Owner<br/>CmsPage"]
    SetupPage --> useSetup["useSetup<br/>useSetupStatus · useCreateOwner"]
    InviteCompletePage --> useInvitation["useInvitation stub<br/>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-0105)
- 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=<currentPath> (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:
<RoleGuard allowedRoles={['Owner']}>
  <SettingsPage />
</RoleGuard>

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-0105 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-0105 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 <Input type={showPassword ? 'text' : 'password'}>
  • 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/statusSetupStatus. 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=xxxInvitationValidation. 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.