@nubitio/admin 1.1.0 → 1.1.2
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/dist/index.cjs +116 -62
- package/dist/index.d.cts +39 -141
- package/dist/index.d.mts +40 -142
- package/dist/index.mjs +116 -62
- 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,54 @@ 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: [
|
|
177
|
-
className: "nb-admin-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
169
|
+
const AdminHeader = ({ title, menuToggleEnabled, toggleMenu, className, actions = [], 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,
|
|
177
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
178
|
+
className: "nb-admin-header-toolbar__before",
|
|
179
|
+
children: [menuToggleEnabled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
|
|
180
|
+
className: "nb-admin-menu-button",
|
|
181
|
+
icon: "ph ph-list",
|
|
182
|
+
label: strings.toggleMenu,
|
|
183
|
+
onClick: toggleMenu
|
|
184
|
+
}), title && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
185
|
+
className: "nb-admin-header-title",
|
|
186
|
+
children: title
|
|
187
|
+
})]
|
|
188
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
189
|
+
className: "nb-admin-header-toolbar__after",
|
|
190
|
+
children: [
|
|
191
|
+
renderThemeSwitcher?.(),
|
|
192
|
+
actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionPopover, { action }, action.id)),
|
|
193
|
+
renderUserMenu && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UserMenuPopover, { renderUserMenu })
|
|
194
|
+
]
|
|
184
195
|
})]
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
renderThemeSwitcher?.(),
|
|
189
|
-
actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionPopover, { action }, action.id)),
|
|
190
|
-
renderUserMenu && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UserMenuPopover, { renderUserMenu })
|
|
191
|
-
]
|
|
192
|
-
})]
|
|
193
|
-
})
|
|
194
|
-
});
|
|
196
|
+
})
|
|
197
|
+
});
|
|
198
|
+
};
|
|
195
199
|
//#endregion
|
|
196
200
|
//#region packages/admin/useScreenSize.ts
|
|
197
201
|
let handlers = [];
|
|
@@ -257,6 +261,7 @@ const getIconClassName = (icon) => {
|
|
|
257
261
|
};
|
|
258
262
|
const AdminSidebarMenu = ({ items, compactMode = false, selectedItemChanged, openMenu, footer }) => {
|
|
259
263
|
const [expandedItemKeys, setExpandedItemKeys] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
264
|
+
const strings = (0, _nubitio_ui.useUiStrings)();
|
|
260
265
|
const { isLarge } = useScreenSize();
|
|
261
266
|
const location = (0, react_router_dom.useLocation)();
|
|
262
267
|
(0, react.useEffect)(() => {
|
|
@@ -307,7 +312,7 @@ const AdminSidebarMenu = ({ items, compactMode = false, selectedItemChanged, ope
|
|
|
307
312
|
className: "nb-admin-menu-container theme-dependent",
|
|
308
313
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("nav", {
|
|
309
314
|
className: "nb-admin-menu__nav",
|
|
310
|
-
"aria-label":
|
|
315
|
+
"aria-label": strings.mainNavigation,
|
|
311
316
|
children: items.map((item) => {
|
|
312
317
|
const selected = isSelected(item.path);
|
|
313
318
|
const containsSelected = parentContainsSelected(item);
|
|
@@ -371,6 +376,7 @@ const AdminSidebarMenu = ({ items, compactMode = false, selectedItemChanged, ope
|
|
|
371
376
|
//#region packages/admin/AdminShell.tsx
|
|
372
377
|
const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThemeSwitcher, footer, children }) => {
|
|
373
378
|
const navigate = (0, react_router_dom.useNavigate)();
|
|
379
|
+
const strings = (0, _nubitio_ui.useUiStrings)();
|
|
374
380
|
const { isXSmall, isLarge } = useScreenSize();
|
|
375
381
|
const [menuStatus, setMenuStatus] = (0, react.useState)(null);
|
|
376
382
|
const getDefaultMenuOpenState = (0, react.useCallback)(() => isLarge ? 2 : 1, [isLarge]);
|
|
@@ -438,7 +444,7 @@ const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThe
|
|
|
438
444
|
children: [
|
|
439
445
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("aside", {
|
|
440
446
|
className: "nb-admin-shell__panel",
|
|
441
|
-
"aria-label":
|
|
447
|
+
"aria-label": strings.mainMenu,
|
|
442
448
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdminSidebarMenu, {
|
|
443
449
|
items: menuItems,
|
|
444
450
|
compactMode: isCompact,
|
|
@@ -450,13 +456,13 @@ const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThe
|
|
|
450
456
|
!isLarge && isMenuOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
451
457
|
className: "nb-admin-shell__scrim",
|
|
452
458
|
type: "button",
|
|
453
|
-
"aria-label":
|
|
459
|
+
"aria-label": strings.closeMenu,
|
|
454
460
|
onClick: closeMenuFromOverlay
|
|
455
461
|
}),
|
|
456
462
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("main", {
|
|
457
463
|
className: "nb-admin-shell__content content",
|
|
458
464
|
tabIndex: 0,
|
|
459
|
-
"aria-label": title ? `${title} content` :
|
|
465
|
+
"aria-label": title ? `${title} content` : strings.mainContent,
|
|
460
466
|
children
|
|
461
467
|
})
|
|
462
468
|
]
|
|
@@ -464,6 +470,35 @@ const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThe
|
|
|
464
470
|
});
|
|
465
471
|
};
|
|
466
472
|
//#endregion
|
|
473
|
+
//#region packages/admin/auth/csrfFetch.ts
|
|
474
|
+
const MUTATING_METHODS = /* @__PURE__ */ new Set([
|
|
475
|
+
"POST",
|
|
476
|
+
"PUT",
|
|
477
|
+
"PATCH",
|
|
478
|
+
"DELETE"
|
|
479
|
+
]);
|
|
480
|
+
/**
|
|
481
|
+
* `fetch` that echoes the double-submit CSRF cookie as `X-CSRF-Token` on
|
|
482
|
+
* state-changing requests.
|
|
483
|
+
*
|
|
484
|
+
* The backend rejects cookie-authenticated POST/PUT/PATCH/DELETE that lack it
|
|
485
|
+
* (`nubit_admin.auth.csrf_protection`, on by default), so the auth pages' own
|
|
486
|
+
* calls — logout, change password, TOTP, session revocation — need it exactly
|
|
487
|
+
* as the CRUD client does. Reads, and requests made before login has issued the
|
|
488
|
+
* cookie, pass through untouched.
|
|
489
|
+
*/
|
|
490
|
+
function csrfFetch(input, init = {}) {
|
|
491
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
492
|
+
const token = MUTATING_METHODS.has(method) ? (0, _nubitio_core.readCsrfToken)() : void 0;
|
|
493
|
+
if (token === void 0) return fetch(input, init);
|
|
494
|
+
const headers = new Headers(init.headers);
|
|
495
|
+
if (!headers.has(_nubitio_core.DEFAULT_CSRF_HEADER_NAME)) headers.set(_nubitio_core.DEFAULT_CSRF_HEADER_NAME, token);
|
|
496
|
+
return fetch(input, {
|
|
497
|
+
...init,
|
|
498
|
+
headers
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
//#endregion
|
|
467
502
|
//#region packages/admin/auth/SessionContext.tsx
|
|
468
503
|
const SessionContext = (0, react.createContext)(null);
|
|
469
504
|
function joinApiPath$7(apiBaseUrl, path) {
|
|
@@ -478,9 +513,10 @@ function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "au
|
|
|
478
513
|
setSession({ status: "anonymous" });
|
|
479
514
|
return;
|
|
480
515
|
}
|
|
516
|
+
const profile = await response.json();
|
|
481
517
|
setSession({
|
|
482
518
|
status: "authenticated",
|
|
483
|
-
profile
|
|
519
|
+
profile
|
|
484
520
|
});
|
|
485
521
|
} catch {
|
|
486
522
|
setSession({ status: "anonymous" });
|
|
@@ -490,7 +526,7 @@ function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "au
|
|
|
490
526
|
refresh();
|
|
491
527
|
}, [refresh]);
|
|
492
528
|
const logout = (0, react.useCallback)(async () => {
|
|
493
|
-
await
|
|
529
|
+
await csrfFetch(joinApiPath$7(apiBaseUrl, logoutPath), {
|
|
494
530
|
method: "POST",
|
|
495
531
|
credentials: "include"
|
|
496
532
|
});
|
|
@@ -551,9 +587,10 @@ function useFeatureConfig(featureKey) {
|
|
|
551
587
|
//#endregion
|
|
552
588
|
//#region packages/admin/features/FeatureGate.tsx
|
|
553
589
|
function FeatureGate({ featureKey, ...props }) {
|
|
590
|
+
const enabled = useFeature(featureKey);
|
|
554
591
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.FeatureGate, {
|
|
555
592
|
featureKey,
|
|
556
|
-
enabled
|
|
593
|
+
enabled,
|
|
557
594
|
...props
|
|
558
595
|
});
|
|
559
596
|
}
|
|
@@ -601,10 +638,13 @@ function PlanPanel({ plans, currentPlanId, subscriptionStatus, trialEndsAt, onSe
|
|
|
601
638
|
const currentBadge = labels?.currentBadge ?? "Current plan";
|
|
602
639
|
const selectingLabel = labels?.selecting ?? "Updating…";
|
|
603
640
|
let statusLine = null;
|
|
604
|
-
if (subscriptionStatus === "trialing" && trialEndsAt)
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
641
|
+
if (subscriptionStatus === "trialing" && trialEndsAt) {
|
|
642
|
+
const trialTemplate = labels?.trialEnds ?? "Trial ends {date}";
|
|
643
|
+
statusLine = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
644
|
+
className: "nb-plan-panel__status",
|
|
645
|
+
children: trialTemplate.replace("{date}", formatTrialEnds(trialEndsAt))
|
|
646
|
+
});
|
|
647
|
+
}
|
|
608
648
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
609
649
|
className: `nb-plan-panel${className ? ` ${className}` : ""}`,
|
|
610
650
|
"aria-labelledby": "plan-panel-title",
|
|
@@ -1000,7 +1040,7 @@ function ForgotPasswordPage({ apiBaseUrl = "/api/" }) {
|
|
|
1000
1040
|
event.preventDefault();
|
|
1001
1041
|
setBusy(true);
|
|
1002
1042
|
try {
|
|
1003
|
-
await
|
|
1043
|
+
await csrfFetch(joinApiPath$5(apiBaseUrl, "auth/password/forgot"), {
|
|
1004
1044
|
method: "POST",
|
|
1005
1045
|
headers: { "Content-Type": "application/json" },
|
|
1006
1046
|
credentials: "include",
|
|
@@ -1076,7 +1116,7 @@ function ResetPasswordPage({ apiBaseUrl = "/api/" }) {
|
|
|
1076
1116
|
setBusy(true);
|
|
1077
1117
|
setError(null);
|
|
1078
1118
|
try {
|
|
1079
|
-
const response = await
|
|
1119
|
+
const response = await csrfFetch(joinApiPath$4(apiBaseUrl, "auth/password/reset"), {
|
|
1080
1120
|
method: "POST",
|
|
1081
1121
|
headers: { "Content-Type": "application/json" },
|
|
1082
1122
|
credentials: "include",
|
|
@@ -1086,7 +1126,8 @@ function ResetPasswordPage({ apiBaseUrl = "/api/" }) {
|
|
|
1086
1126
|
})
|
|
1087
1127
|
});
|
|
1088
1128
|
if (!response.ok) {
|
|
1089
|
-
|
|
1129
|
+
const payload = await response.json().catch(() => null);
|
|
1130
|
+
setError(payload?.message ?? "Reset failed");
|
|
1090
1131
|
return;
|
|
1091
1132
|
}
|
|
1092
1133
|
setDone(true);
|
|
@@ -1168,13 +1209,14 @@ function AcceptInvitationPage({ apiBaseUrl = "/api/" }) {
|
|
|
1168
1209
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
1169
1210
|
(0, react.useEffect)(() => {
|
|
1170
1211
|
let cancelled = false;
|
|
1171
|
-
|
|
1212
|
+
csrfFetch(joinApiPath$3(apiBaseUrl, `invitations/${token}`), { credentials: "include" }).then(async (response) => {
|
|
1172
1213
|
if (cancelled) return;
|
|
1173
1214
|
if (!response.ok) {
|
|
1174
1215
|
setError("This invitation is no longer valid.");
|
|
1175
1216
|
return;
|
|
1176
1217
|
}
|
|
1177
|
-
|
|
1218
|
+
const payload = await response.json();
|
|
1219
|
+
setEmail(payload.email ?? null);
|
|
1178
1220
|
}).catch(() => {
|
|
1179
1221
|
if (!cancelled) setError("Network error");
|
|
1180
1222
|
});
|
|
@@ -1187,14 +1229,15 @@ function AcceptInvitationPage({ apiBaseUrl = "/api/" }) {
|
|
|
1187
1229
|
setBusy(true);
|
|
1188
1230
|
setError(null);
|
|
1189
1231
|
try {
|
|
1190
|
-
const response = await
|
|
1232
|
+
const response = await csrfFetch(joinApiPath$3(apiBaseUrl, `invitations/${token}/accept`), {
|
|
1191
1233
|
method: "POST",
|
|
1192
1234
|
headers: { "Content-Type": "application/json" },
|
|
1193
1235
|
credentials: "include",
|
|
1194
1236
|
body: JSON.stringify({ password })
|
|
1195
1237
|
});
|
|
1196
1238
|
if (!response.ok) {
|
|
1197
|
-
|
|
1239
|
+
const payload = await response.json().catch(() => null);
|
|
1240
|
+
setError(payload?.message ?? "Could not accept the invitation");
|
|
1198
1241
|
return;
|
|
1199
1242
|
}
|
|
1200
1243
|
setDone(true);
|
|
@@ -1294,7 +1337,10 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1294
1337
|
})]);
|
|
1295
1338
|
if (signal?.aborted) return;
|
|
1296
1339
|
if (totpResponse.ok) setTotp(await totpResponse.json());
|
|
1297
|
-
if (sessionResponse.ok)
|
|
1340
|
+
if (sessionResponse.ok) {
|
|
1341
|
+
const payload = await sessionResponse.json();
|
|
1342
|
+
setSessions(payload.sessions ?? []);
|
|
1343
|
+
}
|
|
1298
1344
|
}, [apiBaseUrl]);
|
|
1299
1345
|
(0, react.useEffect)(() => {
|
|
1300
1346
|
const controller = new AbortController();
|
|
@@ -1306,7 +1352,7 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1306
1352
|
const changePassword = async (event) => {
|
|
1307
1353
|
event.preventDefault();
|
|
1308
1354
|
setPasswordMessage(null);
|
|
1309
|
-
const response = await
|
|
1355
|
+
const response = await csrfFetch(joinApiPath$2(apiBaseUrl, "auth/change-password"), {
|
|
1310
1356
|
method: "POST",
|
|
1311
1357
|
headers: { "Content-Type": "application/json" },
|
|
1312
1358
|
credentials: "include",
|
|
@@ -1316,7 +1362,8 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1316
1362
|
})
|
|
1317
1363
|
});
|
|
1318
1364
|
if (!response.ok) {
|
|
1319
|
-
|
|
1365
|
+
const payload = await response.json().catch(() => null);
|
|
1366
|
+
setPasswordMessage(payload?.message ?? "Could not change password");
|
|
1320
1367
|
return;
|
|
1321
1368
|
}
|
|
1322
1369
|
setCurrentPassword("");
|
|
@@ -1326,12 +1373,13 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1326
1373
|
};
|
|
1327
1374
|
const beginTotp = async () => {
|
|
1328
1375
|
setTotpMessage(null);
|
|
1329
|
-
const response = await
|
|
1376
|
+
const response = await csrfFetch(joinApiPath$2(apiBaseUrl, "auth/totp"), {
|
|
1330
1377
|
method: "POST",
|
|
1331
1378
|
credentials: "include"
|
|
1332
1379
|
});
|
|
1333
1380
|
if (!response.ok) {
|
|
1334
|
-
|
|
1381
|
+
const payload = await response.json().catch(() => null);
|
|
1382
|
+
setTotpMessage(payload?.message ?? "Could not start enrolment");
|
|
1335
1383
|
return;
|
|
1336
1384
|
}
|
|
1337
1385
|
const payload = await response.json();
|
|
@@ -1340,14 +1388,15 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1340
1388
|
};
|
|
1341
1389
|
const confirmTotp = async (event) => {
|
|
1342
1390
|
event.preventDefault();
|
|
1343
|
-
const response = await
|
|
1391
|
+
const response = await csrfFetch(joinApiPath$2(apiBaseUrl, "auth/totp/confirm"), {
|
|
1344
1392
|
method: "POST",
|
|
1345
1393
|
headers: { "Content-Type": "application/json" },
|
|
1346
1394
|
credentials: "include",
|
|
1347
1395
|
body: JSON.stringify({ code: totpCode })
|
|
1348
1396
|
});
|
|
1349
1397
|
if (!response.ok) {
|
|
1350
|
-
|
|
1398
|
+
const payload = await response.json().catch(() => null);
|
|
1399
|
+
setTotpMessage(payload?.message ?? "That code is not valid");
|
|
1351
1400
|
return;
|
|
1352
1401
|
}
|
|
1353
1402
|
setTotpCode("");
|
|
@@ -1357,14 +1406,15 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1357
1406
|
};
|
|
1358
1407
|
const disableTotp = async (event) => {
|
|
1359
1408
|
event.preventDefault();
|
|
1360
|
-
const response = await
|
|
1409
|
+
const response = await csrfFetch(joinApiPath$2(apiBaseUrl, "auth/totp"), {
|
|
1361
1410
|
method: "DELETE",
|
|
1362
1411
|
headers: { "Content-Type": "application/json" },
|
|
1363
1412
|
credentials: "include",
|
|
1364
1413
|
body: JSON.stringify({ code: totpCode })
|
|
1365
1414
|
});
|
|
1366
1415
|
if (!response.ok) {
|
|
1367
|
-
|
|
1416
|
+
const payload = await response.json().catch(() => null);
|
|
1417
|
+
setTotpMessage(payload?.message ?? "Could not disable the second factor");
|
|
1368
1418
|
return;
|
|
1369
1419
|
}
|
|
1370
1420
|
setTotpCode("");
|
|
@@ -1372,7 +1422,7 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1372
1422
|
await load();
|
|
1373
1423
|
};
|
|
1374
1424
|
const revokeSession = async (id) => {
|
|
1375
|
-
await
|
|
1425
|
+
await csrfFetch(joinApiPath$2(apiBaseUrl, `auth/sessions/${id}`), {
|
|
1376
1426
|
method: "DELETE",
|
|
1377
1427
|
credentials: "include"
|
|
1378
1428
|
});
|
|
@@ -1558,7 +1608,8 @@ function initialValues(fields) {
|
|
|
1558
1608
|
return Object.fromEntries(fields.map((field) => [field.name, field.defaultValue ?? ""]));
|
|
1559
1609
|
}
|
|
1560
1610
|
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
|
|
1611
|
+
const defaults = (0, react.useMemo)(() => initialValues(fields), [fields]);
|
|
1612
|
+
const [values, setValues] = (0, react.useState)(defaults);
|
|
1562
1613
|
const [error, setError] = (0, react.useState)(null);
|
|
1563
1614
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
1564
1615
|
const setValue = (name, value) => {
|
|
@@ -1572,7 +1623,7 @@ function RegisterPage({ fields, onRegistered, apiBaseUrl = "/api/", registerPath
|
|
|
1572
1623
|
setBusy(true);
|
|
1573
1624
|
setError(null);
|
|
1574
1625
|
try {
|
|
1575
|
-
const response = await
|
|
1626
|
+
const response = await csrfFetch(joinApiPath$1(apiBaseUrl, registerPath), {
|
|
1576
1627
|
method: "POST",
|
|
1577
1628
|
headers: { "Content-Type": "application/json" },
|
|
1578
1629
|
credentials: "include",
|
|
@@ -1716,9 +1767,10 @@ function useRuntimeConfig({ apiBaseUrl = "/api/", path = "runtime-config", enabl
|
|
|
1716
1767
|
setState({ status: "error" });
|
|
1717
1768
|
return;
|
|
1718
1769
|
}
|
|
1770
|
+
const config = await response.json();
|
|
1719
1771
|
setState({
|
|
1720
1772
|
status: "ready",
|
|
1721
|
-
config
|
|
1773
|
+
config
|
|
1722
1774
|
});
|
|
1723
1775
|
} catch {
|
|
1724
1776
|
setState({ status: "error" });
|
|
@@ -2059,6 +2111,7 @@ function resolveShellMenu(config, ctx) {
|
|
|
2059
2111
|
}
|
|
2060
2112
|
function NubitAuthenticatedApp({ config }) {
|
|
2061
2113
|
const session = useSession();
|
|
2114
|
+
const strings = (0, _nubitio_ui.useUiStrings)();
|
|
2062
2115
|
const { runtime, toasts, dismiss } = useAppRuntime();
|
|
2063
2116
|
const apiBaseUrl = config.apiBaseUrl ?? "/api/";
|
|
2064
2117
|
const homePath = config.homePath ?? config.routes[0]?.path ?? "/";
|
|
@@ -2075,7 +2128,7 @@ function NubitAuthenticatedApp({ config }) {
|
|
|
2075
2128
|
if (session.session.status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2076
2129
|
style: { padding: 24 },
|
|
2077
2130
|
"aria-busy": "true",
|
|
2078
|
-
"aria-label":
|
|
2131
|
+
"aria-label": strings.loadingSession,
|
|
2079
2132
|
children: [
|
|
2080
2133
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Skeleton, {
|
|
2081
2134
|
variant: "rect",
|
|
@@ -2126,6 +2179,7 @@ function NubitAuthenticatedApp({ config }) {
|
|
|
2126
2179
|
baseUrl: apiBaseUrl,
|
|
2127
2180
|
refreshPath: "auth/refresh",
|
|
2128
2181
|
loginPath: "auth/login",
|
|
2182
|
+
getCsrfToken: () => (0, _nubitio_core.readCsrfToken)(),
|
|
2129
2183
|
onUnauthorized: () => {
|
|
2130
2184
|
session.logout();
|
|
2131
2185
|
}
|