@nubitio/admin 1.1.1 → 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.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,54 @@ 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: [
|
|
153
|
-
className: "nb-admin-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
145
|
+
const AdminHeader = ({ title, menuToggleEnabled, toggleMenu, className, actions = [], 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,
|
|
153
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
154
|
+
className: "nb-admin-header-toolbar__before",
|
|
155
|
+
children: [menuToggleEnabled && /* @__PURE__ */ jsx(IconButton, {
|
|
156
|
+
className: "nb-admin-menu-button",
|
|
157
|
+
icon: "ph ph-list",
|
|
158
|
+
label: strings.toggleMenu,
|
|
159
|
+
onClick: toggleMenu
|
|
160
|
+
}), title && /* @__PURE__ */ jsx("div", {
|
|
161
|
+
className: "nb-admin-header-title",
|
|
162
|
+
children: title
|
|
163
|
+
})]
|
|
164
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
165
|
+
className: "nb-admin-header-toolbar__after",
|
|
166
|
+
children: [
|
|
167
|
+
renderThemeSwitcher?.(),
|
|
168
|
+
actions.map((action) => /* @__PURE__ */ jsx(ActionPopover, { action }, action.id)),
|
|
169
|
+
renderUserMenu && /* @__PURE__ */ jsx(UserMenuPopover, { renderUserMenu })
|
|
170
|
+
]
|
|
160
171
|
})]
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
renderThemeSwitcher?.(),
|
|
165
|
-
actions.map((action) => /* @__PURE__ */ jsx(ActionPopover, { action }, action.id)),
|
|
166
|
-
renderUserMenu && /* @__PURE__ */ jsx(UserMenuPopover, { renderUserMenu })
|
|
167
|
-
]
|
|
168
|
-
})]
|
|
169
|
-
})
|
|
170
|
-
});
|
|
172
|
+
})
|
|
173
|
+
});
|
|
174
|
+
};
|
|
171
175
|
//#endregion
|
|
172
176
|
//#region packages/admin/useScreenSize.ts
|
|
173
177
|
let handlers = [];
|
|
@@ -233,6 +237,7 @@ const getIconClassName = (icon) => {
|
|
|
233
237
|
};
|
|
234
238
|
const AdminSidebarMenu = ({ items, compactMode = false, selectedItemChanged, openMenu, footer }) => {
|
|
235
239
|
const [expandedItemKeys, setExpandedItemKeys] = useState(/* @__PURE__ */ new Set());
|
|
240
|
+
const strings = useUiStrings();
|
|
236
241
|
const { isLarge } = useScreenSize();
|
|
237
242
|
const location = useLocation();
|
|
238
243
|
useEffect(() => {
|
|
@@ -283,7 +288,7 @@ const AdminSidebarMenu = ({ items, compactMode = false, selectedItemChanged, ope
|
|
|
283
288
|
className: "nb-admin-menu-container theme-dependent",
|
|
284
289
|
children: /* @__PURE__ */ jsx("nav", {
|
|
285
290
|
className: "nb-admin-menu__nav",
|
|
286
|
-
"aria-label":
|
|
291
|
+
"aria-label": strings.mainNavigation,
|
|
287
292
|
children: items.map((item) => {
|
|
288
293
|
const selected = isSelected(item.path);
|
|
289
294
|
const containsSelected = parentContainsSelected(item);
|
|
@@ -347,6 +352,7 @@ const AdminSidebarMenu = ({ items, compactMode = false, selectedItemChanged, ope
|
|
|
347
352
|
//#region packages/admin/AdminShell.tsx
|
|
348
353
|
const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThemeSwitcher, footer, children }) => {
|
|
349
354
|
const navigate = useNavigate();
|
|
355
|
+
const strings = useUiStrings();
|
|
350
356
|
const { isXSmall, isLarge } = useScreenSize();
|
|
351
357
|
const [menuStatus, setMenuStatus] = useState(null);
|
|
352
358
|
const getDefaultMenuOpenState = useCallback(() => isLarge ? 2 : 1, [isLarge]);
|
|
@@ -414,7 +420,7 @@ const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThe
|
|
|
414
420
|
children: [
|
|
415
421
|
/* @__PURE__ */ jsx("aside", {
|
|
416
422
|
className: "nb-admin-shell__panel",
|
|
417
|
-
"aria-label":
|
|
423
|
+
"aria-label": strings.mainMenu,
|
|
418
424
|
children: /* @__PURE__ */ jsx(AdminSidebarMenu, {
|
|
419
425
|
items: menuItems,
|
|
420
426
|
compactMode: isCompact,
|
|
@@ -426,13 +432,13 @@ const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThe
|
|
|
426
432
|
!isLarge && isMenuOpen && /* @__PURE__ */ jsx("button", {
|
|
427
433
|
className: "nb-admin-shell__scrim",
|
|
428
434
|
type: "button",
|
|
429
|
-
"aria-label":
|
|
435
|
+
"aria-label": strings.closeMenu,
|
|
430
436
|
onClick: closeMenuFromOverlay
|
|
431
437
|
}),
|
|
432
438
|
/* @__PURE__ */ jsx("main", {
|
|
433
439
|
className: "nb-admin-shell__content content",
|
|
434
440
|
tabIndex: 0,
|
|
435
|
-
"aria-label": title ? `${title} content` :
|
|
441
|
+
"aria-label": title ? `${title} content` : strings.mainContent,
|
|
436
442
|
children
|
|
437
443
|
})
|
|
438
444
|
]
|
|
@@ -440,6 +446,35 @@ const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThe
|
|
|
440
446
|
});
|
|
441
447
|
};
|
|
442
448
|
//#endregion
|
|
449
|
+
//#region packages/admin/auth/csrfFetch.ts
|
|
450
|
+
const MUTATING_METHODS = /* @__PURE__ */ new Set([
|
|
451
|
+
"POST",
|
|
452
|
+
"PUT",
|
|
453
|
+
"PATCH",
|
|
454
|
+
"DELETE"
|
|
455
|
+
]);
|
|
456
|
+
/**
|
|
457
|
+
* `fetch` that echoes the double-submit CSRF cookie as `X-CSRF-Token` on
|
|
458
|
+
* state-changing requests.
|
|
459
|
+
*
|
|
460
|
+
* The backend rejects cookie-authenticated POST/PUT/PATCH/DELETE that lack it
|
|
461
|
+
* (`nubit_admin.auth.csrf_protection`, on by default), so the auth pages' own
|
|
462
|
+
* calls — logout, change password, TOTP, session revocation — need it exactly
|
|
463
|
+
* as the CRUD client does. Reads, and requests made before login has issued the
|
|
464
|
+
* cookie, pass through untouched.
|
|
465
|
+
*/
|
|
466
|
+
function csrfFetch(input, init = {}) {
|
|
467
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
468
|
+
const token = MUTATING_METHODS.has(method) ? readCsrfToken() : void 0;
|
|
469
|
+
if (token === void 0) return fetch(input, init);
|
|
470
|
+
const headers = new Headers(init.headers);
|
|
471
|
+
if (!headers.has(DEFAULT_CSRF_HEADER_NAME)) headers.set(DEFAULT_CSRF_HEADER_NAME, token);
|
|
472
|
+
return fetch(input, {
|
|
473
|
+
...init,
|
|
474
|
+
headers
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
//#endregion
|
|
443
478
|
//#region packages/admin/auth/SessionContext.tsx
|
|
444
479
|
const SessionContext = createContext(null);
|
|
445
480
|
function joinApiPath$7(apiBaseUrl, path) {
|
|
@@ -454,9 +489,10 @@ function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "au
|
|
|
454
489
|
setSession({ status: "anonymous" });
|
|
455
490
|
return;
|
|
456
491
|
}
|
|
492
|
+
const profile = await response.json();
|
|
457
493
|
setSession({
|
|
458
494
|
status: "authenticated",
|
|
459
|
-
profile
|
|
495
|
+
profile
|
|
460
496
|
});
|
|
461
497
|
} catch {
|
|
462
498
|
setSession({ status: "anonymous" });
|
|
@@ -466,7 +502,7 @@ function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "au
|
|
|
466
502
|
refresh();
|
|
467
503
|
}, [refresh]);
|
|
468
504
|
const logout = useCallback(async () => {
|
|
469
|
-
await
|
|
505
|
+
await csrfFetch(joinApiPath$7(apiBaseUrl, logoutPath), {
|
|
470
506
|
method: "POST",
|
|
471
507
|
credentials: "include"
|
|
472
508
|
});
|
|
@@ -527,9 +563,10 @@ function useFeatureConfig(featureKey) {
|
|
|
527
563
|
//#endregion
|
|
528
564
|
//#region packages/admin/features/FeatureGate.tsx
|
|
529
565
|
function FeatureGate({ featureKey, ...props }) {
|
|
566
|
+
const enabled = useFeature(featureKey);
|
|
530
567
|
return /* @__PURE__ */ jsx(FeatureGate$1, {
|
|
531
568
|
featureKey,
|
|
532
|
-
enabled
|
|
569
|
+
enabled,
|
|
533
570
|
...props
|
|
534
571
|
});
|
|
535
572
|
}
|
|
@@ -577,10 +614,13 @@ function PlanPanel({ plans, currentPlanId, subscriptionStatus, trialEndsAt, onSe
|
|
|
577
614
|
const currentBadge = labels?.currentBadge ?? "Current plan";
|
|
578
615
|
const selectingLabel = labels?.selecting ?? "Updating…";
|
|
579
616
|
let statusLine = null;
|
|
580
|
-
if (subscriptionStatus === "trialing" && trialEndsAt)
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
617
|
+
if (subscriptionStatus === "trialing" && trialEndsAt) {
|
|
618
|
+
const trialTemplate = labels?.trialEnds ?? "Trial ends {date}";
|
|
619
|
+
statusLine = /* @__PURE__ */ jsx("p", {
|
|
620
|
+
className: "nb-plan-panel__status",
|
|
621
|
+
children: trialTemplate.replace("{date}", formatTrialEnds(trialEndsAt))
|
|
622
|
+
});
|
|
623
|
+
}
|
|
584
624
|
return /* @__PURE__ */ jsxs("section", {
|
|
585
625
|
className: `nb-plan-panel${className ? ` ${className}` : ""}`,
|
|
586
626
|
"aria-labelledby": "plan-panel-title",
|
|
@@ -976,7 +1016,7 @@ function ForgotPasswordPage({ apiBaseUrl = "/api/" }) {
|
|
|
976
1016
|
event.preventDefault();
|
|
977
1017
|
setBusy(true);
|
|
978
1018
|
try {
|
|
979
|
-
await
|
|
1019
|
+
await csrfFetch(joinApiPath$5(apiBaseUrl, "auth/password/forgot"), {
|
|
980
1020
|
method: "POST",
|
|
981
1021
|
headers: { "Content-Type": "application/json" },
|
|
982
1022
|
credentials: "include",
|
|
@@ -1052,7 +1092,7 @@ function ResetPasswordPage({ apiBaseUrl = "/api/" }) {
|
|
|
1052
1092
|
setBusy(true);
|
|
1053
1093
|
setError(null);
|
|
1054
1094
|
try {
|
|
1055
|
-
const response = await
|
|
1095
|
+
const response = await csrfFetch(joinApiPath$4(apiBaseUrl, "auth/password/reset"), {
|
|
1056
1096
|
method: "POST",
|
|
1057
1097
|
headers: { "Content-Type": "application/json" },
|
|
1058
1098
|
credentials: "include",
|
|
@@ -1062,7 +1102,8 @@ function ResetPasswordPage({ apiBaseUrl = "/api/" }) {
|
|
|
1062
1102
|
})
|
|
1063
1103
|
});
|
|
1064
1104
|
if (!response.ok) {
|
|
1065
|
-
|
|
1105
|
+
const payload = await response.json().catch(() => null);
|
|
1106
|
+
setError(payload?.message ?? "Reset failed");
|
|
1066
1107
|
return;
|
|
1067
1108
|
}
|
|
1068
1109
|
setDone(true);
|
|
@@ -1144,13 +1185,14 @@ function AcceptInvitationPage({ apiBaseUrl = "/api/" }) {
|
|
|
1144
1185
|
const [busy, setBusy] = useState(false);
|
|
1145
1186
|
useEffect(() => {
|
|
1146
1187
|
let cancelled = false;
|
|
1147
|
-
|
|
1188
|
+
csrfFetch(joinApiPath$3(apiBaseUrl, `invitations/${token}`), { credentials: "include" }).then(async (response) => {
|
|
1148
1189
|
if (cancelled) return;
|
|
1149
1190
|
if (!response.ok) {
|
|
1150
1191
|
setError("This invitation is no longer valid.");
|
|
1151
1192
|
return;
|
|
1152
1193
|
}
|
|
1153
|
-
|
|
1194
|
+
const payload = await response.json();
|
|
1195
|
+
setEmail(payload.email ?? null);
|
|
1154
1196
|
}).catch(() => {
|
|
1155
1197
|
if (!cancelled) setError("Network error");
|
|
1156
1198
|
});
|
|
@@ -1163,14 +1205,15 @@ function AcceptInvitationPage({ apiBaseUrl = "/api/" }) {
|
|
|
1163
1205
|
setBusy(true);
|
|
1164
1206
|
setError(null);
|
|
1165
1207
|
try {
|
|
1166
|
-
const response = await
|
|
1208
|
+
const response = await csrfFetch(joinApiPath$3(apiBaseUrl, `invitations/${token}/accept`), {
|
|
1167
1209
|
method: "POST",
|
|
1168
1210
|
headers: { "Content-Type": "application/json" },
|
|
1169
1211
|
credentials: "include",
|
|
1170
1212
|
body: JSON.stringify({ password })
|
|
1171
1213
|
});
|
|
1172
1214
|
if (!response.ok) {
|
|
1173
|
-
|
|
1215
|
+
const payload = await response.json().catch(() => null);
|
|
1216
|
+
setError(payload?.message ?? "Could not accept the invitation");
|
|
1174
1217
|
return;
|
|
1175
1218
|
}
|
|
1176
1219
|
setDone(true);
|
|
@@ -1270,7 +1313,10 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1270
1313
|
})]);
|
|
1271
1314
|
if (signal?.aborted) return;
|
|
1272
1315
|
if (totpResponse.ok) setTotp(await totpResponse.json());
|
|
1273
|
-
if (sessionResponse.ok)
|
|
1316
|
+
if (sessionResponse.ok) {
|
|
1317
|
+
const payload = await sessionResponse.json();
|
|
1318
|
+
setSessions(payload.sessions ?? []);
|
|
1319
|
+
}
|
|
1274
1320
|
}, [apiBaseUrl]);
|
|
1275
1321
|
useEffect(() => {
|
|
1276
1322
|
const controller = new AbortController();
|
|
@@ -1282,7 +1328,7 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1282
1328
|
const changePassword = async (event) => {
|
|
1283
1329
|
event.preventDefault();
|
|
1284
1330
|
setPasswordMessage(null);
|
|
1285
|
-
const response = await
|
|
1331
|
+
const response = await csrfFetch(joinApiPath$2(apiBaseUrl, "auth/change-password"), {
|
|
1286
1332
|
method: "POST",
|
|
1287
1333
|
headers: { "Content-Type": "application/json" },
|
|
1288
1334
|
credentials: "include",
|
|
@@ -1292,7 +1338,8 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1292
1338
|
})
|
|
1293
1339
|
});
|
|
1294
1340
|
if (!response.ok) {
|
|
1295
|
-
|
|
1341
|
+
const payload = await response.json().catch(() => null);
|
|
1342
|
+
setPasswordMessage(payload?.message ?? "Could not change password");
|
|
1296
1343
|
return;
|
|
1297
1344
|
}
|
|
1298
1345
|
setCurrentPassword("");
|
|
@@ -1302,12 +1349,13 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1302
1349
|
};
|
|
1303
1350
|
const beginTotp = async () => {
|
|
1304
1351
|
setTotpMessage(null);
|
|
1305
|
-
const response = await
|
|
1352
|
+
const response = await csrfFetch(joinApiPath$2(apiBaseUrl, "auth/totp"), {
|
|
1306
1353
|
method: "POST",
|
|
1307
1354
|
credentials: "include"
|
|
1308
1355
|
});
|
|
1309
1356
|
if (!response.ok) {
|
|
1310
|
-
|
|
1357
|
+
const payload = await response.json().catch(() => null);
|
|
1358
|
+
setTotpMessage(payload?.message ?? "Could not start enrolment");
|
|
1311
1359
|
return;
|
|
1312
1360
|
}
|
|
1313
1361
|
const payload = await response.json();
|
|
@@ -1316,14 +1364,15 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1316
1364
|
};
|
|
1317
1365
|
const confirmTotp = async (event) => {
|
|
1318
1366
|
event.preventDefault();
|
|
1319
|
-
const response = await
|
|
1367
|
+
const response = await csrfFetch(joinApiPath$2(apiBaseUrl, "auth/totp/confirm"), {
|
|
1320
1368
|
method: "POST",
|
|
1321
1369
|
headers: { "Content-Type": "application/json" },
|
|
1322
1370
|
credentials: "include",
|
|
1323
1371
|
body: JSON.stringify({ code: totpCode })
|
|
1324
1372
|
});
|
|
1325
1373
|
if (!response.ok) {
|
|
1326
|
-
|
|
1374
|
+
const payload = await response.json().catch(() => null);
|
|
1375
|
+
setTotpMessage(payload?.message ?? "That code is not valid");
|
|
1327
1376
|
return;
|
|
1328
1377
|
}
|
|
1329
1378
|
setTotpCode("");
|
|
@@ -1333,14 +1382,15 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1333
1382
|
};
|
|
1334
1383
|
const disableTotp = async (event) => {
|
|
1335
1384
|
event.preventDefault();
|
|
1336
|
-
const response = await
|
|
1385
|
+
const response = await csrfFetch(joinApiPath$2(apiBaseUrl, "auth/totp"), {
|
|
1337
1386
|
method: "DELETE",
|
|
1338
1387
|
headers: { "Content-Type": "application/json" },
|
|
1339
1388
|
credentials: "include",
|
|
1340
1389
|
body: JSON.stringify({ code: totpCode })
|
|
1341
1390
|
});
|
|
1342
1391
|
if (!response.ok) {
|
|
1343
|
-
|
|
1392
|
+
const payload = await response.json().catch(() => null);
|
|
1393
|
+
setTotpMessage(payload?.message ?? "Could not disable the second factor");
|
|
1344
1394
|
return;
|
|
1345
1395
|
}
|
|
1346
1396
|
setTotpCode("");
|
|
@@ -1348,7 +1398,7 @@ function AccountPage({ apiBaseUrl = "/api/" }) {
|
|
|
1348
1398
|
await load();
|
|
1349
1399
|
};
|
|
1350
1400
|
const revokeSession = async (id) => {
|
|
1351
|
-
await
|
|
1401
|
+
await csrfFetch(joinApiPath$2(apiBaseUrl, `auth/sessions/${id}`), {
|
|
1352
1402
|
method: "DELETE",
|
|
1353
1403
|
credentials: "include"
|
|
1354
1404
|
});
|
|
@@ -1534,7 +1584,8 @@ function initialValues(fields) {
|
|
|
1534
1584
|
return Object.fromEntries(fields.map((field) => [field.name, field.defaultValue ?? ""]));
|
|
1535
1585
|
}
|
|
1536
1586
|
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
|
|
1587
|
+
const defaults = useMemo(() => initialValues(fields), [fields]);
|
|
1588
|
+
const [values, setValues] = useState(defaults);
|
|
1538
1589
|
const [error, setError] = useState(null);
|
|
1539
1590
|
const [busy, setBusy] = useState(false);
|
|
1540
1591
|
const setValue = (name, value) => {
|
|
@@ -1548,7 +1599,7 @@ function RegisterPage({ fields, onRegistered, apiBaseUrl = "/api/", registerPath
|
|
|
1548
1599
|
setBusy(true);
|
|
1549
1600
|
setError(null);
|
|
1550
1601
|
try {
|
|
1551
|
-
const response = await
|
|
1602
|
+
const response = await csrfFetch(joinApiPath$1(apiBaseUrl, registerPath), {
|
|
1552
1603
|
method: "POST",
|
|
1553
1604
|
headers: { "Content-Type": "application/json" },
|
|
1554
1605
|
credentials: "include",
|
|
@@ -1692,9 +1743,10 @@ function useRuntimeConfig({ apiBaseUrl = "/api/", path = "runtime-config", enabl
|
|
|
1692
1743
|
setState({ status: "error" });
|
|
1693
1744
|
return;
|
|
1694
1745
|
}
|
|
1746
|
+
const config = await response.json();
|
|
1695
1747
|
setState({
|
|
1696
1748
|
status: "ready",
|
|
1697
|
-
config
|
|
1749
|
+
config
|
|
1698
1750
|
});
|
|
1699
1751
|
} catch {
|
|
1700
1752
|
setState({ status: "error" });
|
|
@@ -2035,6 +2087,7 @@ function resolveShellMenu(config, ctx) {
|
|
|
2035
2087
|
}
|
|
2036
2088
|
function NubitAuthenticatedApp({ config }) {
|
|
2037
2089
|
const session = useSession();
|
|
2090
|
+
const strings = useUiStrings();
|
|
2038
2091
|
const { runtime, toasts, dismiss } = useAppRuntime();
|
|
2039
2092
|
const apiBaseUrl = config.apiBaseUrl ?? "/api/";
|
|
2040
2093
|
const homePath = config.homePath ?? config.routes[0]?.path ?? "/";
|
|
@@ -2051,7 +2104,7 @@ function NubitAuthenticatedApp({ config }) {
|
|
|
2051
2104
|
if (session.session.status === "loading") return /* @__PURE__ */ jsxs("div", {
|
|
2052
2105
|
style: { padding: 24 },
|
|
2053
2106
|
"aria-busy": "true",
|
|
2054
|
-
"aria-label":
|
|
2107
|
+
"aria-label": strings.loadingSession,
|
|
2055
2108
|
children: [
|
|
2056
2109
|
/* @__PURE__ */ jsx(Skeleton, {
|
|
2057
2110
|
variant: "rect",
|
|
@@ -2102,6 +2155,7 @@ function NubitAuthenticatedApp({ config }) {
|
|
|
2102
2155
|
baseUrl: apiBaseUrl,
|
|
2103
2156
|
refreshPath: "auth/refresh",
|
|
2104
2157
|
loginPath: "auth/login",
|
|
2158
|
+
getCsrfToken: () => readCsrfToken(),
|
|
2105
2159
|
onUnauthorized: () => {
|
|
2106
2160
|
session.logout();
|
|
2107
2161
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nubitio/admin",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Admin shell layout components: responsive sidebar, header, and screen size utilities for Nubit apps.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -53,9 +53,12 @@
|
|
|
53
53
|
"react": "^18.2.0 || ^19.0.0",
|
|
54
54
|
"react-dom": "^18.2.0 || ^19.0.0",
|
|
55
55
|
"react-router-dom": "^6.0.0 || ^7.0.0",
|
|
56
|
-
"@nubitio/core": "^1.1.
|
|
57
|
-
"@nubitio/crud": "^1.1.
|
|
58
|
-
"@nubitio/
|
|
59
|
-
"@nubitio/
|
|
56
|
+
"@nubitio/core": "^1.1.2",
|
|
57
|
+
"@nubitio/crud": "^1.1.2",
|
|
58
|
+
"@nubitio/hydra": "^1.1.2",
|
|
59
|
+
"@nubitio/ui": "^1.1.2"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"react-router-dom": "^6.30.6"
|
|
60
63
|
}
|
|
61
64
|
}
|