Files
suaveplan/openspec/archive/2026-05-19-add-react-auth-pages/proposal.md
2026-05-27 23:29:53 +01:00

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>, 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).