@nsxbet/admin-sdk 0.7.0 → 0.8.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.
Files changed (76) hide show
  1. package/CHECKLIST.md +48 -13
  2. package/README.md +24 -74
  3. package/dist/auth/client/bff.d.ts +38 -0
  4. package/dist/auth/client/bff.js +270 -0
  5. package/dist/auth/client/in-memory.d.ts +1 -1
  6. package/dist/auth/client/in-memory.js +2 -2
  7. package/dist/auth/client/index.d.ts +1 -1
  8. package/dist/auth/client/index.js +2 -2
  9. package/dist/auth/client/interface.d.ts +4 -4
  10. package/dist/auth/client/interface.js +1 -1
  11. package/dist/auth/client/private-network-guidance.d.ts +2 -0
  12. package/dist/auth/client/private-network-guidance.js +38 -0
  13. package/dist/auth/components/LoginPage.d.ts +8 -0
  14. package/dist/auth/components/LoginPage.js +32 -0
  15. package/dist/auth/components/UserSelector.d.ts +29 -0
  16. package/dist/auth/components/UserSelector.js +38 -10
  17. package/dist/auth/components/UserSelector.stories.d.ts +9 -0
  18. package/dist/auth/components/UserSelector.stories.js +70 -0
  19. package/dist/auth/components/index.d.ts +2 -0
  20. package/dist/auth/components/index.js +1 -0
  21. package/dist/auth/index.d.ts +3 -2
  22. package/dist/auth/index.js +2 -2
  23. package/dist/components/AuthProvider.d.ts +3 -3
  24. package/dist/components/AuthProvider.js +35 -17
  25. package/dist/env.d.ts +17 -0
  26. package/dist/env.js +50 -0
  27. package/dist/hooks/useAuth.d.ts +3 -3
  28. package/dist/hooks/useAuth.js +26 -21
  29. package/dist/hooks/useCallbackRef.d.ts +7 -0
  30. package/dist/hooks/useCallbackRef.js +14 -0
  31. package/dist/hooks/useFetch.js +6 -1
  32. package/dist/hooks/useI18n.js +2 -2
  33. package/dist/hooks/usePlatformAPI.d.ts +0 -3
  34. package/dist/hooks/usePlatformAPI.js +6 -4
  35. package/dist/i18n/config.d.ts +2 -1
  36. package/dist/i18n/config.js +4 -3
  37. package/dist/i18n/index.d.ts +1 -1
  38. package/dist/i18n/index.js +1 -1
  39. package/dist/i18n/locales/en-US.json +7 -0
  40. package/dist/i18n/locales/es.json +7 -0
  41. package/dist/i18n/locales/pt-BR.json +7 -0
  42. package/dist/i18n/locales/ro.json +7 -0
  43. package/dist/index.d.ts +7 -5
  44. package/dist/index.js +6 -2
  45. package/dist/registry/AdminShellRegistry.js +4 -3
  46. package/dist/registry/client/http.js +6 -1
  47. package/dist/registry/client/in-memory.js +20 -5
  48. package/dist/registry/types/manifest.d.ts +5 -0
  49. package/dist/registry/types/manifest.js +4 -1
  50. package/dist/registry/types/module.d.ts +6 -2
  51. package/dist/sdk-version.d.ts +5 -0
  52. package/dist/sdk-version.js +5 -0
  53. package/dist/shell/AdminShell.d.ts +12 -9
  54. package/dist/shell/AdminShell.js +56 -70
  55. package/dist/shell/components/ModuleOverview.js +1 -5
  56. package/dist/shell/components/RegistryPage.js +1 -1
  57. package/dist/shell/components/TopBar.js +2 -2
  58. package/dist/shell/components/theme-provider.js +6 -8
  59. package/dist/shell/index.d.ts +1 -1
  60. package/dist/shell/polling-config.d.ts +4 -3
  61. package/dist/shell/polling-config.js +11 -9
  62. package/dist/shell/types.d.ts +3 -1
  63. package/dist/types/platform.d.ts +2 -11
  64. package/dist/vite/config.d.ts +4 -9
  65. package/dist/vite/config.js +85 -27
  66. package/dist/vite/index.d.ts +1 -1
  67. package/dist/vite/index.js +1 -1
  68. package/dist/vite/plugins.js +6 -1
  69. package/package.json +20 -6
  70. package/scripts/write-sdk-version.mjs +21 -0
  71. package/dist/auth/client/keycloak.d.ts +0 -18
  72. package/dist/auth/client/keycloak.js +0 -129
  73. package/dist/shell/BackofficeShell.d.ts +0 -37
  74. package/dist/shell/BackofficeShell.js +0 -339
  75. package/dist/types/keycloak.d.ts +0 -25
  76. package/dist/types/keycloak.js +0 -1
package/CHECKLIST.md CHANGED
@@ -24,20 +24,28 @@
24
24
  - [ ] `src/i18n/locales/pt-BR.json` — Portuguese (Brazil) translations
