@nubitio/admin 0.5.20 → 0.5.23

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
@@ -389,14 +389,14 @@ const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThe
389
389
  //#endregion
390
390
  //#region packages/admin/auth/SessionContext.tsx
391
391
  const SessionContext = (0, react.createContext)(null);
392
- function joinApiPath$2(apiBaseUrl, path) {
392
+ function joinApiPath$3(apiBaseUrl, path) {
393
393
  return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
394
394
  }
395
395
  function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "auth/logout", children }) {
396
396
  const [session, setSession] = (0, react.useState)({ status: "loading" });
397
397
  const refresh = (0, react.useCallback)(async () => {
398
398
  try {
399
- const response = await fetch(joinApiPath$2(apiBaseUrl, mePath), { credentials: "include" });
399
+ const response = await fetch(joinApiPath$3(apiBaseUrl, mePath), { credentials: "include" });
400
400
  if (!response.ok) {
401
401
  setSession({ status: "anonymous" });
402
402
  return;
@@ -413,7 +413,7 @@ function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "au
413
413
  refresh();
414
414
  }, [refresh]);
415
415
  const logout = (0, react.useCallback)(async () => {
416
- await fetch(joinApiPath$2(apiBaseUrl, logoutPath), {
416
+ await fetch(joinApiPath$3(apiBaseUrl, logoutPath), {
417
417
  method: "POST",
418
418
  credentials: "include"
419
419
  });
@@ -440,9 +440,49 @@ function useSession() {
440
440
  if (!context) throw new Error("useSession must be used within a <SessionProvider>.");
441
441
  return context;
442
442
  }
443
+ /** Seeds session state from a known profile (e.g. after a custom /api/me fetch). */
444
+ function StaticSessionProvider({ profile, children }) {
445
+ const value = (0, react.useMemo)(() => ({
446
+ session: {
447
+ status: "authenticated",
448
+ profile
449
+ },
450
+ refresh: async () => void 0,
451
+ logout: async () => void 0,
452
+ roles: profile.roles,
453
+ username: profile.username
454
+ }), [profile]);
455
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionContext.Provider, {
456
+ value,
457
+ children
458
+ });
459
+ }
460
+ //#endregion
461
+ //#region packages/admin/hooks/useFeature.ts
462
+ function useFeature(featureKey) {
463
+ const { session } = useSession();
464
+ if (session.status !== "authenticated") return false;
465
+ return session.profile.features?.[featureKey]?.enabled ?? false;
466
+ }
467
+ function useFeatureConfig(featureKey) {
468
+ const { session } = useSession();
469
+ if (session.status !== "authenticated") return {};
470
+ const entry = session.profile.features?.[featureKey];
471
+ if (!entry?.enabled) return {};
472
+ return entry.config ?? {};
473
+ }
474
+ //#endregion
475
+ //#region packages/admin/features/FeatureGate.tsx
476
+ function FeatureGate({ featureKey, ...props }) {
477
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.FeatureGate, {
478
+ featureKey,
479
+ enabled: useFeature(featureKey),
480
+ ...props
481
+ });
482
+ }
443
483
  //#endregion
444
484
  //#region packages/admin/auth/LoginPage.tsx
445
- function joinApiPath$1(apiBaseUrl, path) {
485
+ function joinApiPath$2(apiBaseUrl, path) {
446
486
  return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
447
487
  }
448
488
  function LoginPage({ onLoggedIn, apiBaseUrl = "/api/", loginPath = "auth/login", title = "Nubit Admin", hint, defaultUsername = "" }) {
@@ -455,7 +495,7 @@ function LoginPage({ onLoggedIn, apiBaseUrl = "/api/", loginPath = "auth/login",
455
495
  setBusy(true);
456
496
  setError(null);
457
497
  try {
458
- const response = await fetch(joinApiPath$1(apiBaseUrl, loginPath), {
498
+ const response = await fetch(joinApiPath$2(apiBaseUrl, loginPath), {
459
499
  method: "POST",
460
500
  headers: { "Content-Type": "application/json" },
461
501
  credentials: "include",
@@ -533,6 +573,125 @@ function LoginPage({ onLoggedIn, apiBaseUrl = "/api/", loginPath = "auth/login",
533
573
  });
534
574
  }
535
575
  //#endregion
576
+ //#region packages/admin/auth/RegisterPage.tsx
577
+ function joinApiPath$1(apiBaseUrl, path) {
578
+ return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
579
+ }
580
+ function initialValues(fields) {
581
+ return Object.fromEntries(fields.map((field) => [field.name, field.defaultValue ?? ""]));
582
+ }
583
+ function RegisterPage({ fields, onRegistered, apiBaseUrl = "/api/", registerPath = "auth/register", title = "Create account", hint, submitLabel = "Create account", busyLabel = "Creating…", loginLink, loginPrompt = "Already have an account?" }) {
584
+ const [values, setValues] = (0, react.useState)((0, react.useMemo)(() => initialValues(fields), [fields]));
585
+ const [error, setError] = (0, react.useState)(null);
586
+ const [busy, setBusy] = (0, react.useState)(false);
587
+ const setValue = (name, value) => {
588
+ setValues((current) => ({
589
+ ...current,
590
+ [name]: value
591
+ }));
592
+ };
593
+ const submit = async (event) => {
594
+ event.preventDefault();
595
+ setBusy(true);
596
+ setError(null);
597
+ try {
598
+ const response = await fetch(joinApiPath$1(apiBaseUrl, registerPath), {
599
+ method: "POST",
600
+ headers: { "Content-Type": "application/json" },
601
+ credentials: "include",
602
+ body: JSON.stringify(values)
603
+ });
604
+ const body = await response.json().catch(() => null);
605
+ if (!response.ok) {
606
+ setError(body?.error ?? body?.message ?? "Registration failed");
607
+ return;
608
+ }
609
+ onRegistered();
610
+ } catch {
611
+ setError("Network error");
612
+ } finally {
613
+ setBusy(false);
614
+ }
615
+ };
616
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
617
+ style: {
618
+ display: "grid",
619
+ placeItems: "center",
620
+ minHeight: "100vh"
621
+ },
622
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Card, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
623
+ onSubmit: submit,
624
+ style: {
625
+ display: "flex",
626
+ flexDirection: "column",
627
+ gap: 12,
628
+ width: 360,
629
+ padding: 8
630
+ },
631
+ children: [
632
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
633
+ style: { margin: 0 },
634
+ children: title
635
+ }),
636
+ hint && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
637
+ style: {
638
+ margin: 0,
639
+ color: "var(--text-secondary)"
640
+ },
641
+ children: hint
642
+ }),
643
+ fields.map((field) => {
644
+ const type = field.type ?? "text";
645
+ if (type === "select") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
646
+ value: values[field.name] ?? "",
647
+ onChange: (e) => setValue(field.name, e.target.value),
648
+ style: { width: "100%" },
649
+ "aria-label": field.placeholder ?? field.name,
650
+ children: (field.options ?? []).map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
651
+ value: option.value,
652
+ children: option.label
653
+ }, option.value))
654
+ }, field.name);
655
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.TextField, {
656
+ placeholder: field.placeholder,
657
+ type: type === "password" ? "password" : type === "email" ? "email" : "text",
658
+ value: values[field.name] ?? "",
659
+ autoComplete: field.autoComplete,
660
+ onChange: (e) => setValue(field.name, e.target.value)
661
+ }, field.name);
662
+ }),
663
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
664
+ style: {
665
+ margin: 0,
666
+ color: "var(--error-color, #dc2626)"
667
+ },
668
+ children: error
669
+ }),
670
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Button, {
671
+ variant: "primary",
672
+ type: "submit",
673
+ disabled: busy,
674
+ children: busy ? busyLabel : submitLabel
675
+ }),
676
+ loginLink && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
677
+ style: {
678
+ margin: 0,
679
+ color: "var(--text-secondary)"
680
+ },
681
+ children: [
682
+ loginPrompt,
683
+ " ",
684
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_router_dom.Link, {
685
+ to: loginLink.to,
686
+ children: loginLink.label
687
+ })
688
+ ]
689
+ })
690
+ ]
691
+ }) })
692
+ });
693
+ }
694
+ //#endregion
536
695
  //#region packages/admin/runtime/useAppRuntime.ts
