first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
# `<AuthProvider>` and `useAuth` / `useUser` / `useIsAuthenticated`
## Purpose
`auth-context.tsx` ships the `<AuthProvider>` React context provider together
with the three hooks `useAuth`, `useUser`, and `useIsAuthenticated`. The
provider wires a tenant-aware auth backend through an injectable `fetch`
(`FetchLike`) and an injectable `navigate(url)` seam so consumers can plug
real backends in production and stub them in tests without `vi.mock`. Every
verb — `login`, `loginWithProvider`, `register`, `logout`,
`requestPasswordReset`, `resetPassword` — is surfaced on the context value
returned by `useAuth()` and translates HTTP failures into stable, code-tagged
errors via `ReactAuthWhitelabelError`.
## Features
- Injectable `fetch: FetchLike` for testability — defaults to
`globalThis.fetch`.
- Injectable `navigate(url)` seam — defaults to `window.location.href = url`
in browsers, no-op in non-browser environments.
- Session probe on mount unless `initialUser` is supplied (SSR/hydration).
- Network failures → `REACT_AUTH_WL.AUTH_NETWORK_FAILED`.
- Non-ok responses → `REACT_AUTH_WL.AUTH_REJECTED` with the backend's
`message` field threaded through when present.
- Hooks called outside the provider throw
`REACT_AUTH_WL.PROVIDER_OUTSIDE_TREE`.
## Usage: Basic
```tsx
<AuthProvider config={{ authUrl: "/api/auth" }}>
<App />
</AuthProvider>
```
```tsx
const { login, user, isAuthenticated } = useAuth();
await login("a@b.c", "pw");
```
## Usage: Advanced
```tsx
const config: AuthConfig = {
authUrl: "https://auth.acme.com",
redirectUrl: "/dashboard",
fetch: customFetch, // testable seam
navigate: (url) => router.navigate(url),
providers: [{ id: "google", name: "Google" }],
};
<AuthProvider config={config} initialUser={ssrUser}>
<Routes>...</Routes>
</AuthProvider>
```
## API Reference
- `AuthProvider({ config, initialUser?, children })` — context provider.
- `useAuth()` — returns `AuthContextValue` (`user`, `isLoading`,
`isAuthenticated`, `error`, `login`, `loginWithProvider`, `register`,
`logout`, `requestPasswordReset`, `resetPassword`).
- `useUser()` — returns `AuthUser | null`.
- `useIsAuthenticated()` — returns `boolean`.
## Implementation Notes
The session probe in `useEffect` swallows transport errors on purpose — a
session check that fails should not surface as a visible error to the user
(they simply remain anonymous). Verbs called from the UI translate failures
to typed errors AND surface them on the context's `error` state so a single
error region can render the current failure without each call site
re-implementing the same try/catch. Every verb sets `isLoading` true/false
around the network call so consumers can disable submit buttons via
`isLoading || isSubmitting`.

View File

@@ -0,0 +1,63 @@
# `auth-http.ts` — fetch + navigate seam helpers
## Purpose
`auth-http.ts` houses the three helpers every `AuthProvider` verb shares:
`resolveFetch`, `resolveNavigate`, and `postJson`. Each helper exists so the
`AuthProvider` body stays under the 300-LOC budget while keeping the
network-translation policy in a single, testable file. Failures from the
configured `fetch` are translated to `REACT_AUTH_WL.AUTH_NETWORK_FAILED`;
non-ok responses are translated to `REACT_AUTH_WL.AUTH_REJECTED` with the
backend-supplied `message` field threaded through the i18n message key when
present.
## Features
- `resolveFetch(config)` — pick the injected `fetch` over `globalThis.fetch`.
- `resolveNavigate(config)` — pick the injected `navigate(url)` over the
default `window.location.href` writer.
- `postJson(fetchImpl, url, body, rejectMessage, options?)` — single helper
for every JSON POST, with error translation, optional credentials, and
best-effort `.json()` recovery on failure responses.
## Usage: Basic
```ts
const fetchImpl = resolveFetch(config);
const navigate = resolveNavigate(config);
const data = await postJson(fetchImpl, url, body, "ui.label.fallback_msg");
```
## Usage: Advanced
```ts
try {
await postJson(fetchImpl, `${config.authUrl}/login`, { email, password },
"ui.login.rejected", { credentials: "include" });
} catch (err) {
if (err.code === REACT_AUTH_WL_CODES.AUTH_NETWORK_FAILED) {
showOfflineBanner();
} else if (err.code === REACT_AUTH_WL_CODES.AUTH_REJECTED) {
focusErrorRegion();
}
throw err;
}
```
## API Reference
- `resolveFetch(config: AuthConfig): FetchLike` — never throws; falls back to
`globalThis.fetch` when the config omits one.
- `defaultNavigate(url: string): void` — no-op when `window` is undefined.
- `resolveNavigate(config: AuthConfig): (url: string) => void` — picks the
injected seam or the default.
- `postJson(fetchImpl, url, body, rejectMessage, options?)` — returns the
parsed JSON body. Errors are subclasses of `ReactAuthWhitelabelError`.
## Implementation Notes
`postJson` swallows `.json()` parse errors on failure responses on purpose:
some backends return an empty 401/403 body, in which case the `rejectMessage`
fallback is the user-visible label. The `.json()` parse on success is also
swallowed and returns `{}` so a 204-style "no content" response keeps a
clean caller signature.

