Files
2026-05-27 23:29:53 +01:00

31 KiB
Raw Permalink Blame History

Capability Spec: react-auth

Package: @suaveplan/react-auth Status: SHIPPED

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

ADDED Requirements

Requirement: AuthClient wraps better-auth with site-domain header propagation

The package SHALL expose an AuthClient class and a createAuthClient(config) factory. The class SHALL wrap better-auth/client and SHALL forward every request through the underlying client. When AuthClientConfig.siteDomain is set, every request SHALL carry an x-site-domain header.

Scenario: site-domain header attached when configured

  • WHEN createAuthClient({ baseURL, siteDomain: "tenant-a" }).signIn({ email, password }) is called and the underlying client method is observed
  • THEN the call options include fetchOptions.headers["x-site-domain"] === "tenant-a"

Scenario: header omitted when not configured

  • WHEN createAuthClient({ baseURL }).getSession() is called
  • THEN the call options omit the x-site-domain header

Scenario: AuthClientConfig validates at the boundary

  • WHEN createAuthClient is invoked with a non-URL baseURL
  • THEN a ReactAuthConfigInvalidError is thrown with code REACT_AUTH.CONFIG_INVALID

Verified by: src/client/auth-client.test.ts::"propagates x-site-domain header on every method when siteDomain is configured" AND src/client/auth-client.test.ts::"omits x-site-domain header when siteDomain is not configured" AND src/client/auth-client.test.ts::"throws ReactAuthConfigInvalidError when baseURL is not a valid URL"


Requirement: AuthClient list-sessions normalises Date fields to ISO strings

The AuthClient.listSessions() method SHALL return Session[] and SHALL convert each session's createdAt / updatedAt / expiresAt fields from Date to ISO string (or leave them as the underlying type when already a string).

Scenario: dates converted

  • WHEN the underlying client returns sessions with Date instances
  • THEN the returned Session[] contains ISO-string createdAt / updatedAt

Verified by: src/client/auth-client.test.ts::"converts Date fields on listSessions to ISO strings"


Requirement: AuthProvider exposes auth state and 11 actions through context

The package SHALL expose an AuthProvider component and a useAuth() hook. The context value SHALL include { user, session, isLoading, isAuthenticated, error, client } and the 11 actions signIn / signUp / signOut / updateProfile / changePassword / forgotPassword / resetPassword / verifyEmail / resendVerificationEmail / deleteAccount / refreshSession. useAuth() outside an AuthProvider SHALL throw ReactAuthContextMissingError with code REACT_AUTH.CONTEXT_MISSING.

Scenario: initial session loaded on mount

  • WHEN AuthProvider mounts with skipInitialCheck=false and the client's getSession() returns { data: { user, session } }
  • THEN the context settles with isAuthenticated=true, user, session, isLoading=false

Scenario: useAuth outside provider throws

  • WHEN useAuth() is called outside any AuthProvider
  • THEN a ReactAuthContextMissingError is thrown with code REACT_AUTH.CONTEXT_MISSING

Scenario: each action settles loading and surfaces errors

  • WHEN any of the 11 actions resolves successfully OR rejects
  • THEN isLoading flips false; on reject error carries the wrapped Error and the action re-throws

Verified by: src/context/auth-context.test.tsx::"loads the initial session and settles isAuthenticated=true" AND src/context/auth-context.test.tsx::"useAuth outside an AuthProvider throws ReactAuthContextMissingError" AND src/context/auth-context.test.tsx::"every action settles isLoading and propagates errors"


Requirement: AuthGuard gates children by auth / guest / verified / roles

The AuthGuard component SHALL render loadingComponent (or a default spinner) while isLoading=true. When loading completes it SHALL render children if and only if every configured predicate (requireAuth / requireGuest / requireVerified / allowedRoles) is satisfied; otherwise it SHALL render fallback (or null).

Scenario: authenticated user passes default requireAuth

  • WHEN an authenticated, verified user enters an AuthGuard requireAuth
  • THEN children is rendered

Scenario: unauthenticated user gets fallback when requireAuth

  • WHEN an unauthenticated user enters an AuthGuard requireAuth fallback={<X/>}
  • THEN <X/> is rendered and children is not

Scenario: role mismatch denied

  • WHEN an authenticated user with role="user" enters AuthGuard allowedRoles={["admin"]}
  • THEN the guard does not render children

Verified by: src/components/auth-guard/auth-guard.test.tsx::"renders children when authenticated and no extra predicates are set" AND src/components/auth-guard/auth-guard.test.tsx::"renders fallback when unauthenticated and requireAuth is set" AND src/components/auth-guard/auth-guard.test.tsx::"denies when allowedRoles does not include the user role"


Requirement: useAuthGuard imperatively redirects via react-router-dom