537
696
  function useAppRuntime() {
538
697
  const [toasts, setToasts] = (0, react.useState)([]);
@@ -800,13 +959,18 @@ function createNubitApp(config) {
800
959
  exports.AdminHeader = AdminHeader;
801
960
  exports.AdminShell = AdminShell;
802
961
  exports.AdminSidebarMenu = AdminSidebarMenu;
962
+ exports.FeatureGate = FeatureGate;
803
963
  exports.LoginPage = LoginPage;
964
+ exports.RegisterPage = RegisterPage;
804
965
  exports.SessionProvider = SessionProvider;
966
+ exports.StaticSessionProvider = StaticSessionProvider;
805
967
  exports.ToastHost = ToastHost;
806
968
  exports.createNubitApp = createNubitApp;
807
969
  exports.filterMenuByRoles = filterMenuByRoles;
808
970
  exports.hasAnyRole = hasAnyRole;
809
971
  exports.useAppRuntime = useAppRuntime;
972
+ exports.useFeature = useFeature;
973
+ exports.useFeatureConfig = useFeatureConfig;
810
974
  exports.useRuntimeConfig = useRuntimeConfig;
811
975
  exports.useScreenSize = useScreenSize;
812
976
  exports.useScreenSizeClass = useScreenSizeClass;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,5 @@
1
1
  import React$1, { ReactNode } from "react";
2
+ import { FeatureGateProps as FeatureGateProps$1 } from "@nubitio/ui";
2
3
  import { CoreRuntime } from "@nubitio/core";
3
4
  import { QueryClient } from "@tanstack/react-query";
4
5
 
@@ -147,6 +148,25 @@ declare function SessionProvider({
147
148
  children: React$1.ReactNode;
148
149
  }): React$1.JSX.Element;
149
150
  declare function useSession(): SessionContextValue;
151
+ /** Seeds session state from a known profile (e.g. after a custom /api/me fetch). */
152
+ declare function StaticSessionProvider({
153
+ profile,
154
+ children
155
+ }: {
156
+ profile: SessionProfile;
157
+ children: React$1.ReactNode;
158
+ }): React$1.JSX.Element;
159
+ //#endregion
160
+ //#region packages/admin/hooks/useFeature.d.ts
161
+ declare function useFeature(featureKey: string): boolean;
162
+ declare function useFeatureConfig(featureKey: string): Record<string, unknown>;
163
+ //#endregion
164
+ //#region packages/admin/features/FeatureGate.d.ts
165
+ type FeatureGateProps = Omit<FeatureGateProps$1, 'enabled'>;
166
+ declare function FeatureGate({
167
+ featureKey,
168
+ ...props
169
+ }: FeatureGateProps): React$1.JSX.Element;
150
170
  //#endregion
151
171
  //#region packages/admin/auth/LoginPage.d.ts
152
172
  interface LoginPageProps {
@@ -166,6 +186,49 @@ declare function LoginPage({
166
186
  defaultUsername
167
187
  }: LoginPageProps): import("react").JSX.Element;
168
188
  //#endregion
189
+ //#region packages/admin/auth/RegisterPage.d.ts
190
+ type RegisterFieldType = 'text' | 'email' | 'password' | 'select';
191
+ interface RegisterSelectOption {
192
+ value: string;
193
+ label: string;
194
+ }
195
+ interface RegisterField {
196
+ /** JSON body key sent to the register endpoint. */
197
+ name: string;
198
+ placeholder?: string;
199
+ type?: RegisterFieldType;
200
+ defaultValue?: string;
201
+ autoComplete?: string;
202
+ options?: RegisterSelectOption[];
203
+ }
204
+ interface RegisterPageProps {
205
+ fields: RegisterField[];
206
+ onRegistered: () => void;
207
+ apiBaseUrl?: string;
208
+ registerPath?: string;
209
+ title?: string;
210
+ hint?: string;
211
+ submitLabel?: string;
212
+ busyLabel?: string;
213
+ loginLink?: {
214
+ to: string;
215
+ label: string;
216
+ };
217
+ loginPrompt?: string;
218
+ }
219
+ declare function RegisterPage({
220
+ fields,
221
+ onRegistered,
222
+ apiBaseUrl,
223
+ registerPath,
224
+ title,
225
+ hint,
226
+ submitLabel,
227
+ busyLabel,
228
+ loginLink,
229
+ loginPrompt
230
+ }: RegisterPageProps): import("react").JSX.Element;
231
+ //#endregion
169
232
  //#region packages/admin/runtime/useAppRuntime.d.ts
170
233
  type NotificationType = 'success' | 'error' | 'warning' | 'info';
171
234
  type ToastItem = {
@@ -287,4 +350,4 @@ declare function hasAnyRole(required: string | string[] | undefined, roles: stri
287
350
  */
288
351
  declare function filterMenuByRoles(items: NubitAppMenuItem[], roles: string[]): AdminMenuItem[];
289
352
  //#endregion
290
- export { AdminHeader, type AdminHeaderAction, type AdminHeaderProps, type AdminMenuItem, type AdminMenuSubItem, AdminShell, type AdminShellProps, AdminSidebarMenu, type AdminSidebarMenuProps, type AdminSidebarMenuSelectEvent, type AppProfile, type CreateNubitAppConfig, LoginPage, type LoginPageProps, type NotificationType, type NubitApp, type NubitAppMenuContext, type NubitAppMenuItem, type NubitAppMenuSubItem, type NubitAppRoute, type NubitAppUserMenuContext, type RuntimeConfig, type RuntimeConfigState, type SessionContextValue, type SessionFeatureEntitlement, type SessionProfile, SessionProvider, type SessionProviderConfig, type SessionState, type SessionTenant, ToastHost, type ToastHostProps, type ToastItem, type UseRuntimeConfigOptions, createNubitApp, filterMenuByRoles, hasAnyRole, useAppRuntime, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
353
+ export { AdminHeader, type AdminHeaderAction, type AdminHeaderProps, type AdminMenuItem, type AdminMenuSubItem, AdminShell, type AdminShellProps, AdminSidebarMenu, type AdminSidebarMenuProps, type AdminSidebarMenuSelectEvent, type AppProfile, type CreateNubitAppConfig, FeatureGate, type FeatureGateProps, LoginPage, type LoginPageProps, type NotificationType, type NubitApp, type NubitAppMenuContext, type NubitAppMenuItem, type NubitAppMenuSubItem, type NubitAppRoute, type NubitAppUserMenuContext, type RegisterField, type RegisterFieldType, RegisterPage, type RegisterPageProps, type RegisterSelectOption, type RuntimeConfig, type RuntimeConfigState, type SessionContextValue, type SessionFeatureEntitlement, type SessionProfile, SessionProvider, type SessionProviderConfig, type SessionState, type SessionTenant, StaticSessionProvider, ToastHost, type ToastHostProps, type ToastItem, type UseRuntimeConfigOptions, createNubitApp, filterMenuByRoles, hasAnyRole, useAppRuntime, useFeature, useFeatureConfig, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  import React$1, { ReactNode } from "react";
2
+ import { FeatureGateProps as FeatureGateProps$1 } from "@nubitio/ui";
2
3
  import { QueryClient } from "@tanstack/react-query";
3
4
  import { CoreRuntime } from "@nubitio/core";
4
5
 
@@ -147,6 +148,25 @@ declare function SessionProvider({
147
148
  children: React$1.ReactNode;
148
149
  }): React$1.JSX.Element;
149
150
  declare function useSession(): SessionContextValue;
151
+ /** Seeds session state from a known profile (e.g. after a custom /api/me fetch). */
152
+ declare function StaticSessionProvider({
153
+ profile,
154
+ children
155
+ }: {
156
+ profile: SessionProfile;
157
+ children: React$1.ReactNode;
158
+ }): React$1.JSX.Element;
159
+ //#endregion
160
+ //#region packages/admin/hooks/useFeature.d.ts
161
+ declare function useFeature(featureKey: string): boolean;
162
+ declare function useFeatureConfig(featureKey: string): Record<string, unknown>;
163
+ //#endregion
164
+ //#region packages/admin/features/FeatureGate.d.ts
165
+ type FeatureGateProps = Omit<FeatureGateProps$1, 'enabled'>;
166
+ declare function FeatureGate({
167
+ featureKey,
168
+ ...props
169
+ }: FeatureGateProps): React$1.JSX.Element;
150
170
  //#endregion
151
171
  //#region packages/admin/auth/LoginPage.d.ts
152
172
  interface LoginPageProps {
@@ -166,6 +186,49 @@ declare function LoginPage({
166
186
  defaultUsername
167
187
  }: LoginPageProps): import("react").JSX.Element;
168
188
  //#endregion
189
+ //#region packages/admin/auth/RegisterPage.d.ts
190
+ type RegisterFieldType = 'text' | 'email' | 'password' | 'select';
191
+ interface RegisterSelectOption {
192
+ value: string;
193
+ label: string;
194
+ }
195
+ interface RegisterField {
196
+ /** JSON body key sent to the register endpoint. */
197
+ name: string;
198
+ placeholder?: string;
199
+ type?: RegisterFieldType;
200
+ defaultValue?: string;
201
+ autoComplete?: string;
202
+ options?: RegisterSelectOption[];
203
+ }
204
+ interface RegisterPageProps {
205
+ fields: RegisterField[];
206
+ onRegistered: () => void;
207
+ apiBaseUrl?: string;
208
+ registerPath?: string;
209
+ title?: string;
210
+ hint?: string;
211
+ submitLabel?: string;
212
+ busyLabel?: string;
213
+ loginLink?: {
214
+ to: string;
215
+ label: string;
216
+ };
217
+ loginPrompt?: string;
218
+ }
219
+ declare function RegisterPage({
220
+ fields,
221
+ onRegistered,
222
+ apiBaseUrl,
223
+ registerPath,
224
+ title,
225
+ hint,
226
+ submitLabel,
227
+ busyLabel,
228
+ loginLink,
229
+ loginPrompt
230
+ }: RegisterPageProps): import("react").JSX.Element;
231
+ //#endregion
169
232
  //#region packages/admin/runtime/useAppRuntime.d.ts
170
233
  type NotificationType = 'success' | 'error' | 'warning' | 'info';
171
234
  type ToastItem = {
@@ -287,4 +350,4 @@ declare function hasAnyRole(required: string | string[] | undefined, roles: stri
287
350
  */
288
351
  declare function filterMenuByRoles(items: NubitAppMenuItem[], roles: string[]): AdminMenuItem[];
289
352
  //#endregion
290
- export { AdminHeader, type AdminHeaderAction, type AdminHeaderProps, type AdminMenuItem, type AdminMenuSubItem, AdminShell, type AdminShellProps, AdminSidebarMenu, type AdminSidebarMenuProps, type AdminSidebarMenuSelectEvent, type AppProfile, type CreateNubitAppConfig, LoginPage, type LoginPageProps, type NotificationType, type NubitApp, type NubitAppMenuContext, type NubitAppMenuItem, type NubitAppMenuSubItem, type NubitAppRoute, type NubitAppUserMenuContext, type RuntimeConfig, type RuntimeConfigState, type SessionContextValue, type SessionFeatureEntitlement, type SessionProfile, SessionProvider, type SessionProviderConfig, type SessionState, type SessionTenant, ToastHost, type ToastHostProps, type ToastItem, type UseRuntimeConfigOptions, createNubitApp, filterMenuByRoles, hasAnyRole, useAppRuntime, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
353
+ export { AdminHeader, type AdminHeaderAction, type AdminHeaderProps, type AdminMenuItem, type AdminMenuSubItem, AdminShell, type AdminShellProps, AdminSidebarMenu, type AdminSidebarMenuProps, type AdminSidebarMenuSelectEvent, type AppProfile, type CreateNubitAppConfig, FeatureGate, type FeatureGateProps, LoginPage, type LoginPageProps, type NotificationType, type NubitApp, type NubitAppMenuContext, type NubitAppMenuItem, type NubitAppMenuSubItem, type NubitAppRoute, type NubitAppUserMenuContext, type RegisterField, type RegisterFieldType, RegisterPage, type RegisterPageProps, type RegisterSelectOption, type RuntimeConfig, type RuntimeConfigState, type SessionContextValue, type SessionFeatureEntitlement, type SessionProfile, SessionProvider, type SessionProviderConfig, type SessionState, type SessionTenant, StaticSessionProvider, ToastHost, type ToastHostProps, type ToastItem, type UseRuntimeConfigOptions, createNubitApp, filterMenuByRoles, hasAnyRole, useAppRuntime, useFeature, useFeatureConfig, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
2
- import { BrowserRouter, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom";
3
- import { Badge, Button, Card, IconButton, TextField, ThemeProvider, ThemeSwitcher, useFloatingPanel } from "@nubitio/ui";
2
+ import { BrowserRouter, Link, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom";
3
+ import { Badge, Button, Card, FeatureGate as FeatureGate$1, IconButton, TextField, ThemeProvider, ThemeSwitcher, useFloatingPanel } from "@nubitio/ui";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
6
6
  import { CoreConfigProvider, CoreProvider, MercureProvider } from "@nubitio/core";
@@ -365,14 +365,14 @@ const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThe
365
365
  //#endregion
366
366
  //#region packages/admin/auth/SessionContext.tsx
367
367
  const SessionContext = createContext(null);
368
- function joinApiPath$2(apiBaseUrl, path) {
368
+ function joinApiPath$3(apiBaseUrl, path) {
369
369
  return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
370
370
  }
371
371
  function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "auth/logout", children }) {
372
372
  const [session, setSession] = useState({ status: "loading" });
373
373
  const refresh = useCallback(async () => {
374
374
  try {
375
- const response = await fetch(joinApiPath$2(apiBaseUrl, mePath), { credentials: "include" });
375
+ const response = await fetch(joinApiPath$3(apiBaseUrl, mePath), { credentials: "include" });
376
376
  if (!response.ok) {
377
377
  setSession({ status: "anonymous" });
378
378
  return;
@@ -389,7 +389,7 @@ function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "au
389
389
  refresh();
390
390
  }, [refresh]);
391
391
  const logout = useCallback(async () => {
392
- await fetch(joinApiPath$2(apiBaseUrl, logoutPath), {
392
+ await fetch(joinApiPath$3(apiBaseUrl, logoutPath), {
393
393
  method: "POST",
394
394
  credentials: "include"
395
395
  });
@@ -416,9 +416,49 @@ function useSession() {
416
416
  if (!context) throw new Error("useSession must be used within a <SessionProvider>.");
417
417
  return context;
418
418
  }
419
+ /** Seeds session state from a known profile (e.g. after a custom /api/me fetch). */
420
+ function StaticSessionProvider({ profile, children }) {
421
+ const value = useMemo(() => ({
422
+ session: {
423
+ status: "authenticated",
424
+ profile
425
+ },
426
+ refresh: async () => void 0,
427
+ logout: async () => void 0,
428
+ roles: profile.roles,
429
+ username: profile.username
430
+ }), [profile]);
431
+ return /* @__PURE__ */ jsx(SessionContext.Provider, {
432
+ value,
433
+ children
434
+ });
435
+ }
436
+ //#endregion
437
+ //#region packages/admin/hooks/useFeature.ts
438
+ function useFeature(featureKey) {
439
+ const { session } = useSession();
440
+ if (session.status !== "authenticated") return false;
441
+ return session.profile.features?.[featureKey]?.enabled ?? false;
442
+ }
443
+ function useFeatureConfig(featureKey) {
444
+ const { session } = useSession();
445
+ if (session.status !== "authenticated") return {};
446
+ const entry = session.profile.features?.[featureKey];
447
+ if (!entry?.enabled) return {};
448
+ return entry.config ?? {};
449
+ }
450
+ //#endregion
451
+ //#region packages/admin/features/FeatureGate.tsx
452
+ function FeatureGate({ featureKey, ...props }) {
453
+ return /* @__PURE__ */ jsx(FeatureGate$1, {
454
+ featureKey,
455
+ enabled: useFeature(featureKey),
456
+ ...props
457
+ });
458
+ }
419
459
  //#endregion
420
460
  //#region packages/admin/auth/LoginPage.tsx
421
- function joinApiPath$1(apiBaseUrl, path) {
461
+ function joinApiPath$2(apiBaseUrl, path) {
422
462
  return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
423
463
  }
424
464
  function LoginPage({ onLoggedIn, apiBaseUrl = "/api/", loginPath = "auth/login", title = "Nubit Admin", hint, defaultUsername = "" }) {
@@ -431,7 +471,7 @@ function LoginPage({ onLoggedIn, apiBaseUrl = "/api/", loginPath = "auth/login",
431
471
  setBusy(true);
432
472
  setError(null);
433
473
  try {
434
- const response = await fetch(joinApiPath$1(apiBaseUrl, loginPath), {
474
+ const response = await fetch(joinApiPath$2(apiBaseUrl, loginPath), {
435
475
  method: "POST",
436
476
  headers: { "Content-Type": "application/json" },
437
477
  credentials: "include",
@@ -509,6 +549,125 @@ function LoginPage({ onLoggedIn, apiBaseUrl = "/api/", loginPath = "auth/login",
509
549
  });
510
550
  }
511
551
  //#endregion
552
+ //#region packages/admin/auth/RegisterPage.tsx
553
+ function joinApiPath$1(apiBaseUrl, path) {
554
+ return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
555
+ }
556
+ function initialValues(fields) {
557
+ return Object.fromEntries(fields.map((field) => [field.name, field.defaultValue ?? ""]));
558
+ }
559
+ function RegisterPage({ fields, onRegistered, apiBaseUrl = "/api/", registerPath = "auth/register", title = "Create account", hint, submitLabel = "Create account", busyLabel = "Creating…", loginLink, loginPrompt = "Already have an account?" }) {
560
+ const [values, setValues] = useState(useMemo(() => initialValues(fields), [fields]));
561
+ const [error, setError] = useState(null);
562
+ const [busy, setBusy] = useState(false);
563
+ const setValue = (name, value) => {
564
+ setValues((current) => ({
565
+ ...current,
566
+ [name]: value
567
+ }));
568
+ };
569
+ const submit = async (event) => {
570
+ event.preventDefault();
571
+ setBusy(true);
572
+ setError(null);
573
+ try {
574
+ const response = await fetch(joinApiPath$1(apiBaseUrl, registerPath), {
575
+ method: "POST",
576
+ headers: { "Content-Type": "application/json" },
577
+ credentials: "include",
578
+ body: JSON.stringify(values)
579
+ });
580
+ const body = await response.json().catch(() => null);
581
+ if (!response.ok) {
582
+ setError(body?.error ?? body?.message ?? "Registration failed");
583
+ return;
584
+ }
585
+ onRegistered();
586
+ } catch {
587
+ setError("Network error");
588
+ } finally {
589
+ setBusy(false);
590
+ }
591
+ };
592
+ return /* @__PURE__ */ jsx("div", {
593
+ style: {
594
+ display: "grid",
595
+ placeItems: "center",
596
+ minHeight: "100vh"
597
+ },
598
+ children: /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs("form", {
599
+ onSubmit: submit,
600
+ style: {
601
+ display: "flex",
602
+ flexDirection: "column",
603
+ gap: 12,
604
+ width: 360,
605
+ padding: 8
606
+ },
607
+ children: [
608
+ /* @__PURE__ */ jsx("h2", {
609
+ style: { margin: 0 },
610
+ children: title
611
+ }),
612
+ hint && /* @__PURE__ */ jsx("p", {
613
+ style: {
614
+ margin: 0,
615
+ color: "var(--text-secondary)"
616
+ },
617
+ children: hint
618
+ }),
619
+ fields.map((field) => {
620
+ const type = field.type ?? "text";
621
+ if (type === "select") return /* @__PURE__ */ jsx("select", {
622
+ value: values[field.name] ?? "",
623
+ onChange: (e) => setValue(field.name, e.target.value),
624
+ style: { width: "100%" },
625
+ "aria-label": field.placeholder ?? field.name,
626
+ children: (field.options ?? []).map((option) => /* @__PURE__ */ jsx("option", {
627
+ value: option.value,
628
+ children: option.label
629
+ }, option.value))
630
+ }, field.name);
631
+ return /* @__PURE__ */ jsx(TextField, {
632
+ placeholder: field.placeholder,
633
+ type: type === "password" ? "password" : type === "email" ? "email" : "text",
634
+ value: values[field.name] ?? "",
635
+ autoComplete: field.autoComplete,
636
+ onChange: (e) => setValue(field.name, e.target.value)
637
+ }, field.name);
638
+ }),
639
+ error && /* @__PURE__ */ jsx("p", {
640
+ style: {
641
+ margin: 0,
642
+ color: "var(--error-color, #dc2626)"
643
+ },
644
+ children: error
645
+ }),
646
+ /* @__PURE__ */ jsx(Button, {
647
+ variant: "primary",
648
+ type: "submit",
649
+ disabled: busy,
650
+ children: busy ? busyLabel : submitLabel
651
+ }),
652
+ loginLink && /* @__PURE__ */ jsxs("p", {
653
+ style: {
654
+ margin: 0,
655
+ color: "var(--text-secondary)"
656
+ },
657
+ children: [
658
+ loginPrompt,
659
+ " ",
660
+ /* @__PURE__ */ jsx(Link, {
661
+ to: loginLink.to,
662
+ children: loginLink.label
663
+ })
664
+ ]
665
+ })
666
+ ]
667
+ }) })
668
+ });
669
+ }
670
+ //#endregion
512
671
  //#region packages/admin/runtime/useAppRuntime.ts
513
672
  function useAppRuntime() {
514
673
  const [toasts, setToasts] = useState([]);
@@ -773,4 +932,4 @@ function createNubitApp(config) {
773
932
  return { App };
774
933
  }
775
934
  //#endregion
776
- export { AdminHeader, AdminShell, AdminSidebarMenu, LoginPage, SessionProvider, ToastHost, createNubitApp, filterMenuByRoles, hasAnyRole, useAppRuntime, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
935
+ export { AdminHeader, AdminShell, AdminSidebarMenu, FeatureGate, LoginPage, RegisterPage, SessionProvider, StaticSessionProvider, ToastHost, createNubitApp, filterMenuByRoles, hasAnyRole, useAppRuntime, useFeature, useFeatureConfig, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubitio/admin",
3
- "version": "0.5.20",
3
+ "version": "0.5.23",
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,9 @@
53
53
  "react": "^19.0.0",
54
54
  "react-dom": "^19.0.0",
55
55
  "react-router-dom": "^6.0.0",
56
- "@nubitio/core": "^0.5.20",
57
- "@nubitio/hydra": "^0.5.20",
58
- "@nubitio/ui": "^0.5.20",
59
- "@nubitio/crud": "^0.5.20"
56
+ "@nubitio/crud": "^0.5.23",
57
+ "@nubitio/core": "^0.5.23",
58
+ "@nubitio/hydra": "^0.5.23",
59
+ "@nubitio/ui": "^0.5.23"
60
60
  }
61
61
  }