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=/inboxand the user submits valid credentials - THEN
useAuth().signInis called with the typed credentials ANDuseNavigateis called with"/inbox"
Scenario: happy path defaults to /dashboard
- WHEN
<LoginPage>is mounted under/auth/login(noreturnTo) and the user submits valid credentials - THEN
useNavigateis called with"/dashboard"
Scenario: signIn rejection does not throw
- WHEN
useAuth().signInrejects - 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().signUpis called with the payload minusconfirmPasswordANDuseNavigate("/dashboard")is invoked
Scenario: signUp rejection does not throw
- WHEN
useAuth().signUprejects - 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
forgotPasswordresolves - THEN the form is re-rendered with
isSuccess={true}ANDemailSentequals the submitted email
Scenario: reset returns to entry view
- WHEN
isSuccessis true and the success view's reset callback fires - THEN the form is re-rendered with
isSuccess={false}ANDemailSent=""
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 withhasToken={false}
Scenario: happy path flips to success
- WHEN the form submits with
?token=abcpresent andresetPasswordresolves - THEN
useAuth().resetPasswordis called with("abc", password)AND the form re-renders withisSuccess={true}
Scenario: navigation CTAs fire
- WHEN the form invokes
onNavigateToLoginoronNavigateToForgotPassword - THEN
useNavigateis called with/auth/loginor/auth/forgot-passwordrespectively
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 ANDuseAuth().verifyEmailis NOT called
Scenario: happy-path sets success status
- WHEN
verifyEmailresolves - THEN
<EmailVerificationView status="success">is rendered
Scenario: expired token classifies as expired
- WHEN
verifyEmailrejects withnew 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
passwordtab - 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
changePasswordis NOT called AND a mismatch error renders
Scenario: delete-account flow
- WHEN the user confirms delete with a password
- THEN
deleteAccount(password)is called ANDuseNavigate("/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_READand 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_READand the gate requires["USER_READ", "ADMIN_WRITE"]withrequireAll={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"