Files
2026-05-27 23:29:53 +01:00

75 lines
8.4 KiB
Markdown

# Add @suaveplan/react-auth — pages + PermissionGate (Wave 3)
**Package**: `@suaveplan/react-auth`
**Status**: PROPOSED
**Type**: feature
- **Spec delta**: ./specs/react-auth/spec.md
## Why
Wave 1 shipped the `@suaveplan/react-auth` core (client, provider, context, hooks, guard) and Wave 2 shipped the five pure-UI form components. Downstream apps still cannot drop the package into a `react-router-dom` app without re-implementing the routing-aware adapter layer on every page. Wave 3 closes the migration by shipping the six page components (`LoginPage`, `RegisterPage`, `ForgotPasswordPage`, `ResetPasswordPage`, `EmailVerificationPage`, `ProfilePage`) that wire Wave 2 forms to the Wave 1 `useAuth()` actions plus `useNavigate` / `useSearchParams` redirects, and the `PermissionGate` component that conditionally renders children against the Wave 1 `usePermission` hook. After this wave the public surface of `@suaveplan/react-auth` reaches full parity with the upstream `../suaveplan/packages/react/react-auth/` package and the migration is complete.
## What changes
- ADDED: `LoginPage` — wraps `<LoginForm>`, calls `useAuth().signIn`, redirects to `?returnTo` or `/dashboard` on success, applies `useAuthGuard({ requireGuest: true })`.
- ADDED: `RegisterPage` — wraps `<RegisterForm>`, strips `confirmPassword`, calls `useAuth().signUp`, redirects to `/dashboard` on success, applies `useAuthGuard({ requireGuest: true })`.
- ADDED: `ForgotPasswordPage` — wraps `<ForgotPasswordForm>`, calls `useAuth().forgotPassword(email)`, manages local success state + reset.
- ADDED: `ResetPasswordPage` — wraps `<ResetPasswordForm>`, extracts `?token` from search params, calls `useAuth().resetPassword(token, password)`, manages success state, navigates to `/auth/login` and `/auth/forgot-password`, applies `useAuthGuard({ requireGuest: true })`.
- ADDED: `EmailVerificationPage` — wraps `<EmailVerificationView>`, extracts `?token`, auto-runs `useAuth().verifyEmail(token)` on mount, classifies errors into `expired` vs `error`, applies `useAuthGuard({ requireGuest: true })`.
- ADDED: `ProfilePage` — three-tab profile surface (profile / password / account) calling `useAuth().updateProfile`, `changePassword`, `deleteAccount`; applies `useAuthGuard({ requireAuth: true })`; uses `react-ui` Tabs/Card primitives + a new `profilePage` translation group.
- ADDED: `PermissionGate` — renders children when `usePermission()` reports the configured bitmask permissions (any-of by default, all-of when `requireAll`); otherwise renders a fallback (default `null`).
- ADDED: `AuthProfilePageStrings` interface + `profilePage` group on every locale pack (`enGb` / `cyGb` / `zhCn`) — purely additive, the existing Wave 1 `profile` group is left untouched.
- ADDED: Subpath exports under `./pages/<page>` for every page and `./components/permission-gate` for the gate, mirroring the Wave 1 + Wave 2 per-symbol convention.
## Out of scope
- MFA / passkey / magic-link flows — enterprise audit §4 enhancements, never part of upstream.
- `RoleGuard` companion to `PermissionGate` — enterprise audit §4 enhancement.
- Device-management / audit-trail pages — enterprise audit §4 enhancements.
## Source mapping
This change derives from `../suaveplan/enterprise/react-auth.md` and `../suaveplan/packages/react/react-auth/src/`.
| Requirement | Source lines | Notes |
|---|---|---|
| `LoginPage` shape | `pages/login/login.tsx:1-42` | Verbatim semantics; uses Wave 2 `<LoginForm>`. |
| `RegisterPage` shape | `pages/register/register.tsx:1-41` | Verbatim semantics; strips `confirmPassword` before calling `signUp`. |
| `ForgotPasswordPage` shape | `pages/forgot-password/forgot-password.tsx:1-43` | Verbatim semantics. |
| `ResetPasswordPage` shape | `pages/reset-password/reset-password.tsx:1-61` | Verbatim semantics; `?token` from search params. |
| `EmailVerificationPage` shape | `pages/email-verification/email-verification.tsx:1-96` | Verbatim semantics; upstream's `useTranslation` swapped for Wave 2 `translations` prop pattern (consistent with Wave 1 i18n decision). The dual `EmailVerification` legacy fork (lines 99-254) is NOT ported — it duplicated the `EmailVerificationView` which already shipped in Wave 2. |
| `ProfilePage` shape | `pages/profile/profile.tsx:1-638` | Three-tab profile / password / account surface. Upstream's `useTranslation` swapped for a `translations` prop defaulting to `enGb.auth.profilePage`. The implementation is split into `profile-page.tsx` (shell) + `profile-page-sections.tsx` (three section renderers) to honour the 300-line hard cap (§3). |
| `PermissionGate` shape | `components/permission-gate/permission-gate.tsx:1-28` | Verbatim semantics. |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| `translations?: Partial<...>` prop on `EmailVerificationPage` and `ProfilePage` | Genesis-specific — Wave 1 dropped `@suaveplan/react-translate`; Wave 2 established the prop-injection precedent. Wave 3 follows. |
| New `profilePage` translation group + `AuthProfilePageStrings` interface | Required because the upstream profile page used 20+ string keys not in Wave 1's slim `profile` group; rather than mutate the Wave 1 group (which Wave 2 also reads), Wave 3 adds a sibling `profilePage` group. |
| 100% statement / branch / function / line coverage | Genesis-wide policy. |
| Sibling `<page>.md` per implementation file | Genesis rule §4. |
| Sibling `<page>.stories.tsx` per page + permission-gate | CLAUDE.md: "Every React component needs `.stories.tsx`." |
| Sibling `.test.tsx` per page + permission-gate | Genesis-wide — upstream's pages had no co-located unit tests at this layer either. |
## Verification approach
Every page and `PermissionGate` ships a sibling `.test.tsx` that uses `@suaveplan/testing/runner` + `@testing-library/react` with real `react-hook-form`, real Zod, real `<MemoryRouter>`, and a real `<AuthProvider>` wrapped around an injectable `AuthClient` stub. No `vi.mock`. The `wrapForm` helper from Wave 2 is reused / extended to mount the auth context.
- `src/pages/login/login.test.tsx` — guard redirect when already authenticated, happy-path navigate to `returnTo`, happy-path default to `/dashboard`, signIn failure swallowed by the page.
- `src/pages/register/register.test.tsx` — guard redirect, happy-path strips `confirmPassword`, navigate to `/dashboard`, signUp failure swallowed.
- `src/pages/forgot-password/forgot-password.test.tsx` — happy-path submit renders success view with email, `onReset` clears, error surfaces.
- `src/pages/reset-password/reset-password.test.tsx` — missing-token branch shows invalid view, happy-path renders success, login navigation CTA, forgot navigation CTA.
- `src/pages/email-verification/email-verification.test.tsx` — missing-token sets `error` state, success sets `success` state, expired-token error classifies as `expired`, `onContinueToLogin` / `onRegisterAgain` / `onTryAgain` navigate correctly.
- `src/pages/profile/profile.test.tsx` — guard redirect for unauthenticated, three tab switches, profile update success, password change validation (required, length, mismatch), delete-account confirmation flow.
- `src/components/permission-gate/permission-gate.test.tsx` — renders children when permitted, renders fallback when denied, `requireAll` AND semantics, unauthenticated denies, numeric permission accepted.
- `src/index.test.ts` — extended to assert the 7 new symbols (`LoginPage`, `RegisterPage`, `ForgotPasswordPage`, `ResetPasswordPage`, `EmailVerificationPage`, `ProfilePage`, `PermissionGate`) appear in the root barrel.
## Migration impact
- `package.json` gains 7 new subpath exports under `./pages/<page>` and `./components/permission-gate`. No new runtime dependencies — every dep is already present from Wave 1 or Wave 2 (`react-router-dom` is a peer + dev dep on Wave 1).
- `vite.config.ts` `lib.entry` gains 7 new entries; `rollupOptions.external` already lists every consumed package.
- No Wave 1 or Wave 2 file is modified except `src/index.ts` (named re-exports for the 7 new symbols added — strictly additive), `src/components/index.ts` (one new entry for `PermissionGate`), `src/index.test.ts` (asserts the new symbols are present), and the three locale-pack files which gain the new `profilePage` group as a strictly additive sibling under `auth`.
- No downstream consumers yet — `@suaveplan/react-auth-whitelabel` is the only sibling and it ships independently. Verified by `bunx turbo ls --filter=...@suaveplan/react-auth` (empty list).