Initial Commit
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: react-auth-whitelabel (add-react-auth-whitelabel)
|
||||
|
||||
**Package**: `@suaveplan/react-auth-whitelabel`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: AuthProvider exposes auth verbs over an injectable transport
|
||||
|
||||
The package SHALL ship an `AuthProvider` React context provider that exposes `login`, `loginWithProvider`, `register`, `logout`, `requestPasswordReset`, and `resetPassword` verbs. Each verb invokes the configured backend through `config.fetch` (a `FetchLike`, defaulting to `globalThis.fetch`) and routes redirect side-effects through `config.navigate` (defaulting to a `window.location.href` writer). The context value also exposes `user`, `isLoading`, `isAuthenticated`, and `error`.
|
||||
|
||||
#### Scenario: login posts to authUrl and stores the returned user
|
||||
|
||||
- **WHEN** `useAuth().login("a@b.c", "pw")` is called with `fetch` returning `{ ok: true, json: { user } }`
|
||||
- **THEN** the request body is `{"email":"a@b.c","password":"pw"}`, the request URL ends with `/login`, and `user` becomes the context's `user`
|
||||
|
||||
#### Scenario: loginWithProvider routes through the navigate seam
|
||||
|
||||
- **WHEN** `useAuth().loginWithProvider("google")` is called
|
||||
- **THEN** `config.navigate(url)` is invoked exactly once with a URL that includes `/oauth/google?redirect=`
|
||||
|
||||
#### Scenario: useAuth outside provider throws PROVIDER_OUTSIDE_TREE
|
||||
|
||||
- **WHEN** `useAuth()` is called with no `<AuthProvider>` ancestor
|
||||
- **THEN** a `ReactAuthWhitelabelError` with `.code === "REACT_AUTH_WL.PROVIDER_OUTSIDE_TREE"` is thrown
|
||||
|
||||
**Verified by:** `src/context/auth-context.test.tsx::"login() posts credentials to {authUrl}/login and sets user"` AND `src/context/auth-context.test.tsx::"loginWithProvider routes through the navigate seam"` AND `src/context/auth-context.test.tsx::"useAuth outside provider throws REACT_AUTH_WL.PROVIDER_OUTSIDE_TREE"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: BrandingProvider writes tenant branding through the react-ui theme mutator
|
||||
|
||||
The package SHALL ship a `BrandingProvider` that, on every change of the resolved `TenantBranding`, calls `useThemeMutator().setExtensionTokens({...})` with keys `--brand-primary`, `--brand-secondary`, `--brand-accent`, and `--brand-font`. Direct `document.documentElement.style.setProperty("--brand-*", ...)` writes from package code are forbidden. Non-token branding surfaces (document title, favicon `<link rel="icon">`, and tenant custom CSS `<style id="tenant-custom-css">`) remain direct DOM writes. The mutator's automatic unmount cleanup removes every `--brand-*` declaration.
|
||||
|
||||
#### Scenario: static branding applies extension tokens after mount
|
||||
|
||||
- **WHEN** `<ThemeProvider>...<BrandingProvider staticBranding={…}>` mounts
|
||||
- **THEN** `document.documentElement.style.getPropertyValue("--brand-primary")` returns the static branding's `primaryColor`
|
||||
|
||||
#### Scenario: extension tokens are removed on unmount
|
||||
|
||||
- **WHEN** the `<BrandingProvider>` is unmounted
|
||||
- **THEN** `document.documentElement.style.getPropertyValue("--brand-primary")` returns the empty string
|
||||
|
||||
#### Scenario: Zod failure on fetched branding raises BRANDING_INVALID_PAYLOAD
|
||||
|
||||
- **WHEN** the fetched branding response fails `TenantBrandingSchema.safeParse`
|
||||
- **THEN** the context exposes a `ReactAuthWhitelabelError` whose `.code === "REACT_AUTH_WL.BRANDING_INVALID_PAYLOAD"`
|
||||
|
||||
#### Scenario: network failure raises BRANDING_FETCH_FAILED
|
||||
|
||||
- **WHEN** the configured `fetch` resolves with `{ ok: false, status: 503 }`
|
||||
- **THEN** the context exposes a `ReactAuthWhitelabelError` whose `.code === "REACT_AUTH_WL.BRANDING_FETCH_FAILED"`
|
||||
|
||||
**Verified by:** `src/context/branding-context.test.tsx::"setExtensionTokens propagates --brand-primary after mount"` AND `src/context/branding-context.test.tsx::"unmount removes every --brand-* property"` AND `src/context/branding-context.test.tsx::"invalid fetched payload exposes REACT_AUTH_WL.BRANDING_INVALID_PAYLOAD"` AND `src/context/branding-context.test.tsx::"fetch failure exposes REACT_AUTH_WL.BRANDING_FETCH_FAILED"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: TenantBranding has a Zod schema
|
||||
|
||||
The package SHALL export a Zod schema `TenantBrandingSchema` that validates every field of `TenantBranding`. The `BrandingProvider` SHALL run `TenantBrandingSchema.safeParse(...)` on every fetched payload before storing it.
|
||||
|
||||
#### Scenario: schema accepts a fully-populated branding payload
|
||||
|
||||
- **WHEN** `TenantBrandingSchema.parse({ tenantId, name, primaryColor: "#fff", secondaryColor: "#000", … })` is called
|
||||
- **THEN** parsing succeeds
|
||||
|
||||
#### Scenario: schema rejects payload without primaryColor
|
||||
|
||||
- **WHEN** `TenantBrandingSchema.safeParse({ tenantId: "t", name: "n", secondaryColor: "#000" })` is called
|
||||
- **THEN** the result is `{ success: false }`
|
||||
|
||||
**Verified by:** `src/types/branding.schema.test.ts::"accepts a fully-populated branding payload"` AND `src/types/branding.schema.test.ts::"rejects payload without primaryColor"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: LoginForm renders accessible email/password form with optional OAuth providers
|
||||
|
||||
The package SHALL ship a `LoginForm` React component with controlled email + password inputs, an optional `rememberMe` checkbox, an optional OAuth provider button list, an optional forgot-password affordance, and an optional register-link affordance. The submit button calls `onSubmit?` if supplied else `useAuth().login(...)`. Errors are rendered in a `role="alert"` region. Every visible string is sourced from `useTranslation()`.
|
||||
|
||||
#### Scenario: renders email + password inputs with required attribute
|
||||
|
||||
- **WHEN** `<LoginForm />` is mounted under an `<AuthProvider>`
|
||||
- **THEN** an `input[name="email"][required]` and `input[name="password"][required]` are present
|
||||
|
||||
#### Scenario: submit calls onSubmit override when supplied
|
||||
|
||||
- **WHEN** `<LoginForm onSubmit={spy} />` is mounted and the form is submitted with valid values
|
||||
- **THEN** `spy` is called once with `{ email, password, rememberMe }`
|
||||
|
||||
#### Scenario: OAuth provider button click invokes loginWithProvider
|
||||
|
||||
- **WHEN** a `LoginForm` rendered with `providers=[{id:"google",name:"Google"}]` has its Google button clicked
|
||||
- **THEN** the auth context's `loginWithProvider` is called once with `"google"`
|
||||
|
||||
**Verified by:** `src/components/login-form/login-form.test.tsx::"renders required email and password inputs"` AND `src/components/login-form/login-form.test.tsx::"submit invokes onSubmit override when provided"` AND `src/components/login-form/login-form.test.tsx::"OAuth provider button click routes through loginWithProvider"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: RegisterForm validates password + terms + name before submitting
|
||||
|
||||
The package SHALL ship a `RegisterForm` React component that validates locally before invoking `onSubmit?` or `useAuth().register(...)`. Validation rules: password length ≥ `minPasswordLength` (default 8); password equals confirmPassword; if `requireName`, name is non-empty; if `showTermsCheckbox`, terms checkbox is checked.
|
||||
|
||||
#### Scenario: password mismatch sets inline error and blocks submission
|
||||
|
||||
- **WHEN** the user submits with password "12345678" and confirmPassword "00000000"
|
||||
- **THEN** the form renders a `role="alert"` text matching `/match/i` and no auth call is made
|
||||
|
||||
#### Scenario: terms unchecked blocks submission when showTermsCheckbox is on
|
||||
|
||||
- **WHEN** the user submits without checking the terms checkbox
|
||||
- **THEN** the form renders an error matching `/terms/i` and no auth call is made
|
||||
|
||||
**Verified by:** `src/components/register-form/register-form.test.tsx::"password mismatch blocks submission with inline error"` AND `src/components/register-form/register-form.test.tsx::"unchecked terms checkbox blocks submission"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PasswordResetForm operates in request and confirm modes
|
||||
|
||||
The package SHALL ship a `PasswordResetForm` React component with `mode: "request" | "confirm"`. Request mode collects email and calls `useAuth().requestPasswordReset(email)`. Confirm mode collects new password + confirmation, validates min length + match, and calls `useAuth().resetPassword(token, password)`. If `mode === "confirm"` and `token` is undefined, an inline `MISSING_RESET_TOKEN` error is shown and no auth call is made. On success a success state is rendered with an optional back-to-login affordance.
|
||||
|
||||
#### Scenario: request mode submits email and shows success state
|
||||
|
||||
- **WHEN** the request form is submitted with `email = "a@b.c"`
|
||||
- **THEN** `requestPasswordReset` is called once with `"a@b.c"` and the success copy is rendered
|
||||
|
||||
#### Scenario: confirm mode without token surfaces MISSING_RESET_TOKEN
|
||||
|
||||
- **WHEN** the confirm form is submitted with `token = undefined`
|
||||
- **THEN** the form renders a `role="alert"` containing the missing-token copy and `resetPassword` is NOT called
|
||||
|
||||
**Verified by:** `src/components/password-reset-form/password-reset-form.test.tsx::"request mode submits email and shows success state"` AND `src/components/password-reset-form/password-reset-form.test.tsx::"confirm mode without token surfaces MISSING_RESET_TOKEN"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: REACT_AUTH_WL error codes are registered and thrown
|
||||
|
||||
The package SHALL register the `REACT_AUTH_WL` error namespace via `defineCodes` and SHALL throw or surface each of these codes from at least one production code path: `BRANDING_FETCH_FAILED`, `BRANDING_INVALID_PAYLOAD`, `AUTH_NETWORK_FAILED`, `AUTH_REJECTED`, `MISSING_RESET_TOKEN`, `PROVIDER_OUTSIDE_TREE`.
|
||||
|
||||
#### Scenario: every registered code is thrown by ≥1 production file
|
||||
|
||||
- **WHEN** the test scans every `.ts`/`.tsx` non-test source file
|
||||
- **THEN** for each code in `REACT_AUTH_WL_CODES`, at least one production file matches the code literal in a throw or surface context
|
||||
|
||||
**Verified by:** `src/errors/error-codes.test.ts::"every REACT_AUTH_WL code is thrown by at least one production file"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: i18n every user-visible string
|
||||
|
||||
The package SHALL source every user-visible string through `useTranslation()` from `@suaveplan/react-i18n` with the `{ defaultValue }` options-bag signature. No bare string literals appear in JSX text content inside form components.
|
||||
|
||||
#### Scenario: every form translates its title and submit button via useTranslation
|
||||
|
||||
- **WHEN** a form is rendered with no overriding `title`/`subtitle` prop
|
||||
- **THEN** the visible title and submit-button text come from a `t(key, { defaultValue })` call
|
||||
|
||||
**Verified by:** `src/components/login-form/login-form.test.tsx::"title and submit copy resolve through useTranslation"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
Reference in New Issue
Block a user