@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.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
- /** Final say. Runs after the built-in checks; may be async. Return false to deny. */
2871
- canAccess?: () => boolean | Promise<boolean>;
2872
- /** Shown while the (possibly async) check resolves. */
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. Default is a simple "Access denied" panel. */
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 denied — e.g. to redirect. */
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
- /** Final say. Runs after the built-in checks; may be async. Return false to deny. */
2871
- canAccess?: () => boolean | Promise<boolean>;
2872
- /** Shown while the (possibly async) check resolves. */
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. Default is a simple "Access denied" panel. */
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 denied — e.g. to redirect. */
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
- setState("checking");
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 (!ok) onDeny?.();
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
- return /* @__PURE__ */ jsx("div", { className: ["flex min-h-[8rem] items-center justify-center", className].filter(Boolean).join(" "), children: loadingFallback ?? /* @__PURE__ */ jsx(Spinner2, {}) });
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
- return /* @__PURE__ */ jsx("div", { className: className || void 0, children: 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: [
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
  ] }) });