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

11 KiB

Capability Spec Delta: react-auth (add-react-auth-pages)

Package: @suaveplan/react-auth Status: PROPOSED

Every requirement below carries a **Verified by:** line at authoring time.

ADDED Requirements

Requirement: LoginPage wires LoginForm to the auth context with router redirect

The package SHALL expose a <LoginPage> component that renders the Wave 2 <LoginForm>, calls useAuth().signIn(values) on submit, redirects to useSearchParams().get("returnTo") (or /dashboard if absent) via useNavigate, and applies useAuthGuard({ requireGuest: true }) so already-authenticated visitors are redirected away. The page SHALL swallow rejections from signIn — the underlying useAuth().error field carries the surfaced message into the form's destructive alert.

Scenario: happy path navigates to returnTo

  • WHEN <LoginPage> is mounted under /auth/login?returnTo=/inbox and the user submits valid credentials
  • THEN useAuth().signIn is called with the typed credentials AND useNavigate is called with "/inbox"

Scenario: happy path defaults to /dashboard

  • WHEN <LoginPage> is mounted under /auth/login (no returnTo) and the user submits valid credentials
  • THEN useNavigate is called with "/dashboard"

Scenario: signIn rejection does not throw

  • WHEN useAuth().signIn rejects
  • THEN the page does not throw and the error message renders inside the <LoginForm> alert

Verified by: src/pages/login/login.test.tsx::"navigates to returnTo on happy path" AND src/pages/login/login.test.tsx::"defaults to /dashboard when returnTo is absent" AND src/pages/login/login.test.tsx::"swallows signIn rejection and surfaces the error in the form"


Requirement: RegisterPage wires RegisterForm to signUp and strips confirmPassword

The package SHALL expose a <RegisterPage> component that renders the Wave 2 <RegisterForm>, strips confirmPassword from the submitted values before calling useAuth().signUp(data), redirects to /dashboard on success, and applies useAuthGuard({ requireGuest: true }). The page SHALL swallow signUp rejections (errors surface via the form alert).

Scenario: happy path strips confirmPassword

  • WHEN the form submits { email, password, confirmPassword, username, ... }
  • THEN useAuth().signUp is called with the payload minus confirmPassword AND useNavigate("/dashboard") is invoked

Scenario: signUp rejection does not throw

  • WHEN useAuth().signUp rejects
  • THEN the page does not throw

Verified by: src/pages/register/register.test.tsx::"strips confirmPassword and navigates to /dashboard on happy path" AND src/pages/register/register.test.tsx::"swallows signUp rejection"


Requirement: ForgotPasswordPage wires ForgotPasswordForm to forgotPassword with success view

The package SHALL expose a <ForgotPasswordPage> component that renders the Wave 2 <ForgotPasswordForm>, calls useAuth().forgotPassword(email) on submit, drives the isSuccess / emailSent props from local state on resolution, and clears that state when the form's onReset callback fires.

Scenario: happy path flips to success view

  • WHEN the form submits and forgotPassword resolves
  • THEN the form is re-rendered with isSuccess={true} AND emailSent equals the submitted email

Scenario: reset returns to entry view

  • WHEN isSuccess is true and the success view's reset callback fires
  • THEN the form is re-rendered with isSuccess={false} AND emailSent=""

Verified by: src/pages/forgot-password/forgot-password.test.tsx::"flips to success view on happy path" AND src/pages/forgot-password/forgot-password.test.tsx::"reset returns to entry view"


Requirement: ResetPasswordPage extracts ?token and drives ResetPasswordForm state

The package SHALL expose a <ResetPasswordPage> component that reads ?token from useSearchParams, passes hasToken={!!token} to the Wave 2 <ResetPasswordForm>, calls useAuth().resetPassword(token, password) on submit when a token exists, flips isSuccess on resolution, and exposes onNavigateToLogin (/auth/login) and onNavigateToForgotPassword (/auth/forgot-password) callbacks for the form's success / invalid-token CTAs. The page SHALL apply useAuthGuard({ requireGuest: true }).

Scenario: missing token renders invalid-token view

  • WHEN the page is mounted under /auth/reset-password (no ?token)
  • THEN <ResetPasswordForm> is rendered with hasToken={false}

Scenario: happy path flips to success

  • WHEN the form submits with ?token=abc present and resetPassword resolves
  • THEN useAuth().resetPassword is called with ("abc", password) AND the form re-renders with isSuccess={true}

Scenario: navigation CTAs fire

  • WHEN the form invokes onNavigateToLogin or onNavigateToForgotPassword
  • THEN useNavigate is called with /auth/login or /auth/forgot-password respectively

Verified by: src/pages/reset-password/reset-password.test.tsx::"renders invalid-token view when ?token is absent" AND src/pages/reset-password/reset-password.test.tsx::"flips to success on happy path" AND src/pages/reset-password/reset-password.test.tsx::"navigates on login and forgot-password CTAs"


