@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.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: "User menu",
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": "User menu",
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 }) => /* @__PURE__ */ jsx("header", {
145
- className: ["nb-admin-header-component", className].filter(Boolean).join(" "),
146
- children: /* @__PURE__ */ jsxs("div", {
147
- className: "nb-admin-header-toolbar",
148
- role: "toolbar",
149
- "aria-label": "Main toolbar",
150
- children: [/* @__PURE__ */ jsxs("div", {
151
- className: "nb-admin-header-toolbar__before",
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
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
- }), /* @__PURE__ */ jsxs("div", {
162
- className: "nb-admin-header-toolbar__after",
163
- children: [
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": "Main navigation",
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": "Main menu",
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": "Close menu",
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` : "Main 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: await response.json()
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 fetch(joinApiPath$7(apiBaseUrl, logoutPath), {
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: useFeature(featureKey),
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) statusLine = /* @__PURE__ */ jsx("p", {
581
- className: "nb-plan-panel__status",
582
- children: (labels?.trialEnds ?? "Trial ends {date}").replace("{date}", formatTrialEnds(trialEndsAt))
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 fetch(joinApiPath$5(apiBaseUrl, "auth/password/forgot"), {
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 fetch(joinApiPath$4(apiBaseUrl, "auth/password/reset"), {
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
- setError((await response.json().catch(() => null))?.message ?? "Reset failed");
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
- fetch(joinApiPath$3(apiBaseUrl, `invitations/${token}`), { credentials: "include" }).then(async (response) => {
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
- setEmail((await response.json()).email ?? null);
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 fetch(joinApiPath$3(apiBaseUrl, `invitations/${token}/accept`), {
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
- setError((await response.json().catch(() => null))?.message ?? "Could not accept the invitation");
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) setSessions((await sessionResponse.json()).sessions ?? []);
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 fetch(joinApiPath$2(apiBaseUrl, "auth/change-password"), {
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
- setPasswordMessage((await response.json().catch(() => null))?.message ?? "Could not change password");
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 fetch(joinApiPath$2(apiBaseUrl, "auth/totp"), {
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
- setTotpMessage((await response.json().catch(() => null))?.message ?? "Could not start enrolment");
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 fetch(joinApiPath$2(apiBaseUrl, "auth/totp/confirm"), {
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
- setTotpMessage((await response.json().catch(() => null))?.message ?? "That code is not valid");
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 fetch(joinApiPath$2(apiBaseUrl, "auth/totp"), {
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
- setTotpMessage((await response.json().catch(() => null))?.message ?? "Could not disable the second factor");
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 fetch(joinApiPath$2(apiBaseUrl, `auth/sessions/${id}`), {
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 [values, setValues] = useState(useMemo(() => initialValues(fields), [fields]));
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 fetch(joinApiPath$1(apiBaseUrl, registerPath), {
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: await response.json()
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": "Loading session",
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.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.1",
57
- "@nubitio/crud": "^1.1.1",
58
- "@nubitio/ui": "^1.1.1",
59
- "@nubitio/hydra": "^1.1.1"
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
  }