@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.cjs
CHANGED
|
@@ -16,7 +16,7 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
16
|
}
|
|
17
17
|
return to;
|
|
18
18
|
};
|
|
19
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
20
20
|
value: mod,
|
|
21
21
|
enumerable: true
|
|
22
22
|
}) : target, mod));
|
|
@@ -26,8 +26,8 @@ let react_jsx_runtime = require("react/jsx-runtime");
|
|
|
26
26
|
let react = require("react");
|
|
27
27
|
react = __toESM(react, 1);
|
|
28
28
|
let _nubitio_ui = require("@nubitio/ui");
|
|
29
|
-
let _tanstack_react_query = require("@tanstack/react-query");
|
|
30
29
|
let _nubitio_core = require("@nubitio/core");
|
|
30
|
+
let _tanstack_react_query = require("@tanstack/react-query");
|
|
31
31
|
let _nubitio_crud = require("@nubitio/crud");
|
|
32
32
|
let _nubitio_hydra = require("@nubitio/hydra");
|
|
33
33
|
//#region packages/admin/FeatureHubLayout.tsx
|
|
@@ -148,50 +148,61 @@ function ActionPopover({ action }) {
|
|
|
148
148
|
});
|
|
149
149
|
}
|
|
150
150
|
function UserMenuPopover({ renderUserMenu }) {
|
|
151
|
+
const strings = (0, _nubitio_ui.useUiStrings)();
|
|
151
152
|
const { open, toggle, setOpen, containerRef } = (0, _nubitio_ui.useFloatingPanel)();
|
|
152
153
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
153
154
|
className: "nb-admin-header-popover",
|
|
154
155
|
ref: containerRef,
|
|
155
156
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
|
|
156
157
|
icon: "ph ph-user-circle",
|
|
157
|
-
label:
|
|
158
|
+
label: strings.userMenu,
|
|
158
159
|
"aria-expanded": open,
|
|
159
160
|
onClick: toggle
|
|
160
161
|
}), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
161
162
|
className: "nb-admin-header-popover__panel",
|
|
162
163
|
role: "dialog",
|
|
163
|
-
"aria-label":
|
|
164
|
+
"aria-label": strings.userMenu,
|
|
164
165
|
children: renderUserMenu({ close: () => setOpen(false) })
|
|
165
166
|
})]
|
|
166
167
|
});
|
|
167
168
|
}
|
|
168
|
-
const AdminHeader = ({ title, menuToggleEnabled, toggleMenu, className, actions = [], renderUserMenu, renderThemeSwitcher }) =>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
className: "nb-admin-header-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
children: [menuToggleEnabled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
|
|
177
|
-
className: "nb-admin-menu-button",
|
|
178
|
-
icon: "ph ph-list",
|
|
179
|
-
label: "Toggle menu",
|
|
180
|
-
onClick: toggleMenu
|
|
181
|
-
}), title && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
182
|
-
className: "nb-admin-header-title",
|
|
183
|
-
children: title
|
|
184
|
-
})]
|
|
185
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
186
|
-
className: "nb-admin-header-toolbar__after",
|
|
169
|
+
const AdminHeader = ({ title, menuToggleEnabled, toggleMenu, className, actions = [], content, renderUserMenu, renderThemeSwitcher }) => {
|
|
170
|
+
const strings = (0, _nubitio_ui.useUiStrings)();
|
|
171
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("header", {
|
|
172
|
+
className: ["nb-admin-header-component", className].filter(Boolean).join(" "),
|
|
173
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
174
|
+
className: "nb-admin-header-toolbar",
|
|
175
|
+
role: "toolbar",
|
|
176
|
+
"aria-label": strings.mainToolbar,
|
|
187
177
|
children: [
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
178
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
179
|
+
className: "nb-admin-header-toolbar__before",
|
|
180
|
+
children: [menuToggleEnabled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
|
|
181
|
+
className: "nb-admin-menu-button",
|
|
182
|
+
icon: "ph ph-list",
|
|
183
|
+
label: strings.toggleMenu,
|
|
184
|
+
onClick: toggleMenu
|
|
185
|
+
}), title && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
186
|
+
className: "nb-admin-header-title",
|
|
187
|
+
children: title
|
|
188
|
+
})]
|
|
189
|
+
}),
|
|
190
|
+
content && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
191
|
+
className: "nb-admin-header-content",
|
|
192
|
+
children: content
|
|
193
|
+
}),
|
|
194
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
195
|
+
className: "nb-admin-header-toolbar__after",
|
|
196
|
+
children: [
|
|
197
|
+
renderThemeSwitcher?.(),
|
|
198
|
+
actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionPopover, { action }, action.id)),
|
|
199
|
+
renderUserMenu && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UserMenuPopover, { renderUserMenu })
|
|
200
|
+
]
|
|
201
|
+
})
|
|
191
202
|
]
|
|
192
|
-
})
|
|
193
|
-
})
|
|
194
|
-
}
|
|
203
|
+
})
|
|
204
|
+
});
|
|
205
|
+
};
|
|
195
206
|
//#endregion
|
|
196
207
|
//#region packages/admin/useScreenSize.ts
|
|
197
208
|
let handlers = [];
|
|
@@ -255,8 +266,9 @@ const getIconClassName = (icon) => {
|
|
|
255
266
|
if (!icon) return "";
|
|
256
267
|
return icon.includes(" ") ? icon : `ph ph-${icon}`;
|
|
257
268
|
};
|
|
258
|
-
const AdminSidebarMenu = ({ items, compactMode = false, selectedItemChanged, openMenu, footer }) => {
|
|
269
|
+
const AdminSidebarMenu = ({ items, compactMode = false, selectedItemChanged, openMenu, footer, brand, context }) => {
|
|
259
270
|
const [expandedItemKeys, setExpandedItemKeys] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
271
|
+
const strings = (0, _nubitio_ui.useUiStrings)();
|
|
260
272
|
const { isLarge } = useScreenSize();
|
|
261
273
|
const location = (0, react_router_dom.useLocation)();
|
|
262
274
|
(0, react.useEffect)(() => {
|
|
@@ -303,74 +315,86 @@ const AdminSidebarMenu = ({ items, compactMode = false, selectedItemChanged, ope
|
|
|
303
315
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
304
316
|
className: `nb-admin-menu${compactMode ? " compact" : ""}`,
|
|
305
317
|
onPointerDown: openMenu,
|
|
306
|
-
children: [
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
"aria-hidden": "true"
|
|
343
|
-
})
|
|
344
|
-
]
|
|
345
|
-
}), expanded && (item.items?.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
346
|
-
className: "nb-admin-menu__children",
|
|
347
|
-
children: item.items.map((subItem) => {
|
|
348
|
-
const childSelected = isSelected(subItem.path);
|
|
349
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
350
|
-
className: `nb-admin-menu__item nb-admin-menu__item--child${childSelected ? " is-selected" : ""}`,
|
|
351
|
-
type: "button",
|
|
352
|
-
"aria-current": childSelected ? "page" : void 0,
|
|
353
|
-
onClick: (event) => handleSelect(subItem.path, childSelected, event),
|
|
354
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
318
|
+
children: [
|
|
319
|
+
brand && !compactMode && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
320
|
+
className: "nb-admin-menu__brand",
|
|
321
|
+
children: brand
|
|
322
|
+
}),
|
|
323
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
324
|
+
className: "nb-admin-menu-container theme-dependent",
|
|
325
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("nav", {
|
|
326
|
+
className: "nb-admin-menu__nav",
|
|
327
|
+
"aria-label": strings.mainNavigation,
|
|
328
|
+
children: items.map((item) => {
|
|
329
|
+
const selected = isSelected(item.path);
|
|
330
|
+
const containsSelected = parentContainsSelected(item);
|
|
331
|
+
const itemKey = item.path || item.text;
|
|
332
|
+
const hasChildren = (item.items?.length ?? 0) > 0;
|
|
333
|
+
const expanded = !compactMode && hasChildren && expandedItemKeys.has(itemKey);
|
|
334
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
335
|
+
className: `nb-admin-menu__section${containsSelected ? " has-selected-child" : ""}`,
|
|
336
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
337
|
+
className: `nb-admin-menu__item nb-admin-menu__item--parent${selected ? " is-selected" : ""}`,
|
|
338
|
+
type: "button",
|
|
339
|
+
"aria-current": selected ? "page" : void 0,
|
|
340
|
+
"aria-expanded": hasChildren ? expanded : void 0,
|
|
341
|
+
onClick: (event) => {
|
|
342
|
+
if (hasChildren) {
|
|
343
|
+
toggleExpanded(itemKey);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
handleSelect(item.path, selected, event);
|
|
347
|
+
},
|
|
348
|
+
children: [
|
|
349
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
350
|
+
className: `nb-admin-menu__icon ${getIconClassName(item.icon)}`,
|
|
351
|
+
"aria-hidden": "true"
|
|
352
|
+
}),
|
|
353
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
355
354
|
className: "nb-admin-menu__text",
|
|
356
|
-
children:
|
|
355
|
+
children: item.text
|
|
356
|
+
}),
|
|
357
|
+
hasChildren && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
358
|
+
className: `nb-admin-menu__chevron${expanded ? " is-expanded" : ""}`,
|
|
359
|
+
"aria-hidden": "true"
|
|
357
360
|
})
|
|
358
|
-
|
|
359
|
-
})
|
|
360
|
-
|
|
361
|
-
|
|
361
|
+
]
|
|
362
|
+
}), expanded && (item.items?.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
363
|
+
className: "nb-admin-menu__children",
|
|
364
|
+
children: item.items.map((subItem) => {
|
|
365
|
+
const childSelected = isSelected(subItem.path);
|
|
366
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
367
|
+
className: `nb-admin-menu__item nb-admin-menu__item--child${childSelected ? " is-selected" : ""}`,
|
|
368
|
+
type: "button",
|
|
369
|
+
"aria-current": childSelected ? "page" : void 0,
|
|
370
|
+
onClick: (event) => handleSelect(subItem.path, childSelected, event),
|
|
371
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
372
|
+
className: "nb-admin-menu__text",
|
|
373
|
+
children: subItem.text
|
|
374
|
+
})
|
|
375
|
+
}, subItem.path || subItem.text);
|
|
376
|
+
})
|
|
377
|
+
})]
|
|
378
|
+
}, itemKey);
|
|
379
|
+
})
|
|
362
380
|
})
|
|
381
|
+
}),
|
|
382
|
+
context && !compactMode && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
383
|
+
className: "nb-admin-menu__context",
|
|
384
|
+
children: context
|
|
385
|
+
}),
|
|
386
|
+
footer && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("footer", {
|
|
387
|
+
className: "nb-admin-footer",
|
|
388
|
+
children: footer
|
|
363
389
|
})
|
|
364
|
-
|
|
365
|
-
className: "nb-admin-footer",
|
|
366
|
-
children: footer
|
|
367
|
-
})]
|
|
390
|
+
]
|
|
368
391
|
});
|
|
369
392
|
};
|
|
370
393
|
//#endregion
|
|
371
394
|
//#region packages/admin/AdminShell.tsx
|
|
372
|
-
const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThemeSwitcher, footer, children }) => {
|
|
395
|
+
const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThemeSwitcher, footer, variant = "default", brand, headerContent, sidebarContext, bottomNavigation, children }) => {
|
|
373
396
|
const navigate = (0, react_router_dom.useNavigate)();
|
|
397
|
+
const strings = (0, _nubitio_ui.useUiStrings)();
|
|
374
398
|
const { isXSmall, isLarge } = useScreenSize();
|
|
375
399
|
const [menuStatus, setMenuStatus] = (0, react.useState)(null);
|
|
376
400
|
const getDefaultMenuOpenState = (0, react.useCallback)(() => isLarge ? 2 : 1, [isLarge]);
|
|
@@ -424,45 +448,121 @@ const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThe
|
|
|
424
448
|
isXSmall ? "is-xsmall" : ""
|
|
425
449
|
].filter(Boolean).join(" ");
|
|
426
450
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
427
|
-
className:
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
451
|
+
className: [
|
|
452
|
+
"nb-admin-shell",
|
|
453
|
+
variant === "workspace" && "nb-admin-shell--workspace",
|
|
454
|
+
variant === "workspace" && isLarge && "is-workspace-desktop",
|
|
455
|
+
variant === "workspace" && isLarge && isCompact && "is-workspace-compact"
|
|
456
|
+
].filter(Boolean).join(" "),
|
|
457
|
+
children: [
|
|
458
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdminHeader, {
|
|
459
|
+
className: "nb-admin-layout-header",
|
|
460
|
+
menuToggleEnabled: true,
|
|
461
|
+
toggleMenu,
|
|
462
|
+
title,
|
|
463
|
+
actions: headerActions,
|
|
464
|
+
content: headerContent,
|
|
465
|
+
renderUserMenu,
|
|
466
|
+
renderThemeSwitcher
|
|
467
|
+
}),
|
|
468
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
469
|
+
className: bodyClassName,
|
|
470
|
+
children: [
|
|
471
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("aside", {
|
|
472
|
+
className: "nb-admin-shell__panel",
|
|
473
|
+
"aria-label": strings.mainMenu,
|
|
474
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdminSidebarMenu, {
|
|
475
|
+
items: menuItems,
|
|
476
|
+
compactMode: isCompact,
|
|
477
|
+
selectedItemChanged: onNavigationChanged,
|
|
478
|
+
openMenu: temporaryOpenMenu,
|
|
479
|
+
footer,
|
|
480
|
+
brand,
|
|
481
|
+
context: sidebarContext
|
|
482
|
+
})
|
|
483
|
+
}),
|
|
484
|
+
!isLarge && isMenuOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
485
|
+
className: "nb-admin-shell__scrim",
|
|
486
|
+
type: "button",
|
|
487
|
+
"aria-label": strings.closeMenu,
|
|
488
|
+
onClick: closeMenuFromOverlay
|
|
489
|
+
}),
|
|
490
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("main", {
|
|
491
|
+
className: "nb-admin-shell__content content",
|
|
492
|
+
tabIndex: 0,
|
|
493
|
+
"aria-label": title ? `${title} content` : strings.mainContent,
|
|
494
|
+
children
|
|
448
495
|
})
|
|
496
|
+
]
|
|
497
|
+
}),
|
|
498
|
+
isXSmall && bottomNavigation && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
499
|
+
className: "nb-admin-shell__bottom-navigation",
|
|
500
|
+
children: bottomNavigation
|
|
501
|
+
})
|
|
502
|
+
]
|
|
503
|
+
});
|
|
504
|
+
};
|
|
505
|
+
//#endregion
|
|
506
|
+
//#region packages/admin/AdminBottomNavigation.tsx
|
|
507
|
+
function AdminBottomNavigation({ items, label }) {
|
|
508
|
+
const { pathname } = (0, react_router_dom.useLocation)();
|
|
509
|
+
const strings = (0, _nubitio_ui.useUiStrings)();
|
|
510
|
+
const normalize = (path) => path.replace(/\/+$/, "") || "/";
|
|
511
|
+
const currentPath = normalize(pathname);
|
|
512
|
+
const activePath = items.map((item) => normalize(item.path)).filter((path) => currentPath === path || path !== "/" && currentPath.startsWith(`${path}/`)).sort((a, b) => b.length - a.length)[0];
|
|
513
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("nav", {
|
|
514
|
+
className: "nb-admin-bottom-nav",
|
|
515
|
+
"aria-label": label ?? strings.mainNavigation,
|
|
516
|
+
children: items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_router_dom.Link, {
|
|
517
|
+
to: item.path,
|
|
518
|
+
className: "nb-admin-bottom-nav__item",
|
|
519
|
+
"aria-current": normalize(item.path) === activePath ? "page" : void 0,
|
|
520
|
+
children: [
|
|
521
|
+
item.icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
522
|
+
className: `nb-admin-bottom-nav__icon ${item.icon.includes(" ") ? item.icon : `ph ph-${item.icon}`}`,
|
|
523
|
+
"aria-hidden": "true"
|
|
449
524
|
}),
|
|
450
|
-
|
|
451
|
-
className: "nb-admin-
|
|
452
|
-
|
|
453
|
-
"aria-label": "Close menu",
|
|
454
|
-
onClick: closeMenuFromOverlay
|
|
525
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
526
|
+
className: "nb-admin-bottom-nav__label",
|
|
527
|
+
children: item.text
|
|
455
528
|
}),
|
|
456
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
457
|
-
className: "nb-admin-
|
|
458
|
-
|
|
459
|
-
"aria-label": title ? `${title} content` : "Main content",
|
|
460
|
-
children
|
|
529
|
+
item.badge != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
530
|
+
className: "nb-admin-bottom-nav__badge",
|
|
531
|
+
children: item.badge
|
|
461
532
|
})
|
|
462
533
|
]
|
|
463
|
-
})
|
|
534
|
+
}, item.path))
|
|
464
535
|
});
|
|
465
|
-
}
|
|
536
|
+
}
|
|
537
|
+
//#endregion
|
|
538
|
+
//#region packages/admin/auth/csrfFetch.ts
|
|
539
|
+
const MUTATING_METHODS = /* @__PURE__ */ new Set([
|
|
540
|
+
"POST",
|
|
541
|
+
"PUT",
|
|
542
|
+
"PATCH",
|
|
543
|
+
"DELETE"
|
|
544
|
+
]);
|
|
545
|
+
/**
|
|
546
|
+
* `fetch` that echoes the double-submit CSRF cookie as `X-CSRF-Token` on
|
|
547
|
+
* state-changing requests.
|
|
548
|
+
*
|
|
549
|
+
* The backend rejects cookie-authenticated POST/PUT/PATCH/DELETE that lack it
|
|
550
|
+
* (`nubit_admin.auth.csrf_protection`, on by default), so the auth pages' own
|
|
551
|
+
* calls — logout, change password, TOTP, session revocation — need it exactly
|
|
552
|
+
* as the CRUD client does. Reads, and requests made before login has issued the
|
|
553
|
+
* cookie, pass through untouched.
|
|
554
|
+
*/
|
|
555
|
+
function csrfFetch(input, init = {}) {
|
|
556
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
557
|
+
const token = MUTATING_METHODS.has(method) ? (0, _nubitio_core.readCsrfToken)() : void 0;
|
|
558
|
+
if (token === void 0) return fetch(input, init);
|
|
559
|
+
const headers = new Headers(init.headers);
|
|
560
|
+
if (!headers.has(_nubitio_core.DEFAULT_CSRF_HEADER_NAME)) headers.set(_nubitio_core.DEFAULT_CSRF_HEADER_NAME, token);
|
|
561
|
+
return fetch(input, {
|
|
562
|
+
...init,
|
|
563
|
+
headers
|
|
564
|
+
});
|
|
565
|
+
}
|
|
466
566
|
//#endregion
|
|
467
567
|
//#region packages/admin/auth/SessionContext.tsx
|
|
468
568
|
const SessionContext = (0, react.createContext)(null);
|
|
@@ -478,9 +578,10 @@ function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "au
|
|
|
478
578
|
setSession({ status: "anonymous" });
|
|
479
579
|
return;
|
|
480
580
|
}
|
|
581
|
+
const profile = await response.json();
|
|
481
582
|
setSession({
|
|
482
583
|
status: "authenticated",
|
|
483
|
-
profile
|
|
584
|
+
profile
|
|
484
585
|
});
|
|
485
586
|
} catch {
|
|
486
587
|
setSession({ status: "anonymous" });
|
|
@@ -490,7 +591,7 @@ function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "au
|
|
|
490
591
|
refresh();
|
|
491
592
|
}, [refresh]);
|
|
492
593
|
const logout = (0, react.useCallback)(async () => {
|
|
493
|
-
await
|
|
594
|
+
await csrfFetch(joinApiPath$7(apiBaseUrl, logoutPath), {
|
|
494
595
|
method: "POST",
|
|
495
596
|
credentials: "include"
|
|
496
597
|
});
|
|
@@ -551,9 +652,10 @@ function useFeatureConfig(featureKey) {
|
|
|
551
652
|
//#endregion
|
|
552
653
|
//#region packages/admin/features/FeatureGate.tsx
|
|
553
654
|
function FeatureGate({ featureKey, ...props }) {
|
|
655
|
+
const enabled = useFeature(featureKey);
|
|
554
656
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.FeatureGate, {
|
|
555
657
|
featureKey,
|
|
556
|
-
enabled
|
|
658
|
+
enabled,
|
|
557
659
|
...props
|
|
558
660
|
});
|
|
559
661
|
}
|
|
@@ -601,10 +703,13 @@ function PlanPanel({ plans, currentPlanId, subscriptionStatus, trialEndsAt, onSe
|
|
|
601
703
|
const currentBadge = labels?.currentBadge ?? "Current plan";
|
|
602
704
|
const selectingLabel = labels?.selecting ?? "Updating…";
|
|
603
705
|
let statusLine = null;
|
|
604
|
-
if (subscriptionStatus === "trialing" && trialEndsAt)
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
706
|
+
if (subscriptionStatus === "trialing" && trialEndsAt) {
|
|
707
|
+
const trialTemplate = labels?.trialEnds ?? "Trial ends {date}";
|
|
708
|
+
statusLine = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
709
|
+
className: "nb-plan-panel__status",
|
|
710
|
+
children: trialTemplate.replace("{date}", formatTrialEnds(trialEndsAt))
|
|
711
|
+
});
|
|
712
|
+
}
|
|
608
713
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
609
714
|
className: `nb-plan-panel${className ? ` ${className}` : ""}`,
|
|
610
715
|
"aria-labelledby": "plan-panel-title",
|
|
@@ -1000,7 +1105,7 @@ function ForgotPasswordPage({ apiBaseUrl = "/api/" }) {
|
|
|
1000
1105
|
event.preventDefault();
|
|
1001
1106
|
setBusy(true);
|
|
1002
1107
|
try {
|
|
1003
|
-
await
|
|
1108
|
+
await csrfFetch(joinApiPath$5(apiBaseUrl, "auth/password/forgot"), {
|
|
1004
1109
|
method: "POST",
|
|
1005
1110
|
headers: { "Content-Type": "application/json" },
|
|
1006
1111
|
credentials: "include",
|
|
@@ -1076,7 +1181,7 @@ function ResetPasswordPage({ apiBaseUrl = "/api/" }) {
|
|
|
1076
1181
|
setBusy(true);
|
|
1077
1182
|
setError(null);
|
|
1078
1183
|
try {
|
|
1079
|
-
const response = await
|
|
1184
|
+
const response = await csrfFetch(joinApiPath$4(apiBaseUrl, "auth/password/reset"), {
|
|
1080
1185
|
method: "POST",
|
|
1081
1186
|
headers: { "Content-Type": "application/json" },
|
|
1082
1187
|
credentials: "include",
|
|
@@ -1086,7 +1191,8 @@ function ResetPasswordPage({ apiBaseUrl = "/api/" }) {
|
|
|
1086
1191
|
})
|
|
1087
1192
|
});
|
|
1088
1193
|
if (!response.ok) {
|
|
1089
|
-
|
|
1194
|
+
const payload = await response.json().catch(() => null);
|
|
1195
|
+
setError(payload?.message ?? "Reset failed");
|
|
1090
1196
|
return;
|
|
1091
1197
|
}
|
|
1092
1198
|
setDone(true);
|
|
@@ -1168,13 +1274,14 @@ function AcceptInvitationPage({ apiBaseUrl = "/api/" }) {
|
|
|
1168
1274
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
1169
1275
|
(0, react.useEffect)(() => {
|
|
1170
1276
|
let cancelled = false;
|
|
1171
|
-
|
|
1277
|
+
csrfFetch(joinApiPath$3(apiBaseUrl, `invitations/${token}`), { credentials: "include" }).then(async (response) => {
|
|
1172
1278
|
if (cancelled) return;
|
|
1173
1279
|
if (!response.ok) {
|
|
1174
1280
|
setError("This invitation is no longer valid.");
|
|
1175
1281
|
return;
|
|
1176
1282
|
}
|
|
1177
|
-
|
|
1283
|
+
const payload = await response.json();
|
|
1284
|
+
setEmail(payload.email ?? null);
|
|
1178
1285
|
}).catch(() => {
|
|
1179
1286
|
if (!cancelled) setError("Network error");
|
|
1180
1287
|
});
|
|
@@ -1187,14 +1294,15 @@ function AcceptInvitationPage({ apiBaseUrl = "/api/" }) {
|
|
|
1187
1294
|
setBusy(true);
|
|
1188
1295
|
setError(null);
|
|
1189
1296
|
try {
|
|
1190
|
-
const response = await
|
|
1297
|
+
const response = await csrfFetch(joinApiPath$3(apiBaseUrl, `invitations/${token}/accept`), {
|
|
1191
1298
|
method: "POST",
|
|
1192
1299
|
headers: { "Content-Type": "application/json" },
|
|
1193
1300
|
credentials: "include",
|
|
1194
1301
|
body: JSON.stringify({ password })
|
|
1195
1302
|
});
|
|
1196
1303
|
if (!response.ok) {
|
|
1197
|
-
|
|
1304
|
+
const payload = await response.json().catch(() => null);
|
|
1305
|
+
setError(payload?.message ?? "Could not accept the invitation");
|
|
1198
1306
|
return;
|
|
1199
1307
|
}
|
|
1200
1308
|
setDone(true);
|
|
@@ -1294,7 +1402,10 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1294
1402
|
})]);
|
|
1295
1403
|
if (signal?.aborted) return;
|
|
1296
1404
|
if (totpResponse.ok) setTotp(await totpResponse.json());
|
|
1297
|
-
if (sessionResponse.ok)
|
|
1405
|
+
if (sessionResponse.ok) {
|
|
1406
|
+
const payload = await sessionResponse.json();
|
|
1407
|
+
setSessions(payload.sessions ?? []);
|
|
1408
|
+
}
|
|
1298
1409
|
}, [apiBaseUrl]);
|
|
1299
1410
|
(0, react.useEffect)(() => {
|
|
1300
1411
|
const controller = new AbortController();
|
|
@@ -1306,7 +1417,7 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1306
1417
|
const changePassword = async (event) => {
|
|
1307
1418
|
event.preventDefault();
|
|
1308
1419
|
setPasswordMessage(null);
|
|
1309
|
-
const response = await
|
|
1420
|
+
const response = await csrfFetch(joinApiPath$2(apiBaseUrl, "auth/change-password"), {
|
|
1310
1421
|
method: "POST",
|
|
1311
1422
|
headers: { "Content-Type": "application/json" },
|
|
1312
1423
|
credentials: "include",
|
|
@@ -1316,7 +1427,8 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1316
1427
|
})
|
|
1317
1428
|
});
|
|
1318
1429
|
if (!response.ok) {
|
|
1319
|
-
|
|
1430
|
+
const payload = await response.json().catch(() => null);
|
|
1431
|
+
setPasswordMessage(payload?.message ?? "Could not change password");
|
|
1320
1432
|
return;
|
|
1321
1433
|
}
|
|
1322
1434
|
setCurrentPassword("");
|
|
@@ -1326,12 +1438,13 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1326
1438
|
};
|
|
1327
1439
|
const beginTotp = async () => {
|
|
1328
1440
|
setTotpMessage(null);
|
|
1329
|
-
const response = await
|
|
1441
|
+
const response = await csrfFetch(joinApiPath$2(apiBaseUrl, "auth/totp"), {
|
|
1330
1442
|
method: "POST",
|
|
1331
1443
|
credentials: "include"
|
|
1332
1444
|
});
|
|
1333
1445
|
if (!response.ok) {
|
|
1334
|
-
|
|
1446
|
+
const payload = await response.json().catch(() => null);
|
|
1447
|
+
setTotpMessage(payload?.message ?? "Could not start enrolment");
|
|
1335
1448
|
return;
|
|
1336
1449
|
}
|
|
1337
1450
|
const payload = await response.json();
|
|
@@ -1340,14 +1453,15 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1340
1453
|
};
|
|
1341
1454
|
const confirmTotp = async (event) => {
|
|
1342
1455
|
event.preventDefault();
|
|
1343
|
-
const response = await
|
|
1456
|
+
const response = await csrfFetch(joinApiPath$2(apiBaseUrl, "auth/totp/confirm"), {
|
|
1344
1457
|
method: "POST",
|
|
1345
1458
|
headers: { "Content-Type": "application/json" },
|
|
1346
1459
|
credentials: "include",
|
|
1347
1460
|
body: JSON.stringify({ code: totpCode })
|
|
1348
1461
|
});
|
|
1349
1462
|
if (!response.ok) {
|
|
1350
|
-
|
|
1463
|
+
const payload = await response.json().catch(() => null);
|
|
1464
|
+
setTotpMessage(payload?.message ?? "That code is not valid");
|
|
1351
1465
|
return;
|
|
1352
1466
|
}
|
|
1353
1467
|
setTotpCode("");
|
|
@@ -1357,14 +1471,15 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1357
1471
|
};
|
|
1358
1472
|
const disableTotp = async (event) => {
|
|
1359
1473
|
event.preventDefault();
|
|
1360
|
-
const response = await
|
|
1474
|
+
const response = await csrfFetch(joinApiPath$2(apiBaseUrl, "auth/totp"), {
|
|
1361
1475
|
method: "DELETE",
|
|
1362
1476
|
headers: { "Content-Type": "application/json" },
|
|
1363
1477
|
credentials: "include",
|
|
1364
1478
|
body: JSON.stringify({ code: totpCode })
|
|
1365
1479
|
});
|
|
1366
1480
|
if (!response.ok) {
|
|
1367
|
-
|
|
1481
|
+
const payload = await response.json().catch(() => null);
|
|
1482
|
+
setTotpMessage(payload?.message ?? "Could not disable the second factor");
|
|
1368
1483
|
return;
|
|
1369
1484
|
}
|
|
1370
1485
|
setTotpCode("");
|
|
@@ -1372,7 +1487,7 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1372
1487
|
await load();
|
|
1373
1488
|
};
|
|
1374
1489
|
const revokeSession = async (id) => {
|
|
1375
|
-
await
|
|
1490
|
+
await csrfFetch(joinApiPath$2(apiBaseUrl, `auth/sessions/${id}`), {
|
|
1376
1491
|
method: "DELETE",
|
|
1377
1492
|
credentials: "include"
|
|
1378
1493
|
});
|
|
@@ -1558,7 +1673,8 @@ function initialValues(fields) {
|
|
|
1558
1673
|
return Object.fromEntries(fields.map((field) => [field.name, field.defaultValue ?? ""]));
|
|
1559
1674
|
}
|
|
1560
1675
|
function RegisterPage({ fields, onRegistered, apiBaseUrl = "/api/", registerPath = "auth/register", title = "Create account", hint, submitLabel = "Create account", busyLabel = "Creating…", loginLink, loginPrompt = "Already have an account?" }) {
|
|
1561
|
-
const
|
|
1676
|
+
const defaults = (0, react.useMemo)(() => initialValues(fields), [fields]);
|
|
1677
|
+
const [values, setValues] = (0, react.useState)(defaults);
|
|
1562
1678
|
const [error, setError] = (0, react.useState)(null);
|
|
1563
1679
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
1564
1680
|
const setValue = (name, value) => {
|
|
@@ -1572,7 +1688,7 @@ function RegisterPage({ fields, onRegistered, apiBaseUrl = "/api/", registerPath
|
|
|
1572
1688
|
setBusy(true);
|
|
1573
1689
|
setError(null);
|
|
1574
1690
|
try {
|
|
1575
|
-
const response = await
|
|
1691
|
+
const response = await csrfFetch(joinApiPath$1(apiBaseUrl, registerPath), {
|
|
1576
1692
|
method: "POST",
|
|
1577
1693
|
headers: { "Content-Type": "application/json" },
|
|
1578
1694
|
credentials: "include",
|
|
@@ -1716,9 +1832,10 @@ function useRuntimeConfig({ apiBaseUrl = "/api/", path = "runtime-config", enabl
|
|
|
1716
1832
|
setState({ status: "error" });
|
|
1717
1833
|
return;
|
|
1718
1834
|
}
|
|
1835
|
+
const config = await response.json();
|
|
1719
1836
|
setState({
|
|
1720
1837
|
status: "ready",
|
|
1721
|
-
config
|
|
1838
|
+
config
|
|
1722
1839
|
});
|
|
1723
1840
|
} catch {
|
|
1724
1841
|
setState({ status: "error" });
|
|
@@ -2059,6 +2176,7 @@ function resolveShellMenu(config, ctx) {
|
|
|
2059
2176
|
}
|
|
2060
2177
|
function NubitAuthenticatedApp({ config }) {
|
|
2061
2178
|
const session = useSession();
|
|
2179
|
+
const strings = (0, _nubitio_ui.useUiStrings)();
|
|
2062
2180
|
const { runtime, toasts, dismiss } = useAppRuntime();
|
|
2063
2181
|
const apiBaseUrl = config.apiBaseUrl ?? "/api/";
|
|
2064
2182
|
const homePath = config.homePath ?? config.routes[0]?.path ?? "/";
|
|
@@ -2075,7 +2193,7 @@ function NubitAuthenticatedApp({ config }) {
|
|
|
2075
2193
|
if (session.session.status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2076
2194
|
style: { padding: 24 },
|
|
2077
2195
|
"aria-busy": "true",
|
|
2078
|
-
"aria-label":
|
|
2196
|
+
"aria-label": strings.loadingSession,
|
|
2079
2197
|
children: [
|
|
2080
2198
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Skeleton, {
|
|
2081
2199
|
variant: "rect",
|
|
@@ -2093,8 +2211,7 @@ function NubitAuthenticatedApp({ config }) {
|
|
|
2093
2211
|
const shell = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdminShell, {
|
|
2094
2212
|
title: config.title,
|
|
2095
2213
|
menuItems,
|
|
2096
|
-
|
|
2097
|
-
footer: config.shell?.footer,
|
|
2214
|
+
...config.shell,
|
|
2098
2215
|
renderThemeSwitcher,
|
|
2099
2216
|
renderUserMenu: ({ close }) => renderUserMenu({
|
|
2100
2217
|
...menuContext,
|
|
@@ -2126,6 +2243,7 @@ function NubitAuthenticatedApp({ config }) {
|
|
|
2126
2243
|
baseUrl: apiBaseUrl,
|
|
2127
2244
|
refreshPath: "auth/refresh",
|
|
2128
2245
|
loginPath: "auth/login",
|
|
2246
|
+
getCsrfToken: () => (0, _nubitio_core.readCsrfToken)(),
|
|
2129
2247
|
onUnauthorized: () => {
|
|
2130
2248
|
session.logout();
|
|
2131
2249
|
}
|
|
@@ -2192,6 +2310,7 @@ function createNubitApp(config) {
|
|
|
2192
2310
|
client: queryClient,
|
|
2193
2311
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.ThemeProvider, {
|
|
2194
2312
|
basePath: config.themeBasePath,
|
|
2313
|
+
visualStyle: config.visualStyle,
|
|
2195
2314
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionProvider, {
|
|
2196
2315
|
apiBaseUrl,
|
|
2197
2316
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NubitAuthenticatedApp, { config })
|
|
@@ -2204,6 +2323,7 @@ function createNubitApp(config) {
|
|
|
2204
2323
|
//#endregion
|
|
2205
2324
|
exports.AcceptInvitationPage = AcceptInvitationPage;
|
|
2206
2325
|
exports.AccountPage = AccountPage;
|
|
2326
|
+
exports.AdminBottomNavigation = AdminBottomNavigation;
|
|
2207
2327
|
exports.AdminHeader = AdminHeader;
|
|
2208
2328
|
exports.AdminShell = AdminShell;
|
|
2209
2329
|
exports.AdminSidebarMenu = AdminSidebarMenu;
|