@nubitio/admin 1.1.1 → 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/README.md +34 -0
- package/dist/index.cjs +272 -152
- package/dist/index.d.cts +69 -144
- package/dist/index.d.mts +70 -145
- package/dist/index.mjs +272 -153
- package/dist/style.css +167 -4
- package/package.json +8 -5
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { BrowserRouter, Link, NavLink, Navigate, Outlet, Route, Routes, useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import React, { Component, createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
4
|
-
import { Badge, Button, Card, EmptyState, FeatureGate as FeatureGate$1, FormField, IconButton, Skeleton, TextField, ThemeProvider, ThemeSwitcher, useFloatingPanel } from "@nubitio/ui";
|
|
4
|
+
import { Badge, Button, Card, EmptyState, FeatureGate as FeatureGate$1, FormField, IconButton, Skeleton, TextField, ThemeProvider, ThemeSwitcher, useFloatingPanel, useUiStrings } from "@nubitio/ui";
|
|
5
|
+
import { CoreConfigProvider, CoreProvider, DEFAULT_CSRF_HEADER_NAME, MercureProvider, readCsrfToken, useCoreHttpClient, useMercureSubscription } from "@nubitio/core";
|
|
5
6
|
import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
6
|
-
import { CoreConfigProvider, CoreProvider, MercureProvider, useCoreHttpClient, useMercureSubscription } from "@nubitio/core";
|
|
7
7
|
import { DevToolsProvider, SessionPermissionsProvider, SmartCrudRolesProvider, isDevEnvironment, useDevTools } from "@nubitio/crud";
|
|
8
8
|
import { HydraResourceSchemaProvider, HydraResourceStoreProvider, SchemaProvider } from "@nubitio/hydra";
|
|
9
9
|
//#region packages/admin/FeatureHubLayout.tsx
|
|
@@ -124,50 +124,61 @@ function ActionPopover({ action }) {
|
|
|
124
124
|
});
|
|
125
125
|
}
|
|
126
126
|
function UserMenuPopover({ renderUserMenu }) {
|
|
127
|
+
const strings = useUiStrings();
|
|
127
128
|
const { open, toggle, setOpen, containerRef } = useFloatingPanel();
|
|
128
129
|
return /* @__PURE__ */ jsxs("div", {
|
|
129
130
|
className: "nb-admin-header-popover",
|
|
130
131
|
ref: containerRef,
|
|
131
132
|
children: [/* @__PURE__ */ jsx(IconButton, {
|
|
132
133
|
icon: "ph ph-user-circle",
|
|
133
|
-
label:
|
|
134
|
+
label: strings.userMenu,
|
|
134
135
|
"aria-expanded": open,
|
|
135
136
|
onClick: toggle
|
|
136
137
|
}), open && /* @__PURE__ */ jsx("div", {
|
|
137
138
|
className: "nb-admin-header-popover__panel",
|
|
138
139
|
role: "dialog",
|
|
139
|
-
"aria-label":
|
|
140
|
+
"aria-label": strings.userMenu,
|
|
140
141
|
children: renderUserMenu({ close: () => setOpen(false) })
|
|
141
142
|
})]
|
|
142
143
|
});
|
|
143
144
|
}
|
|
144
|
-
const AdminHeader = ({ title, menuToggleEnabled, toggleMenu, className, actions = [], renderUserMenu, renderThemeSwitcher }) =>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
className: "nb-admin-header-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
children: [menuToggleEnabled && /* @__PURE__ */ jsx(IconButton, {
|
|
153
|
-
className: "nb-admin-menu-button",
|
|
154
|
-
icon: "ph ph-list",
|
|
155
|
-
label: "Toggle menu",
|
|
156
|
-
onClick: toggleMenu
|
|
157
|
-
}), title && /* @__PURE__ */ jsx("div", {
|
|
158
|
-
className: "nb-admin-header-title",
|
|
159
|
-
children: title
|
|
160
|
-
})]
|
|
161
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
162
|
-
className: "nb-admin-header-toolbar__after",
|
|
145
|
+
const AdminHeader = ({ title, menuToggleEnabled, toggleMenu, className, actions = [], content, renderUserMenu, renderThemeSwitcher }) => {
|
|
146
|
+
const strings = useUiStrings();
|
|
147
|
+
return /* @__PURE__ */ jsx("header", {
|
|
148
|
+
className: ["nb-admin-header-component", className].filter(Boolean).join(" "),
|
|
149
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
150
|
+
className: "nb-admin-header-toolbar",
|
|
151
|
+
role: "toolbar",
|
|
152
|
+
"aria-label": strings.mainToolbar,
|
|
163
153
|
children: [
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
154
|
+
/* @__PURE__ */ jsxs("div", {
|
|
155
|
+
className: "nb-admin-header-toolbar__before",
|
|
156
|
+
children: [menuToggleEnabled && /* @__PURE__ */ jsx(IconButton, {
|
|
157
|
+
className: "nb-admin-menu-button",
|
|
158
|
+
icon: "ph ph-list",
|
|
159
|
+
label: strings.toggleMenu,
|
|
160
|
+
onClick: toggleMenu
|
|
161
|
+
}), title && /* @__PURE__ */ jsx("div", {
|
|
162
|
+
className: "nb-admin-header-title",
|
|
163
|
+
children: title
|
|
164
|
+
})]
|
|
165
|
+
}),
|
|
166
|
+
content && /* @__PURE__ */ jsx("div", {
|
|
167
|
+
className: "nb-admin-header-content",
|
|
168
|
+
children: content
|
|
169
|
+
}),
|
|
170
|
+
/* @__PURE__ */ jsxs("div", {
|
|
171
|
+
className: "nb-admin-header-toolbar__after",
|
|
172
|
+
children: [
|
|
173
|
+
renderThemeSwitcher?.(),
|
|
174
|
+
actions.map((action) => /* @__PURE__ */ jsx(ActionPopover, { action }, action.id)),
|
|
175
|
+
renderUserMenu && /* @__PURE__ */ jsx(UserMenuPopover, { renderUserMenu })
|
|
176
|
+
]
|
|
177
|
+
})
|
|
167
178
|
]
|
|
168
|
-
})
|
|
169
|
-
})
|
|
170
|
-
}
|
|
179
|
+
})
|
|
180
|
+
});
|
|
181
|
+
};
|
|
171
182
|
//#endregion
|
|
172
183
|
//#region packages/admin/useScreenSize.ts
|
|
173
184
|
let handlers = [];
|
|
@@ -231,8 +242,9 @@ const getIconClassName = (icon) => {
|
|
|
231
242
|
if (!icon) return "";
|
|
232
243
|
return icon.includes(" ") ? icon : `ph ph-${icon}`;
|
|
233
244
|
};
|
|
234
|
-
const AdminSidebarMenu = ({ items, compactMode = false, selectedItemChanged, openMenu, footer }) => {
|
|
245
|
+
const AdminSidebarMenu = ({ items, compactMode = false, selectedItemChanged, openMenu, footer, brand, context }) => {
|
|
235
246
|
const [expandedItemKeys, setExpandedItemKeys] = useState(/* @__PURE__ */ new Set());
|
|
247
|
+
const strings = useUiStrings();
|
|
236
248
|
const { isLarge } = useScreenSize();
|
|
237
249
|
const location = useLocation();
|
|
238
250
|
useEffect(() => {
|
|
@@ -279,74 +291,86 @@ const AdminSidebarMenu = ({ items, compactMode = false, selectedItemChanged, ope
|
|
|
279
291
|
return /* @__PURE__ */ jsxs("div", {
|
|
280
292
|
className: `nb-admin-menu${compactMode ? " compact" : ""}`,
|
|
281
293
|
onPointerDown: openMenu,
|
|
282
|
-
children: [
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
"aria-hidden": "true"
|
|
319
|
-
})
|
|
320
|
-
]
|
|
321
|
-
}), expanded && (item.items?.length ?? 0) > 0 && /* @__PURE__ */ jsx("div", {
|
|
322
|
-
className: "nb-admin-menu__children",
|
|
323
|
-
children: item.items.map((subItem) => {
|
|
324
|
-
const childSelected = isSelected(subItem.path);
|
|
325
|
-
return /* @__PURE__ */ jsx("button", {
|
|
326
|
-
className: `nb-admin-menu__item nb-admin-menu__item--child${childSelected ? " is-selected" : ""}`,
|
|
327
|
-
type: "button",
|
|
328
|
-
"aria-current": childSelected ? "page" : void 0,
|
|
329
|
-
onClick: (event) => handleSelect(subItem.path, childSelected, event),
|
|
330
|
-
children: /* @__PURE__ */ jsx("span", {
|
|
294
|
+
children: [
|
|
295
|
+
brand && !compactMode && /* @__PURE__ */ jsx("div", {
|
|
296
|
+
className: "nb-admin-menu__brand",
|
|
297
|
+
children: brand
|
|
298
|
+
}),
|
|
299
|
+
/* @__PURE__ */ jsx("div", {
|
|
300
|
+
className: "nb-admin-menu-container theme-dependent",
|
|
301
|
+
children: /* @__PURE__ */ jsx("nav", {
|
|
302
|
+
className: "nb-admin-menu__nav",
|
|
303
|
+
"aria-label": strings.mainNavigation,
|
|
304
|
+
children: items.map((item) => {
|
|
305
|
+
const selected = isSelected(item.path);
|
|
306
|
+
const containsSelected = parentContainsSelected(item);
|
|
307
|
+
const itemKey = item.path || item.text;
|
|
308
|
+
const hasChildren = (item.items?.length ?? 0) > 0;
|
|
309
|
+
const expanded = !compactMode && hasChildren && expandedItemKeys.has(itemKey);
|
|
310
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
311
|
+
className: `nb-admin-menu__section${containsSelected ? " has-selected-child" : ""}`,
|
|
312
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
313
|
+
className: `nb-admin-menu__item nb-admin-menu__item--parent${selected ? " is-selected" : ""}`,
|
|
314
|
+
type: "button",
|
|
315
|
+
"aria-current": selected ? "page" : void 0,
|
|
316
|
+
"aria-expanded": hasChildren ? expanded : void 0,
|
|
317
|
+
onClick: (event) => {
|
|
318
|
+
if (hasChildren) {
|
|
319
|
+
toggleExpanded(itemKey);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
handleSelect(item.path, selected, event);
|
|
323
|
+
},
|
|
324
|
+
children: [
|
|
325
|
+
/* @__PURE__ */ jsx("span", {
|
|
326
|
+
className: `nb-admin-menu__icon ${getIconClassName(item.icon)}`,
|
|
327
|
+
"aria-hidden": "true"
|
|
328
|
+
}),
|
|
329
|
+
/* @__PURE__ */ jsx("span", {
|
|
331
330
|
className: "nb-admin-menu__text",
|
|
332
|
-
children:
|
|
331
|
+
children: item.text
|
|
332
|
+
}),
|
|
333
|
+
hasChildren && /* @__PURE__ */ jsx("span", {
|
|
334
|
+
className: `nb-admin-menu__chevron${expanded ? " is-expanded" : ""}`,
|
|
335
|
+
"aria-hidden": "true"
|
|
333
336
|
})
|
|
334
|
-
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
-
|
|
337
|
+
]
|
|
338
|
+
}), expanded && (item.items?.length ?? 0) > 0 && /* @__PURE__ */ jsx("div", {
|
|
339
|
+
className: "nb-admin-menu__children",
|
|
340
|
+
children: item.items.map((subItem) => {
|
|
341
|
+
const childSelected = isSelected(subItem.path);
|
|
342
|
+
return /* @__PURE__ */ jsx("button", {
|
|
343
|
+
className: `nb-admin-menu__item nb-admin-menu__item--child${childSelected ? " is-selected" : ""}`,
|
|
344
|
+
type: "button",
|
|
345
|
+
"aria-current": childSelected ? "page" : void 0,
|
|
346
|
+
onClick: (event) => handleSelect(subItem.path, childSelected, event),
|
|
347
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
348
|
+
className: "nb-admin-menu__text",
|
|
349
|
+
children: subItem.text
|
|
350
|
+
})
|
|
351
|
+
}, subItem.path || subItem.text);
|
|
352
|
+
})
|
|
353
|
+
})]
|
|
354
|
+
}, itemKey);
|
|
355
|
+
})
|
|
338
356
|
})
|
|
357
|
+
}),
|
|
358
|
+
context && !compactMode && /* @__PURE__ */ jsx("div", {
|
|
359
|
+
className: "nb-admin-menu__context",
|
|
360
|
+
children: context
|
|
361
|
+
}),
|
|
362
|
+
footer && /* @__PURE__ */ jsx("footer", {
|
|
363
|
+
className: "nb-admin-footer",
|
|
364
|
+
children: footer
|
|
339
365
|
})
|
|
340
|
-
|
|
341
|
-
className: "nb-admin-footer",
|
|
342
|
-
children: footer
|
|
343
|
-
})]
|
|
366
|
+
]
|
|
344
367
|
});
|
|
345
368
|
};
|
|
346
369
|
//#endregion
|
|
347
370
|
//#region packages/admin/AdminShell.tsx
|
|
348
|
-
const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThemeSwitcher, footer, children }) => {
|
|
371
|
+
const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThemeSwitcher, footer, variant = "default", brand, headerContent, sidebarContext, bottomNavigation, children }) => {
|
|
349
372
|
const navigate = useNavigate();
|
|
373
|
+
const strings = useUiStrings();
|
|
350
374
|
const { isXSmall, isLarge } = useScreenSize();
|
|
351
375
|
const [menuStatus, setMenuStatus] = useState(null);
|
|
352
376
|
const getDefaultMenuOpenState = useCallback(() => isLarge ? 2 : 1, [isLarge]);
|
|
@@ -400,45 +424,121 @@ const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThe
|
|
|
400
424
|
isXSmall ? "is-xsmall" : ""
|
|
401
425
|
].filter(Boolean).join(" ");
|
|
402
426
|
return /* @__PURE__ */ jsxs("div", {
|
|
403
|
-
className:
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
427
|
+
className: [
|
|
428
|
+
"nb-admin-shell",
|
|
429
|
+
variant === "workspace" && "nb-admin-shell--workspace",
|
|
430
|
+
variant === "workspace" && isLarge && "is-workspace-desktop",
|
|
431
|
+
variant === "workspace" && isLarge && isCompact && "is-workspace-compact"
|
|
432
|
+
].filter(Boolean).join(" "),
|
|
433
|
+
children: [
|
|
434
|
+
/* @__PURE__ */ jsx(AdminHeader, {
|
|
435
|
+
className: "nb-admin-layout-header",
|
|
436
|
+
menuToggleEnabled: true,
|
|
437
|
+
toggleMenu,
|
|
438
|
+
title,
|
|
439
|
+
actions: headerActions,
|
|
440
|
+
content: headerContent,
|
|
441
|
+
renderUserMenu,
|
|
442
|
+
renderThemeSwitcher
|
|
443
|
+
}),
|
|
444
|
+
/* @__PURE__ */ jsxs("div", {
|
|
445
|
+
className: bodyClassName,
|
|
446
|
+
children: [
|
|
447
|
+
/* @__PURE__ */ jsx("aside", {
|
|
448
|
+
className: "nb-admin-shell__panel",
|
|
449
|
+
"aria-label": strings.mainMenu,
|
|
450
|
+
children: /* @__PURE__ */ jsx(AdminSidebarMenu, {
|
|
451
|
+
items: menuItems,
|
|
452
|
+
compactMode: isCompact,
|
|
453
|
+
selectedItemChanged: onNavigationChanged,
|
|
454
|
+
openMenu: temporaryOpenMenu,
|
|
455
|
+
footer,
|
|
456
|
+
brand,
|
|
457
|
+
context: sidebarContext
|
|
458
|
+
})
|
|
459
|
+
}),
|
|
460
|
+
!isLarge && isMenuOpen && /* @__PURE__ */ jsx("button", {
|
|
461
|
+
className: "nb-admin-shell__scrim",
|
|
462
|
+
type: "button",
|
|
463
|
+
"aria-label": strings.closeMenu,
|
|
464
|
+
onClick: closeMenuFromOverlay
|
|
465
|
+
}),
|
|
466
|
+
/* @__PURE__ */ jsx("main", {
|
|
467
|
+
className: "nb-admin-shell__content content",
|
|
468
|
+
tabIndex: 0,
|
|
469
|
+
"aria-label": title ? `${title} content` : strings.mainContent,
|
|
470
|
+
children
|
|
424
471
|
})
|
|
472
|
+
]
|
|
473
|
+
}),
|
|
474
|
+
isXSmall && bottomNavigation && /* @__PURE__ */ jsx("div", {
|
|
475
|
+
className: "nb-admin-shell__bottom-navigation",
|
|
476
|
+
children: bottomNavigation
|
|
477
|
+
})
|
|
478
|
+
]
|
|
479
|
+
});
|
|
480
|
+
};
|
|
481
|
+
//#endregion
|
|
482
|
+
//#region packages/admin/AdminBottomNavigation.tsx
|
|
483
|
+
function AdminBottomNavigation({ items, label }) {
|
|
484
|
+
const { pathname } = useLocation();
|
|
485
|
+
const strings = useUiStrings();
|
|
486
|
+
const normalize = (path) => path.replace(/\/+$/, "") || "/";
|
|
487
|
+
const currentPath = normalize(pathname);
|
|
488
|
+
const activePath = items.map((item) => normalize(item.path)).filter((path) => currentPath === path || path !== "/" && currentPath.startsWith(`${path}/`)).sort((a, b) => b.length - a.length)[0];
|
|
489
|
+
return /* @__PURE__ */ jsx("nav", {
|
|
490
|
+
className: "nb-admin-bottom-nav",
|
|
491
|
+
"aria-label": label ?? strings.mainNavigation,
|
|
492
|
+
children: items.map((item) => /* @__PURE__ */ jsxs(Link, {
|
|
493
|
+
to: item.path,
|
|
494
|
+
className: "nb-admin-bottom-nav__item",
|
|
495
|
+
"aria-current": normalize(item.path) === activePath ? "page" : void 0,
|
|
496
|
+
children: [
|
|
497
|
+
item.icon && /* @__PURE__ */ jsx("span", {
|
|
498
|
+
className: `nb-admin-bottom-nav__icon ${item.icon.includes(" ") ? item.icon : `ph ph-${item.icon}`}`,
|
|
499
|
+
"aria-hidden": "true"
|
|
425
500
|
}),
|
|
426
|
-
|
|
427
|
-
className: "nb-admin-
|
|
428
|
-
|
|
429
|
-
"aria-label": "Close menu",
|
|
430
|
-
onClick: closeMenuFromOverlay
|
|
501
|
+
/* @__PURE__ */ jsx("span", {
|
|
502
|
+
className: "nb-admin-bottom-nav__label",
|
|
503
|
+
children: item.text
|
|
431
504
|
}),
|
|
432
|
-
/* @__PURE__ */ jsx("
|
|
433
|
-
className: "nb-admin-
|
|
434
|
-
|
|
435
|
-
"aria-label": title ? `${title} content` : "Main content",
|
|
436
|
-
children
|
|
505
|
+
item.badge != null && /* @__PURE__ */ jsx("span", {
|
|
506
|
+
className: "nb-admin-bottom-nav__badge",
|
|
507
|
+
children: item.badge
|
|
437
508
|
})
|
|
438
509
|
]
|
|
439
|
-
})
|
|
510
|
+
}, item.path))
|
|
440
511
|
});
|
|
441
|
-
}
|
|
512
|
+
}
|
|
513
|
+
//#endregion
|
|
514
|
+
//#region packages/admin/auth/csrfFetch.ts
|
|
515
|
+
const MUTATING_METHODS = /* @__PURE__ */ new Set([
|
|
516
|
+
"POST",
|
|
517
|
+
"PUT",
|
|
518
|
+
"PATCH",
|
|
519
|
+
"DELETE"
|
|
520
|
+
]);
|
|
521
|
+
/**
|
|
522
|
+
* `fetch` that echoes the double-submit CSRF cookie as `X-CSRF-Token` on
|
|
523
|
+
* state-changing requests.
|
|
524
|
+
*
|
|
525
|
+
* The backend rejects cookie-authenticated POST/PUT/PATCH/DELETE that lack it
|
|
526
|
+
* (`nubit_admin.auth.csrf_protection`, on by default), so the auth pages' own
|
|
527
|
+
* calls — logout, change password, TOTP, session revocation — need it exactly
|
|
528
|
+
* as the CRUD client does. Reads, and requests made before login has issued the
|
|
529
|
+
* cookie, pass through untouched.
|
|
530
|
+
*/
|
|
531
|
+
function csrfFetch(input, init = {}) {
|
|
532
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
533
|
+
const token = MUTATING_METHODS.has(method) ? readCsrfToken() : void 0;
|
|
534
|
+
if (token === void 0) return fetch(input, init);
|
|
535
|
+
const headers = new Headers(init.headers);
|
|
536
|
+
if (!headers.has(DEFAULT_CSRF_HEADER_NAME)) headers.set(DEFAULT_CSRF_HEADER_NAME, token);
|
|
537
|
+
return fetch(input, {
|
|
538
|
+
...init,
|
|
539
|
+
headers
|
|
540
|
+
});
|
|
541
|
+
}
|
|
442
542
|
//#endregion
|
|
443
543
|
//#region packages/admin/auth/SessionContext.tsx
|
|
444
544
|
const SessionContext = createContext(null);
|
|
@@ -454,9 +554,10 @@ function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "au
|
|
|
454
554
|
setSession({ status: "anonymous" });
|
|
455
555
|
return;
|
|
456
556
|
}
|
|
557
|
+
const profile = await response.json();
|
|
457
558
|
setSession({
|
|
458
559
|
status: "authenticated",
|
|
459
|
-
profile
|
|
560
|
+
profile
|
|
460
561
|
});
|
|
461
562
|
} catch {
|
|
462
563
|
setSession({ status: "anonymous" });
|
|
@@ -466,7 +567,7 @@ function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "au
|
|
|
466
567
|
refresh();
|
|
467
568
|
}, [refresh]);
|
|
468
569
|
const logout = useCallback(async () => {
|
|
469
|
-
await
|
|
570
|
+
await csrfFetch(joinApiPath$7(apiBaseUrl, logoutPath), {
|
|
470
571
|
method: "POST",
|
|
471
572
|
credentials: "include"
|
|
472
573
|
});
|
|
@@ -527,9 +628,10 @@ function useFeatureConfig(featureKey) {
|
|
|
527
628
|
//#endregion
|
|
528
629
|
//#region packages/admin/features/FeatureGate.tsx
|
|
529
630
|
function FeatureGate({ featureKey, ...props }) {
|
|
631
|
+
const enabled = useFeature(featureKey);
|
|
530
632
|
return /* @__PURE__ */ jsx(FeatureGate$1, {
|
|
531
633
|
featureKey,
|
|
532
|
-
enabled
|
|
634
|
+
enabled,
|
|
533
635
|
...props
|
|
534
636
|
});
|
|
535
637
|
}
|
|
@@ -577,10 +679,13 @@ function PlanPanel({ plans, currentPlanId, subscriptionStatus, trialEndsAt, onSe
|
|
|
577
679
|
const currentBadge = labels?.currentBadge ?? "Current plan";
|
|
578
680
|
const selectingLabel = labels?.selecting ?? "Updating…";
|
|
579
681
|
let statusLine = null;
|
|
580
|
-
if (subscriptionStatus === "trialing" && trialEndsAt)
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
682
|
+
if (subscriptionStatus === "trialing" && trialEndsAt) {
|
|
683
|
+
const trialTemplate = labels?.trialEnds ?? "Trial ends {date}";
|
|
684
|
+
statusLine = /* @__PURE__ */ jsx("p", {
|
|
685
|
+
className: "nb-plan-panel__status",
|
|
686
|
+
children: trialTemplate.replace("{date}", formatTrialEnds(trialEndsAt))
|
|
687
|
+
});
|
|
688
|
+
}
|
|
584
689
|
return /* @__PURE__ */ jsxs("section", {
|
|
585
690
|
className: `nb-plan-panel${className ? ` ${className}` : ""}`,
|
|
586
691
|
"aria-labelledby": "plan-panel-title",
|
|
@@ -976,7 +1081,7 @@ function ForgotPasswordPage({ apiBaseUrl = "/api/" }) {
|
|
|
976
1081
|
event.preventDefault();
|
|
977
1082
|
setBusy(true);
|
|
978
1083
|
try {
|
|
979
|
-
await
|
|
1084
|
+
await csrfFetch(joinApiPath$5(apiBaseUrl, "auth/password/forgot"), {
|
|
980
1085
|
method: "POST",
|
|
981
1086
|
headers: { "Content-Type": "application/json" },
|
|
982
1087
|
credentials: "include",
|
|
@@ -1052,7 +1157,7 @@ function ResetPasswordPage({ apiBaseUrl = "/api/" }) {
|
|
|
1052
1157
|
setBusy(true);
|
|
1053
1158
|
setError(null);
|
|
1054
1159
|
try {
|
|
1055
|
-
const response = await
|
|
1160
|
+
const response = await csrfFetch(joinApiPath$4(apiBaseUrl, "auth/password/reset"), {
|
|
1056
1161
|
method: "POST",
|
|
1057
1162
|
headers: { "Content-Type": "application/json" },
|
|
1058
1163
|
credentials: "include",
|
|
@@ -1062,7 +1167,8 @@ function ResetPasswordPage({ apiBaseUrl = "/api/" }) {
|
|
|
1062
1167
|
})
|
|
1063
1168
|
});
|
|
1064
1169
|
if (!response.ok) {
|
|
1065
|
-
|
|
1170
|
+
const payload = await response.json().catch(() => null);
|
|
1171
|
+
setError(payload?.message ?? "Reset failed");
|
|
1066
1172
|
return;
|
|
1067
1173
|
}
|
|
1068
1174
|
setDone(true);
|
|
@@ -1144,13 +1250,14 @@ function AcceptInvitationPage({ apiBaseUrl = "/api/" }) {
|
|
|
1144
1250
|
const [busy, setBusy] = useState(false);
|
|
1145
1251
|
useEffect(() => {
|
|
1146
1252
|
let cancelled = false;
|
|
1147
|
-
|
|
1253
|
+
csrfFetch(joinApiPath$3(apiBaseUrl, `invitations/${token}`), { credentials: "include" }).then(async (response) => {
|
|
1148
1254
|
if (cancelled) return;
|
|
1149
1255
|
if (!response.ok) {
|
|
1150
1256
|
setError("This invitation is no longer valid.");
|
|
1151
1257
|
return;
|
|
1152
1258
|
}
|
|
1153
|
-
|
|
1259
|
+
const payload = await response.json();
|
|
1260
|
+
setEmail(payload.email ?? null);
|
|
1154
1261
|
}).catch(() => {
|
|
1155
1262
|
if (!cancelled) setError("Network error");
|
|
1156
1263
|
});
|
|
@@ -1163,14 +1270,15 @@ function AcceptInvitationPage({ apiBaseUrl = "/api/" }) {
|
|
|
1163
1270
|
setBusy(true);
|
|
1164
1271
|
setError(null);
|
|
1165
1272
|
try {
|
|
1166
|
-
const response = await
|
|
1273
|
+
const response = await csrfFetch(joinApiPath$3(apiBaseUrl, `invitations/${token}/accept`), {
|
|
1167
1274
|
method: "POST",
|
|
1168
1275
|
headers: { "Content-Type": "application/json" },
|
|
1169
1276
|
credentials: "include",
|
|
1170
1277
|
body: JSON.stringify({ password })
|
|
1171
1278
|
});
|
|
1172
1279
|
if (!response.ok) {
|
|
1173
|
-
|
|
1280
|
+
const payload = await response.json().catch(() => null);
|
|
1281
|
+
setError(payload?.message ?? "Could not accept the invitation");
|
|
1174
1282
|
return;
|
|
1175
1283
|
}
|
|
1176
1284
|
setDone(true);
|
|
@@ -1270,7 +1378,10 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1270
1378
|
})]);
|
|
1271
1379
|
if (signal?.aborted) return;
|
|
1272
1380
|
if (totpResponse.ok) setTotp(await totpResponse.json());
|
|
1273
|
-
if (sessionResponse.ok)
|
|
1381
|
+
if (sessionResponse.ok) {
|
|
1382
|
+
const payload = await sessionResponse.json();
|
|
1383
|
+
setSessions(payload.sessions ?? []);
|
|
1384
|
+
}
|
|
1274
1385
|
}, [apiBaseUrl]);
|
|
1275
1386
|
useEffect(() => {
|
|
1276
1387
|
const controller = new AbortController();
|
|
@@ -1282,7 +1393,7 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1282
1393
|
const changePassword = async (event) => {
|
|
1283
1394
|
event.preventDefault();
|
|
1284
1395
|
setPasswordMessage(null);
|
|
1285
|
-
const response = await
|
|
1396
|
+
const response = await csrfFetch(joinApiPath$2(apiBaseUrl, "auth/change-password"), {
|
|
1286
1397
|
method: "POST",
|
|
1287
1398
|
headers: { "Content-Type": "application/json" },
|
|
1288
1399
|
credentials: "include",
|
|
@@ -1292,7 +1403,8 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1292
1403
|
})
|
|
1293
1404
|
});
|
|
1294
1405
|
if (!response.ok) {
|
|
1295
|
-
|
|
1406
|
+
const payload = await response.json().catch(() => null);
|
|
1407
|
+
setPasswordMessage(payload?.message ?? "Could not change password");
|
|
1296
1408
|
return;
|
|
1297
1409
|
}
|
|
1298
1410
|
setCurrentPassword("");
|
|
@@ -1302,12 +1414,13 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1302
1414
|
};
|
|
1303
1415
|
const beginTotp = async () => {
|
|
1304
1416
|
setTotpMessage(null);
|
|
1305
|
-
const response = await
|
|
1417
|
+
const response = await csrfFetch(joinApiPath$2(apiBaseUrl, "auth/totp"), {
|
|
1306
1418
|
method: "POST",
|
|
1307
1419
|
credentials: "include"
|
|
1308
1420
|
});
|
|
1309
1421
|
if (!response.ok) {
|
|
1310
|
-
|
|
1422
|
+
const payload = await response.json().catch(() => null);
|
|
1423
|
+
setTotpMessage(payload?.message ?? "Could not start enrolment");
|
|
1311
1424
|
return;
|
|
1312
1425
|
}
|
|
1313
1426
|
const payload = await response.json();
|
|
@@ -1316,14 +1429,15 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1316
1429
|
};
|
|
1317
1430
|
const confirmTotp = async (event) => {
|
|
1318
1431
|
event.preventDefault();
|
|
1319
|
-
const response = await
|
|
1432
|
+
const response = await csrfFetch(joinApiPath$2(apiBaseUrl, "auth/totp/confirm"), {
|
|
1320
1433
|
method: "POST",
|
|
1321
1434
|
headers: { "Content-Type": "application/json" },
|
|
1322
1435
|
credentials: "include",
|
|
1323
1436
|
body: JSON.stringify({ code: totpCode })
|
|
1324
1437
|
});
|
|
1325
1438
|
if (!response.ok) {
|
|
1326
|
-
|
|
1439
|
+
const payload = await response.json().catch(() => null);
|
|
1440
|
+
setTotpMessage(payload?.message ?? "That code is not valid");
|
|
1327
1441
|
return;
|
|
1328
1442
|
}
|
|
1329
1443
|
setTotpCode("");
|
|
@@ -1333,14 +1447,15 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1333
1447
|
};
|
|
1334
1448
|
const disableTotp = async (event) => {
|
|
1335
1449
|
event.preventDefault();
|
|
1336
|
-
const response = await
|
|
1450
|
+
const response = await csrfFetch(joinApiPath$2(apiBaseUrl, "auth/totp"), {
|
|
1337
1451
|
method: "DELETE",
|
|
1338
1452
|
headers: { "Content-Type": "application/json" },
|
|
1339
1453
|
credentials: "include",
|
|
1340
1454
|
body: JSON.stringify({ code: totpCode })
|
|
1341
1455
|
});
|
|
1342
1456
|
if (!response.ok) {
|
|
1343
|
-
|
|
1457
|
+
const payload = await response.json().catch(() => null);
|
|
1458
|
+
setTotpMessage(payload?.message ?? "Could not disable the second factor");
|
|
1344
1459
|
return;
|
|
1345
1460
|
}
|
|
1346
1461
|
setTotpCode("");
|
|
@@ -1348,7 +1463,7 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1348
1463
|
await load();
|
|
1349
1464
|
};
|
|
1350
1465
|
const revokeSession = async (id) => {
|
|
1351
|
-
await
|
|
1466
|
+
await csrfFetch(joinApiPath$2(apiBaseUrl, `auth/sessions/${id}`), {
|
|
1352
1467
|
method: "DELETE",
|
|
1353
1468
|
credentials: "include"
|
|
1354
1469
|
});
|
|
@@ -1534,7 +1649,8 @@ function initialValues(fields) {
|
|
|
1534
1649
|
return Object.fromEntries(fields.map((field) => [field.name, field.defaultValue ?? ""]));
|
|
1535
1650
|
}
|
|
1536
1651
|
function RegisterPage({ fields, onRegistered, apiBaseUrl = "/api/", registerPath = "auth/register", title = "Create account", hint, submitLabel = "Create account", busyLabel = "Creating…", loginLink, loginPrompt = "Already have an account?" }) {
|
|
1537
|
-
const
|
|
1652
|
+
const defaults = useMemo(() => initialValues(fields), [fields]);
|
|
1653
|
+
const [values, setValues] = useState(defaults);
|
|
1538
1654
|
const [error, setError] = useState(null);
|
|
1539
1655
|
const [busy, setBusy] = useState(false);
|
|
1540
1656
|
const setValue = (name, value) => {
|
|
@@ -1548,7 +1664,7 @@ function RegisterPage({ fields, onRegistered, apiBaseUrl = "/api/", registerPath
|
|
|
1548
1664
|
setBusy(true);
|
|
1549
1665
|
setError(null);
|
|
1550
1666
|
try {
|
|
1551
|
-
const response = await
|
|
1667
|
+
const response = await csrfFetch(joinApiPath$1(apiBaseUrl, registerPath), {
|
|
1552
1668
|
method: "POST",
|
|
1553
1669
|
headers: { "Content-Type": "application/json" },
|
|
1554
1670
|
credentials: "include",
|
|
@@ -1692,9 +1808,10 @@ function useRuntimeConfig({ apiBaseUrl = "/api/", path = "runtime-config", enabl
|
|
|
1692
1808
|
setState({ status: "error" });
|
|
1693
1809
|
return;
|
|
1694
1810
|
}
|
|
1811
|
+
const config = await response.json();
|
|
1695
1812
|
setState({
|
|
1696
1813
|
status: "ready",
|
|
1697
|
-
config
|
|
1814
|
+
config
|
|
1698
1815
|
});
|
|
1699
1816
|
} catch {
|
|
1700
1817
|
setState({ status: "error" });
|
|
@@ -2035,6 +2152,7 @@ function resolveShellMenu(config, ctx) {
|
|
|
2035
2152
|
}
|
|
2036
2153
|
function NubitAuthenticatedApp({ config }) {
|
|
2037
2154
|
const session = useSession();
|
|
2155
|
+
const strings = useUiStrings();
|
|
2038
2156
|
const { runtime, toasts, dismiss } = useAppRuntime();
|
|
2039
2157
|
const apiBaseUrl = config.apiBaseUrl ?? "/api/";
|
|
2040
2158
|
const homePath = config.homePath ?? config.routes[0]?.path ?? "/";
|
|
@@ -2051,7 +2169,7 @@ function NubitAuthenticatedApp({ config }) {
|
|
|
2051
2169
|
if (session.session.status === "loading") return /* @__PURE__ */ jsxs("div", {
|
|
2052
2170
|
style: { padding: 24 },
|
|
2053
2171
|
"aria-busy": "true",
|
|
2054
|
-
"aria-label":
|
|
2172
|
+
"aria-label": strings.loadingSession,
|
|
2055
2173
|
children: [
|
|
2056
2174
|
/* @__PURE__ */ jsx(Skeleton, {
|
|
2057
2175
|
variant: "rect",
|
|
@@ -2069,8 +2187,7 @@ function NubitAuthenticatedApp({ config }) {
|
|
|
2069
2187
|
const shell = /* @__PURE__ */ jsx(AdminShell, {
|
|
2070
2188
|
title: config.title,
|
|
2071
2189
|
menuItems,
|
|
2072
|
-
|
|
2073
|
-
footer: config.shell?.footer,
|
|
2190
|
+
...config.shell,
|
|
2074
2191
|
renderThemeSwitcher,
|
|
2075
2192
|
renderUserMenu: ({ close }) => renderUserMenu({
|
|
2076
2193
|
...menuContext,
|
|
@@ -2102,6 +2219,7 @@ function NubitAuthenticatedApp({ config }) {
|
|
|
2102
2219
|
baseUrl: apiBaseUrl,
|
|
2103
2220
|
refreshPath: "auth/refresh",
|
|
2104
2221
|
loginPath: "auth/login",
|
|
2222
|
+
getCsrfToken: () => readCsrfToken(),
|
|
2105
2223
|
onUnauthorized: () => {
|
|
2106
2224
|
session.logout();
|
|
2107
2225
|
}
|
|
@@ -2168,6 +2286,7 @@ function createNubitApp(config) {
|
|
|
2168
2286
|
client: queryClient,
|
|
2169
2287
|
children: /* @__PURE__ */ jsx(ThemeProvider, {
|
|
2170
2288
|
basePath: config.themeBasePath,
|
|
2289
|
+
visualStyle: config.visualStyle,
|
|
2171
2290
|
children: /* @__PURE__ */ jsx(SessionProvider, {
|
|
2172
2291
|
apiBaseUrl,
|
|
2173
2292
|
children: /* @__PURE__ */ jsx(NubitAuthenticatedApp, { config })
|
|
@@ -2178,4 +2297,4 @@ function createNubitApp(config) {
|
|
|
2178
2297
|
return { App };
|
|
2179
2298
|
}
|
|
2180
2299
|
//#endregion
|
|
2181
|
-
export { AcceptInvitationPage, AccountPage, AdminHeader, AdminShell, AdminSidebarMenu, FeatureGate, FeatureHubLayout, ForgotPasswordPage, LoginPage, NotificationPanel, NubitDevToolsPanel, PlanPanel, QuotaUsageBanner, RegisterPage, ResetPasswordPage, SessionProvider, StaticSessionProvider, ToastHost, createNubitApp, filterMenuByRoles, hasAnyRole, parseQuotaError, quotaErrorToastMessage, quotaUsageAboveGrid, useAppRuntime, useFeature, useFeatureConfig, useNotifications, useQuotaUsage, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
|
|
2300
|
+
export { AcceptInvitationPage, AccountPage, AdminBottomNavigation, AdminHeader, AdminShell, AdminSidebarMenu, FeatureGate, FeatureHubLayout, ForgotPasswordPage, LoginPage, NotificationPanel, NubitDevToolsPanel, PlanPanel, QuotaUsageBanner, RegisterPage, ResetPasswordPage, SessionProvider, StaticSessionProvider, ToastHost, createNubitApp, filterMenuByRoles, hasAnyRole, parseQuotaError, quotaErrorToastMessage, quotaUsageAboveGrid, useAppRuntime, useFeature, useFeatureConfig, useNotifications, useQuotaUsage, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
|