@geomak/ui 6.21.1 → 6.23.0
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 +36 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -7
- package/dist/index.d.ts +49 -7
- package/dist/index.js +36 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -2867,13 +2867,29 @@ interface SecureLayoutProps {
|
|
|
2867
2867
|
/** Required permission(s). One needed (or all, with `requireAllPermissions`). */
|
|
2868
2868
|
requiredPermissions?: string[];
|
|
2869
2869
|
requireAllPermissions?: boolean;
|
|
2870
|
-
/**
|
|
2871
|
-
|
|
2872
|
-
|
|
2870
|
+
/**
|
|
2871
|
+
* The current route path (e.g. `location.pathname`). Pass it from your
|
|
2872
|
+
* router to do **per-route** access from a single root wrapper: the check
|
|
2873
|
+
* re-runs whenever `route` changes (i.e. on navigation), and the value is
|
|
2874
|
+
* forwarded to `canAccess`. Omit it for a static, app-wide gate.
|
|
2875
|
+
*/
|
|
2876
|
+
route?: string;
|
|
2877
|
+
/**
|
|
2878
|
+
* Final say. Runs after the built-in checks; may be async; return false to
|
|
2879
|
+
* deny. Receives the current `route`, so a single wrapper can decide access
|
|
2880
|
+
* per path (e.g. consult a route → permissions map). Keep it synchronous
|
|
2881
|
+
* for instant, flash-free per-route guarding.
|
|
2882
|
+
*/
|
|
2883
|
+
canAccess?: (route?: string) => boolean | Promise<boolean>;
|
|
2884
|
+
/** Shown while the (possibly async) check resolves. Pass `null` to render nothing. */
|
|
2873
2885
|
loadingFallback?: React__default.ReactNode;
|
|
2874
|
-
/** Shown when access is denied.
|
|
2886
|
+
/** Shown when access is denied. Defaults to a simple "Access denied" panel;
|
|
2887
|
+
* pass `null` to render nothing (e.g. when `onDeny` redirects away). */
|
|
2875
2888
|
fallback?: React__default.ReactNode;
|
|
2876
|
-
/** Fired once when access is
|
|
2889
|
+
/** Fired once when access is granted — e.g. to hydrate app state or redirect
|
|
2890
|
+
* to a landing route after a successful token check. */
|
|
2891
|
+
onGranted?: () => void;
|
|
2892
|
+
/** Fired once when access is denied — e.g. to redirect to login / logout. */
|
|
2877
2893
|
onDeny?: () => void;
|
|
2878
2894
|
className?: string;
|
|
2879
2895
|
}
|
|
@@ -2887,13 +2903,39 @@ interface SecureLayoutProps {
|
|
|
2887
2903
|
* This is a UI guard, not a security boundary — it controls what renders.
|
|
2888
2904
|
* Real authorization must be enforced by your API/server.
|
|
2889
2905
|
*
|
|
2890
|
-
* @example
|
|
2906
|
+
* @example Full RBAC + PBAC gate
|
|
2891
2907
|
* <SecureLayout token={jwt} requiredRoles={['admin']} permissions={user.perms}
|
|
2892
2908
|
* requiredPermissions={['reports:read']} onDeny={() => navigate('/login')}>
|
|
2893
2909
|
* <AdminDashboard />
|
|
2894
2910
|
* </SecureLayout>
|
|
2911
|
+
*
|
|
2912
|
+
* @example Simple JWT-only gate with a token-login bootstrap (no roles/perms)
|
|
2913
|
+
* <SecureLayout
|
|
2914
|
+
* canAccess={async () => {
|
|
2915
|
+
* const jwt = localStorage.getItem('jwt')
|
|
2916
|
+
* if (!jwt) return false
|
|
2917
|
+
* await tokenLogin(jwt) // hydrate app state from the server
|
|
2918
|
+
* return true
|
|
2919
|
+
* }}
|
|
2920
|
+
* onGranted={() => navigate('/dashboard')}
|
|
2921
|
+
* onDeny={() => navigate('/logout')}
|
|
2922
|
+
* fallback={null} // redirecting — don't flash a panel
|
|
2923
|
+
* >
|
|
2924
|
+
* <AppRoutes />
|
|
2925
|
+
* </SecureLayout>
|
|
2926
|
+
*
|
|
2927
|
+
* @example Per-route access from a single root wrapper
|
|
2928
|
+
* // Pass the path so the check re-runs on navigation; decide per route.
|
|
2929
|
+
* const allowed = { '/admin': ['admin'], '/reports': ['analyst', 'admin'] }
|
|
2930
|
+
* <SecureLayout
|
|
2931
|
+
* route={location.pathname}
|
|
2932
|
+
* canAccess={(path) => (allowed[path] ?? []).some((r) => user.roles.includes(r))}
|
|
2933
|
+
* onDeny={() => navigate('/403')}
|
|
2934
|
+
* >
|
|
2935
|
+
* <AppRoutes />
|
|
2936
|
+
* </SecureLayout>
|
|
2895
2937
|
*/
|
|
2896
|
-
declare function SecureLayout({ children, isAuthenticated, token, roles, requiredRoles, requireAllRoles, permissions, requiredPermissions, requireAllPermissions, canAccess, loadingFallback, fallback, onDeny, className, }: SecureLayoutProps): react_jsx_runtime.JSX.Element;
|
|
2938
|
+
declare function SecureLayout({ children, isAuthenticated, token, roles, requiredRoles, requireAllRoles, permissions, requiredPermissions, requireAllPermissions, route, canAccess, loadingFallback, fallback, onGranted, onDeny, className, }: SecureLayoutProps): react_jsx_runtime.JSX.Element;
|
|
2897
2939
|
|
|
2898
2940
|
interface ThemeColors {
|
|
2899
2941
|
background?: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -2867,13 +2867,29 @@ interface SecureLayoutProps {
|
|
|
2867
2867
|
/** Required permission(s). One needed (or all, with `requireAllPermissions`). */
|
|
2868
2868
|
requiredPermissions?: string[];
|
|
2869
2869
|
requireAllPermissions?: boolean;
|
|
2870
|
-
/**
|
|
2871
|
-
|
|
2872
|
-
|
|
2870
|
+
/**
|
|
2871
|
+
* The current route path (e.g. `location.pathname`). Pass it from your
|
|
2872
|
+
* router to do **per-route** access from a single root wrapper: the check
|
|
2873
|
+
* re-runs whenever `route` changes (i.e. on navigation), and the value is
|
|
2874
|
+
* forwarded to `canAccess`. Omit it for a static, app-wide gate.
|
|
2875
|
+
*/
|
|
2876
|
+
route?: string;
|
|
2877
|
+
/**
|
|
2878
|
+
* Final say. Runs after the built-in checks; may be async; return false to
|
|
2879
|
+
* deny. Receives the current `route`, so a single wrapper can decide access
|
|
2880
|
+
* per path (e.g. consult a route → permissions map). Keep it synchronous
|
|
2881
|
+
* for instant, flash-free per-route guarding.
|
|
2882
|
+
*/
|
|
2883
|
+
canAccess?: (route?: string) => boolean | Promise<boolean>;
|
|
2884
|
+
/** Shown while the (possibly async) check resolves. Pass `null` to render nothing. */
|
|
2873
2885
|
loadingFallback?: React__default.ReactNode;
|
|
2874
|
-
/** Shown when access is denied.
|
|
2886
|
+
/** Shown when access is denied. Defaults to a simple "Access denied" panel;
|
|
2887
|
+
* pass `null` to render nothing (e.g. when `onDeny` redirects away). */
|
|
2875
2888
|
fallback?: React__default.ReactNode;
|
|
2876
|
-
/** Fired once when access is
|
|
2889
|
+
/** Fired once when access is granted — e.g. to hydrate app state or redirect
|
|
2890
|
+
* to a landing route after a successful token check. */
|
|
2891
|
+
onGranted?: () => void;
|
|
2892
|
+
/** Fired once when access is denied — e.g. to redirect to login / logout. */
|
|
2877
2893
|
onDeny?: () => void;
|
|
2878
2894
|
className?: string;
|
|
2879
2895
|
}
|
|
@@ -2887,13 +2903,39 @@ interface SecureLayoutProps {
|
|
|
2887
2903
|
* This is a UI guard, not a security boundary — it controls what renders.
|
|
2888
2904
|
* Real authorization must be enforced by your API/server.
|
|
2889
2905
|
*
|
|
2890
|
-
* @example
|
|
2906
|
+
* @example Full RBAC + PBAC gate
|
|
2891
2907
|
* <SecureLayout token={jwt} requiredRoles={['admin']} permissions={user.perms}
|
|
2892
2908
|
* requiredPermissions={['reports:read']} onDeny={() => navigate('/login')}>
|
|
2893
2909
|
* <AdminDashboard />
|
|
2894
2910
|
* </SecureLayout>
|
|
2911
|
+
*
|
|
2912
|
+
* @example Simple JWT-only gate with a token-login bootstrap (no roles/perms)
|
|
2913
|
+
* <SecureLayout
|
|
2914
|
+
* canAccess={async () => {
|
|
2915
|
+
* const jwt = localStorage.getItem('jwt')
|
|
2916
|
+
* if (!jwt) return false
|
|
2917
|
+
* await tokenLogin(jwt) // hydrate app state from the server
|
|
2918
|
+
* return true
|
|
2919
|
+
* }}
|
|
2920
|
+
* onGranted={() => navigate('/dashboard')}
|
|
2921
|
+
* onDeny={() => navigate('/logout')}
|
|
2922
|
+
* fallback={null} // redirecting — don't flash a panel
|
|
2923
|
+
* >
|
|
2924
|
+
* <AppRoutes />
|
|
2925
|
+
* </SecureLayout>
|
|
2926
|
+
*
|
|
2927
|
+
* @example Per-route access from a single root wrapper
|
|
2928
|
+
* // Pass the path so the check re-runs on navigation; decide per route.
|
|
2929
|
+
* const allowed = { '/admin': ['admin'], '/reports': ['analyst', 'admin'] }
|
|
2930
|
+
* <SecureLayout
|
|
2931
|
+
* route={location.pathname}
|
|
2932
|
+
* canAccess={(path) => (allowed[path] ?? []).some((r) => user.roles.includes(r))}
|
|
2933
|
+
* onDeny={() => navigate('/403')}
|
|
2934
|
+
* >
|
|
2935
|
+
* <AppRoutes />
|
|
2936
|
+
* </SecureLayout>
|
|
2895
2937
|
*/
|
|
2896
|
-
declare function SecureLayout({ children, isAuthenticated, token, roles, requiredRoles, requireAllRoles, permissions, requiredPermissions, requireAllPermissions, canAccess, loadingFallback, fallback, onDeny, className, }: SecureLayoutProps): react_jsx_runtime.JSX.Element;
|
|
2938
|
+
declare function SecureLayout({ children, isAuthenticated, token, roles, requiredRoles, requireAllRoles, permissions, requiredPermissions, requireAllPermissions, route, canAccess, loadingFallback, fallback, onGranted, onDeny, className, }: SecureLayoutProps): react_jsx_runtime.JSX.Element;
|
|
2897
2939
|
|
|
2898
2940
|
interface ThemeColors {
|
|
2899
2941
|
background?: string;
|
package/dist/index.js
CHANGED
|
@@ -5847,42 +5847,59 @@ function SecureLayout({
|
|
|
5847
5847
|
permissions,
|
|
5848
5848
|
requiredPermissions,
|
|
5849
5849
|
requireAllPermissions,
|
|
5850
|
+
route,
|
|
5850
5851
|
canAccess,
|
|
5851
5852
|
loadingFallback,
|
|
5852
5853
|
fallback,
|
|
5854
|
+
onGranted,
|
|
5853
5855
|
onDeny,
|
|
5854
5856
|
className = ""
|
|
5855
5857
|
}) {
|
|
5856
5858
|
const reduced = useReducedMotion();
|
|
5857
|
-
const [state, setState] = useState("checking");
|
|
5858
5859
|
const rolesKey = JSON.stringify(roles);
|
|
5859
5860
|
const requiredRolesKey = JSON.stringify(requiredRoles);
|
|
5860
5861
|
const permissionsKey = JSON.stringify(permissions);
|
|
5861
5862
|
const requiredPermissionsKey = JSON.stringify(requiredPermissions);
|
|
5863
|
+
const passesSync = () => {
|
|
5864
|
+
let authed = isAuthenticated;
|
|
5865
|
+
if (authed === void 0 && token !== void 0) authed = tokenValid(token);
|
|
5866
|
+
if (authed === void 0) authed = true;
|
|
5867
|
+
if (!authed) return false;
|
|
5868
|
+
if (requiredRoles?.length && !has(roles, requiredRoles, requireAllRoles)) return false;
|
|
5869
|
+
if (requiredPermissions?.length && !has(permissions, requiredPermissions, requireAllPermissions)) return false;
|
|
5870
|
+
return true;
|
|
5871
|
+
};
|
|
5872
|
+
const [state, setState] = useState(
|
|
5873
|
+
() => !passesSync() ? "denied" : canAccess ? "checking" : "granted"
|
|
5874
|
+
);
|
|
5862
5875
|
useEffect(() => {
|
|
5863
5876
|
let cancelled = false;
|
|
5864
|
-
|
|
5865
|
-
const evaluate = async () => {
|
|
5866
|
-
let authed = isAuthenticated;
|
|
5867
|
-
if (authed === void 0 && token !== void 0) authed = tokenValid(token);
|
|
5868
|
-
if (authed === void 0) authed = true;
|
|
5869
|
-
if (!authed) return false;
|
|
5870
|
-
if (requiredRoles?.length && !has(roles, requiredRoles, requireAllRoles)) return false;
|
|
5871
|
-
if (requiredPermissions?.length && !has(permissions, requiredPermissions, requireAllPermissions)) return false;
|
|
5872
|
-
if (canAccess && !await canAccess()) return false;
|
|
5873
|
-
return true;
|
|
5874
|
-
};
|
|
5875
|
-
evaluate().then((ok) => {
|
|
5877
|
+
const finish = (ok) => {
|
|
5876
5878
|
if (cancelled) return;
|
|
5877
5879
|
setState(ok ? "granted" : "denied");
|
|
5878
|
-
if (
|
|
5879
|
-
|
|
5880
|
+
if (ok) onGranted?.();
|
|
5881
|
+
else onDeny?.();
|
|
5882
|
+
};
|
|
5883
|
+
if (!passesSync()) {
|
|
5884
|
+
finish(false);
|
|
5885
|
+
} else if (!canAccess) {
|
|
5886
|
+
finish(true);
|
|
5887
|
+
} else {
|
|
5888
|
+
const result = canAccess(route);
|
|
5889
|
+
if (result && typeof result.then === "function") {
|
|
5890
|
+
setState("checking");
|
|
5891
|
+
result.then((ok) => finish(Boolean(ok)));
|
|
5892
|
+
} else {
|
|
5893
|
+
finish(Boolean(result));
|
|
5894
|
+
}
|
|
5895
|
+
}
|
|
5880
5896
|
return () => {
|
|
5881
5897
|
cancelled = true;
|
|
5882
5898
|
};
|
|
5883
5899
|
}, [
|
|
5884
5900
|
isAuthenticated,
|
|
5885
5901
|
token,
|
|
5902
|
+
route,
|
|
5886
5903
|
requireAllRoles,
|
|
5887
5904
|
requireAllPermissions,
|
|
5888
5905
|
canAccess,
|
|
@@ -5892,10 +5909,12 @@ function SecureLayout({
|
|
|
5892
5909
|
requiredPermissionsKey
|
|
5893
5910
|
]);
|
|
5894
5911
|
if (state === "checking") {
|
|
5895
|
-
|
|
5912
|
+
if (loadingFallback === null) return null;
|
|
5913
|
+
return /* @__PURE__ */ jsx("div", { className: ["flex min-h-[8rem] items-center justify-center", className].filter(Boolean).join(" "), children: loadingFallback !== void 0 ? loadingFallback : /* @__PURE__ */ jsx(Spinner2, {}) });
|
|
5896
5914
|
}
|
|
5897
5915
|
if (state === "denied") {
|
|
5898
|
-
|
|
5916
|
+
if (fallback === null) return null;
|
|
5917
|
+
return /* @__PURE__ */ jsx("div", { className: className || void 0, children: fallback !== void 0 ? fallback : /* @__PURE__ */ jsxs("div", { className: "flex min-h-[8rem] flex-col items-center justify-center gap-1 rounded-xl border border-border bg-surface p-8 text-center", children: [
|
|
5899
5918
|
/* @__PURE__ */ jsx("div", { className: "text-sm font-semibold text-foreground", children: "Access denied" }),
|
|
5900
5919
|
/* @__PURE__ */ jsx("div", { className: "text-xs text-foreground-muted", children: "You don\u2019t have permission to view this content." })
|
|
5901
5920
|
] }) });
|