@nsxbet/admin-sdk 0.7.1 → 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.
- package/CHECKLIST.md +48 -13
- package/README.md +24 -74
- package/dist/auth/client/bff.d.ts +38 -0
- package/dist/auth/client/bff.js +270 -0
- package/dist/auth/client/in-memory.d.ts +1 -1
- package/dist/auth/client/in-memory.js +2 -2
- package/dist/auth/client/index.d.ts +1 -1
- package/dist/auth/client/index.js +2 -2
- package/dist/auth/client/interface.d.ts +4 -4
- package/dist/auth/client/interface.js +1 -1
- package/dist/auth/components/LoginPage.d.ts +8 -0
- package/dist/auth/components/LoginPage.js +32 -0
- package/dist/auth/components/UserSelector.js +2 -2
- package/dist/auth/components/index.d.ts +2 -0
- package/dist/auth/components/index.js +1 -0
- package/dist/auth/index.d.ts +3 -2
- package/dist/auth/index.js +2 -2
- package/dist/components/AuthProvider.d.ts +3 -3
- package/dist/components/AuthProvider.js +25 -10
- package/dist/env.d.ts +17 -0
- package/dist/env.js +50 -0
- package/dist/hooks/useAuth.d.ts +3 -3
- package/dist/hooks/useAuth.js +1 -1
- package/dist/hooks/useFetch.js +6 -1
- package/dist/hooks/useI18n.js +2 -2
- package/dist/i18n/config.d.ts +2 -1
- package/dist/i18n/config.js +4 -3
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/locales/en-US.json +7 -0
- package/dist/i18n/locales/es.json +7 -0
- package/dist/i18n/locales/pt-BR.json +7 -0
- package/dist/i18n/locales/ro.json +7 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.js +5 -2
- package/dist/registry/client/http.js +6 -1
- package/dist/registry/client/in-memory.js +20 -5
- package/dist/registry/types/manifest.d.ts +5 -0
- package/dist/registry/types/manifest.js +4 -1
- package/dist/registry/types/module.d.ts +6 -2
- package/dist/sdk-version.d.ts +5 -0
- package/dist/sdk-version.js +5 -0
- package/dist/shell/AdminShell.d.ts +12 -9
- package/dist/shell/AdminShell.js +56 -70
- package/dist/shell/components/ModuleOverview.js +1 -5
- package/dist/shell/components/RegistryPage.js +1 -1
- package/dist/shell/components/TopBar.js +2 -2
- package/dist/shell/index.d.ts +1 -1
- package/dist/shell/polling-config.d.ts +4 -3
- package/dist/shell/polling-config.js +11 -9
- package/dist/shell/types.d.ts +3 -1
- package/dist/types/platform.d.ts +2 -11
- package/dist/vite/config.d.ts +4 -9
- package/dist/vite/config.js +85 -27
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.js +1 -1
- package/dist/vite/plugins.js +6 -1
- package/package.json +11 -5
- package/scripts/write-sdk-version.mjs +21 -0
- package/dist/auth/client/keycloak.d.ts +0 -18
- package/dist/auth/client/keycloak.js +0 -129
- package/dist/shell/BackofficeShell.d.ts +0 -37
- package/dist/shell/BackofficeShell.js +0 -339
- package/dist/types/keycloak.d.ts +0 -25
- 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` —
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
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": {
|
|
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": {
|
|
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
|
-
{
|
|
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]}
|
|
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()`
|
|
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
|
-
|
|
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: [
|
|
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
|
-
|
|
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
|
-
|
|
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).
|
|
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
|
|
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
|
|
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
|
-
| `
|
|
1019
|
-
| `
|
|
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 | `
|
|
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 `
|
|
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. `
|
|
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
|
|
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
|
-
|
|
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
|
-
##
|
|
1140
|
+
## BFF / Okta cookie auth (recommended)
|
|
1163
1141
|
|
|
1164
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
1156
|
+
### Production Guard
|
|
1203
1157
|
|
|
1204
|
-
|
|
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
|
-
/**
|
|
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;
|