The useAuthGuard(options) hook SHALL call react-router-dom's useNavigate() and SHALL navigate to redirectTo (default /auth/login) when requireAuth fails, to / when requireGuest fails, to /auth/verify-email when requireVerified fails, and to /unauthorized when allowedRoles fails. It SHALL invoke onUnauthorized before the auth / role redirects. The hook SHALL also return { isLoading, isAuthenticated, user, canAccess } for inline checks.

Scenario: redirects to login when not authenticated

  • WHEN an unauthenticated user mounts a component calling useAuthGuard({ requireAuth: true })
  • THEN navigate("/auth/login") is called once

Scenario: canAccess reflects every predicate

  • WHEN any predicate fails
  • THEN canAccess === false; when all predicates pass, canAccess === true

Verified by: src/hooks/use-auth-guard.test.tsx::"redirects to /auth/login when requireAuth fails" AND src/hooks/use-auth-guard.test.tsx::"canAccess matrix reflects every predicate combination"


Requirement: usePermission exposes bitwise permission checks

The usePermission() hook SHALL expose hasPermission / hasAnyPermission / hasAllPermissions / getPermissionNames / userRoles. Each predicate SHALL accept either a Permission string key or a numeric bitmask and SHALL test (user.roles & bit) === bit. With no authenticated user, every predicate SHALL return false and getPermissionNames() SHALL return [].

Scenario: string and numeric permission accepted

  • WHEN hasPermission("USER_READ") and hasPermission(1) are called with user.roles = 0b1
  • THEN both return true

Scenario: unauthenticated bails out

  • WHEN no user is present
  • THEN hasPermission, hasAnyPermission, hasAllPermissions all return false

Verified by: src/hooks/use-permission.test.tsx::"accepts string and numeric Permission inputs" AND src/hooks/use-permission.test.tsx::"returns false for every predicate when unauthenticated"


Requirement: better-auth response adapter narrows union to internal shapes

The package SHALL expose isBetterAuthError / hasBetterAuthData / transformSessionResponse / transformAuthResponse / transformGenericResponse / transformUpdateProfileResponse. Each transform SHALL accept unknown (no any), SHALL detect the error-tagged and data-tagged variants, and SHALL emit the package's internal User / Session / AuthResponse shapes with ISO-string timestamps.

Scenario: session response with Date timestamps normalised

  • WHEN transformSessionResponse({ data: { user: { ..., createdAt: Date }, session: { ..., expiresAt: Date } } }) is called
  • THEN the returned { user, session } carries ISO strings

Scenario: error variant surfaces message

  • WHEN the response carries { error, message }
  • THEN the transform returns { session: null, user: null, error: <message> }

Verified by: src/lib/better-auth-adapter.test.ts::"transformSessionResponse normalises Date timestamps to ISO strings" AND src/lib/better-auth-adapter.test.ts::"transforms surface the better-auth error message"


Requirement: REACT_AUTH error code registry covers every thrown error

The package SHALL export REACT_AUTH_CODES = defineCodes("REACT_AUTH", [...]) with at least the 10 codes CONFIG_INVALID, CONTEXT_MISSING, GUARD_PROPS_INVALID, GUARD_OPTIONS_INVALID, CLIENT_NOT_INITIALISED, BETTER_AUTH_RESPONSE_INVALID, SIGN_IN_FAILED, SIGN_UP_FAILED, SESSION_FETCH_FAILED, PERMISSION_DENIED. Every code SHALL be thrown by at least one production path and SHALL surface through a typed ReactAuthError subclass.

Scenario: every code has a thrown call site

  • WHEN the test suite enumerates REACT_AUTH_CODES values
  • THEN every code is reachable via a typed subclass that ships in src/error-codes/error-codes.ts

Verified by: src/error-codes/error-codes.test.ts::"every REACT_AUTH code is thrown by at least one production path"


Requirement: OTel instruments emit through suaveplan.react-auth meter

The package SHALL build OTel instruments lazily under metrics.getMeter("suaveplan.react-auth") and SHALL emit at minimum: react_auth.sign_in.total, react_auth.sign_up.total, react_auth.sign_out.total, react_auth.guard.allow.total, react_auth.guard.deny.total counters, and react_auth.session_fetch.duration_ms histogram.

Scenario: in-memory MeterProvider captures every emit

  • WHEN a real MeterProvider + InMemoryMetricExporter is wired and every production path that emits is executed
  • THEN the corresponding instrument records the expected attributes

Verified by: src/observability/react-auth-metrics.test.ts::"emits sign-in / sign-up / sign-out / guard / session-fetch instruments through suaveplan.react-auth"


Requirement: Locale packs ship for en-GB / cy-GB / zh-CN with key parity