Requirement: EmailVerificationPage auto-verifies ?token and classifies errors

The package SHALL expose an <EmailVerificationPage> component that reads ?token from useSearchParams, calls useAuth().verifyEmail(token) once on mount, drives the Wave 2 <EmailVerificationView> status via local state (verifying → success / error / expired), classifies any error whose message contains expired or invalid (case-insensitive) as expired, and exposes navigation callbacks (onContinueToLogin → /auth/login, onRegisterAgain → /auth/register, onTryAgain reloads). The page SHALL apply useAuthGuard({ requireGuest: true }).

Scenario: missing token sets error status

  • WHEN the page mounts with no ?token
  • THEN <EmailVerificationView status="error"> is rendered AND useAuth().verifyEmail is NOT called

Scenario: happy-path sets success status

  • WHEN verifyEmail resolves
  • THEN <EmailVerificationView status="success"> is rendered

Scenario: expired token classifies as expired

  • WHEN verifyEmail rejects with new Error("token has expired")
  • THEN <EmailVerificationView status="expired"> is rendered

Verified by: src/pages/email-verification/email-verification.test.tsx::"renders error status when ?token is absent" AND src/pages/email-verification/email-verification.test.tsx::"renders success status on happy path" AND src/pages/email-verification/email-verification.test.tsx::"classifies expired-token rejection as expired"


Requirement: ProfilePage renders a three-tab account surface bound to useAuth

The package SHALL expose a <ProfilePage> (aliased as Profile for upstream compatibility) component that renders a three-tab surface (profile, password, account) using @suaveplan/react-ui <Tabs>, hydrates the profile form from useAuth().user, calls useAuth().updateProfile({ name, email }) on profile-tab submit, calls useAuth().changePassword({ currentPassword, newPassword }) on password-tab submit (after local validation: required, ≥8 chars, match), and calls useAuth().deleteAccount(currentPassword) on account-tab confirmation (then navigates to /auth/login). The page SHALL apply useAuthGuard({ requireAuth: true }). The page SHALL accept a translations?: Partial<AuthProfilePageStrings> prop defaulting to enGb.auth.profilePage.

Scenario: guards unauthenticated visitors

  • WHEN the page is mounted with useAuth().isAuthenticated === false
  • THEN useNavigate("/auth/login") is invoked

Scenario: tab switching renders the requested section

  • WHEN the user clicks the password tab
  • THEN the password form is rendered AND the profile form is hidden

Scenario: profile update calls updateProfile and shows success

  • WHEN the profile form is submitted with edited name + email
  • THEN updateProfile({ name, email }) is called AND a success alert appears

Scenario: password change validates locally before calling changePassword

  • WHEN newPassword and confirmPassword do not match
  • THEN changePassword is NOT called AND a mismatch error renders

Scenario: delete-account flow

  • WHEN the user confirms delete with a password
  • THEN deleteAccount(password) is called AND useNavigate("/auth/login") is invoked

Verified by: src/pages/profile/profile.test.tsx::"guards unauthenticated visitors" AND src/pages/profile/profile.test.tsx::"switches tabs on click" AND src/pages/profile/profile.test.tsx::"profile update success" AND src/pages/profile/profile.test.tsx::"password change validates mismatch locally" AND src/pages/profile/profile.test.tsx::"delete account confirmation calls deleteAccount and navigates"


Requirement: PermissionGate conditionally renders by bitmask permission check

The package SHALL expose a <PermissionGate> component that consumes usePermission() from Wave 1 and renders children when the configured permissions array passes the check, otherwise renders fallback (default null). The default semantics SHALL be ANY-of; setting requireAll={true} SHALL switch to ALL-of semantics. Each entry in permissions MAY be a named Permission key OR a raw number bitmask.

Scenario: any-of permits when one bit matches

  • WHEN the user has Permissions.USER_READ and the gate requires ["USER_READ", "ADMIN_WRITE"]
  • THEN the children are rendered

Scenario: requireAll denies when one bit missing

  • WHEN the user has only Permissions.USER_READ and the gate requires ["USER_READ", "ADMIN_WRITE"] with requireAll={true}
  • THEN the fallback is rendered

Scenario: numeric permission accepted

  • WHEN the gate is given a raw numeric bitmask matching the user's bits
  • THEN the children are rendered

Scenario: unauthenticated user always denied

  • WHEN useAuth().user === null
  • THEN the fallback is rendered

Verified by: src/components/permission-gate/permission-gate.test.tsx::"renders children when any-of matches" AND src/components/permission-gate/permission-gate.test.tsx::"renders fallback when requireAll fails" AND src/components/permission-gate/permission-gate.test.tsx::"accepts numeric permission" AND src/components/permission-gate/permission-gate.test.tsx::"renders fallback when unauthenticated"