@flowselections/core 1.0.22 → 1.0.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.
Files changed (55) hide show
  1. package/README.md +61 -0
  2. package/dist/auth/redirect.d.ts +33 -4
  3. package/dist/auth/redirect.d.ts.map +1 -1
  4. package/dist/auth/redirect.js +58 -7
  5. package/dist/auth/useAuth.d.ts +1 -1
  6. package/dist/auth/useAuth.d.ts.map +1 -1
  7. package/dist/auth/useIsSuperadmin.d.ts +19 -0
  8. package/dist/auth/useIsSuperadmin.d.ts.map +1 -0
  9. package/dist/auth/useIsSuperadmin.js +16 -0
  10. package/dist/components/layout/AuthenticatedLayout.d.ts +18 -9
  11. package/dist/components/layout/AuthenticatedLayout.d.ts.map +1 -1
  12. package/dist/components/layout/AuthenticatedLayout.js +42 -8
  13. package/dist/components/layout/InstellingenPage.d.ts.map +1 -1
  14. package/dist/components/layout/InstellingenPage.js +9 -1
  15. package/dist/components/layout/ResetWachtwoordPage.d.ts +3 -1
  16. package/dist/components/layout/ResetWachtwoordPage.d.ts.map +1 -1
  17. package/dist/components/layout/ResetWachtwoordPage.js +5 -3
  18. package/dist/components/layout/Sidebar.d.ts +30 -6
  19. package/dist/components/layout/Sidebar.d.ts.map +1 -1
  20. package/dist/components/layout/Sidebar.js +31 -9
  21. package/dist/extensions/ShellExtensions.d.ts +14 -1
  22. package/dist/extensions/ShellExtensions.d.ts.map +1 -1
  23. package/dist/extensions/ShellExtensions.js +21 -2
  24. package/dist/hooks/useLogoSrc.d.ts +7 -0
  25. package/dist/hooks/useLogoSrc.d.ts.map +1 -0
  26. package/dist/hooks/useLogoSrc.js +29 -0
  27. package/dist/index.d.ts +17 -8
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +11 -4
  30. package/dist/permissions/resolve.d.ts +22 -0
  31. package/dist/permissions/resolve.d.ts.map +1 -0
  32. package/dist/permissions/resolve.js +76 -0
  33. package/dist/permissions/store.d.ts +38 -0
  34. package/dist/permissions/store.d.ts.map +1 -0
  35. package/dist/permissions/store.js +180 -0
  36. package/dist/permissions/useModulePermission.d.ts +52 -32
  37. package/dist/permissions/useModulePermission.d.ts.map +1 -1
  38. package/dist/permissions/useModulePermission.js +72 -104
  39. package/dist/routes/index.d.ts +48 -0
  40. package/dist/routes/index.d.ts.map +1 -0
  41. package/dist/routes/index.js +63 -0
  42. package/dist/server/index.d.ts +1 -1
  43. package/dist/server/index.d.ts.map +1 -1
  44. package/dist/server/index.js +1 -1
  45. package/dist/server/permissions.server.d.ts +21 -8
  46. package/dist/server/permissions.server.d.ts.map +1 -1
  47. package/dist/server/permissions.server.js +54 -9
  48. package/dist/shell/ShellContext.d.ts +26 -0
  49. package/dist/shell/ShellContext.d.ts.map +1 -0
  50. package/dist/shell/ShellContext.js +14 -0
  51. package/dist/styles.css +127 -0
  52. package/dist/types/module.d.ts +39 -4
  53. package/dist/types/module.d.ts.map +1 -1
  54. package/dist/types/module.js +33 -5
  55. package/package.json +13 -5