The package SHALL export enGb, cyGb, zhCn (default exports) and an aggregate authTranslations = { "en-gb": enGb, "cy-gb": cyGb, "zh-cn": zhCn }. Every locale SHALL carry the same set of auth.<group>.<key> paths so Wave-2 forms can resolve without locale-specific guards.

Scenario: key parity across locales

  • WHEN the locale-pack test flattens every dotted key in each locale
  • THEN the three sets are equal

Verified by: src/translations/translations.test.ts::"every locale pack exposes the same dotted key set"


Requirement: Root barrel uses explicit named re-exports

The root src/index.ts and every feature barrel SHALL use explicit export { … } re-exports — never export *. The barrel SHALL expose the public surface enumerated in proposal.md ## What changes and nothing else.

Scenario: package roots are explicit

  • WHEN repo-wide grep -rn '^export \\*' packages/react/react-auth/src/ is run
  • THEN the result is empty

Verified by: src/index.test.ts::"the root barrel exports exactly the documented public surface"


Requirement: LoginForm renders a Zod-validated sign-in form with email, password, and remember-me

The package SHALL expose a LoginForm component that renders a card-layout sign-in form with email + password + remember-me fields. The form SHALL validate via react-hook-form + zodResolver against an internal loginSchema requiring email to be a non-empty valid email and password to be at least 8 characters. Submit SHALL invoke the caller-supplied onSubmit({ email, password, rememberMe }). The component SHALL render a destructive Alert when error is non-null, disable the submit button when isLoading or formState.isSubmitting is true, and accept a translations prop overriding the default enGb.auth.login strings.

Scenario: invalid email shows Zod error on blur

  • WHEN the user types not-an-email into the email field and submits
  • THEN the text "Invalid email address" appears

Scenario: short password rejected

  • WHEN the user submits with password "abc"
  • THEN the text "Password must be at least 8 characters" appears and onSubmit is NOT called

Scenario: happy path submit

  • WHEN the user fills alice@example.com + correcthorsebatterystaple and clicks Sign in
  • THEN onSubmit({ email: "alice@example.com", password: "correcthorsebatterystaple", rememberMe: true }) is called

Scenario: error prop renders alert

  • WHEN error={new Error("Invalid credentials")} is passed
  • THEN an alert containing "Invalid credentials" is in the document

Scenario: translations prop overrides defaults

  • WHEN translations={{ submit: "Connexion" }} is passed
  • THEN the submit button displays "Connexion"

Verified by: src/components/login-form/login-form.test.tsx::"shows Zod error when email is invalid" AND src/components/login-form/login-form.test.tsx::"shows Zod error when password is shorter than 8 characters" AND src/components/login-form/login-form.test.tsx::"calls onSubmit with the typed payload on happy path" AND src/components/login-form/login-form.test.tsx::"renders the error prop in a destructive Alert" AND src/components/login-form/login-form.test.tsx::"overrides the submit copy when translations prop is supplied"


Requirement: RegisterForm renders a Zod-validated sign-up form with cross-field password match

The package SHALL expose a RegisterForm component rendering email, username, password, confirm-password, optional first-name, surname, date-of-birth, sex (4-option select), gender, company fields. The Zod schema SHALL enforce email validity, username ^[a-zA-Z0-9_]+$ with length 3–30, password ≥ 8 chars, and a confirmPassword cross-field refinement matching password. Submit SHALL fire only when validation passes. The component SHALL accept error, isLoading, and translations props mirroring LoginForm semantics.

Scenario: confirmPassword mismatch blocks submit

  • WHEN password "longenough1" and confirmPassword "longenough2" are submitted
  • THEN "Passwords don't match" appears under the confirm field and onSubmit is NOT called

Scenario: username regex enforced

  • WHEN username "has space" is submitted
  • THEN the Zod regex error appears and onSubmit is NOT called

Scenario: happy path submit

  • WHEN every required field is valid and the form is submitted
  • THEN onSubmit receives the full typed payload including the default sex: "prefer-not-to-say"

Verified by: src/components/register-form/register-form.test.tsx::"blocks submit when passwords do not match" AND src/components/register-form/register-form.test.tsx::"rejects usernames that do not match the regex" AND src/components/register-form/register-form.test.tsx::"calls onSubmit with the typed payload on happy path" AND src/components/register-form/register-form.test.tsx::"renders the error prop in a destructive Alert"


Requirement: ForgotPasswordForm renders an email-only Zod-validated form with success view

The package SHALL expose a ForgotPasswordForm component rendering an email-only form. When isSuccess is true the component SHALL switch to a confirmation card displaying emailSent, a "send another" button wired to onReset, and a "back to login" link. Otherwise it SHALL render the email form with Zod validation, a destructive alert when error is non-null, and a submit button disabled while isLoading or isSubmitting. A translations prop SHALL override enGb.auth.forgotPassword defaults.

