@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 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: "User menu",
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": "User menu",
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 }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("header", {
169
- className: ["nb-admin-header-component", className].filter(Boolean).join(" "),
170
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
171
- className: "nb-admin-header-toolbar",
172
- role: "toolbar",
173
- "aria-label": "Main toolbar",
174
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
175
- className: "nb-admin-header-toolbar__before",
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
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
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
186
- className: "nb-admin-header-toolbar__after",
187
- children: [
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": "Main navigation",
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": "Main menu",
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": "Close menu",
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` : "Main 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: await response.json()
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 fetch(joinApiPath$7(apiBaseUrl, logoutPath), {
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: useFeature(featureKey),
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) statusLine = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
605
- className: "nb-plan-panel__status",
606
- children: (labels?.trialEnds ?? "Trial ends {date}").replace("{date}", formatTrialEnds(trialEndsAt))
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 fetch(joinApiPath$5(apiBaseUrl, "auth/password/forgot"), {
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 fetch(joinApiPath$4(apiBaseUrl, "auth/password/reset"), {
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
- setError((await response.json().catch(() => null))?.message ?? "Reset failed");
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
- fetch(joinApiPath$3(apiBaseUrl, `invitations/${token}`), { credentials: "include" }).then(async (response) => {
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
- setEmail((await response.json()).email ?? null);
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 fetch(joinApiPath$3(apiBaseUrl, `invitations/${token}/accept`), {
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
- setError((await response.json().catch(() => null))?.message ?? "Could not accept the invitation");
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) setSessions((await sessionResponse.json()).sessions ?? []);
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 fetch(joinApiPath$2(apiBaseUrl, "auth/change-password"), {
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
- setPasswordMessage((await response.json().catch(() => null))?.message ?? "Could not change password");
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 fetch(joinApiPath$2(apiBaseUrl, "auth/totp"), {
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
- setTotpMessage((await response.json().catch(() => null))?.message ?? "Could not start enrolment");
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 fetch(joinApiPath$2(apiBaseUrl, "auth/totp/confirm"), {
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
- setTotpMessage((await response.json().catch(() => null))?.message ?? "That code is not valid");
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 fetch(joinApiPath$2(apiBaseUrl, "auth/totp"), {
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
- setTotpMessage((await response.json().catch(() => null))?.message ?? "Could not disable the second factor");
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 fetch(joinApiPath$2(apiBaseUrl, `auth/sessions/${id}`), {
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 [values, setValues] = (0, react.useState)((0, react.useMemo)(() => initialValues(fields), [fields]));
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 fetch(joinApiPath$1(apiBaseUrl, registerPath), {
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: await response.json()
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": "Loading session",
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
  }