@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 +169 -5
- package/dist/index.d.cts +64 -1
- package/dist/index.d.mts +64 -1
- package/dist/index.mjs +167 -8
- package/package.json +5 -5
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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.
|
|
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/
|
|
57
|
-
"@nubitio/
|
|
58
|
-
"@nubitio/
|
|
59
|
-
"@nubitio/
|
|
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
|
}
|