@dloizides/auth-web 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,120 @@
1
+ # Changelog
2
+
3
+ ## 1.2.0 (2026-05-22)
4
+
5
+ Additive release for Phase 3d of the unified-auth plan — the event-scoped PIN
6
+ UI. Adds the native, branded event-PIN login surface so an app (Kefi) can offer
7
+ "sign in with your event PIN" for operational staff (door / DJ / media) without
8
+ the user ever bouncing to Keycloak's hosted UI. No breaking changes; every
9
+ 1.1.0 export is unchanged.
10
+
11
+ ### Added
12
+
13
+ - **`<PinForm>`** — the ready-made, themeable single-step event-PIN login form,
14
+ built on `usePinLogin`. A PIN field + a "sign in" button; the
15
+ `eventExternalId` is a prop (the event context comes from the route/page, it
16
+ is never typed by the user). Styled by the same `AuthTheme` token bag as the
17
+ other forms; copy is supplied through the i18n-agnostic `PinFormLabels` bag
18
+ (English `DEFAULT_PIN_LABELS` fallback). `onSuccess` hands back the signed-in
19
+ `BffUser`.
20
+ - **`usePinLogin`** — the headless PIN hook (the custom-layout escape hatch). A
21
+ single-step flow: `submit(pin)` → `POST /bff/pin/login` → `BffUser`, with
22
+ `reset()`. Mirrors `useOtpLogin` (mounted-guard, memoised result,
23
+ latest-`eventExternalId` ref).
24
+ - **`PinFormLabels`** + **`DEFAULT_PIN_LABELS`** — the PIN label bag.
25
+ - New `AuthTestIds` entries for every PIN interactive element.
26
+ - Re-exports `BffPinLoginRequest` from `@dloizides/auth-client`.
27
+
28
+ ### Changed
29
+
30
+ - Peer dependency `@dloizides/auth-client` raised to `>=3.2.0` — the version
31
+ that adds `BffAuthClient.pinLogin`.
32
+
33
+ ## 1.1.0 (2026-05-22)
34
+
35
+ Additive release for Phase 2d of the unified-auth plan — the email-OTP UI. Adds
36
+ the native, branded email-OTP login surface so an app can offer "sign in with a
37
+ code" without the user ever bouncing to Keycloak's hosted UI. No breaking
38
+ changes; every 1.0.0 export is unchanged.
39
+
40
+ ### Added
41
+
42
+ - **`<OtpForm>`** — the ready-made, themeable two-step email-OTP login form,
43
+ built on `useOtpLogin`. Step 1 collects the email and sends a code; step 2
44
+ collects the code, verifies it, and offers "resend code" and "use a different
45
+ email". Styled by the same `AuthTheme` token bag as the other forms; copy is
46
+ supplied through the i18n-agnostic `OtpFormLabels` bag (English
47
+ `DEFAULT_OTP_LABELS` fallback). `onSuccess` hands back the signed-in `BffUser`.
48
+ - **`useOtpLogin`** — the headless OTP hook (the custom-layout escape hatch). A
49
+ small forward state machine over `OtpLoginStep`: `requestCode(identifier)` →
50
+ `EnterCode`, then `verifyCode(otp)` → `BffUser`, with `resend()` and
51
+ `reset()`. Mirrors `useBffAuth` (mounted-guard, memoised result).
52
+ - **`OtpLoginStep`** — the `RequestCode` / `EnterCode` step enum.
53
+ - **`OtpFormLabels`** + **`DEFAULT_OTP_LABELS`** — the OTP label bag.
54
+ - New `AuthTestIds` entries for every OTP interactive element.
55
+ - Re-exports `BffOtpRequestRequest`, `BffOtpVerifyRequest` and
56
+ `BffOtpRequestResult` from `@dloizides/auth-client`.
57
+
58
+ ### Changed
59
+
60
+ - Peer dependency `@dloizides/auth-client` raised to `>=3.1.0` — the version
61
+ that ships `BffAuthClient.requestOtp` / `.verifyOtp` (`POST /bff/otp/request`
62
+ and `POST /bff/otp/verify`).
63
+
64
+ ### Notes
65
+
66
+ - Still strictly thin: UI + the same-origin `/bff/*` client. The OTP client
67
+ methods carry the `X-BFF-Csrf` header like every other state-changing call;
68
+ no token handling, no secrets in the browser.
69
+ - This release adds the OTP surface to the package only. Wiring an app onto it
70
+ (the Kefi pilot) is Phase 3.
71
+
72
+ ## 1.0.0 (2026-05-22)
73
+
74
+ Initial release — the frontend half of the unified-auth plan (Phase 1b).
75
+
76
+ `@dloizides/auth-web` is the shared, themeable, branded auth UI for the
77
+ dloizides.com portfolio. It is extracted and generalised from the per-app auth
78
+ code that lived inside `apps/katalogos-web` so katalogos, erevna and kefi can
79
+ share one implementation instead of copy-pasting auth adapters.
80
+
81
+ ### Added
82
+
83
+ - **Themeable components**
84
+ - `<LoginForm>` — password login, built on `useBffAuth`.
85
+ - `<ForgotPasswordForm>` — request-a-reset-link, built on
86
+ `useBffForgotPassword`; no-enumeration confirmation state.
87
+ - `<ResetPasswordForm>` — choose-a-new-password, built on
88
+ `useResetPasswordForm`.
89
+ - All three are styled by an `AuthTheme` design-token bag so visually
90
+ distinct apps share one implementation.
91
+ - **Theming** — `AuthThemeProvider`, `useAuthTheme`, `defaultAuthTheme`, and
92
+ the `AuthTheme` token contract (colors / radii / spacing / typography).
93
+ Precedence: explicit `theme` prop → `<AuthThemeProvider>` context → default.
94
+ - **i18n-agnostic label bags** — `LoginFormLabels`, `ForgotPasswordFormLabels`,
95
+ `ResetPasswordFormLabels` plus English `DEFAULT_*` constants. The package
96
+ ships no `t()` / `FM()`; consuming apps pass already-localised strings.
97
+ - **Headless hooks** (the custom-layout escape hatch)
98
+ - `useBffAuth` — login / logout / `/bff/me` session, lifecycle status.
99
+ - `useBffForgotPassword`, `useBffResetPassword` — React Query mutations.
100
+ - `useResetPasswordForm` — reset-password form-state + validation logic.
101
+ - **`createBffAuthClient`** — one-line wiring of a same-origin `BffAuthClient`
102
+ with a lazily-resolved `fetch` (load-time side-effect-free).
103
+ - **Role-based post-login router** — `resolvePostLoginRoute(user, table)` and
104
+ `collectUserRoles`. The consuming app supplies the role → route table.
105
+ - **Password policy** — `validatePasswordPolicy`, `isPasswordValid`,
106
+ `PasswordPolicyError`; mirrors the TenantService backend rules.
107
+ - **Test IDs** — `AuthTestIds` + `withTestIdPrefix` for E2E targeting.
108
+ - Re-exports `BffAuthClient`, `createFetchHttpClient` and the BFF request /
109
+ response types from `@dloizides/auth-client` so consumers have a single
110
+ import surface.
111
+
112
+ ### Notes
113
+
114
+ - Strictly thin: UI + a same-origin `/bff/*` fetch client + the router helper.
115
+ **No secrets, no token handling, no Keycloak calls** — the per-app BFF owns
116
+ all of that server-side.
117
+ - Built on `@dloizides/auth-client` (peer dependency `>=3.0.0`), which ships
118
+ `BffAuthClient`.
119
+ - This release creates the package only. Porting `apps/katalogos-web` onto it
120
+ is Phase 1c.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dloizides
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,229 @@
1
+ # @dloizides/auth-web
2
+
3
+ Themeable, branded auth UI for the dloizides.com portfolio — the frontend half
4
+ of the unified-auth plan.
5
+
6
+ Every app gets a **native, branded login experience**: the login / forgot /
7
+ reset forms live inside the app's own frontend; the user is **never redirected
8
+ to Keycloak's hosted login UI**. All credential exchange happens server-side in
9
+ a per-app **BFF** (`bff-katalogos`, `bff-erevna`, ...). This package talks only
10
+ to a same-origin `/bff/*` — **no secrets, no token handling, no Keycloak calls
11
+ in the browser.**
12
+
13
+ Built on [`@dloizides/auth-client`](https://www.npmjs.com/package/@dloizides/auth-client),
14
+ which provides the lower-level `BffAuthClient`.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install @dloizides/auth-web
20
+ ```
21
+
22
+ Peer dependencies: `react`, `react-native` (optional), `@tanstack/react-query`,
23
+ `@dloizides/auth-client` (`>=3.0.0`).
24
+
25
+ ## Two ways to consume it
26
+
27
+ ### 1. Ready-made themeable components
28
+
29
+ ```tsx
30
+ import {
31
+ AuthThemeProvider,
32
+ LoginForm,
33
+ createBffAuthClient,
34
+ resolvePostLoginRoute,
35
+ } from '@dloizides/auth-web';
36
+
37
+ import { katalogosAuthTheme } from './theme'; // your AuthTheme token bag
38
+ import { roleRoutes } from './roleRoutes'; // your RoleRouteTable
39
+ import { authLabels } from './authLabels'; // your localised labels
40
+
41
+ const client = createBffAuthClient(); // same-origin /bff/*
42
+
43
+ function LoginScreen() {
44
+ const router = useRouter();
45
+ return (
46
+ <AuthThemeProvider theme={katalogosAuthTheme}>
47
+ <LoginForm
48
+ client={client}
49
+ labels={authLabels.login}
50
+ onForgotPassword={() => router.push('/forgot-password')}
51
+ onSuccess={(user) => {
52
+ const route = resolvePostLoginRoute(user, roleRoutes);
53
+ router.replace(route ?? '/no-access');
54
+ }}
55
+ />
56
+ </AuthThemeProvider>
57
+ );
58
+ }
59
+ ```
60
+
61
+ `<ForgotPasswordForm>` and `<ResetPasswordForm>` follow the same shape.
62
+
63
+ ### Email-OTP login — `<OtpForm>`
64
+
65
+ A native, branded "sign in with a code" surface — the user is never bounced to
66
+ Keycloak's hosted UI. It is a two-step form: step 1 collects the email and asks
67
+ the BFF to email a one-time code; step 2 collects the code, verifies it, and
68
+ offers "resend code" / "use a different email".
69
+
70
+ ```tsx
71
+ import { OtpForm, createBffAuthClient, resolvePostLoginRoute } from '@dloizides/auth-web';
72
+
73
+ const client = createBffAuthClient();
74
+
75
+ function OtpLoginScreen() {
76
+ const router = useRouter();
77
+ return (
78
+ <OtpForm
79
+ client={client}
80
+ labels={authLabels.otp}
81
+ onSuccess={(user) => {
82
+ const route = resolvePostLoginRoute(user, roleRoutes);
83
+ router.replace(route ?? '/no-access');
84
+ }}
85
+ />
86
+ );
87
+ }
88
+ ```
89
+
90
+ `<OtpForm>` POSTs to the same-origin `/bff/otp/request` and `/bff/otp/verify`
91
+ endpoints (added in `Bff.AspNetCore`). The BFF runs the OTP direct-grant against
92
+ Keycloak server-side; the browser receives only the httpOnly session cookie.
93
+
94
+ ### Event-PIN login — `<PinForm>`
95
+
96
+ A native, branded "sign in with your event PIN" surface for operational staff
97
+ (door / DJ / media on Kefi). It is a single-step form: a PIN field + a "sign
98
+ in" button. The `eventExternalId` is a prop — the event context comes from the
99
+ route/page, never typed by the user. The `(event, pin)` pair alone identifies
100
+ the staff member; no username/password ever leaves the browser.
101
+
102
+ ```tsx
103
+ import { PinForm, createBffAuthClient, resolvePostLoginRoute } from '@dloizides/auth-web';
104
+
105
+ const client = createBffAuthClient();
106
+
107
+ function PinLoginScreen({ eventExternalId }: { eventExternalId: string }) {
108
+ const router = useRouter();
109
+ return (
110
+ <PinForm
111
+ client={client}
112
+ eventExternalId={eventExternalId}
113
+ labels={authLabels.pin}
114
+ onSuccess={(user) => {
115
+ const route = resolvePostLoginRoute(user, roleRoutes);
116
+ router.replace(route ?? '/no-access');
117
+ }}
118
+ />
119
+ );
120
+ }
121
+ ```
122
+
123
+ `<PinForm>` POSTs to the same-origin `/bff/pin/login` endpoint (added in
124
+ `Bff.AspNetCore`). The BFF runs the event-scoped PIN direct-grant against
125
+ Keycloak server-side; the browser receives only the httpOnly session cookie.
126
+
127
+ ### 2. Headless hooks (custom layout)
128
+
129
+ ```tsx
130
+ import { useBffAuth, createBffAuthClient } from '@dloizides/auth-web';
131
+
132
+ const client = createBffAuthClient();
133
+
134
+ function CustomLogin() {
135
+ const { login, isSubmitting, error } = useBffAuth({ client, probeOnMount: false });
136
+ // ...render your own form, call login({ username, password })
137
+ }
138
+ ```
139
+
140
+ For a custom OTP layout, `useOtpLogin` exposes the two-step machine:
141
+
142
+ ```tsx
143
+ import { useOtpLogin, OtpLoginStep, createBffAuthClient } from '@dloizides/auth-web';
144
+
145
+ const client = createBffAuthClient();
146
+
147
+ function CustomOtpLogin() {
148
+ const otp = useOtpLogin({ client });
149
+ // step 1: otp.requestCode(email) → otp.step becomes OtpLoginStep.EnterCode
150
+ // step 2: otp.verifyCode(code) → resolves to the signed-in BffUser
151
+ // otp.resend() / otp.reset() for the step-2 affordances
152
+ }
153
+ ```
154
+
155
+ For a custom PIN layout, `usePinLogin` exposes the single-step flow:
156
+
157
+ ```tsx
158
+ import { usePinLogin, createBffAuthClient } from '@dloizides/auth-web';
159
+
160
+ const client = createBffAuthClient();
161
+
162
+ function CustomPinLogin({ eventExternalId }: { eventExternalId: string }) {
163
+ const pin = usePinLogin({ client, eventExternalId });
164
+ // pin.submit(pinValue) → resolves to the signed-in BffUser
165
+ // pin.reset() → clears the error
166
+ }
167
+ ```
168
+
169
+ ## Theming
170
+
171
+ The package owns no brand. Each app maps its own theme system onto the flat
172
+ `AuthTheme` token bag (`colors`, `radii`, `spacing`, `typography`) and supplies
173
+ it via `<AuthThemeProvider>` or a `theme` prop on an individual component.
174
+ Precedence: **prop → context → `defaultAuthTheme`**.
175
+
176
+ ```ts
177
+ import { defaultAuthTheme, type AuthTheme } from '@dloizides/auth-web';
178
+
179
+ export const katalogosAuthTheme: AuthTheme = {
180
+ ...defaultAuthTheme,
181
+ colors: { ...defaultAuthTheme.colors, primary: '#c2410c' },
182
+ };
183
+ ```
184
+
185
+ Because all three forms share one `useAuthStyles` token-to-style mapping,
186
+ re-theming `<LoginForm>` automatically re-themes the others.
187
+
188
+ ## Internationalisation
189
+
190
+ `@dloizides/auth-web` ships **no** i18n framework. Every user-facing string is
191
+ supplied through a typed `labels` prop. Apps pass strings already localised
192
+ with their own `FM()` / `t()`. Each label bag is partial — unspecified keys
193
+ fall back to the English `DEFAULT_*` constants.
194
+
195
+ ## Role-based post-login routing
196
+
197
+ ```ts
198
+ import { resolvePostLoginRoute, type RoleRouteTable } from '@dloizides/auth-web';
199
+
200
+ const roleRoutes: RoleRouteTable = {
201
+ routes: [
202
+ { role: 'superUser', route: '/admin/super' },
203
+ { role: 'admin', route: '/admin' },
204
+ { role: 'user', route: '/dashboard' },
205
+ ],
206
+ fallback: '/no-access',
207
+ };
208
+
209
+ // The first table entry whose role the user holds wins — list most privileged first.
210
+ const route = resolvePostLoginRoute(user, roleRoutes);
211
+ ```
212
+
213
+ ## API surface
214
+
215
+ | Export | Kind |
216
+ |--------|------|
217
+ | `LoginForm`, `ForgotPasswordForm`, `ResetPasswordForm`, `OtpForm`, `PinForm` | Components |
218
+ | `AuthThemeProvider`, `useAuthTheme`, `defaultAuthTheme`, `AuthTheme` | Theming |
219
+ | `DEFAULT_LOGIN_LABELS`, `DEFAULT_FORGOT_PASSWORD_LABELS`, `DEFAULT_RESET_PASSWORD_LABELS`, `DEFAULT_OTP_LABELS`, `DEFAULT_PIN_LABELS` | Label bags |
220
+ | `useBffAuth`, `useBffForgotPassword`, `useBffResetPassword`, `useResetPasswordForm`, `useOtpLogin`, `usePinLogin` | Headless hooks |
221
+ | `OtpLoginStep` | OTP step enum |
222
+ | `createBffAuthClient`, `BffAuthClient` (re-export) | Client |
223
+ | `resolvePostLoginRoute`, `collectUserRoles`, `RoleRouteTable` | Router |
224
+ | `validatePasswordPolicy`, `isPasswordValid`, `PasswordPolicyError` | Password policy |
225
+ | `AuthTestIds`, `withTestIdPrefix` | Test IDs |
226
+
227
+ ## License
228
+
229
+ MIT