@@ -1 +1 @@
1
- {"version":3,"file":"ShellExtensions.d.ts","sourceRoot":"","sources":["../../src/core/extensions/ShellExtensions.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,uFAAuF;AACvF,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,UAAU,EAAE,CAAC;AAE1E,MAAM,MAAM,sBAAsB,GAAG,MAAM,mBAAmB,CAAC;AAS/D,MAAM,WAAW,4BAA4B;IAC3C;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,sBAAsB,GAAG,sBAAsB,EAAE,CAAC;IAClE,kFAAkF;IAClF,KAAK,CAAC,EAAE,aAAa,CAAC;QAAE,QAAQ,EAAE,SAAS,CAAA;KAAE,CAAC,CAAC;IAC/C,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,wBAAgB,uBAAuB,CAAC,EACtC,aAAa,EACb,KAAK,EACL,QAAQ,GACT,EAAE,4BAA4B,2CAY9B;AAED,wBAAgB,aAAa,IAAI,aAAa,CAAC;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,CAAC,GAAG,SAAS,CAElF;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAuB1E"}
1
+ {"version":3,"file":"ShellExtensions.d.ts","sourceRoot":"","sources":["../../src/core/extensions/ShellExtensions.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,uFAAuF;AACvF,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,UAAU,EAAE,CAAC;AAE1E,MAAM,MAAM,sBAAsB,GAAG,MAAM,mBAAmB,CAAC;AAE/D;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,OAAO,CAAC;CACnB;AACD,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,WAAW,KAAK,WAAW,CAAC;AAClE,MAAM,MAAM,kBAAkB,GAAG,MAAM,eAAe,CAAC;AAYvD,uGAAuG;AACvG,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,CAc7D;AAED,MAAM,WAAW,4BAA4B;IAC3C;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,sBAAsB,GAAG,sBAAsB,EAAE,CAAC;IAClE,kFAAkF;IAClF,KAAK,CAAC,EAAE,aAAa,CAAC;QAAE,QAAQ,EAAE,SAAS,CAAA;KAAE,CAAC,CAAC;IAC/C,+FAA+F;IAC/F,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,wBAAgB,uBAAuB,CAAC,EACtC,aAAa,EACb,KAAK,EACL,SAAS,EACT,QAAQ,GACT,EAAE,4BAA4B,2CAY9B;AAED,wBAAgB,aAAa,IAAI,aAAa,CAAC;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,CAAC,GAAG,SAAS,CAElF;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAuB1E"}
@@ -1,13 +1,32 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createContext, useContext, useMemo } from "react";
3
3
  const ShellExtensionsContext = createContext({ transforms: [] });
4
- export function ShellExtensionsProvider({ useModuleList, guard, children, }) {
4
+ const identityFooter = () => (items) => items;
5
+ /** Sinds 1.0.23. Past de footer-transform toe; fout → ongewijzigd. Zichtbaarheid, geen beveiliging. */
6
+ export function useFooterItems(base) {
7
+ const useF = useContext(ShellExtensionsContext).useFooter ?? identityFooter;
8
+ let t = (i) => i;
9
+ try {
10
+ t = useF();
11
+ }
12
+ catch {
13
+ /* fail-open */
14
+ }
15
+ try {
16
+ const next = t(base);
17
+ return { settings: typeof next?.settings === "boolean" ? next.settings : base.settings };
18
+ }
19
+ catch {
20
+ return base;
21
+ }
22
+ }
23
+ export function ShellExtensionsProvider({ useModuleList, guard, useFooter, children, }) {
5
24
  const transforms = useMemo(() => (Array.isArray(useModuleList) ? useModuleList : useModuleList ? [useModuleList] : []),
6
25
  // Bewust op de referentie(s): de hooks moeten stabiel zijn, zodat de
7
26
  // hook-volgorde tussen renders nooit verandert.
8
27
  // eslint-disable-next-line react-hooks/exhaustive-deps
9
28
  [useModuleList]);
10
- const value = useMemo(() => ({ transforms, guard }), [transforms, guard]);
29
+ const value = useMemo(() => ({ transforms, guard, useFooter }), [transforms, guard, useFooter]);
11
30
  return _jsx(ShellExtensionsContext.Provider, { value: value, children: children });
12
31
  }
13
32
  export function useShellGuard() {
@@ -0,0 +1,7 @@
1
+ export declare const DEFAULT_LOGO_SRC = "/logo.png";
2
+ /**
3
+ * Sinds 1.0.23. Logopad: prop wint, dan `logo` uit `/huisstijl.json`, dan
4
+ * `/logo.png`.
5
+ */
6
+ export declare function useLogoSrc(prop?: string): string;
7
+ //# sourceMappingURL=useLogoSrc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useLogoSrc.d.ts","sourceRoot":"","sources":["../../src/core/hooks/useLogoSrc.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,gBAAgB,cAAc,CAAC;AAa5C;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAWhD"}
@@ -0,0 +1,29 @@
1
+ import { useEffect, useState } from "react";
2
+ export const DEFAULT_LOGO_SRC = "/logo.png";
3
+ let huisstijlLogo = null;
4
+ function loadHuisstijlLogo() {
5
+ if (!huisstijlLogo) {
6
+ huisstijlLogo = fetch("/huisstijl.json")
7
+ .then((r) => (r.ok ? r.json() : null))
8
+ .then((j) => (j && typeof j.logo === "string" && j.logo ? j.logo : null))
9
+ .catch(() => null);
10
+ }
11
+ return huisstijlLogo;
12
+ }
13
+ /**
14
+ * Sinds 1.0.23. Logopad: prop wint, dan `logo` uit `/huisstijl.json`, dan
15
+ * `/logo.png`.
16
+ */
17
+ export function useLogoSrc(prop) {
18
+ const [fromFile, setFromFile] = useState(null);
19
+ useEffect(() => {
20
+ if (prop || typeof window === "undefined")
21
+ return;
22
+ let alive = true;
23
+ loadHuisstijlLogo().then((v) => alive && setFromFile(v));
24
+ return () => {
25
+ alive = false;
26
+ };
27
+ }, [prop]);
28
+ return prop || fromFile || DEFAULT_LOGO_SRC;
29
+ }
package/dist/index.d.ts CHANGED
@@ -4,7 +4,9 @@ export { supabase } from "./supabase/client";
4
4
  export type { Database } from "./supabase/types";
5
5
  export { LoginPage } from "./components/layout/LoginPage";
6
6
  export { requireAuth, safeRedirectPath, readRedirectParam } from "./auth/redirect";
7
- export type { RequireAuthArgs } from "./auth/redirect";
7
+ export { resolveModuleAccess, canAccessModule } from "./auth/redirect";
8
+ export type { RequireAuthArgs, ModuleAccessContext } from "./auth/redirect";
9
+ export { useIsSuperadmin, roleFlags } from "./auth/useIsSuperadmin.js";
8
10
  export { HandoverPage } from "./components/layout/HandoverPage";
9
11
  export type { HandoverPageProps } from "./components/layout/HandoverPage";
10
12
  export { ResetWachtwoordPage } from "./components/layout/ResetWachtwoordPage";
@@ -12,7 +14,11 @@ export { Header } from "./components/layout/Header";
12
14
  export { InstellingenPage } from "./components/layout/InstellingenPage";
13
15
  export { Sidebar } from "./components/layout/Sidebar";
14
16
  export type { SidebarProps } from "./components/layout/Sidebar";
15
- export { AuthenticatedLayout } from "./components/layout/AuthenticatedLayout";
17
+ export { AuthenticatedLayout, NoAccess } from "./components/layout/AuthenticatedLayout";
18
+ export { useSidebarModules } from "./components/layout/Sidebar";
19
+ export { useShell, useShellModules } from "./shell/ShellContext.js";
20
+ export type { ShellContextValue } from "./shell/ShellContext.js";
21
+ export { useLogoSrc } from "./hooks/useLogoSrc.js";
16
22
  export type { AuthenticatedLayoutProps } from "./components/layout/AuthenticatedLayout";
17
23
  export { Toaster } from "./components/ui/sonner";
18
24
  export { Button } from "./components/ui/button";
@@ -37,13 +43,16 @@ export { cn } from "./lib/utils";
37
43
  export { validateFileUpload, ALLOWED_FILE_TYPES, loginSchema } from "./lib/validationSchemas";
38
44
  export { useIsMobile } from "./hooks/use-mobile";
39
45
  export { useHuisstijl } from "./hooks/useHuisstijl";
40
- export type { FlowModule, NavItem, SettingsCard } from "./types/module";
41
- export { moduleKey, navKey } from "./types/module.js";
42
- export { ShellExtensionsProvider, useModuleListTransform } from "./extensions/ShellExtensions.js";
43
- export type { ModuleListTransform, UseModuleListTransform, ShellExtensionsProviderProps, } from "./extensions/ShellExtensions.js";
46
+ export type { FlowModule, NavItem, SettingsCard, FeatureDefinition } from "./types/module";
47
+ export { moduleKey, navKey, featureKey, moduleFeatures } from "./types/module.js";
48
+ export { ShellExtensionsProvider, useModuleListTransform, useFooterItems } from "./extensions/ShellExtensions.js";
49
+ export type { ModuleListTransform, UseModuleListTransform, ShellExtensionsProviderProps, FooterItems, FooterTransform, UseFooterTransform, } from "./extensions/ShellExtensions.js";
44
50
  export { ModuleGuardSlot } from "./extensions/ModuleGuardSlot.js";
45
- export { useMyModulePermissions, useModulePermission, useModulePermissionTransform, } from "./permissions/useModulePermission.js";
46
- export type { PermissionLevel, ModulePermissionRow } from "./permissions/useModulePermission.js";
51
+ export { useMyModulePermissions, useModulePermission, useModulePermissionTransform, useCan, Can, usePermissionFilteredModules, } from "./permissions/useModulePermission.js";
52
+ export type { PermissionLevel, ModulePermissionRow, CanProps } from "./permissions/useModulePermission.js";
53
+ export { filterModules, resolveLevel } from "./permissions/resolve.js";
54
+ export { usePermissionSnapshot } from "./permissions/store.js";
55
+ export type { PermissionSnapshot, PermissionStatus } from "./permissions/store.js";
47
56
  export { ModuleAccessGuard } from "./permissions/ModuleAccessGuard.js";
48
57
  export type { ModuleAccessGuardProps } from "./permissions/ModuleAccessGuard.js";
49
58
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/core/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/core/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -5,12 +5,17 @@ export { supabase } from "./supabase/client";
5
5
  // Layout
6
6
  export { LoginPage } from "./components/layout/LoginPage";
7
7
  export { requireAuth, safeRedirectPath, readRedirectParam } from "./auth/redirect";
8
+ export { resolveModuleAccess, canAccessModule } from "./auth/redirect";
9
+ export { useIsSuperadmin, roleFlags } from "./auth/useIsSuperadmin.js";
8
10
  export { HandoverPage } from "./components/layout/HandoverPage";
9
11
  export { ResetWachtwoordPage } from "./components/layout/ResetWachtwoordPage";
10
12
  export { Header } from "./components/layout/Header";
11
13
  export { InstellingenPage } from "./components/layout/InstellingenPage";
12
14
  export { Sidebar } from "./components/layout/Sidebar";
13
- export { AuthenticatedLayout } from "./components/layout/AuthenticatedLayout";
15
+ export { AuthenticatedLayout, NoAccess } from "./components/layout/AuthenticatedLayout";
16
+ export { useSidebarModules } from "./components/layout/Sidebar";
17
+ export { useShell, useShellModules } from "./shell/ShellContext.js";
18
+ export { useLogoSrc } from "./hooks/useLogoSrc.js";
14
19
  export { Toaster } from "./components/ui/sonner";
15
20
  // UI components
16
21
  export { Button } from "./components/ui/button";
@@ -36,10 +41,12 @@ export { cn } from "./lib/utils";
36
41
  export { validateFileUpload, ALLOWED_FILE_TYPES, loginSchema } from "./lib/validationSchemas";
37
42
  export { useIsMobile } from "./hooks/use-mobile";
38
43
  export { useHuisstijl } from "./hooks/useHuisstijl";
39
- export { moduleKey, navKey } from "./types/module.js";
44
+ export { moduleKey, navKey, featureKey, moduleFeatures } from "./types/module.js";
40
45
  // Shell-extensiepunten (allemaal optioneel; zonder provider ongewijzigd gedrag)
41
- export { ShellExtensionsProvider, useModuleListTransform } from "./extensions/ShellExtensions.js";
46
+ export { ShellExtensionsProvider, useModuleListTransform, useFooterItems } from "./extensions/ShellExtensions.js";
42
47
  export { ModuleGuardSlot } from "./extensions/ModuleGuardSlot.js";
43
48
  // Rechten (leeskant; fail-open, zie README)
44
- export { useMyModulePermissions, useModulePermission, useModulePermissionTransform, } from "./permissions/useModulePermission.js";
49
+ export { useMyModulePermissions, useModulePermission, useModulePermissionTransform, useCan, Can, usePermissionFilteredModules, } from "./permissions/useModulePermission.js";
50
+ export { filterModules, resolveLevel } from "./permissions/resolve.js";
51
+ export { usePermissionSnapshot } from "./permissions/store.js";
45
52
  export { ModuleAccessGuard } from "./permissions/ModuleAccessGuard.js";
@@ -0,0 +1,22 @@
1
+ import type { FlowModule } from "../types/module.js";
2
+ export type PermissionLevel = "none" | "view" | "edit";
3
+ export interface ModulePermissionRow {
4
+ moduleId: string;
5
+ featureId: string | null;
6
+ permission: PermissionLevel;
7
+ }
8
+ export declare const RANK: Record<PermissionLevel, number>;
9
+ export declare function normalizeLevel(value: unknown): PermissionLevel;
10
+ /** RPC-rijen → genormaliseerd. Moduleniveau is `feature_id = ''` → `null`. */
11
+ export declare function normalizeRows(data: unknown): ModulePermissionRow[];
12
+ /** Feature-rij, anders modulerij, anders default-allow. */
13
+ export declare function resolveLevel(rows: ModulePermissionRow[], moduleId: string, featureId?: string | null): PermissionLevel;
14
+ /**
15
+ * Pure zijbalkfilter. `none` weg; groep zonder zichtbare kinderen verdwijnt,
16
+ * tenzij de module een eigen link heeft (nav.href niet tussen de kinderen) en
17
+ * zelf toegankelijk is — dan blijft hij als gewone link staan.
18
+ */
19
+ export declare function filterModules(modules: FlowModule[], rows: ModulePermissionRow[]): FlowModule[];
20
+ /** Paden die nooit door de route-guard worden beperkt. */
21
+ export declare const EXEMPT_KEYS: Set<string>;
22
+ //# sourceMappingURL=resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/core/permissions/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGrD,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEvD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,eAAe,CAAC;CAC7B;AAED,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,eAAe,EAAE,MAAM,CAAiC,CAAC;AAEnF,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAI9D;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,mBAAmB,EAAE,CAalE;AAED,2DAA2D;AAC3D,wBAAgB,YAAY,CAC1B,IAAI,EAAE,mBAAmB,EAAE,EAC3B,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GACxB,eAAe,CAajB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE,GAAG,UAAU,EAAE,CAyB9F;AAED,0DAA0D;AAC1D,eAAO,MAAM,WAAW,aAAsF,CAAC"}
@@ -0,0 +1,76 @@
1
+ import { featureKey, moduleKey, navKey } from "../types/module.js";
2
+ export const RANK = { none: 0, view: 1, edit: 2 };
3
+ export function normalizeLevel(value) {
4
+ const v = typeof value === "string" ? value.toLowerCase() : "";
5
+ if (v === "none" || v === "view" || v === "edit")
6
+ return v;
7
+ return "edit"; // onbekend → fail-open
8
+ }
9
+ /** RPC-rijen → genormaliseerd. Moduleniveau is `feature_id = ''` → `null`. */
10
+ export function normalizeRows(data) {
11
+ if (!Array.isArray(data))
12
+ return [];
13
+ return data.map((raw) => {
14
+ const row = (raw ?? {});
15
+ const rawModule = String(row["module_id"] ?? row["moduleId"] ?? "");
16
+ const rawFeature = row["feature_id"] ?? row["featureId"] ?? null;
17
+ const feature = typeof rawFeature === "string" && rawFeature !== "" ? rawFeature : null;
18
+ return {
19
+ moduleId: navKey(rawModule),
20
+ featureId: feature,
21
+ permission: normalizeLevel(row["permission"] ?? row["level"] ?? row["permission_level"]),
22
+ };
23
+ });
24
+ }
25
+ /** Feature-rij, anders modulerij, anders default-allow. */
26
+ export function resolveLevel(rows, moduleId, featureId) {
27
+ const key = navKey(moduleId) || moduleId;
28
+ const forModule = rows.filter((r) => r.moduleId === key);
29
+ if (forModule.length === 0)
30
+ return "edit";
31
+ if (featureId) {
32
+ const feature = forModule.find((r) => r.featureId === featureId);
33
+ if (feature)
34
+ return feature.permission;
35
+ }
36
+ const moduleLevel = forModule.find((r) => !r.featureId);
37
+ if (moduleLevel)
38
+ return moduleLevel.permission;
39
+ if (forModule.some((r) => r.permission === "edit"))
40
+ return "edit";
41
+ if (forModule.some((r) => r.permission === "view"))
42
+ return "view";
43
+ return "none";
44
+ }
45
+ /**
46
+ * Pure zijbalkfilter. `none` weg; groep zonder zichtbare kinderen verdwijnt,
47
+ * tenzij de module een eigen link heeft (nav.href niet tussen de kinderen) en
48
+ * zelf toegankelijk is — dan blijft hij als gewone link staan.
49
+ */
50
+ export function filterModules(modules, rows) {
51
+ if (rows.length === 0)
52
+ return modules;
53
+ const result = [];
54
+ for (const mod of modules) {
55
+ const children = mod.nav.children;
56
+ if (children?.length) {
57
+ const allowed = children.filter((child) => resolveLevel(rows, navKey(child.href), child.featureId ?? (featureKey(child.href) || null)) !==
58
+ "none");
59
+ if (allowed.length > 0) {
60
+ result.push({ ...mod, nav: { ...mod.nav, children: allowed } });
61
+ continue;
62
+ }
63
+ const ownLink = !children.some((c) => c.href === mod.nav.href);
64
+ if (ownLink && resolveLevel(rows, moduleKey(mod)) !== "none") {
65
+ result.push({ ...mod, nav: { ...mod.nav, children: undefined } });
66
+ }
67
+ continue;
68
+ }
69
+ if (resolveLevel(rows, moduleKey(mod), mod.nav.featureId ?? null) === "none")
70
+ continue;
71
+ result.push(mod);
72
+ }
73
+ return result;
74
+ }
75
+ /** Paden die nooit door de route-guard worden beperkt. */
76
+ export const EXEMPT_KEYS = new Set(["", "instellingen", "login", "logout", "auth", "reset-wachtwoord", "api"]);
@@ -0,0 +1,38 @@
1
+ import { type ModulePermissionRow } from "./resolve.js";
2
+ export type PermissionStatus = "loading" | "ready" | "error" | "timeout";
3
+ export interface PermissionSnapshot {
4
+ userId: string | null;
5
+ status: PermissionStatus;
6
+ rows: ModulePermissionRow[];
7
+ /** Rollen uit de JWT-claim `user_roles`, terugval tabel `user_roles`. */
8
+ roles: string[];
9
+ rolesLoaded: boolean;
10
+ }
11
+ export declare const PERMISSION_TIMEOUT_MS = 3000;
12
+ type Fetcher = () => Promise<unknown>;
13
+ type RolesFetcher = (userId: string) => Promise<string[]>;
14
+ export declare function subscribePermissions(listener: () => void): () => boolean;
15
+ export declare function getPermissionSnapshot(): PermissionSnapshot;
16
+ /** Rollen uit een JWT (claim `user_roles`). Geen verificatie: alleen UI. */
17
+ export declare function rolesFromAccessToken(token: string | undefined | null): string[] | null;
18
+ /**
19
+ * Laadt rechten (en rollen) voor `userId`. Dedupliceert en cachet 5 min.
20
+ * Na 3 s zonder antwoord gaat de status op `timeout` (fail-open); komt het
21
+ * antwoord later alsnog, dan wordt het `ready`.
22
+ */
23
+ export declare function loadPermissions(userId: string | null, accessToken?: string | null, force?: boolean): Promise<void>;
24
+ /** Wacht maximaal `ms` op rechten. Resolvet altijd (fail-open). */
25
+ export declare function waitForPermissions(userId: string | null, accessToken?: string | null, ms?: number): Promise<PermissionSnapshot>;
26
+ /** Koppelt de store eenmalig aan de Supabase-sessie (alleen in de browser). */
27
+ export declare function ensureSessionWiring(): void;
28
+ /** React-hook op de store. Werkt zonder QueryClientProvider. */
29
+ export declare function usePermissionSnapshot(): PermissionSnapshot;
30
+ export declare function __setPermissionFetchers(next: {
31
+ rows?: Fetcher;
32
+ roles?: RolesFetcher;
33
+ }): void;
34
+ export declare function __resetPermissionStore(): void;
35
+ /** Markeert de wiring als gedaan (tests/preview met een mock). */
36
+ export declare function __disableSessionWiring(): void;
37
+ export {};
38
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/core/permissions/store.ts"],"names":[],"mappings":"AASA,OAAO,EAAiB,KAAK,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEvE,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;AAEzE,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,gBAAgB,CAAC;IACzB,IAAI,EAAE,mBAAmB,EAAE,CAAC;IAC5B,yEAAyE;IACzE,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAG1C,KAAK,OAAO,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;AACtC,KAAK,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;AA8B1D,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,IAAI,iBAGxD;AACD,wBAAgB,qBAAqB,uBAEpC;AAED,4EAA4E;AAC5E,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,EAAE,GAAG,IAAI,CAetF;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,EAC3B,KAAK,UAAQ,GACZ,OAAO,CAAC,IAAI,CAAC,CA0Cf;AAED,mEAAmE;AACnE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,EAAE,SAAwB,+BAchH;AAGD,+EAA+E;AAC/E,wBAAgB,mBAAmB,SAmBlC;AAED,gEAAgE;AAChE,wBAAgB,qBAAqB,IAAI,kBAAkB,CAM1D;AAID,wBAAgB,uBAAuB,CAAC,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,YAAY,CAAA;CAAE,QAGrF;AACD,wBAAgB,sBAAsB,SAQrC;AACD,kEAAkE;AAClE,wBAAgB,sBAAsB,SAErC"}
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Gedeelde rechten- en rollencache zonder React Query (sinds 1.0.23).
3
+ *
4
+ * De automatische paden (zijbalkfilter, route-guard, useIsSuperadmin) lezen
5
+ * hier via useSyncExternalStore. Werkt dus ook in shells zonder
6
+ * QueryClientProvider. Zichtbaarheid, geen beveiliging.
7
+ */
8
+ import { useEffect, useSyncExternalStore } from "react";
9
+ import { supabase } from "../supabase/client.js";
10
+ import { normalizeRows } from "./resolve.js";
11
+ export const PERMISSION_TIMEOUT_MS = 3000;
12
+ const STALE_MS = 5 * 60 * 1000;
13
+ const defaultFetcher = async () => {
14
+ const { data, error } = await supabase.rpc("my_module_permissions");
15
+ if (error)
16
+ throw error;
17
+ return data;
18
+ };
19
+ const defaultRolesFetcher = async (userId) => {
20
+ const { data, error } = await supabase.from("user_roles").select("role").eq("user_id", userId);
21
+ if (error || !Array.isArray(data))
22
+ return [];
23
+ return data.map((r) => String(r.role));
24
+ };
25
+ let fetcher = defaultFetcher;
26
+ let rolesFetcher = defaultRolesFetcher;
27
+ let snapshot = { userId: null, status: "loading", rows: [], roles: [], rolesLoaded: false };
28
+ let fetchedAt = 0;
29
+ let inflight = null;
30
+ let inflightUser;
31
+ const listeners = new Set();
32
+ function set(next) {
33
+ snapshot = { ...snapshot, ...next };
34
+ listeners.forEach((l) => l());
35
+ }
36
+ export function subscribePermissions(listener) {
37
+ listeners.add(listener);
38
+ return () => listeners.delete(listener);
39
+ }
40
+ export function getPermissionSnapshot() {
41
+ return snapshot;
42
+ }
43
+ /** Rollen uit een JWT (claim `user_roles`). Geen verificatie: alleen UI. */
44
+ export function rolesFromAccessToken(token) {
45
+ if (!token)
46
+ return null;
47
+ try {
48
+ const part = token.split(".")[1];
49
+ if (!part)
50
+ return null;
51
+ const json = typeof atob === "function"
52
+ ? atob(part.replace(/-/g, "+").replace(/_/g, "/"))
53
+ : Buffer.from(part, "base64").toString("utf8");
54
+ const claims = JSON.parse(json);
55
+ const roles = claims["user_roles"];
56
+ return Array.isArray(roles) ? roles.map(String) : null;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ /**
63
+ * Laadt rechten (en rollen) voor `userId`. Dedupliceert en cachet 5 min.
64
+ * Na 3 s zonder antwoord gaat de status op `timeout` (fail-open); komt het
65
+ * antwoord later alsnog, dan wordt het `ready`.
66
+ */
67
+ export function loadPermissions(userId, accessToken, force = false) {
68
+ if (!userId) {
69
+ fetchedAt = Date.now();
70
+ set({ userId: null, status: "ready", rows: [], roles: [], rolesLoaded: true });
71
+ return Promise.resolve();
72
+ }
73
+ const fresh = snapshot.userId === userId && snapshot.status === "ready" && Date.now() - fetchedAt < STALE_MS;
74
+ if (!force && fresh)
75
+ return Promise.resolve();
76
+ if (!force && inflight && inflightUser === userId)
77
+ return inflight;
78
+ if (snapshot.userId !== userId) {
79
+ const claimRoles = rolesFromAccessToken(accessToken);
80
+ set({ userId, status: "loading", rows: [], roles: claimRoles ?? [], rolesLoaded: !!claimRoles });
81
+ }
82
+ else if (snapshot.status !== "ready") {
83
+ set({ status: "loading" });
84
+ }
85
+ inflightUser = userId;
86
+ const timer = setTimeout(() => {
87
+ if (snapshot.userId === userId && snapshot.status === "loading")
88
+ set({ status: "timeout" });
89
+ }, PERMISSION_TIMEOUT_MS);
90
+ const p = (async () => {
91
+ const [perm, roles] = await Promise.allSettled([
92
+ fetcher(),
93
+ snapshot.rolesLoaded ? Promise.resolve(snapshot.roles) : rolesFetcher(userId),
94
+ ]);
95
+ clearTimeout(timer);
96
+ if (snapshot.userId !== userId)
97
+ return;
98
+ const roleList = roles.status === "fulfilled" ? roles.value : snapshot.roles;
99
+ if (perm.status === "fulfilled") {
100
+ fetchedAt = Date.now();
101
+ set({ status: "ready", rows: normalizeRows(perm.value), roles: roleList, rolesLoaded: true });
102
+ }
103
+ else {
104
+ // Ontbrekende RPC of andere fout: fail-open, geen retry.
105
+ set({ status: "error", rows: [], roles: roleList, rolesLoaded: true });
106
+ }
107
+ })().finally(() => {
108
+ if (inflight === p)
109
+ inflight = null;
110
+ });
111
+ inflight = p;
112
+ return p;
113
+ }
114
+ /** Wacht maximaal `ms` op rechten. Resolvet altijd (fail-open). */
115
+ export function waitForPermissions(userId, accessToken, ms = PERMISSION_TIMEOUT_MS) {
116
+ return new Promise((resolve) => {
117
+ const t = setTimeout(() => resolve(snapshot), ms);
118
+ loadPermissions(userId, accessToken).then(() => {
119
+ clearTimeout(t);
120
+ resolve(snapshot);
121
+ }, () => {
122
+ clearTimeout(t);
123
+ resolve(snapshot);
124
+ });
125
+ });
126
+ }
127
+ let sessionWired = false;
128
+ /** Koppelt de store eenmalig aan de Supabase-sessie (alleen in de browser). */
129
+ export function ensureSessionWiring() {
130
+ if (sessionWired || typeof window === "undefined")
131
+ return;
132
+ sessionWired = true;
133
+ try {
134
+ supabase.auth.getSession().then(({ data }) => {
135
+ loadPermissions(data.session?.user.id ?? null, data.session?.access_token);
136
+ }, () => set({ status: "error" }));
137
+ supabase.auth.onAuthStateChange((event, session) => {
138
+ if (event === "SIGNED_OUT")
139
+ return void loadPermissions(null);
140
+ if (event === "SIGNED_IN" || event === "USER_UPDATED" || event === "TOKEN_REFRESHED") {
141
+ const id = session?.user.id ?? null;
142
+ const claim = rolesFromAccessToken(session?.access_token);
143
+ if (id && id === snapshot.userId && claim)
144
+ set({ roles: claim, rolesLoaded: true });
145
+ void loadPermissions(id, session?.access_token, event === "USER_UPDATED");
146
+ }
147
+ });
148
+ }
149
+ catch {
150
+ set({ status: "error", rolesLoaded: true });
151
+ }
152
+ }
153
+ /** React-hook op de store. Werkt zonder QueryClientProvider. */
154
+ export function usePermissionSnapshot() {
155
+ useEffect(() => {
156
+ ensureSessionWiring();
157
+ }, []);
158
+ // Server-snapshot = huidige snapshot: de _authenticated-tree draait met ssr:false.
159
+ return useSyncExternalStore(subscribePermissions, getPermissionSnapshot, getPermissionSnapshot);
160
+ }
161
+ // ─── Test- en preview-haken (niet in de hoofdbarrel) ──────────────────────────
162
+ export function __setPermissionFetchers(next) {
163
+ if (next.rows)
164
+ fetcher = next.rows;
165
+ if (next.roles)
166
+ rolesFetcher = next.roles;
167
+ }
168
+ export function __resetPermissionStore() {
169
+ fetcher = defaultFetcher;
170
+ rolesFetcher = defaultRolesFetcher;
171
+ snapshot = { userId: null, status: "loading", rows: [], roles: [], rolesLoaded: false };
172
+ fetchedAt = 0;
173
+ inflight = null;
174
+ inflightUser = undefined;
175
+ listeners.forEach((l) => l());
176
+ }
177
+ /** Markeert de wiring als gedaan (tests/preview met een mock). */
178
+ export function __disableSessionWiring() {
179
+ sessionWired = true;
180
+ }