25
25
  - [ ] `src/i18n/locales/es.json` — Spanish translations
26
26
  - [ ] `src/i18n/locales/ro.json` — Romanian translations
27
- - [ ] `.env.staging` — Gateway environment config (staging mode)
27
+ - [ ] `.env` / `.env.development` and/or `.env.staging` — gateway / runtime env (see [Environment Configuration](#environment-configuration))
28
28
 
29
29
  ## Environment Configuration
30
30
 
31
- - [ ] `.env.staging` exists with `VITE_ADMIN_GATEWAY_URL` configured
32
- - [ ] `dev` script uses `--mode staging` to load `.env.staging` by default
31
+ - [ ] `ADMIN_GATEWAY_URL` is set in `.env`, `.env.development`, or `.env.staging` (use `.env.staging` when your `dev` script runs with `--mode staging`)
32
+ - [ ] `dev` script uses `--mode staging` to load `.env.staging` by default (non-Lovable flow; Lovable can rely on `adminModule()` defaults — see below)
33
33
 
34
- Expected `.env.staging`:
34
+ ### Runtime (`/env.js`)
35
+
36
+ - [ ] `index.html` includes `<script src="/env.js"></script>` before the app entry (`adminModule()` / `defineModuleConfig()` inject this when gateway injection is enabled)
37
+ - [ ] Shared runtime fields are read via `import { env } from "@nsxbet/admin-sdk"` — not `import.meta.env.VITE_*` for `ADMIN_GATEWAY_URL`, `MOCK_AUTH`, etc.
38
+
39
+ Expected env snippet (place in `.env`, `.env.development`, or `.env.staging` depending on your setup):
35
40
 
36
41
  ```bash
37
42
  # Gateway URL for BFF token integration (InMemory auth)
38
43
  # Fetches real signed JWTs from the admin gateway instead of mock tokens.
39
44
  # Staging: https://admin-bff-stg.nsx.dev | Homol: https://admin-bff-homol.nsx.dev | Local: http://localhost:8080
40
- VITE_ADMIN_GATEWAY_URL=https://admin-bff-stg.nsx.dev
45
+ ADMIN_GATEWAY_URL=https://admin-bff-stg.nsx.dev
46
+
47
+ # Optional: exposed on window.__ENV__ for app code
48
+ # MOCK_AUTH=true
41
49
  ```
42
50
 
43
51
  ## Module Manifest (`admin.module.json`)
@@ -57,13 +65,32 @@ Expected structure:
57
65
  ```json
58
66
  {
59
67
  "id": "@admin/my-module",
60
- "title": { "en-US": "My Module", "pt-BR": "Meu Módulo", "es": "Mi Módulo", "ro": "Modulul Meu" },
68
+ "title": {
69
+ "en-US": "My Module",
70
+ "pt-BR": "Meu Módulo",
71
+ "es": "Mi Módulo",
72
+ "ro": "Modulul Meu"
73
+ },
61
74
  "routeBase": "/my-module",
62
- "description": { "en-US": "My Module admin module", "pt-BR": "...", "es": "...", "ro": "..." },
75
+ "description": {
76
+ "en-US": "My Module admin module",
77
+ "pt-BR": "...",
78
+ "es": "...",
79
+ "ro": "..."
80
+ },
63
81
  "version": "1.0.0",
64
82
  "category": "Tools",
65
83
  "commands": [
66
- { "id": "home", "title": { "en-US": "Home", "pt-BR": "Início", "es": "Inicio", "ro": "Acasă" }, "route": "/my-module/home" }
84
+ {
85
+ "id": "home",
86
+ "title": {
87
+ "en-US": "Home",
88
+ "pt-BR": "Início",
89
+ "es": "Inicio",
90
+ "ro": "Acasă"
91
+ },
92
+ "route": "/my-module/home"
93
+ }
67
94
  ],
68
95
  "permissions": {
69
96
  "view": ["admin.my-module.view"],
@@ -108,6 +135,7 @@ Expected structure:
108
135
 
109
136
  - [ ] Uses `defineModuleConfig` from `@nsxbet/admin-sdk/vite` OR spreads `adminModule()` into an existing Vite config
110
137
  - [ ] Imports and uses a React plugin (`@vitejs/plugin-react` or `@vitejs/plugin-react-swc`)
138
+ - [ ] `adminModule()` / `defineModuleConfig()` serve `/env.js` in dev/preview (sets `window.__ENV__`) and inject the script tag when gateway injection is enabled; pass `gatewayUrl: null` to disable
111
139
 
112
140
  Expected (option A — full config):
113
141
 
@@ -196,6 +224,7 @@ export default App;
196
224
  - [ ] Uses `AdminShell` from `@nsxbet/admin-sdk`
197
225
  - [ ] Imports manifest and passes to `modules` prop
198
226
  - [ ] Permissions use namespace `admin.<module-name>.*`
227
+ - [ ] Does not pass `bff` on `AdminShell` — standalone module dev uses `createInMemoryAuthClient` and optional gateway JWTs; BFF/Okta cookie auth is for the platform shell in production
199
228
 
200
229
  Expected (simplified):
201
230
 
@@ -204,7 +233,7 @@ import { AdminShell } from "@nsxbet/admin-sdk";
204
233
  import manifest from "../admin.module.json";
205
234
  import { App } from "./App";
206
235
 
207
- <AdminShell modules={[manifest]} keycloak={config}>
236
+ <AdminShell modules={[manifest]} authClient={authClient}>
208
237
  <App />
209
238
  </AdminShell>
210
239
  ```
@@ -258,13 +287,14 @@ If your module was scaffolded by Lovable, the following differences apply:
258
287
 
259
288
  ### Environment Configuration (automatic)
260
289
 
261
- `adminModule()` automatically injects `VITE_ADMIN_GATEWAY_URL` with the staging gateway URL (`https://admin-bff-stg.nsx.dev`). No `.env.staging` file or `--mode staging` script is needed for Lovable projects.
290
+ `adminModule()` serves `/env.js` in dev/preview so `window.__ENV__` includes `ADMIN_GATEWAY_URL` (default staging gateway `https://admin-bff-stg.nsx.dev`). No `.env.staging` file or `--mode staging` script is needed for Lovable projects.
262
291
 
263
292
  - [ ] Do NOT create `.env.staging` — the Vite plugin handles it
264
293
  - [ ] Do NOT add `--mode staging` to the dev script
265
294
 
266
295
  To override the gateway URL, either:
267
- - Set `VITE_ADMIN_GATEWAY_URL` in a `.env` or `.env.local` file
296
+
297
+ - Set `ADMIN_GATEWAY_URL` in a `.env` or `.env.local` file
268
298
  - Pass `gatewayUrl` option: `...adminModule({ gatewayUrl: "http://localhost:8080" })`
269
299
  - Disable injection: `...adminModule({ gatewayUrl: null })`
270
300
 
@@ -314,7 +344,11 @@ import "./index.css";
314
344
  const moduleManifest = manifest as AdminModuleManifest;
315
345
 
316
346
  const mockUsers = createMockUsersFromRoles({
317
- admin: ["admin.mymodule.view", "admin.mymodule.edit", "admin.mymodule.delete"],
347
+ admin: [
348
+ "admin.mymodule.view",
349
+ "admin.mymodule.edit",
350
+ "admin.mymodule.delete",
351
+ ],
318
352
  editor: ["admin.mymodule.view", "admin.mymodule.edit"],
319
353
  viewer: ["admin.mymodule.view"],
320
354
  noAccess: [],
@@ -327,7 +361,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
327
361
  <AdminShell modules={[moduleManifest]} authClient={authClient}>
328
362
  <App />
329
363
  </AdminShell>
330
- </React.StrictMode>
364
+ </React.StrictMode>,
331
365
  );
332
366
  ```
333
367
 
@@ -341,3 +375,4 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
341
375
  - [ ] No raw `fetch()` or `window.fetch()` calls — use `useFetch()` from the SDK
342
376
  - [ ] No direct date formatting (`toLocaleDateString`, `toLocaleTimeString`, `Intl.DateTimeFormat`, dayjs/moment `.format()`, date-fns `format`) — use `<Timestamp />` or `useTimestamp()` from the SDK
343
377
  - [ ] Do NOT scaffold shadcn/ui components locally — use `@nsxbet/admin-ui` which ships them pre-configured
378
+ - [ ] No `import.meta.env.VITE_*` for runtime gateway/auth settings (`ADMIN_GATEWAY_URL`, `MOCK_AUTH`, etc.) — use `/env.js` and `import { env } from "@nsxbet/admin-sdk"`
package/README.md CHANGED
@@ -192,10 +192,6 @@ initI18n();
192
192
 
193
193
  const moduleManifest = manifest as AdminModuleManifest;
194
194
 
195
- // Check environment variable to toggle between mock auth and Keycloak
196
- const useKeycloak = import.meta.env.VITE_MOCK_AUTH === "false";
197
-
198
- // Create mock users with module-specific permissions
199
195
  const mockUsers = createMockUsersFromRoles({
200
196
  admin: ["admin.mymodule.view", "admin.mymodule.edit", "admin.mymodule.delete"],
201
197
  editor: ["admin.mymodule.view", "admin.mymodule.edit"],
@@ -203,25 +199,13 @@ const mockUsers = createMockUsersFromRoles({
203
199
  noAccess: [],
204
200
  });
205
201
 
206
- // Use in-memory auth by default (shows user selector), or Keycloak if configured
207
- const authClient = useKeycloak
208
- ? undefined
209
- : createInMemoryAuthClient({ users: mockUsers });
202
+ const authClient = createInMemoryAuthClient({ users: mockUsers });
210
203
 
211
204
  ReactDOM.createRoot(document.getElementById("root")!).render(
212
205
  <React.StrictMode>
213
206
  <AdminShell
214
207
  modules={[moduleManifest]}
215
208
  authClient={authClient}
216
- keycloak={
217
- useKeycloak
218
- ? {
219
- url: "http://localhost:8080",
220
- realm: "admin",
221
- clientId: "admin-shell",
222
- }
223
- : undefined
224
- }
225
209
  >
226
210
  <App />
227
211
  </AdminShell>
@@ -229,6 +213,8 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
229
213
  );
230
214
  ```
231
215
 
216
+ **Module standalone dev** uses the in-memory client and **UserSelector**, which fetches JWTs via **`GET {gateway}/auth/token`**. Do not pass **`bff`** here — BFF / Okta cookie auth is for the **platform shell** in production, not for local module dev servers.
217
+
232
218
  ### File: `src/App.tsx`
233
219
 
234
220
  ```tsx
@@ -468,7 +454,6 @@ TypeScript declarations for environment variables and platform API:
468
454
 
469
455
  declare global {
470
456
  interface ImportMetaEnv {
471
- readonly VITE_MOCK_AUTH?: string;
472
457
  readonly VITE_ALLOWED_MODULE_ORIGINS?: string;
473
458
  }
474
459
 
@@ -480,9 +465,6 @@ declare global {
480
465
  __ADMIN_PLATFORM_API__?: import("@nsxbet/admin-sdk").PlatformAPI;
481
466
  __ENV__?: {
482
467
  ENVIRONMENT: string;
483
- KEYCLOAK_URL: string;
484
- KEYCLOAK_REALM: string;
485
- KEYCLOAK_CLIENT_ID: string;
486
468
  };
487
469
  }
488
470
  }
@@ -514,10 +496,7 @@ Environment configuration for development:
514
496
 
515
497
  ```bash
516
498
  # Use mock authentication with user selector (default: true)
517
- VITE_MOCK_AUTH=true
518
-
519
- # Set to "false" to use Keycloak authentication
520
- # VITE_MOCK_AUTH=false
499
+ MOCK_AUTH=true
521
500
 
522
501
  # Module URL allowlist (shell mode only). Comma-separated patterns.
523
502
  # Supports *.domain wildcard (e.g. *.nsx.dev matches modules.nsx.dev, nsx.dev).
@@ -621,7 +600,7 @@ export default defineModuleConfig({
621
600
  });
622
601
  ```
623
602
 
624
- > **Important:** Each module must use a unique port. The default is `8080` (matching Lovable's default). Modules running alongside Keycloak (which also uses port 8080) should specify an explicit port. Standard assignments:
603
+ > **Important:** Each module must use a unique port. The default is `8080` (matching Lovable's default). Standard assignments:
625
604
  > - Shell: 3000
626
605
  > - API: 4000
627
606
  > - Modules: 3002, 3003, 3004, etc.
@@ -680,7 +659,7 @@ export default defineConfig(({ mode }) => ({
680
659
 
681
660
  ### Automatic Environment Injection
682
661
 
683
- `adminModule()` includes an env injection plugin that automatically sets `VITE_ADMIN_GATEWAY_URL` at build time. This ensures Lovable projects get real JWT tokens from the staging gateway without any manual `.env` configuration.
662
+ `adminModule()` includes a plugin that serves `/env.js` in dev/preview and injects `<script src="/env.js">` into `index.html`. The script sets `window.__ENV__` (including `ADMIN_GATEWAY_URL` from your `.env` or a default staging URL). The SDK reads this via `import { env } from "@nsxbet/admin-sdk"`.
684
663
 
685
664
  **Precedence order** (highest to lowest):
686
665
 
@@ -815,7 +794,7 @@ function MyComponent() {
815
794
 
816
795
  | Method | Returns | Description |
817
796
  |--------|---------|-------------|
818
- | `hasPermission(perm)` | boolean | Check if user has permission. Returns `false` during Keycloak initialization until auth completes. |
797
+ | `hasPermission(perm)` | boolean | Check if user has permission. Returns `false` during auth initialization until auth completes. |
819
798
  | `getUser()` | User | Get current user info |
820
799
  | `getAccessToken()` | Promise\<string\> | Get JWT token |
821
800
  | `logout()` | void | Log out user |
@@ -1015,8 +994,8 @@ The polling interval is resolved from `REGISTRY_POLL_INTERVAL` (milliseconds):
1015
994
 
1016
995
  | Source | Example |
1017
996
  |--------|---------|
1018
- | `window.__ENV__.REGISTRY_POLL_INTERVAL` | Docker runtime injection |
1019
- | `import.meta.env.VITE_REGISTRY_POLL_INTERVAL` | Vite env var |
997
+ | Explicit `registryPollInterval` prop on `AdminShell` | Shell passes from env config |
998
+ | `window.__ENV__.REGISTRY_POLL_INTERVAL` | Fallback for non-shell consumers |
1020
999
  | Environment default | `local`: 60s, `staging`: 5min, `production`: 15min |
1021
1000
 
1022
1001
  Set `REGISTRY_POLL_INTERVAL=0` to disable polling entirely.
@@ -1114,12 +1093,12 @@ const authClient = createInMemoryAuthClient({ users: customUsers });
1114
1093
  |--------|------|---------|-------------|
1115
1094
  | `users` | MockUser[] | **required** | Mock users available for selection |
1116
1095
  | `storageKey` | string | `"@nsxbet/auth"` | localStorage key for persistence |
1117
- | `gatewayUrl` | string \| null | `VITE_ADMIN_GATEWAY_URL` env var | Admin gateway URL for real JWT tokens |
1096
+ | `gatewayUrl` | string \| null | `null` | Admin gateway URL for real JWT tokens |
1118
1097
  | `tokenTimeout` | number | `5000` | Timeout in ms for gateway token fetch |
1119
1098
 
1120
1099
  ### Gateway Token Integration (BFF)
1121
1100
 
1122
- When `VITE_ADMIN_GATEWAY_URL` is set (or `gatewayUrl` is passed explicitly), the InMemory auth client fetches real signed JWTs from the admin gateway instead of returning mock token strings. This enables end-to-end testing against backends that validate tokens.
1101
+ When `gatewayUrl` is passed explicitly, the InMemory auth client fetches real signed JWTs from the admin gateway instead of returning mock token strings. This enables end-to-end testing against backends that validate tokens.
1123
1102
 
1124
1103
  **How it works:**
1125
1104
 
@@ -1131,11 +1110,10 @@ When `VITE_ADMIN_GATEWAY_URL` is set (or `gatewayUrl` is passed explicitly), the
1131
1110
 
1132
1111
  **Gateway URL resolution order:**
1133
1112
 
1134
- 1. Explicit `gatewayUrl` option passed to `createInMemoryAuthClient()`
1135
- 2. `import.meta.env.VITE_ADMIN_GATEWAY_URL` environment variable
1136
- 3. If neither is set, BFF integration is disabled (mock tokens, current behavior)
1113
+ 1. Explicit `gatewayUrl` option passed to `createInMemoryAuthClient()` (or `null` to disable gateway tokens)
1114
+ 2. Otherwise `env.adminGatewayUrl` from `window.__ENV__` (set by `/env.js` from the shell or the `adminModule()` dev server)
1137
1115
 
1138
- > **Vite plugin auto-injection:** When using `adminModule()` or `defineModuleConfig()` from `@nsxbet/admin-sdk/vite`, the `VITE_ADMIN_GATEWAY_URL` environment variable is automatically set to the staging gateway URL. No `.env.staging` file or `--mode staging` script is needed. See the [Vite Configuration](#vite-configuration) section for override options.
1116
+ > **Vite plugin:** When using `adminModule()` or `defineModuleConfig()`, `/env.js` is served in dev and lists `ADMIN_GATEWAY_URL` from your `.env` or the default staging gateway. See the [Vite Configuration](#vite-configuration) section for override options (`gatewayUrl: null` disables `/env.js` entirely).
1139
1117
 
1140
1118
  **Error handling:**
1141
1119
 
@@ -1146,7 +1124,7 @@ The UserSelector shows loading, error, and timeout states during the token fetch
1146
1124
 
1147
1125
  ```bash
1148
1126
  # Add to your .env file to enable gateway token integration
1149
- VITE_ADMIN_GATEWAY_URL=https://admin-bff-stg.nsx.dev
1127
+ ADMIN_GATEWAY_URL=https://admin-bff-stg.nsx.dev
1150
1128
  ```
1151
1129
 
1152
1130
  ## Module URL Allowlist (Shell Mode)
@@ -1159,53 +1137,25 @@ When the shell loads modules dynamically from URLs, it validates each URL agains
1159
1137
 
1160
1138
  **Production:** Set `VITE_ALLOWED_MODULE_ORIGINS` in your build environment. If unset or empty, all module loads fail with a clear error.
1161
1139
 
1162
- ## Keycloak Configuration (Production Auth)
1140
+ ## BFF / Okta cookie auth (recommended)
1163
1141
 
1164
- For production or when testing with real authentication, configure Keycloak:
1142
+ Production auth should go through **admin-bff**: the BFF sets an HttpOnly `access_token` cookie and exposes **`GET /me`** for identity. The shell does not read the JWT in the browser.
1165
1143
 
1166
- ### Prerequisites
1167
-
1168
- 1. Keycloak server running (default: `http://localhost:8080`)
1169
- 2. Realm named `admin` created
1170
- 3. Client named `admin-shell` configured with:
1171
- - Client Protocol: `openid-connect`
1172
- - Access Type: `public`
1173
- - Valid Redirect URIs: `http://localhost:3003/*`
1174
-
1175
- ### Enable Keycloak in Development
1176
-
1177
- Set the environment variable in `.env.local`:
1178
-
1179
- ```bash
1180
- VITE_MOCK_AUTH=false
1181
- ```
1182
-
1183
- Or pass the `keycloak` prop directly without `authClient`:
1144
+ ### AdminShell
1184
1145
 
1185
1146
  ```tsx
1186
1147
  <AdminShell
1187
1148
  modules={[manifest]}
1188
- keycloak={{
1189
- url: "http://localhost:8080",
1190
- realm: "admin",
1191
- clientId: "admin-shell",
1192
- }}
1193
- >
1194
- <App />
1195
- </AdminShell>
1149
+ bff
1150
+ // or: bff={{ baseUrl: "https://admin.example.com" }}
1151
+ />
1196
1152
  ```
1197
1153
 
1198
- ### Mock Auth Production Guard
1199
-
1200
- In production builds, AdminShell requires authentication configuration. If you omit both `authClient` and `keycloak` props, the shell throws an error unless you explicitly set `VITE_MOCK_AUTH=true`. When mock auth is used in production with `VITE_MOCK_AUTH=true`, a console warning is logged. Always use Keycloak (or another real auth client) for production deployments.
1154
+ Use the same origin as the BFF (or a dev proxy) so `credentials: 'include'` sends the cookie. **`getAccessToken()`** returns **`null`** for this client — use **`useFetch`** / **`api.fetch`**, which send credentialed requests when the token is null.
1201
1155
 
1202
- ### Keycloak Configuration Options
1156
+ ### Production Guard
1203
1157
 
1204
- | Option | Type | Description |
1205
- |--------|------|-------------|
1206
- | `url` | string | Keycloak server URL |
1207
- | `realm` | string | Realm name |
1208
- | `clientId` | string | Client ID |
1158
+ In production deployments, configure **`bff`** (admin-bff) for real authentication. If neither `authClient` nor `bff` is provided, AdminShell uses in-memory mock authentication.
1209
1159
 
1210
1160
  ## DO NOT (Common Mistakes)
1211
1161
 
@@ -0,0 +1,38 @@
1
+ /**
2
+ * BFF / Okta cookie auth client (admin-bff)
3
+ *
4
+ * After the Okta OAuth flow, the BFF redirects back with `token`, `expires`,
5
+ * and `token_type` as query params. The client extracts these, decodes the JWT
6
+ * to build the user, and persists the token in localStorage. Falls back to
7
+ * `GET /me` (cookie-based) when no token is present.
8
+ */
9
+ import type { AuthClient } from './interface';
10
+ export interface BffAuthClientOptions {
11
+ /** Base URL of admin-bff (no trailing slash). Defaults to `window.location.origin`. */
12
+ bffBaseUrl?: string;
13
+ }
14
+ /** admin-bff `GET /me` JSON (subset). */
15
+ export interface BffMeResponse {
16
+ subject: string;
17
+ email: string;
18
+ groups?: string[];
19
+ roles?: string[];
20
+ scopes?: string[];
21
+ }
22
+ export interface BffAuthClient extends AuthClient {
23
+ readonly type: 'bff';
24
+ readonly bffBaseUrl: string;
25
+ }
26
+ /** Mark that the user explicitly logged out — skips `/me` auto-login until cleared (e.g. by LoginPage). */
27
+ export declare function setLoggedOutFlag(): void;
28
+ export declare function clearLoggedOutFlag(): void;
29
+ export declare function isLoggedOutFlag(): boolean;
30
+ /**
31
+ * Create an auth client that uses the admin-bff Okta flow.
32
+ *
33
+ * Auth flow: BFF redirects back with `?token=<JWT>&expires=...&token_type=Bearer`.
34
+ * The JWT is decoded client-side for user identity. `getAccessToken()` returns the
35
+ * JWT for bearer auth on API calls. Falls back to `GET /me` (cookie-based) when
36
+ * running behind the same origin as the BFF.
37
+ */
38
+ export declare function createBffAuthClient(options?: BffAuthClientOptions): BffAuthClient;
@@ -0,0 +1,270 @@
1
+ /**
2
+ * BFF / Okta cookie auth client (admin-bff)
3
+ *
4
+ * After the Okta OAuth flow, the BFF redirects back with `token`, `expires`,
5
+ * and `token_type` as query params. The client extracts these, decodes the JWT
6
+ * to build the user, and persists the token in localStorage. Falls back to
7
+ * `GET /me` (cookie-based) when no token is present.
8
+ */
9
+ const TOKEN_STORAGE_KEY = 'admin-bff-token';
10
+ const EXPIRES_STORAGE_KEY = 'admin-bff-token-expires';
11
+ const LOGGED_OUT_FLAG_KEY = 'admin-bff-logged-out';
12
+ /** Mark that the user explicitly logged out — skips `/me` auto-login until cleared (e.g. by LoginPage). */
13
+ export function setLoggedOutFlag() {
14
+ try {
15
+ localStorage.setItem(LOGGED_OUT_FLAG_KEY, '1');
16
+ }
17
+ catch {
18
+ /* private browsing */
19
+ }
20
+ }
21
+ export function clearLoggedOutFlag() {
22
+ try {
23
+ localStorage.removeItem(LOGGED_OUT_FLAG_KEY);
24
+ }
25
+ catch {
26
+ /* ok */
27
+ }
28
+ }
29
+ export function isLoggedOutFlag() {
30
+ try {
31
+ return localStorage.getItem(LOGGED_OUT_FLAG_KEY) === '1';
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ function normalizeBaseUrl(url) {
38
+ return url.replace(/\/+$/, '');
39
+ }
40
+ function resolveDefaultBffBaseUrl() {
41
+ if (typeof window !== 'undefined' && window.location?.origin) {
42
+ return normalizeBaseUrl(window.location.origin);
43
+ }
44
+ return 'http://localhost:3000';
45
+ }
46
+ function meToUser(me) {
47
+ const roles = [...(me.groups ?? []), ...(me.roles ?? [])];
48
+ return {
49
+ id: me.subject,
50
+ email: me.email ?? '',
51
+ displayName: me.email || me.subject,
52
+ roles,
53
+ };
54
+ }
55
+ /** Decode a JWT payload without signature verification (the BFF already verified it). */
56
+ function decodeJwtPayload(token) {
57
+ const parts = token.split('.');
58
+ if (parts.length !== 3)
59
+ throw new Error('Invalid JWT format');
60
+ const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
61
+ return JSON.parse(atob(base64));
62
+ }
63
+ function jwtToUser(payload) {
64
+ const groups = payload.groups ?? [];
65
+ const roles = payload.roles ?? [];
66
+ return {
67
+ id: payload.sub ?? '',
68
+ email: payload.email ?? '',
69
+ displayName: payload.email || payload.sub || '',
70
+ roles: [...groups, ...roles],
71
+ };
72
+ }
73
+ function matchesPermission(userRoles, permission) {
74
+ if (userRoles.includes('*'))
75
+ return true;
76
+ return userRoles.some((role) => {
77
+ if (role === permission)
78
+ return true;
79
+ if (role.endsWith('.*')) {
80
+ const prefix = role.slice(0, -2);
81
+ return permission.startsWith(prefix);
82
+ }
83
+ return false;
84
+ });
85
+ }
86
+ /** Read token + expires from URL query params, then clean the URL. */
87
+ function consumeTokenFromUrl() {
88
+ if (typeof window === 'undefined')
89
+ return null;
90
+ const params = new URLSearchParams(window.location.search);
91
+ const token = params.get('token');
92
+ const expires = params.get('expires');
93
+ if (!token)
94
+ return null;
95
+ params.delete('token');
96
+ params.delete('expires');
97
+ params.delete('token_type');
98
+ const cleanSearch = params.toString();
99
+ const cleanUrl = window.location.pathname + (cleanSearch ? `?${cleanSearch}` : '') + window.location.hash;
100
+ window.history.replaceState(null, '', cleanUrl);
101
+ return { token, expires: expires ?? '' };
102
+ }
103
+ function storeToken(token, expires) {
104
+ try {
105
+ localStorage.setItem(TOKEN_STORAGE_KEY, token);
106
+ localStorage.setItem(EXPIRES_STORAGE_KEY, expires);
107
+ }
108
+ catch { /* private browsing */ }
109
+ }
110
+ function loadStoredToken() {
111
+ try {
112
+ const token = localStorage.getItem(TOKEN_STORAGE_KEY);
113
+ const expires = localStorage.getItem(EXPIRES_STORAGE_KEY);
114
+ if (!token)
115
+ return null;
116
+ return { token, expires: expires ?? '' };
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ }
122
+ function clearStoredToken() {
123
+ try {
124
+ localStorage.removeItem(TOKEN_STORAGE_KEY);
125
+ localStorage.removeItem(EXPIRES_STORAGE_KEY);
126
+ }
127
+ catch { /* ok */ }
128
+ }
129
+ function isTokenExpired(expires) {
130
+ if (!expires)
131
+ return false;
132
+ try {
133
+ return new Date(decodeURIComponent(expires)).getTime() <= Date.now();
134
+ }
135
+ catch {
136
+ return false;
137
+ }
138
+ }
139
+ /**
140
+ * Create an auth client that uses the admin-bff Okta flow.
141
+ *
142
+ * Auth flow: BFF redirects back with `?token=<JWT>&expires=...&token_type=Bearer`.
143
+ * The JWT is decoded client-side for user identity. `getAccessToken()` returns the
144
+ * JWT for bearer auth on API calls. Falls back to `GET /me` (cookie-based) when
145
+ * running behind the same origin as the BFF.
146
+ */
147
+ export function createBffAuthClient(options = {}) {
148
+ const bffBaseUrl = normalizeBaseUrl(options.bffBaseUrl ?? resolveDefaultBffBaseUrl());
149
+ let currentUser = null;
150
+ let accessToken = null;
151
+ let initialized = false;
152
+ const subscribers = new Set();
153
+ function notifySubscribers() {
154
+ const state = {
155
+ isAuthenticated: currentUser !== null,
156
+ user: currentUser,
157
+ };
158
+ subscribers.forEach((cb) => cb(state));
159
+ }
160
+ async function fetchMe() {
161
+ const res = await fetch(`${bffBaseUrl}/me`, {
162
+ credentials: 'include',
163
+ });
164
+ if (res.status === 401) {
165
+ return { ok: false };
166
+ }
167
+ if (!res.ok) {
168
+ throw new Error(`BFF /me failed: ${res.status}`);
169
+ }
170
+ const data = (await res.json());
171
+ if (!data?.subject) {
172
+ throw new Error('BFF /me: missing subject');
173
+ }
174
+ return { ok: true, user: meToUser(data) };
175
+ }
176
+ function authenticateWithToken(token, expires) {
177
+ try {
178
+ if (isTokenExpired(expires)) {
179
+ clearStoredToken();
180
+ return false;
181
+ }
182
+ const payload = decodeJwtPayload(token);
183
+ currentUser = jwtToUser(payload);
184
+ accessToken = token;
185
+ storeToken(token, expires);
186
+ return true;
187
+ }
188
+ catch (e) {
189
+ console.warn('[BffAuthClient] Failed to decode token from redirect:', e);
190
+ clearStoredToken();
191
+ return false;
192
+ }
193
+ }
194
+ const client = {
195
+ type: 'bff',
196
+ bffBaseUrl,
197
+ async initialize() {
198
+ // 1. Check URL for token from BFF redirect (highest priority)
199
+ const fromUrl = consumeTokenFromUrl();
200
+ if (fromUrl && authenticateWithToken(fromUrl.token, fromUrl.expires)) {
201
+ initialized = true;
202
+ notifySubscribers();
203
+ return true;
204
+ }
205
+ // 2. Check localStorage for a previously stored token
206
+ const stored = loadStoredToken();
207
+ if (stored && authenticateWithToken(stored.token, stored.expires)) {
208
+ initialized = true;
209
+ notifySubscribers();
210
+ return true;
211
+ }
212
+ if (isLoggedOutFlag()) {
213
+ initialized = true;
214
+ currentUser = null;
215
+ accessToken = null;
216
+ notifySubscribers();
217
+ return false;
218
+ }
219
+ // 3. Fall back to /me (same-origin / cookie mode)
220
+ try {
221
+ const result = await fetchMe();
222
+ initialized = true;
223
+ if (result.ok) {
224
+ currentUser = result.user;
225
+ notifySubscribers();
226
+ return true;
227
+ }
228
+ currentUser = null;
229
+ notifySubscribers();
230
+ return false;
231
+ }
232
+ catch (e) {
233
+ console.warn('[BffAuthClient] /me failed; redirecting flow will send user to login:', e);
234
+ initialized = true;
235
+ currentUser = null;
236
+ notifySubscribers();
237
+ return false;
238
+ }
239
+ },
240
+ isAuthenticated() {
241
+ return currentUser !== null;
242
+ },
243
+ getUser() {
244
+ return currentUser;
245
+ },
246
+ async getAccessToken() {
247
+ return accessToken;
248
+ },
249
+ hasPermission(permission) {
250
+ if (!initialized || !currentUser) {
251
+ return false;
252
+ }
253
+ return matchesPermission(currentUser.roles, permission);
254
+ },
255
+ logout() {
256
+ currentUser = null;
257
+ accessToken = null;
258
+ clearStoredToken();
259
+ setLoggedOutFlag();
260
+ notifySubscribers();
261
+ },
262
+ subscribe(callback) {
263
+ subscribers.add(callback);
264
+ return () => {
265
+ subscribers.delete(callback);
266
+ };
267
+ },
268
+ };
269
+ return client;
270
+ }
@@ -46,7 +46,7 @@ export interface InMemoryAuthClientOptions {
46
46
  users: MockUser[];
47
47
  /** localStorage key prefix (defaults to '@nsxbet/auth') */
48
48
  storageKey?: string;
49
- /** Admin gateway URL for fetching real signed JWTs. If not set, falls back to import.meta.env.VITE_ADMIN_GATEWAY_URL. If neither is set, BFF integration is disabled. */
49
+ /** Override gateway URL for fetching real signed JWTs. Defaults to `env.adminGatewayUrl` from `/env.js`. Pass `null` to disable. */
50
50
  gatewayUrl?: string | null;
51
51
  /** Timeout in milliseconds for gateway token fetch requests (defaults to 5000) */
52
52
  tokenTimeout?: number;