Adds unit 2 functional design and code generation plan and gap 4 report
This commit is contained in:
+228
-305
@@ -1,320 +1,243 @@
|
||||
# Code Generation Plan — Unit 2: Authentication Pages
|
||||
|
||||
**Status**: ✅ Complete
|
||||
**Status**: 📋 Ready for approval
|
||||
|
||||
## Unit Context
|
||||
|
||||
- **Unit**: Unit 2 — Authentication Pages
|
||||
- **Application code root**: `frontend/` (Vite + React 19 + TypeScript)
|
||||
- **Depends on**: Unit 1 (ApiClient, AuthContext, router.tsx, shadcn primitives, MSW)
|
||||
- **Stories**: US-01, US-02, US-03, US-04, US-05, US-06, US-07, US-13, US-14
|
||||
**Unit**: Unit 2 — Authentication Pages
|
||||
**Type**: Frontend (React/TypeScript with TanStack Router)
|
||||
**Depends on**: Unit 1 (ApiClient, AuthContext, router.tsx, MSW, shadcn primitives)
|
||||
**Stories Covered**: US-01, US-02, US-03, US-04, US-05, US-06, US-07, US-13, US-14 (9 user stories)
|
||||
|
||||
## Key Architectural Decisions
|
||||
|
||||
- **TanStack Query is NOT installed** — hooks use `useState`/`useEffect` with a module-level session cache
|
||||
- **InitGuard strategy**: `async beforeLoad` on rootRoute + module-level `fetchSetupStatus()` singleton (fetches once, caches for the session); uses TanStack Router's `pendingComponent` for the loading state
|
||||
- **RoleGuard strategy**: React wrapper component rendering inline "Access Denied" (no redirect, no /403 route); applied to route `component` wrappers in router.tsx
|
||||
- **UserRole note**: Backend uses `'Administrator'` not `'Admin'` (confirmed from `src/api/types.ts`)
|
||||
- **Password schema**: Moved from LoginPage inline to `src/lib/schemas/auth.ts` (shared)
|
||||
- **Error handling pattern**: Zod inline field errors on blur (`mode: 'onTouched'`) + dismissible banner for API/network errors
|
||||
|
||||
## Unit 1 Deviations Carried Forward
|
||||
|
||||
- Code-based routing in `src/router.tsx` (not `src/routes/`)
|
||||
- Pages in `src/pages/` (not `src/routes/`)
|
||||
- 4-space indentation throughout
|
||||
|
||||
## Stories Covered
|
||||
|
||||
| Story | Description | Steps |
|
||||
|---|---|---|
|
||||
| US-01 | Login with email and password | 2, 14 |
|
||||
| US-02 | Session persistence via refresh token | (already in Unit 1; confirmed by ProtectedRoute) |
|
||||
| US-03 | Logout | (already in Unit 1; AuthContext.logout()) |
|
||||
| US-04 | Redirect to login when not authenticated | 5, 13 |
|
||||
| US-05 | Auto token refresh on expiry | (already in Unit 1; ApiClient 401 interceptor) |
|
||||
| US-06 | Initialize system as first Owner | 1, 2, 6, 8, 11, 12, 13, 15 |
|
||||
| US-07 | Redirect to setup when not initialized | 6, 13 |
|
||||
| US-13 | Complete account setup via invitation link | 1, 2, 7, 9, 10, 11, 12, 16 |
|
||||
| US-14 | Handle expired/invalid invitation token | 7, 9, 16 |
|
||||
**Key Deliverables**:
|
||||
- SetupPage component (first Owner account creation with language preference)
|
||||
- InviteCompletePage component (user invitation completion with token validation)
|
||||
- InitGuard hook (system initialization status check before route access)
|
||||
- RoleGuard hook (role-based access control for protected routes)
|
||||
- API hooks: useSetup, useValidateInvitation, useCompleteInvitation
|
||||
- Shared password validation schema (src/lib/schemas/auth.ts)
|
||||
- Error handling components (FormErrorBanner, FieldError)
|
||||
- i18n translations (extend translation.json for setup/invite/errors)
|
||||
- MSW mock handlers (extend setupHandlers, new invitationHandlers)
|
||||
- Unit tests for all components, hooks, and API integration
|
||||
|
||||
---
|
||||
|
||||
## Steps
|
||||
## Code Generation Steps
|
||||
|
||||
### Step 1 — Extend `src/api/types.ts`
|
||||
- [x] Add `SetupRequest` interface: `{ name: string; email: string; password: string; }`
|
||||
- [x] Add `InvitationValidation` interface: `{ email: string; name: string | null; isValid: boolean; errorCode: 'EXPIRED' | 'USED' | 'NOT_FOUND' | null; }`
|
||||
- [x] Add `InviteCompleteRequest` interface: `{ token: string; name: string; password: string; }`
|
||||
### Step 1: Create Shared Password Validation Schema
|
||||
- [ ] Create `src/lib/schemas/auth.ts` with:
|
||||
- `passwordSchema` — Zod validation (8+ chars, uppercase, digit, special)
|
||||
- `confirmPasswordSchema` — Confirm password field
|
||||
- Export both for use in SetupPage, InviteCompletePage, LoginPage
|
||||
- [ ] Validate schema with LoginPage password validation (ensure alignment)
|
||||
- [ ] **Story traceability**: US-01 (login), US-06 (setup), US-13 (invite complete)
|
||||
|
||||
### Step 2: Create FormErrorBanner Component
|
||||
- [ ] Create `src/components/ui/FormErrorBanner.tsx`:
|
||||
- Props: `error: FormError | null`, `onDismiss: () => void`
|
||||
- Red background with error message
|
||||
- Dismissible close button
|
||||
- Fade animation on dismiss
|
||||
- [ ] Add `data-testid="form-error-banner"` attribute
|
||||
- [ ] **Story traceability**: US-06, US-13 (error display)
|
||||
|
||||
### Step 3: Create FieldError Component
|
||||
- [ ] Create `src/components/ui/FieldError.tsx`:
|
||||
- Props: `message?: string`
|
||||
- Small red text below input field
|
||||
- Only render if message exists
|
||||
- [ ] Add `data-testid="field-error-[fieldname]"` attributes
|
||||
- [ ] **Story traceability**: US-06, US-13 (field validation)
|
||||
|
||||
### Step 4: Create API Hooks — Setup
|
||||
- [ ] Create `src/api/useSetup.ts`:
|
||||
- `useSetup()` hook for POST /Setup
|
||||
- Returns: `{ mutate, isLoading, error }`
|
||||
- Request: `{ name, email, password, language }`
|
||||
- Response: `SetupResponse` with user data
|
||||
- Error handling: ProblemDetails format
|
||||
- [ ] Add error logging for debugging
|
||||
- [ ] **Story traceability**: US-06 (initialize system)
|
||||
|
||||
### Step 5: Create API Hooks — Invitation
|
||||
- [ ] Create `src/api/useInvitation.ts`:
|
||||
- `useValidateInvitation(token: string)` — GET /Invitation/validate
|
||||
- Auto-runs on mount if token provided
|
||||
- Returns: `{ data, isLoading, error }`
|
||||
- `useCompleteInvitation()` — POST /Invitation/complete
|
||||
- Returns: `{ mutate, isLoading, error }`
|
||||
- Request: `{ token, name, password }`
|
||||
- Response: `InvitationCompletionResponse` with user data
|
||||
- Error handling: ProblemDetails format
|
||||
- [ ] Add loading/error state management
|
||||
- [ ] **Story traceability**: US-13, US-14 (complete invitation)
|
||||
|
||||
### Step 6: Extend Password Schema in LoginPage
|
||||
- [ ] Modify `src/pages/LoginPage.tsx`:
|
||||
- Update to use shared `passwordSchema` from `src/lib/schemas/auth.ts`
|
||||
- Ensure no duplicate definitions
|
||||
- Maintain existing LoginPage functionality
|
||||
- [ ] Run tests to ensure no regressions
|
||||
- [ ] **Story traceability**: US-01 (login)
|
||||
|
||||
### Step 7: Create SetupPage Component
|
||||
- [ ] Create `src/pages/SetupPage.tsx`:
|
||||
- Form fields: name, email, password, confirmPassword, language
|
||||
- Use react-hook-form + Zod (shared passwordSchema)
|
||||
- Form state: `{ isLoading, error }`
|
||||
- Submit: POST /Setup via useSetup hook
|
||||
- Validation: Real-time inline (onBlur) + banner (onSubmit)
|
||||
- Success: Show success message → redirect to /login after 2–3 sec
|
||||
- Error: Show error banner with backend message
|
||||
- [ ] Add data-testid attributes (form, fields, button)
|
||||
- [ ] Use shadcn primitives (Button, Input, Label, Card)
|
||||
- [ ] Use FormErrorBanner + FieldError for error display
|
||||
- [ ] Add i18n keys: `setup.*`
|
||||
- [ ] **Story traceability**: US-06 (initialize system), US-07 (redirect to setup)
|
||||
|
||||
### Step 8: Create InviteCompletePage Component
|
||||
- [ ] Create `src/pages/InviteCompletePage.tsx`:
|
||||
- Extract token from URL query param
|
||||
- On mount: validate token via useValidateInvitation
|
||||
- States: loading → form/error
|
||||
- If valid: Show form (email read-only, name, password, confirmPassword)
|
||||
- If invalid: Show error state with link for new invitation
|
||||
- Submit: POST /Invitation/complete
|
||||
- Validation: Real-time inline + banner
|
||||
- Success: Show success message → redirect to /login
|
||||
- Error: Show error banner
|
||||
- [ ] Add data-testid attributes
|
||||
- [ ] Use shadcn primitives
|
||||
- [ ] Use FormErrorBanner + FieldError
|
||||
- [ ] Add i18n keys: `inviteComplete.*`
|
||||
- [ ] **Story traceability**: US-13, US-14
|
||||
|
||||
### Step 9: Create InitGuard Hook
|
||||
- [ ] Create `src/auth/InitGuard.tsx`:
|
||||
- Hook: `useInitGuard(): { initialized, isLoading, error }`
|
||||
- Calls `GET /Setup/status` on app mount
|
||||
- Caching: `staleTime: Infinity` (session-level)
|
||||
- Integrate into `src/router.tsx` — `__root.tsx` route `beforeLoad`
|
||||
- Bypass routes: `/setup`, `/login`, `/invite/complete`
|
||||
- [ ] **Story traceability**: US-07
|
||||
|
||||
### Step 10: Create RoleGuard Integration
|
||||
- [ ] Implement RoleGuard logic in `src/router.tsx`:
|
||||
- `/users`: Owner or Admin role required
|
||||
- `/settings`: Owner role required
|
||||
- `/cms`: Owner role required
|
||||
- Page-level inline "Access Denied" message if denied
|
||||
- [ ] **Story traceability**: Role-restricted routes
|
||||
|
||||
### Step 11: Extend i18n Translations
|
||||
- [ ] Modify `src/i18n/locales/en/translation.json`:
|
||||
- Add `setup`, `inviteComplete`, extend `errors` sections
|
||||
- [ ] Modify `src/i18n/locales/nl/translation.json`:
|
||||
- Mirror structure in Dutch
|
||||
- [ ] Verify both locales are valid JSON
|
||||
- [ ] **Story traceability**: All UI strings
|
||||
|
||||
### Step 12: Extend MSW Mock Handlers — Setup
|
||||
- [ ] Modify `src/mocks/setup/`:
|
||||
- Extend `GET /Setup/status`
|
||||
- Add `POST /Setup` handler
|
||||
- After POST: mark `initialized: true`
|
||||
- [ ] **Story traceability**: US-06
|
||||
|
||||
### Step 13: Create MSW Mock Handlers — Invitation
|
||||
- [ ] Create `src/mocks/invitation/index.ts`:
|
||||
- Export `invitationHandlers`
|
||||
- `GET /Invitation/validate?token=xxx`
|
||||
- `POST /Invitation/complete`
|
||||
- Add to `src/mocks/browser.ts` and `src/mocks/server.ts`
|
||||
- [ ] **Story traceability**: US-13, US-14
|
||||
|
||||
### Step 14: Update MSW Handler Index
|
||||
- [ ] Modify `src/mocks/index.ts`:
|
||||
- Export `invitationHandlers`
|
||||
|
||||
### Step 15: Create Unit Tests — SetupPage
|
||||
- [ ] Create `src/pages/SetupPage.test.tsx`:
|
||||
- Successful setup flow
|
||||
- Validation errors (inline + banner)
|
||||
- Backend errors
|
||||
- Network error handling
|
||||
- Language selection
|
||||
- [ ] Target: >70% coverage
|
||||
- [ ] **Story traceability**: US-06
|
||||
|
||||
### Step 16: Create Unit Tests — InviteCompletePage
|
||||
- [ ] Create `src/pages/InviteCompletePage.test.tsx`:
|
||||
- Token validation (valid/invalid)
|
||||
- Form fill and submission
|
||||
- Error handling
|
||||
- Read-only email field
|
||||
- [ ] Target: >70% coverage
|
||||
- [ ] **Story traceability**: US-13, US-14
|
||||
|
||||
### Step 17: Create Unit Tests — InitGuard Hook
|
||||
- [ ] Create `src/auth/InitGuard.test.tsx`:
|
||||
- Initialized true/false scenarios
|
||||
- Loading/error states
|
||||
- Session cache behavior
|
||||
- [ ] Target: >80% coverage
|
||||
- [ ] **Story traceability**: US-07
|
||||
|
||||
### Step 18: Create Unit Tests — RoleGuard
|
||||
- [ ] Create tests for RoleGuard logic:
|
||||
- Owner access to restricted routes
|
||||
- Non-Owner denied with inline message
|
||||
- Public routes bypass
|
||||
- [ ] Target: >80% coverage
|
||||
|
||||
### Step 19: Create Unit Tests — API Hooks
|
||||
- [ ] Create `src/api/useSetup.test.ts` and `src/api/useInvitation.test.ts`:
|
||||
- Success, error, loading states
|
||||
- Token validation
|
||||
- [ ] Target: >75% coverage
|
||||
|
||||
### Step 20: Create Integration Tests
|
||||
- [ ] Create `src/test/integration/unit-2-auth-flow.test.tsx`:
|
||||
- Setup flow end-to-end
|
||||
- Invitation flow end-to-end
|
||||
- Role guard integration
|
||||
- 401 refresh + retry
|
||||
- [ ] Target: Happy path coverage
|
||||
|
||||
### Step 21: Update Router Configuration
|
||||
- [ ] Modify `src/router.tsx`:
|
||||
- Add InitGuard to `__root.tsx`
|
||||
- Add `/setup` route (SetupPage)
|
||||
- Add `/invite/complete` route (InviteCompletePage)
|
||||
- Add RoleGuard checks for protected routes
|
||||
- [ ] **Story traceability**: All routing stories
|
||||
|
||||
### Step 22: Update README & Documentation
|
||||
- [ ] Update `frontend/README.md`:
|
||||
- New Unit 2 features section
|
||||
- Authentication flow documentation
|
||||
- Role-based access control
|
||||
- [ ] Add brief JSDoc comments to new hooks/components
|
||||
|
||||
### Step 23: Final Verification
|
||||
- [ ] [ ] `pnpm build` — no errors
|
||||
- [ ] [ ] `pnpm lint` — all checks pass
|
||||
- [ ] [ ] `pnpm format:check` — formatting correct
|
||||
- [ ] [ ] `pnpm test` — all tests pass
|
||||
- [ ] [ ] Dev server boots, test all flows
|
||||
- [ ] [ ] Coverage: auth-related >70%, core >80%
|
||||
- [ ] [ ] All data-testid attributes present
|
||||
|
||||
### Step 24: Commit Changes
|
||||
- [ ] Stage all files
|
||||
- [ ] Create commit with proper message
|
||||
- [ ] Reference stories: US-01–US-07, US-13–US-14
|
||||
|
||||
---
|
||||
|
||||
### Step 2 — Create `src/lib/schemas/auth.ts`
|
||||
- [x] Create file with shared Zod schemas:
|
||||
- `passwordSchema` — `.min(8)` + 4 regex rules (uppercase, lowercase, digit, non-alphanumeric) matching backend `IdentityOptions` (BR-U2-01–05)
|
||||
- `loginSchema` — `{ email: z.string().email(), password: z.string().min(1) }` (extracted from LoginPage — see Step 14)
|
||||
- `setupSchema` — `{ name, email, password, confirmPassword, locale }` with `.superRefine()` for password match (BR-U2-06); `locale` is `z.enum(['en', 'nl'])`
|
||||
- `inviteCompleteSchema` — `{ name, password, confirmPassword }` with password match refine
|
||||
## Total Steps: 24
|
||||
|
||||
---
|
||||
**Estimated Scope**: 600–800 LOC, 4–6 hours, >70% coverage
|
||||
|
||||
### Step 3 — Create `src/components/ui/PasswordField.tsx`
|
||||
- [x] Wrapper around shadcn `Input` with internal `showPassword: boolean` state
|
||||
- [ ] Show/hide toggle button using lucide-react `Eye`/`EyeOff` icons
|
||||
- [ ] Props: `id: string`, `placeholder?: string`, `autoComplete: string`, `...register` (spread from react-hook-form)
|
||||
- [ ] `data-testid` on input: `{id}-input`; on toggle: `{id}-toggle`
|
||||
- [ ] `aria-label` on toggle button for accessibility
|
||||
|
||||
---
|
||||
|
||||
### Step 4 — Create `src/components/ui/FormBannerError.tsx`
|
||||
- [x] Props: `error: { message: string } | null`, `onDismiss: () => void`
|
||||
- [ ] Returns `null` when `error` is `null`
|
||||
- [ ] Uses shadcn Card or a `div` with `role="alert"`, `data-testid="form-error-banner"`, destructive color classes
|
||||
- [ ] Dismiss `×` button calls `onDismiss`
|
||||
|
||||
---
|
||||
|
||||
### Step 5 — Create `src/components/auth/RoleGuard.tsx`
|
||||
- [x] Props: `allowedRoles: UserRole[]`, `children: ReactNode`
|
||||
- [ ] Reads `user` from `useAuth()`
|
||||
- [ ] If `user.role` is in `allowedRoles` → render `children`
|
||||
- [ ] Otherwise → render inline `AccessDeniedMessage` (heading + body naming the required roles + back-to-dashboard link)
|
||||
- [ ] `data-testid="access-denied-message"` on the fallback element
|
||||
|
||||
---
|
||||
|
||||
### Step 6 — Create `src/api/useSetup.ts`
|
||||
- [x] Module-level cache: `let _cachedStatus: SetupStatus | null = null;`
|
||||
- [ ] `useSetupStatus()` hook:
|
||||
- State: `{ status: SetupStatus | null; isLoading: boolean; error: Error | null }`
|
||||
- Fetches `GET /Setup/status` on first call; subsequent calls return `_cachedStatus` synchronously
|
||||
- [ ] `useCreateOwner()` hook — returns `{ mutate, isLoading, error }`:
|
||||
- `mutate(data: SetupRequest): Promise<void>` calls `POST /Setup`
|
||||
- On success: sets `_cachedStatus = { initialized: true }` (updates session cache)
|
||||
- Throws on API/network error (caller handles display)
|
||||
|
||||
---
|
||||
|
||||
### Step 7 — Create `src/api/useInvitation.ts` (stub for Unit 2)
|
||||
- [x] `useValidateInvitation(token: string | undefined)` hook:
|
||||
- Fetches `GET /Invitation/validate?token={token}` when token is defined
|
||||
- State: `{ data: InvitationValidation | null; isLoading: boolean; error: Error | null }`
|
||||
- [ ] `useCompleteSetup()` hook — returns `{ mutate, isLoading, error }`:
|
||||
- `mutate(data: InviteCompleteRequest): Promise<void>` calls `POST /Invitation/complete`
|
||||
- Throws on error
|
||||
|
||||
---
|
||||
|
||||
### Step 8 — Extend `src/mocks/setup/handlers.ts`
|
||||
- [x] Keep existing `GET /Setup/status` handler (returns `{ initialized: true }`)
|
||||
- [ ] Add `POST /Setup` — success scenario: returns `201 Created` with empty body
|
||||
- [ ] Add `POST /Setup` — already-initialized scenario: export as `setupAlreadyInitializedHandlers` (or a named variant) returning `409 Conflict` with ProblemDetails
|
||||
|
||||
---
|
||||
|
||||
### Step 9 — Create `src/mocks/invitation/handlers.ts`
|
||||
- [x] `GET /Invitation/validate?token=valid-token` → `{ isValid: true, email: "invited@example.com", name: null }`
|
||||
- [ ] `GET /Invitation/validate?token=expired-token` → `{ isValid: false, errorCode: "EXPIRED", email: null, name: null }`
|
||||
- [ ] `GET /Invitation/validate?token=used-token` → `{ isValid: false, errorCode: "USED", email: null, name: null }`
|
||||
- [ ] `POST /Invitation/complete` — success: `200 OK`
|
||||
- [ ] Export as `invitationHandlers`
|
||||
|
||||
---
|
||||
|
||||
### Step 10 — Update `src/mocks/index.ts`
|
||||
- [x] Import `invitationHandlers` from `./invitation/handlers`
|
||||
- [ ] Add to the combined `handlers` array
|
||||
- [ ] Add re-export line for `invitationHandlers`
|
||||
|
||||
---
|
||||
|
||||
### Step 11 — Update `src/i18n/locales/en/translation.json`
|
||||
- [x] Add `setup` section:
|
||||
```json
|
||||
"setup": {
|
||||
"title": "System Setup",
|
||||
"subtitle": "Create the first Owner account to get started.",
|
||||
"fields": {
|
||||
"name": "Full name",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"confirmPassword": "Confirm password",
|
||||
"locale": "Language"
|
||||
},
|
||||
"localeOptions": { "en": "English", "nl": "Dutch" },
|
||||
"submit": "Create account",
|
||||
"submitting": "Creating account…",
|
||||
"success": "Account created. You can now sign in.",
|
||||
"errors": {
|
||||
"alreadyInitialized": "This system has already been set up.",
|
||||
"generic": "Something went wrong. Please try again."
|
||||
}
|
||||
}
|
||||
```
|
||||
- [ ] Add `inviteComplete` section:
|
||||
```json
|
||||
"inviteComplete": {
|
||||
"title": "Complete your account",
|
||||
"subtitle": "You have been invited to {{appName}}.",
|
||||
"loading": "Validating your invitation…",
|
||||
"fields": {
|
||||
"email": "Email",
|
||||
"name": "Full name",
|
||||
"password": "Password",
|
||||
"confirmPassword": "Confirm password"
|
||||
},
|
||||
"submit": "Complete setup",
|
||||
"submitting": "Completing setup…",
|
||||
"success": "Account setup complete. You can now sign in.",
|
||||
"errors": {
|
||||
"expired": "This invitation link has expired. Please request a new one.",
|
||||
"used": "This invitation link has already been used.",
|
||||
"notFound": "This invitation link is not valid.",
|
||||
"noToken": "Invalid invitation link.",
|
||||
"generic": "Something went wrong. Please try again."
|
||||
}
|
||||
}
|
||||
```
|
||||
- [ ] Add to `nav`: `"profile": "Profile"`, `"settings": "Settings"`
|
||||
- [ ] Add to `errors`: `"accessDenied": "You do not have permission to view this page."`, `"sessionExpired": "Your session has expired. Please sign in again."`
|
||||
|
||||
---
|
||||
|
||||
### Step 12 — Update `src/i18n/locales/nl/translation.json`
|
||||
- [x] Mirror all keys from Step 11 with Dutch translations:
|
||||
- `setup.title`: "Systeeminstallatie"
|
||||
- `setup.subtitle`: "Maak het eerste Owner-account aan om te beginnen."
|
||||
- `setup.fields.name`: "Volledige naam" / `email`: "E-mail" / `password`: "Wachtwoord" / `confirmPassword`: "Wachtwoord bevestigen" / `locale`: "Taal"
|
||||
- `setup.localeOptions`: `{ "en": "Engels", "nl": "Nederlands" }`
|
||||
- `setup.submit`: "Account aanmaken" / `submitting`: "Account aanmaken…" / `success`: "Account aangemaakt. Je kunt nu inloggen."
|
||||
- `inviteComplete.title`: "Account voltooien" / `subtitle`: "Je bent uitgenodigd voor {{appName}}." / `loading`: "Uitnodiging valideren…"
|
||||
- `inviteComplete.submit`: "Setup voltooien" / `success`: "Account-setup voltooid. Je kunt nu inloggen."
|
||||
- All error messages in Dutch
|
||||
- `nav.profile`: "Profiel" / `nav.settings`: "Instellingen"
|
||||
- `errors.accessDenied`: "Je hebt geen toestemming om deze pagina te bekijken."
|
||||
- `errors.sessionExpired`: "Je sessie is verlopen. Meld je opnieuw aan."
|
||||
|
||||
---
|
||||
|
||||
### Step 13 — Update `src/router.tsx`
|
||||
- [x] Add module-level setup status cache and fetch function above `rootRoute`:
|
||||
```ts
|
||||
let _setupStatusPromise: Promise<SetupStatus> | null = null;
|
||||
let _setupStatusCache: SetupStatus | null = null;
|
||||
|
||||
async function fetchSetupStatus(): Promise<SetupStatus> {
|
||||
if (_setupStatusCache !== null) return _setupStatusCache;
|
||||
if (_setupStatusPromise === null) {
|
||||
_setupStatusPromise = apiClient
|
||||
.get<SetupStatus>('/Setup/status')
|
||||
.then((s) => { _setupStatusCache = s; return s; });
|
||||
}
|
||||
return _setupStatusPromise;
|
||||
}
|
||||
```
|
||||
- [ ] Update `rootRoute` with `async beforeLoad` (InitGuard logic) and `pendingComponent`:
|
||||
- On fetch success: if `!initialized && pathname !== '/setup'` → `throw redirect({ to: '/setup' })`
|
||||
- On fetch success: if `initialized && pathname === '/setup'` → `throw redirect({ to: '/login' })`
|
||||
- On fetch error: log warning, allow navigation (API calls will also fail)
|
||||
- `pendingComponent: BootstrapSplash` (reuse existing component from main.tsx — import or duplicate inline)
|
||||
- `pendingMs: 0` (show spinner immediately)
|
||||
- [ ] Add `/invite/complete` route (public, under rootRoute):
|
||||
- `path: '/invite/complete'`
|
||||
- `validateSearch`: extract `token?: string`
|
||||
- `component`: lazy `InviteCompletePage`
|
||||
- [ ] Wrap `usersRoute` component with `RoleGuard` (allowedRoles: `['Owner', 'Administrator']`)
|
||||
- [ ] Wrap `cmsRoute` component with `RoleGuard` (allowedRoles: `['Owner']`)
|
||||
- [ ] Add `inviteCompleteRoute` to `routeTree` (alongside `setupRoute` and `loginRoute`)
|
||||
- [ ] Import `RoleGuard` from `@/components/auth/RoleGuard`
|
||||
- [ ] Import `apiClient` from `@/lib/api-client`
|
||||
|
||||
---
|
||||
|
||||
### Step 14 — Update `src/pages/LoginPage.tsx`
|
||||
- [x] Remove inline `schema` definition; import `loginSchema` from `@/lib/schemas/auth`
|
||||
- [ ] Change `useForm` mode to `mode: 'onTouched'` (inline validation after first blur; re-validates on change)
|
||||
- [ ] Keep `reValidateMode: 'onChange'` (default) so errors clear as user types after the first blur
|
||||
- [ ] No other structural changes; existing banner, submit button, and testids stay
|
||||
|
||||
---
|
||||
|
||||
### Step 15 — Replace `src/pages/SetupPage.tsx`
|
||||
- [x] Full implementation replacing the "Coming soon" stub
|
||||
- [ ] Import `setupSchema` from `@/lib/schemas/auth`
|
||||
- [ ] Import `PasswordField` and `FormBannerError`
|
||||
- [ ] Import `useCreateOwner` from `@/api/useSetup`
|
||||
- [ ] Import `useTranslation` and `i18n` for locale live-switch
|
||||
- [ ] Form fields: name, email, password (PasswordField), confirmPassword (PasswordField), locale (select)
|
||||
- [ ] Locale `select` defaults to `i18n.language` if supported (`'en'` | `'nl'`), falls back to `'en'`
|
||||
- [ ] `onChange` on locale select calls `i18n.changeLanguage(value)` immediately (BR-U2-23)
|
||||
- [ ] Form mode: `onTouched` (BR-U2-32/33)
|
||||
- [ ] On submit: call `useCreateOwner().mutate(...)`, handle success/error
|
||||
- [ ] On success: show `t('setup.success')` in a success alert, then `navigate({ to: '/login', replace: true })` after 1500ms
|
||||
- [ ] On error: set banner error with API message or network fallback
|
||||
- [ ] `data-testid` attributes: `setup-name-input`, `setup-email-input`, `setup-locale-select`, `setup-submit-button`, `setup-success`, `setup-error-banner`
|
||||
- [ ] `autoComplete="off"` on name; `"email"` on email; `"new-password"` on password fields
|
||||
|
||||
---
|
||||
|
||||
### Step 16 — Create `src/pages/InviteCompletePage.tsx`
|
||||
- [x] Import `inviteCompleteSchema` from `@/lib/schemas/auth`
|
||||
- [ ] Import `PasswordField` and `FormBannerError`
|
||||
- [ ] Import `useValidateInvitation`, `useCompleteSetup` from `@/api/useInvitation`
|
||||
- [ ] Extract `token` from search params using `useSearch({ strict: false })`
|
||||
- [ ] Call `useValidateInvitation(token)` on mount; show loading spinner while `isLoading`
|
||||
- [ ] If `!token` or `data.isValid === false`: show error state with `data.errorCode` mapped to i18n key + link to `/login`
|
||||
- [ ] If `data.isValid === true`: render form with `email` pre-filled and read-only
|
||||
- [ ] `name` field pre-filled from `data.name` if not null
|
||||
- [ ] Form mode: `onTouched`
|
||||
- [ ] On submit: call `useCompleteSetup().mutate(...)`, handle success/error
|
||||
- [ ] On success: show `t('inviteComplete.success')`, navigate to `/login` after 1500ms
|
||||
- [ ] `data-testid` attributes: `invite-loading`, `invite-error`, `invite-name-input`, `invite-email-input`, `invite-submit-button`, `invite-success`
|
||||
|
||||
---
|
||||
|
||||
### Step 17 — Create `src/pages/SetupPage.test.tsx`
|
||||
- [x] Render SetupPage with MSW `setupHandlers` + `POST /Setup` success handler
|
||||
- [ ] Test: all 5 fields render
|
||||
- [ ] Test: submit with empty fields shows inline errors
|
||||
- [ ] Test: weak password shows inline error
|
||||
- [ ] Test: mismatched confirm password shows inline error
|
||||
- [ ] Test: valid submission shows success message
|
||||
- [ ] Test: 409 response shows "already initialized" banner
|
||||
- [ ] Test: locale change updates visible text language
|
||||
|
||||
---
|
||||
|
||||
### Step 18 — Create `src/pages/InviteCompletePage.test.tsx`
|
||||
- [x] Render InviteCompletePage with MSW `invitationHandlers`
|
||||
- [ ] Test: loading state shown on mount
|
||||
- [ ] Test: invalid token (`?token=expired-token`) shows error state, no form
|
||||
- [ ] Test: missing token shows error state immediately
|
||||
- [ ] Test: valid token (`?token=valid-token`) shows form with email read-only
|
||||
- [ ] Test: successful submission shows success message
|
||||
|
||||
---
|
||||
|
||||
### Step 19 — Update `src/test/RouteGuard.test.tsx`
|
||||
- [x] Add test: when `initialized: false` MSW returns `GET /Setup/status` with `{ initialized: false }` → navigating to `/login` or `/dashboard` redirects to `/setup`
|
||||
- [ ] Add test: when `initialized: true` and visiting `/setup` → redirects to `/login`
|
||||
- [ ] Add test: `RoleGuard` with `allowedRoles: ['Owner']` renders children when `user.role === 'Owner'`
|
||||
- [ ] Add test: `RoleGuard` renders inline "Access Denied" when `user.role === 'User'`
|
||||
|
||||
---
|
||||
|
||||
### Step 20 — Create code generation summary
|
||||
- [x] Create `aidlc-docs/features/cms-frontend/construction/unit-2/code/code-generation-summary.md`
|
||||
- [ ] List all created and modified files
|
||||
- [ ] Document any deviations from the plan
|
||||
- [ ] Record story coverage
|
||||
|
||||
---
|
||||
|
||||
## Deviations from Functional Design (pre-noted)
|
||||
|
||||
| Design spec | Actual implementation | Reason |
|
||||
|---|---|---|
|
||||
| `useSetupStatus()` with `staleTime: Infinity` (TanStack Query style) | Module-level cache + `useState`/`useEffect` | TanStack Query not installed; equivalent session-cache behavior |
|
||||
| `UserRole = 'Owner' \| 'Admin' \| 'User'` | `UserRole = 'Owner' \| 'Administrator' \| 'User'` | Matches existing `src/api/types.ts` and backend |
|
||||
| InitGuard as component with `useEffect` | `async beforeLoad` on rootRoute | More idiomatic for TanStack Router; avoids render flash |
|
||||
**Ready for Approval**: Yes
|
||||
|
||||
Reference in New Issue
Block a user