31 KiB
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-domainheader
Scenario: AuthClientConfig validates at the boundary
- WHEN
createAuthClientis invoked with a non-URLbaseURL - THEN a
ReactAuthConfigInvalidErroris thrown with codeREACT_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
Dateinstances - THEN the returned
Session[]contains ISO-stringcreatedAt/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
AuthProvidermounts withskipInitialCheck=falseand the client'sgetSession()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 anyAuthProvider - THEN a
ReactAuthContextMissingErroris thrown with codeREACT_AUTH.CONTEXT_MISSING
Scenario: each action settles loading and surfaces errors
- WHEN any of the 11 actions resolves successfully OR rejects
- THEN
isLoadingflips false; on rejecterrorcarries the wrappedErrorand 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
childrenis rendered
Scenario: unauthenticated user gets fallback when requireAuth
- WHEN an unauthenticated user enters an
AuthGuard requireAuth fallback={<X/>} - THEN
<X/>is rendered andchildrenis not
Scenario: role mismatch denied
- WHEN an authenticated user with
role="user"entersAuthGuard 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")andhasPermission(1)are called withuser.roles = 0b1 - THEN both return
true
Scenario: unauthenticated bails out
- WHEN no user is present
- THEN
hasPermission,hasAnyPermission,hasAllPermissionsall returnfalse
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_CODESvalues - 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+InMemoryMetricExporteris 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-emailinto 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
onSubmitis NOT called
Scenario: happy path submit
- WHEN the user fills
alice@example.com+correcthorsebatterystapleand 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
onSubmitis NOT called
Scenario: username regex enforced
- WHEN username
"has space"is submitted - THEN the Zod regex error appears and
onSubmitis NOT called
Scenario: happy path submit
- WHEN every required field is valid and the form is submitted
- THEN
onSubmitreceives the full typed payload including the defaultsex: "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
onSubmitis NOT called
Scenario: success view rendered
- WHEN
isSuccessis true andemailSent="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
onSubmitis 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
onContinueToLoginis called once
Scenario: error CTA fires onTryAgain and respects isLoading
- WHEN
status="error"andisLoading=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
onRegisterAgainis 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=/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"