8.4 KiB
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>, callsuseAuth().signIn, redirects to?returnToor/dashboardon success, appliesuseAuthGuard({ requireGuest: true }). - ADDED:
RegisterPage— wraps<RegisterForm>, stripsconfirmPassword, callsuseAuth().signUp, redirects to/dashboardon success, appliesuseAuthGuard({ requireGuest: true }). - ADDED:
ForgotPasswordPage— wraps<ForgotPasswordForm>, callsuseAuth().forgotPassword(email), manages local success state + reset. - ADDED:
ResetPasswordPage— wraps<ResetPasswordForm>, extracts?tokenfrom search params, callsuseAuth().resetPassword(token, password), manages success state, navigates to/auth/loginand/auth/forgot-password, appliesuseAuthGuard({ requireGuest: true }). - ADDED:
EmailVerificationPage— wraps<EmailVerificationView>, extracts?token, auto-runsuseAuth().verifyEmail(token)on mount, classifies errors intoexpiredvserror, appliesuseAuthGuard({ requireGuest: true }). - ADDED:
ProfilePage— three-tab profile surface (profile / password / account) callinguseAuth().updateProfile,changePassword,deleteAccount; appliesuseAuthGuard({ requireAuth: true }); usesreact-uiTabs/Card primitives + a newprofilePagetranslation group. - ADDED:
PermissionGate— renders children whenusePermission()reports the configured bitmask permissions (any-of by default, all-of whenrequireAll); otherwise renders a fallback (defaultnull). - ADDED:
AuthProfilePageStringsinterface +profilePagegroup on every locale pack (enGb/cyGb/zhCn) — purely additive, the existing Wave 1profilegroup is left untouched. - ADDED: Subpath exports under
./pages/<page>for every page and./components/permission-gatefor 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.
RoleGuardcompanion toPermissionGate— 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 toreturnTo, happy-path default to/dashboard, signIn failure swallowed by the page.src/pages/register/register.test.tsx— guard redirect, happy-path stripsconfirmPassword, navigate to/dashboard, signUp failure swallowed.src/pages/forgot-password/forgot-password.test.tsx— happy-path submit renders success view with email,onResetclears, 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 setserrorstate, success setssuccessstate, expired-token error classifies asexpired,onContinueToLogin/onRegisterAgain/onTryAgainnavigate 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,requireAllAND 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.jsongains 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-domis a peer + dev dep on Wave 1).vite.config.tslib.entrygains 7 new entries;rollupOptions.externalalready 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 forPermissionGate),src/index.test.ts(asserts the new symbols are present), and the three locale-pack files which gain the newprofilePagegroup as a strictly additive sibling underauth. - No downstream consumers yet —
@suaveplan/react-auth-whitelabelis the only sibling and it ships independently. Verified bybunx turbo ls --filter=...@suaveplan/react-auth(empty list).