Initial Commit
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# 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"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
Reference in New Issue
Block a user