View File

@@ -0,0 +1,79 @@
# `<BrandingProvider>` and tenant branding hooks
## Purpose
`branding-context.tsx` ships the `<BrandingProvider>` React context provider
together with the hooks `useBranding`, `useTenantBranding`, and `useLogo`.
The provider's job is to resolve a `TenantBranding` payload (either static
via prop or fetched from a configurable endpoint) and to project that
payload onto two surfaces: (1) the `@suaveplan/react-ui` runtime
theme-token layer for every `--brand-*` CSS custom property, and (2) the
non-CSS DOM chrome — `document.title`, `<link rel="icon">`, and the tenant
custom-CSS `<style>` injection. The CSS path goes through
`useThemeMutator().setExtensionTokens(...)` so per-tenant tokens stack on
top of the active chassis theme exactly like any other mutator layer, with
automatic cleanup on unmount.
## Features
- Static branding via prop OR endpoint fetch via `brandingEndpoint` (+
optional `/tenantId` segment).
- Injectable `fetch: FetchLike` for testability.
- Zod validation (`TenantBrandingSchema.safeParse`) on every fetched
payload — Zod failures surface as `BRANDING_INVALID_PAYLOAD`.
- Network failures (transport throw or non-ok response) surface as
`BRANDING_FETCH_FAILED`.
- `--brand-primary`, `--brand-secondary`, `--brand-accent`, `--brand-font`
written via `setExtensionTokens` — cleanup is automatic on unmount.
- `document.title`, favicon `<link>`, and tenant `<style>` injection are
direct DOM writes (not CSS variables; no conflict with the mutator).
## Usage: Basic
```tsx
import { ThemeProvider, lightTheme } from "@suaveplan/react-ui/theme";
import { BrandingProvider } from "@suaveplan/react-auth-whitelabel";
<ThemeProvider themes={[lightTheme]} defaultTheme="light">
<BrandingProvider staticBranding={tenantBranding}>
<App />
</BrandingProvider>
</ThemeProvider>
```
## Usage: Advanced
```tsx
<BrandingProvider
brandingEndpoint="/api/branding"
tenantId={tenantSlug}
fetch={customFetch}
>
<App />
</BrandingProvider>
```
```tsx
const branding = useTenantBranding();
const lightLogo = useLogo(false);
const darkLogo = useLogo(true);
```
## API Reference
- `BrandingProvider(props)` — props include `brandingEndpoint?`,
`tenantId?`, `staticBranding?`, `fetch?`, `children`.
- `useBranding()` — returns `{ branding, isLoading, error, refresh }`.
- `useTenantBranding()` — returns the current `TenantBranding`.
- `useLogo(preferDark?)` — returns the most-appropriate logo URL or
`undefined`.
## Implementation Notes
The mutator effect runs on every `branding` change AND on every
`mutator` identity change. The mutator handle is stable across re-renders
(it's `useMemo`-wrapped inside `@suaveplan/react-ui`), so the effect only
re-fires when the branding payload actually changes. The cleanup
contract for `--brand-*` is delegated to `useThemeMutator` — when the
provider unmounts, the runtime invokes the mutator's `clear()` automatically,
removing every `--brand-*` declaration from `document.documentElement.style`.