Scenario: invalid email blocked

  • WHEN the user submits "not-email"
  • THEN "Invalid email address" appears and onSubmit is NOT called

Scenario: success view rendered

  • WHEN isSuccess is true and emailSent="alice@example.com"
  • THEN the document contains "alice@example.com" and a button whose click calls onReset

Verified by: src/components/forgot-password-form/forgot-password-form.test.tsx::"blocks submit when email is invalid" AND src/components/forgot-password-form/forgot-password-form.test.tsx::"calls onSubmit with the typed payload on happy path" AND src/components/forgot-password-form/forgot-password-form.test.tsx::"renders the success view with emailSent and a working onReset button" AND src/components/forgot-password-form/forgot-password-form.test.tsx::"renders the error prop in a destructive Alert"


Requirement: ResetPasswordForm renders new+confirm password with success and invalid-token views

The package SHALL expose a ResetPasswordForm component supporting three view modes: form (default), success (isSuccess=true), and invalid token (hasToken=false). The schema SHALL require both password and confirmPassword ≥ 8 chars with a cross-field match refinement. The success view SHALL render a "sign in" CTA wired to onNavigateToLogin; the invalid-token view SHALL render a "request new link" CTA wired to onNavigateToForgotPassword. The form view SHALL accept error, isLoading, and translations props.

Scenario: password mismatch blocks submit

  • WHEN password "longenough1" and confirm "longenough2" are submitted
  • THEN "Passwords don't match" appears and onSubmit is NOT called

Scenario: success view renders CTA

  • WHEN isSuccess=true
  • THEN clicking the Sign in CTA calls onNavigateToLogin

Scenario: invalid token view renders CTA

  • WHEN hasToken=false
  • THEN clicking the Request new reset link CTA calls onNavigateToForgotPassword

Verified by: src/components/reset-password-form/reset-password-form.test.tsx::"blocks submit when passwords do not match" AND src/components/reset-password-form/reset-password-form.test.tsx::"calls onSubmit with the typed payload on happy path" AND src/components/reset-password-form/reset-password-form.test.tsx::"renders the success view with a working onNavigateToLogin CTA" AND src/components/reset-password-form/reset-password-form.test.tsx::"renders the invalid-token view with a working onNavigateToForgotPassword CTA"


Requirement: EmailVerificationView renders four status modes with caller-supplied translations

The package SHALL expose an EmailVerificationView component accepting status: "verifying" | "success" | "expired" | "error". Each status SHALL render a Card with an icon, title, description, and (where applicable) a CTA wired to one of onContinueToLogin / onRegisterAgain / onTryAgain. The error status SHALL render a destructive Alert; the error retry CTA SHALL be disabled while isLoading. A translations prop SHALL override enGb.auth.emailVerification defaults; an optional message prop SHALL override the default status description.

Scenario: verifying state renders spinner and copy

  • WHEN status="verifying"
  • THEN a role="status" spinner is present and the default English "Verifying email..." copy is visible

Scenario: success CTA fires onContinueToLogin

  • WHEN status="success" and the CTA is clicked
  • THEN onContinueToLogin is called once

Scenario: error CTA fires onTryAgain and respects isLoading

  • WHEN status="error" and isLoading=false
  • THEN the retry CTA is enabled and clicking it calls onTryAgain

Scenario: expired CTA fires onRegisterAgain

  • WHEN status="expired" and the CTA is clicked
  • THEN onRegisterAgain is called once

Verified by: src/components/email-verification-view/email-verification-view.test.tsx::"renders the verifying status with a spinner" AND src/components/email-verification-view/email-verification-view.test.tsx::"fires onContinueToLogin from the success CTA" AND src/components/email-verification-view/email-verification-view.test.tsx::"fires onTryAgain from the error CTA" AND src/components/email-verification-view/email-verification-view.test.tsx::"fires onRegisterAgain from the expired CTA" AND src/components/email-verification-view/email-verification-view.test.tsx::"overrides the default copy when translations prop is supplied"


Requirement: Wave 2 form components reachable from the root barrel

The @suaveplan/react-auth root entry SHALL re-export the 5 Wave 2 form components as named exports (LoginForm, RegisterForm, ForgotPasswordForm, ResetPasswordForm, EmailVerificationView) with no export *. Each form SHALL also be reachable via a dedicated subpath export (./components/login-form, etc.) for tree-shaking.

Scenario: Wave 2 surface present in root barrel

  • WHEN the root-barrel test enumerates Object.keys(import("@suaveplan/react-auth"))
  • THEN every Wave 2 form symbol is present alongside the Wave 1 surface

Verified by: src/index.test.ts::"the root barrel exports exactly the documented public surface"

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"