@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,22 +1,22 @@
1
+ import type { ReactNode } from "react";
2
+ import type { FlowModule } from "../types/module.js";
1
3
  import type { ModuleListTransform } from "../extensions/ShellExtensions.js";
2
- export type PermissionLevel = "none" | "view" | "edit";
3
- export interface ModulePermissionRow {
4
- moduleId: string;
5
- featureId: string | null;
6
- permission: PermissionLevel;
7
- }
4
+ import { type ModulePermissionRow, type PermissionLevel } from "./resolve.js";
5
+ export type { PermissionLevel, ModulePermissionRow } from "./resolve.js";
8
6
  /**
9
- * Leest de rechten van de ingelogde gebruiker via RPC `my_module_permissions()`.
7
+ * Rechten van de ingelogde gebruiker via RPC `my_module_permissions()`.
10
8
  *
11
- * Ontbreekt de RPC (add-on niet geïnstalleerd) of faalt de call, dan is het
12
- * resultaat een LEGE lijst zonder zichtbare fout en zonder retry — en dat
13
- * betekent bij alle consumers default-allow.
9
+ * Draait op de gedeelde store; gebruikt React Query alleen als er een
10
+ * QueryClientProvider is, anders een interne client — crasht nooit zonder
11
+ * provider. Fail-open: fout of ontbrekende RPC → lege lijst → default-allow.
12
+ * Zichtbaarheid, geen beveiliging.
14
13
  */
15
14
  export declare function useMyModulePermissions(): {
15
+ data: ModulePermissionRow[] | undefined;
16
16
  permissions: ModulePermissionRow[];
17
17
  /** True zolang we niets weten; callers behandelen dit als allow. */
18
18
  isUnknown: boolean;
19
- data: ModulePermissionRow[];
19
+ status: import("./store.js").PermissionStatus;
20
20
  error: Error;
21
21
  isError: true;
22
22
  isPending: false;
@@ -25,7 +25,6 @@ export declare function useMyModulePermissions(): {
25
25
  isRefetchError: true;
26
26
  isSuccess: false;
27
27
  isPlaceholderData: false;
28
- status: "error";
29
28
  dataUpdatedAt: number;
30
29
  errorUpdatedAt: number;
31
30
  failureCount: number;
@@ -43,10 +42,11 @@ export declare function useMyModulePermissions(): {
43
42
  fetchStatus: import("@tanstack/query-core").FetchStatus;
44
43
  promise: Promise<ModulePermissionRow[]>;
45
44
  } | {
45
+ data: ModulePermissionRow[] | undefined;
46
46
  permissions: ModulePermissionRow[];
47
47
  /** True zolang we niets weten; callers behandelen dit als allow. */
48
48
  isUnknown: boolean;
49
- data: ModulePermissionRow[];
49
+ status: import("./store.js").PermissionStatus;
50
50
  error: null;
51
51
  isError: false;
52
52
  isPending: false;
@@ -55,7 +55,6 @@ export declare function useMyModulePermissions(): {
55
55
  isRefetchError: false;
56
56
  isSuccess: true;
57
57
  isPlaceholderData: false;
58
- status: "success";
59
58
  dataUpdatedAt: number;
60
59
  errorUpdatedAt: number;
61
60
  failureCount: number;
@@ -73,10 +72,11 @@ export declare function useMyModulePermissions(): {
73
72
  fetchStatus: import("@tanstack/query-core").FetchStatus;
74
73
  promise: Promise<ModulePermissionRow[]>;
75
74
  } | {
75
+ data: ModulePermissionRow[] | undefined;
76
76
  permissions: ModulePermissionRow[];
77
77
  /** True zolang we niets weten; callers behandelen dit als allow. */
78
78
  isUnknown: boolean;
79
- data: undefined;
79
+ status: import("./store.js").PermissionStatus;
80
80
  error: Error;
81
81
  isError: true;
82
82
  isPending: false;
@@ -85,7 +85,6 @@ export declare function useMyModulePermissions(): {
85
85
  isRefetchError: false;
86
86
  isSuccess: false;
87
87
  isPlaceholderData: false;
88
- status: "error";
89
88
  dataUpdatedAt: number;
90
89
  errorUpdatedAt: number;
91
90
  failureCount: number;
@@ -103,10 +102,11 @@ export declare function useMyModulePermissions(): {
103
102
  fetchStatus: import("@tanstack/query-core").FetchStatus;
104
103
  promise: Promise<ModulePermissionRow[]>;
105
104
  } | {
105
+ data: ModulePermissionRow[] | undefined;
106
106
  permissions: ModulePermissionRow[];
107
107
  /** True zolang we niets weten; callers behandelen dit als allow. */
108
108
  isUnknown: boolean;
109
- data: undefined;
109
+ status: import("./store.js").PermissionStatus;
110
110
  error: null;
111
111
  isError: false;
112
112
  isPending: true;
@@ -115,7 +115,6 @@ export declare function useMyModulePermissions(): {
115
115
  isRefetchError: false;
116
116
  isSuccess: false;
117
117
  isPlaceholderData: false;
118
- status: "pending";
119
118
  dataUpdatedAt: number;
120
119
  errorUpdatedAt: number;
121
120
  failureCount: number;
@@ -133,10 +132,11 @@ export declare function useMyModulePermissions(): {
133
132
  fetchStatus: import("@tanstack/query-core").FetchStatus;
134
133
  promise: Promise<ModulePermissionRow[]>;
135
134
  } | {
135
+ data: ModulePermissionRow[] | undefined;
136
136
  permissions: ModulePermissionRow[];
137
137
  /** True zolang we niets weten; callers behandelen dit als allow. */
138
138
  isUnknown: boolean;
139
- data: undefined;
139
+ status: import("./store.js").PermissionStatus;
140
140
  error: null;
141
141
  isError: false;
142
142
  isPending: true;
@@ -144,7 +144,6 @@ export declare function useMyModulePermissions(): {
144
144
  isRefetchError: false;
145
145
  isSuccess: false;
146
146
  isPlaceholderData: false;
147
- status: "pending";
148
147
  dataUpdatedAt: number;
149
148
  errorUpdatedAt: number;
150
149
  failureCount: number;
@@ -163,10 +162,11 @@ export declare function useMyModulePermissions(): {
163
162
  fetchStatus: import("@tanstack/query-core").FetchStatus;
164
163
  promise: Promise<ModulePermissionRow[]>;
165
164
  } | {
165
+ data: ModulePermissionRow[] | undefined;
166
166
  permissions: ModulePermissionRow[];
167
167
  /** True zolang we niets weten; callers behandelen dit als allow. */
168
168
  isUnknown: boolean;
169
- data: ModulePermissionRow[];
169
+ status: import("./store.js").PermissionStatus;
170
170
  isError: false;
171
171
  error: null;
172
172
  isPending: false;
@@ -175,7 +175,6 @@ export declare function useMyModulePermissions(): {
175
175
  isRefetchError: false;
176
176
  isSuccess: true;
177
177
  isPlaceholderData: true;
178
- status: "success";
179
178
  dataUpdatedAt: number;
180
179
  errorUpdatedAt: number;
181
180
  failureCount: number;
@@ -194,8 +193,8 @@ export declare function useMyModulePermissions(): {
194
193
  promise: Promise<ModulePermissionRow[]>;
195
194
  };
196
195
  /**
197
- * Rechtenniveau voor één module (optioneel één feature). Tijdens laden en bij
198
- * elke fout is het antwoord `edit` (fail-open).
196
+ * Rechtenniveau voor één module (optioneel één feature). Tijdens laden, bij
197
+ * een fout en voor admin/superadmin: `edit`. Zichtbaarheid, geen beveiliging.
199
198
  */
200
199
  export declare function useModulePermission(moduleId: string, featureId?: string | null): {
201
200
  level: PermissionLevel;
@@ -204,14 +203,35 @@ export declare function useModulePermission(moduleId: string, featureId?: string
204
203
  isLoading: boolean;
205
204
  };
206
205
  /**
207
- * ModuleListTransform die zijbalkitems met `none` weglaat.
208
- *
209
- * - Tijdens laden / bij fouten: lijst ongewijzigd.
210
- * - Modules zonder rij in het rechtenschema: default-allow.
211
- * - Groepen: de kinderen bepalen zichtbaarheid. Blijft er geen kind over, dan
212
- * valt de groep weg. Is de parent-sleutel geweigerd maar een kind toegestaan,
213
- * dan blijft de groep staan met alleen de toegestane kinderen — de parent-href
214
- * van een groep is een uitklapknop, geen bestemming.
206
+ * Sinds 1.0.23. `allowed` = niveau ≥ `level` (standaard `view`). Laden → true.
207
+ * Zichtbaarheid, geen beveiliging.
208
+ */
209
+ export declare function useCan(moduleId: string, featureId?: string | null, level?: PermissionLevel): {
210
+ allowed: boolean;
211
+ level: PermissionLevel;
212
+ isLoading: boolean;
213
+ };
214
+ export interface CanProps {
215
+ module: string;
216
+ feature?: string | null;
217
+ level?: PermissionLevel;
218
+ fallback?: ReactNode;
219
+ children: ReactNode;
220
+ }
221
+ /** Sinds 1.0.23. Rendert children als `useCan` toestaat. Zichtbaarheid, geen beveiliging. */
222
+ export declare function Can({ module, feature, level, fallback, children }: CanProps): import("react").FunctionComponentElement<import("react").FragmentProps>;
223
+ /**
224
+ * ModuleListTransform die zijbalkitems met `none` weglaat. Laden/fout/admin →
225
+ * ongewijzigd. Zichtbaarheid, geen beveiliging.
215
226
  */
216
227
  export declare function useModulePermissionTransform(): ModuleListTransform;
228
+ /**
229
+ * Sinds 1.0.23. Zijbalkfilter met laadstatus: `loading` = eerste laden (toon
230
+ * skeleton); na fout of 3 s time-out de volledige lijst.
231
+ * Zichtbaarheid, geen beveiliging.
232
+ */
233
+ export declare function usePermissionFilteredModules(modules: FlowModule[], enabled?: boolean): {
234
+ modules: FlowModule[];
235
+ loading: boolean;
236
+ };
217
237
  //# sourceMappingURL=useModulePermission.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useModulePermission.d.ts","sourceRoot":"","sources":["../../src/core/permissions/useModulePermission.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E,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;AAuCD;;;;;;GAMG;AACH,wBAAgB,sBAAsB;;IA0BlC,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAApE,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAApE,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAApE,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAApE,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAApE,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAGvE;AAyBD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;;;;;EAW9E;AAED;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,IAAI,mBAAmB,CAyBlE"}
1
+ {"version":3,"file":"useModulePermission.d.ts","sourceRoot":"","sources":["../../src/core/permissions/useModulePermission.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAIL,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACrB,MAAM,cAAc,CAAC;AAGtB,YAAY,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAiBzE;;;;;;;GAOG;AACH,wBAAgB,sBAAsB;;;IAuBlC,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAApE,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAApE,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAApE,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAApE,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAApE,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;EAIvE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;;;;;EAW9E;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,GAAE,eAAwB;;;;EAGlG;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,6FAA6F;AAC7F,wBAAgB,GAAG,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,KAAc,EAAE,QAAe,EAAE,QAAQ,EAAE,EAAE,QAAQ,2EAG3F;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,IAAI,mBAAmB,CASlE;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,OAAO,UAAO;;;EAQjF"}
@@ -1,137 +1,105 @@
1
- import { useCallback } from "react";
2
- import { useQuery } from "@tanstack/react-query";
3
- import { supabase } from "../supabase/client.js";
4
- import { moduleKey, navKey } from "../types/module.js";
5
- function normalizeLevel(value) {
6
- const v = typeof value === "string" ? value.toLowerCase() : "";
7
- if (v === "none" || v === "view" || v === "edit")
8
- return v;
9
- // Onbekende waarde → fail-open (zichtbaarheid is geen beveiliging).
10
- return "edit";
1
+ import { createElement, Fragment, useCallback, useContext } from "react";
2
+ import { QueryClient, QueryClientContext, useQuery } from "@tanstack/react-query";
3
+ import { filterModules, RANK, resolveLevel, } from "./resolve.js";
4
+ import { getPermissionSnapshot, loadPermissions, usePermissionSnapshot } from "./store.js";
5
+ let fallbackClient = null;
6
+ function getFallbackClient() {
7
+ if (!fallbackClient)
8
+ fallbackClient = new QueryClient();
9
+ return fallbackClient;
11
10
  }
12
- function normalizeRows(data) {
13
- if (!Array.isArray(data))
14
- return [];
15
- return data.map((raw) => {
16
- const row = (raw ?? {});
17
- const rawModule = (row["module_id"] ?? row["moduleId"] ?? "");
18
- const rawFeature = (row["feature_id"] ?? row["featureId"] ?? null);
19
- return {
20
- moduleId: navKey(String(rawModule ?? "")),
21
- featureId: rawFeature ? String(rawFeature) : null,
22
- permission: normalizeLevel(row["permission"] ?? row["level"] ?? row["permission_level"]),
23
- };
24
- });
11
+ function rolesAreAdmin(roles) {
12
+ return roles.includes("admin") || roles.includes("superadmin");
25
13
  }
26
- /** Waar of de fout betekent "de rechten-add-on staat niet op deze database". */
27
- function isMissingRpc(error) {
28
- const e = (error ?? {});
29
- const code = e.code ?? "";
30
- const message = (e.message ?? "").toLowerCase();
31
- return (code === "42883" ||
32
- code === "PGRST202" ||
33
- code === "PGRST302" ||
34
- e.status === 404 ||
35
- message.includes("does not exist") ||
36
- message.includes("could not find the function") ||
37
- message.includes("schema cache"));
14
+ /** Snapshot → bekend? Laden = onbekend; fout/time-out = bekend (leeg → allow). */
15
+ function isKnown(s) {
16
+ return s.status !== "loading";
38
17
  }
39
18
  /**
40
- * Leest de rechten van de ingelogde gebruiker via RPC `my_module_permissions()`.
19
+ * Rechten van de ingelogde gebruiker via RPC `my_module_permissions()`.
41
20
  *
42
- * Ontbreekt de RPC (add-on niet geïnstalleerd) of faalt de call, dan is het
43
- * resultaat een LEGE lijst zonder zichtbare fout en zonder retry — en dat
44
- * betekent bij alle consumers default-allow.
21
+ * Draait op de gedeelde store; gebruikt React Query alleen als er een
22
+ * QueryClientProvider is, anders een interne client — crasht nooit zonder
23
+ * provider. Fail-open: fout of ontbrekende RPC → lege lijst → default-allow.
24
+ * Zichtbaarheid, geen beveiliging.
45
25
  */
46
26
  export function useMyModulePermissions() {
27
+ const snap = usePermissionSnapshot();
28
+ const ctxClient = useContext(QueryClientContext);
47
29
  const query = useQuery({
48
- queryKey: ["my-module-permissions"],
30
+ queryKey: ["my-module-permissions", snap.userId],
49
31
  queryFn: async () => {
50
- const { data, error } = await supabase.rpc("my_module_permissions");
51
- if (error) {
52
- if (isMissingRpc(error))
53
- return [];
54
- // Ook andere fouten mogen niet blokkeren: fail-open, geen retry.
55
- return [];
56
- }
57
- return normalizeRows(data);
32
+ await loadPermissions(getPermissionSnapshot().userId);
33
+ return getPermissionSnapshot().rows;
58
34
  },
59
- // GEEN retry: een ontbrekende RPC mag nooit een retry-storm veroorzaken.
35
+ enabled: snap.status !== "loading",
36
+ initialData: isKnown(snap) ? snap.rows : undefined,
60
37
  retry: false,
61
38
  refetchOnWindowFocus: false,
62
39
  staleTime: 5 * 60 * 1000,
63
- });
40
+ }, ctxClient ?? getFallbackClient());
64
41
  return {
65
42
  ...query,
66
- permissions: query.data ?? [],
43
+ data: isKnown(snap) ? snap.rows : query.data,
44
+ permissions: isKnown(snap) ? snap.rows : [],
67
45
  /** True zolang we niets weten; callers behandelen dit als allow. */
68
- isUnknown: query.isLoading || query.data === undefined,
46
+ isUnknown: !isKnown(snap),
47
+ status: snap.status,
69
48
  };
70
49
  }
71
- function resolveLevel(rows, moduleId, featureId) {
72
- const key = navKey(moduleId);
73
- const forModule = rows.filter((r) => r.moduleId === key);
74
- // Geen rij voor deze module → default-allow.
75
- if (forModule.length === 0)
76
- return "edit";
77
- if (featureId) {
78
- const feature = forModule.find((r) => r.featureId === featureId);
79
- if (feature)
80
- return feature.permission;
81
- }
82
- const moduleLevel = forModule.find((r) => !r.featureId);
83
- if (moduleLevel)
84
- return moduleLevel.permission;
85
- // Alleen feature-rijen bekend: hoogste feature-recht bepaalt moduletoegang.
86
- if (forModule.some((r) => r.permission === "edit"))
87
- return "edit";
88
- if (forModule.some((r) => r.permission === "view"))
89
- return "view";
90
- return "none";
91
- }
92
50
  /**
93
- * Rechtenniveau voor één module (optioneel één feature). Tijdens laden en bij
94
- * elke fout is het antwoord `edit` (fail-open).
51
+ * Rechtenniveau voor één module (optioneel één feature). Tijdens laden, bij
52
+ * een fout en voor admin/superadmin: `edit`. Zichtbaarheid, geen beveiliging.
95
53
  */
96
54
  export function useModulePermission(moduleId, featureId) {
97
- const { permissions, isUnknown } = useMyModulePermissions();
98
- const level = isUnknown ? "edit" : resolveLevel(permissions, moduleId, featureId);
55
+ const snap = usePermissionSnapshot();
56
+ const unknown = !isKnown(snap);
57
+ const level = unknown || rolesAreAdmin(snap.roles) ? "edit" : resolveLevel(snap.rows, moduleId, featureId);
99
58
  return {
100
59
  level,
101
60
  canView: level === "view" || level === "edit",
102
61
  canEdit: level === "edit",
103
- isLoading: isUnknown,
62
+ isLoading: unknown,
104
63
  };
105
64
  }
106
65
  /**
107
- * ModuleListTransform die zijbalkitems met `none` weglaat.
108
- *
109
- * - Tijdens laden / bij fouten: lijst ongewijzigd.
110
- * - Modules zonder rij in het rechtenschema: default-allow.
111
- * - Groepen: de kinderen bepalen zichtbaarheid. Blijft er geen kind over, dan
112
- * valt de groep weg. Is de parent-sleutel geweigerd maar een kind toegestaan,
113
- * dan blijft de groep staan met alleen de toegestane kinderen — de parent-href
114
- * van een groep is een uitklapknop, geen bestemming.
66
+ * Sinds 1.0.23. `allowed` = niveau ≥ `level` (standaard `view`). Laden → true.
67
+ * Zichtbaarheid, geen beveiliging.
68
+ */
69
+ export function useCan(moduleId, featureId, level = "view") {
70
+ const p = useModulePermission(moduleId, featureId);
71
+ return { allowed: RANK[p.level] >= RANK[level], level: p.level, isLoading: p.isLoading };
72
+ }
73
+ /** Sinds 1.0.23. Rendert children als `useCan` toestaat. Zichtbaarheid, geen beveiliging. */
74
+ export function Can({ module, feature, level = "view", fallback = null, children }) {
75
+ const { allowed } = useCan(module, feature ?? null, level);
76
+ return createElement(Fragment, null, allowed ? children : fallback);
77
+ }
78
+ /**
79
+ * ModuleListTransform die zijbalkitems met `none` weglaat. Laden/fout/admin →
80
+ * ongewijzigd. Zichtbaarheid, geen beveiliging.
115
81
  */
116
82
  export function useModulePermissionTransform() {
117
- const { permissions, isUnknown } = useMyModulePermissions();
83
+ const snap = usePermissionSnapshot();
118
84
  return useCallback((modules) => {
119
- if (isUnknown || permissions.length === 0)
85
+ if (!isKnown(snap) || rolesAreAdmin(snap.roles))
120
86
  return modules;
121
- const result = [];
122
- for (const mod of modules) {
123
- const children = mod.nav.children;
124
- if (children?.length) {
125
- const allowed = children.filter((child) => resolveLevel(permissions, navKey(child.href)) !== "none");
126
- if (allowed.length === 0)
127
- continue;
128
- result.push({ ...mod, nav: { ...mod.nav, children: allowed } });
129
- continue;
130
- }
131
- if (resolveLevel(permissions, moduleKey(mod)) === "none")
132
- continue;
133
- result.push(mod);
134
- }
135
- return result;
136
- }, [permissions, isUnknown]);
87
+ return filterModules(modules, snap.rows);
88
+ }, [snap]);
89
+ }
90
+ /**
91
+ * Sinds 1.0.23. Zijbalkfilter met laadstatus: `loading` = eerste laden (toon
92
+ * skeleton); na fout of 3 s time-out de volledige lijst.
93
+ * Zichtbaarheid, geen beveiliging.
94
+ */
95
+ export function usePermissionFilteredModules(modules, enabled = true) {
96
+ const snap = usePermissionSnapshot();
97
+ if (!enabled)
98
+ return { modules, loading: false };
99
+ if (snap.status === "loading") {
100
+ return { modules, loading: !rolesAreAdmin(snap.roles) };
101
+ }
102
+ if (rolesAreAdmin(snap.roles) || snap.status !== "ready")
103
+ return { modules, loading: false };
104
+ return { modules: filterModules(modules, snap.rows), loading: false };
137
105
  }
@@ -0,0 +1,48 @@
1
+ export interface SystemRouteOptions {
2
+ /** Pad als er geen geldig `?redirect` is (na inloggen, handover, reset). */
3
+ landing: string;
4
+ /** Pad van de inlogpagina. Standaard "/login". */
5
+ loginPath?: string;
6
+ }
7
+ export interface LoginSearch {
8
+ redirect?: string;
9
+ }
10
+ /** `/login`: ingelogd → `?redirect` (alleen same-site) wint van `landing`. */
11
+ export declare function loginRoute({ landing }: SystemRouteOptions): {
12
+ validateSearch: (search: Record<string, unknown>) => LoginSearch;
13
+ beforeLoad: ({ context, search }: {
14
+ context: unknown;
15
+ search: LoginSearch;
16
+ }) => void;
17
+ head: () => {
18
+ meta: {
19
+ title: string;
20
+ }[];
21
+ };
22
+ component: () => import("react/jsx-runtime").JSX.Element;
23
+ };
24
+ /** `/reset-wachtwoord`: de core-pagina, daarna naar `landing`. */
25
+ export declare function resetPasswordRoute({ landing, loginPath }: SystemRouteOptions): {
26
+ head: () => {
27
+ meta: {
28
+ title: string;
29
+ }[];
30
+ };
31
+ component: () => import("react/jsx-runtime").JSX.Element;
32
+ };
33
+ /** `/`: redirect in beforeLoad, dus er wordt niets gerenderd (geen flits). */
34
+ export declare function landingRoute({ landing }: Pick<SystemRouteOptions, "landing">): {
35
+ beforeLoad: () => never;
36
+ component: () => null;
37
+ };
38
+ /** `/auth/handover`: client-only, want de sessie staat alleen in localStorage. */
39
+ export declare function handoverRoute({ landing, loginPath }: SystemRouteOptions): {
40
+ ssr: false;
41
+ head: () => {
42
+ meta: {
43
+ title: string;
44
+ }[];
45
+ };
46
+ component: () => import("react/jsx-runtime").JSX.Element;
47
+ };
48
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/routes/index.tsx"],"names":[],"mappings":"AAeA,MAAM,WAAW,kBAAkB;IACjC,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAMD,8EAA8E;AAC9E,wBAAgB,UAAU,CAAC,EAAE,OAAO,EAAE,EAAE,kBAAkB;6BAE7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,WAAW;sCAE5B;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,WAAW,CAAA;KAAE;;;;;;;EAU9E;AAED,kEAAkE;AAClE,wBAAgB,kBAAkB,CAAC,EAAE,OAAO,EAAE,SAAoB,EAAE,EAAE,kBAAkB;;;;;;;EAOvF;AAED,8EAA8E;AAC9E,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE,IAAI,CAAC,kBAAkB,EAAE,SAAS,CAAC;sBAEzD,KAAK;;EAOxB;AAED,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,EAAE,OAAO,EAAE,SAAoB,EAAE,EAAE,kBAAkB;;;;;;;;EAQlF"}
@@ -0,0 +1,63 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Route-opties voor de systeemroutes. Shells houden per route een dun,
4
+ * gegenereerd omhulsel:
5
+ *
6
+ * export const Route = createFileRoute("/login")(loginRoute({ landing: "/voorraad" }));
7
+ *
8
+ * Core levert de volledige opties, zodat een wijziging aan een systeempagina
9
+ * met één core-update in alle shells landt.
10
+ */
11
+ import { redirect } from "@tanstack/react-router";
12
+ import { LoginPage } from "../components/layout/LoginPage.js";
13
+ import { ResetWachtwoordPage } from "../components/layout/ResetWachtwoordPage.js";
14
+ import { HandoverPage } from "../components/layout/HandoverPage.js";
15
+ import { safeRedirectPath } from "../auth/redirect.js";
16
+ function isAuthenticated(context) {
17
+ return Boolean(context?.auth?.isAuthenticated);
18
+ }
19
+ /** `/login`: ingelogd → `?redirect` (alleen same-site) wint van `landing`. */
20
+ export function loginRoute({ landing }) {
21
+ return {
22
+ validateSearch: (search) => typeof search.redirect === "string" ? { redirect: search.redirect } : {},
23
+ beforeLoad: ({ context, search }) => {
24
+ if (isAuthenticated(context)) {
25
+ throw redirect({ href: safeRedirectPath(search?.redirect, landing) });
26
+ }
27
+ },
28
+ head: () => ({ meta: [{ title: "Inloggen" }] }),
29
+ component: function LoginRouteComponent() {
30
+ return _jsx(LoginPage, { redirectTo: landing });
31
+ },
32
+ };
33
+ }
34
+ /** `/reset-wachtwoord`: de core-pagina, daarna naar `landing`. */
35
+ export function resetPasswordRoute({ landing, loginPath = "/login" }) {
36
+ return {
37
+ head: () => ({ meta: [{ title: "Nieuw wachtwoord instellen" }] }),
38
+ component: function ResetPasswordRouteComponent() {
39
+ return _jsx(ResetWachtwoordPage, { redirectTo: landing, loginPath: loginPath });
40
+ },
41
+ };
42
+ }
43
+ /** `/`: redirect in beforeLoad, dus er wordt niets gerenderd (geen flits). */
44
+ export function landingRoute({ landing }) {
45
+ return {
46
+ beforeLoad: () => {
47
+ throw redirect({ href: landing });
48
+ },
49
+ component: function LandingRouteComponent() {
50
+ return null;
51
+ },
52
+ };
53
+ }
54
+ /** `/auth/handover`: client-only, want de sessie staat alleen in localStorage. */
55
+ export function handoverRoute({ landing, loginPath = "/login" }) {
56
+ return {
57
+ ssr: false,
58
+ head: () => ({ meta: [{ title: "Aanmelden" }] }),
59
+ component: function HandoverRouteComponent() {
60
+ return _jsx(HandoverPage, { redirectTo: landing, loginPath: loginPath });
61
+ },
62
+ };
63
+ }
@@ -3,6 +3,6 @@ export type { ProviderAccount } from "./provider-token.js";
3
3
  export { exchangeHandoverCode, HandoverExchangeError } from "./provider-token.js";
4
4
  export type { HandoverSession } from "./provider-token.js";
5
5
  export { CentralAuthConfigError, ProviderNotConnectedError, ProviderUpstreamError, CentralAuthUnavailableError, AmbiguousAccountError, } from "./provider-token.js";
6
- export { requireModulePermission } from "./permissions.server.js";
6
+ export { requireModulePermission, getRoleFlags, isSuperadminServer } from "./permissions.server.js";
7
7
  export type { PermissionLevel } from "./permissions.server.js";
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACpG,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAClF,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,qBAAqB,EACrB,2BAA2B,EAC3B,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAI7B,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAClE,YAAY,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACpG,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAClF,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,qBAAqB,EACrB,2BAA2B,EAC3B,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAI7B,OAAO,EAAE,uBAAuB,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AACpG,YAAY,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC"}
@@ -3,4 +3,4 @@ export { exchangeHandoverCode, HandoverExchangeError } from "./provider-token.js
3
3
  export { CentralAuthConfigError, ProviderNotConnectedError, ProviderUpstreamError, CentralAuthUnavailableError, AmbiguousAccountError, } from "./provider-token.js";
4
4
  // Server-only rechtencheck. Uitsluitend via het "./server" subpath, nooit via
5
5
  // de hoofdbarrel.
6
- export { requireModulePermission } from "./permissions.server.js";
6
+ export { requireModulePermission, getRoleFlags, isSuperadminServer } from "./permissions.server.js";
@@ -1,16 +1,29 @@
1
1
  import type { SupabaseClient } from "@supabase/supabase-js";
2
2
  export type PermissionLevel = "none" | "view" | "edit";
3
3
  /**
4
- * Server-side rechtencheck via RPC `effective_module_permission()`.
4
+ * Server-side rechtencheck via RPC `effective_module_permission(_user_id,
5
+ * _module_id, _feature_id)` (3 argumenten).
5
6
  *
6
- * Bedoeld om te combineren met `requireSupabaseAuth`: geef de authenticated
7
- * client uit `context.supabase` mee.
7
+ * Sinds 1.0.23: `_user_id` komt uitsluitend uit de geverifieerde sessie van het
8
+ * verzoek; moduleniveau = `_feature_id ''`. **Vóór 1.0.23 liet deze functie
9
+ * alles door** (de RPC werd nooit gevonden → default-allow).
8
10
  *
9
- * - Ontbreekt de RPC (rechten-add-on niet geïnstalleerd) → toegestaan
10
- * (default-allow), zodat bestaande shells blijven werken.
11
- * - Onvoldoende rechten → gooit een `Response` met status 403.
12
- *
13
- * Core raakt géén enkele tabel uit het rechtenschema; alleen deze RPC.
11
+ * - Ontbreekt de RPC (add-on niet geïnstalleerd) → toegestaan (default-allow).
12
+ * - Geen geldige sessie → 401. Onvoldoende rechten → 403.
14
13
  */
15
14
  export declare function requireModulePermission(supabase: SupabaseClient<any, any, any>, moduleId: string, featureId?: string | null, level?: PermissionLevel): Promise<PermissionLevel>;
15
+ /**
16
+ * Sinds 1.0.23. Rolvlaggen uit geverifieerde claims (bijv. `context.claims` uit
17
+ * requireSupabaseAuth). superadmin ⇒ admin. Zichtbaarheid, geen beveiliging.
18
+ */
19
+ export declare function getRoleFlags(claims: Record<string, unknown> | undefined | null): {
20
+ roles: string[];
21
+ isSuperadmin: boolean;
22
+ isAdmin: boolean;
23
+ };
24
+ /**
25
+ * Sinds 1.0.23. Superadmin? Claims eerst, terugval tabel `user_roles` (RLS:
26
+ * eigen rijen). Zichtbaarheid, geen beveiliging.
27
+ */
28
+ export declare function isSuperadminServer(supabase: SupabaseClient<any, any, any>, userId: string, claims?: Record<string, unknown> | null): Promise<boolean>;
16
29
  //# sourceMappingURL=permissions.server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"permissions.server.d.ts","sourceRoot":"","sources":["../../src/core/server/permissions.server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAkBvD;;;;;;;;;;;GAWG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EACvC,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,MAAM,GAAG,IAAW,EAC/B,KAAK,GAAE,eAAwB,GAC9B,OAAO,CAAC,eAAe,CAAC,CAuB1B"}
1
+ {"version":3,"file":"permissions.server.d.ts","sourceRoot":"","sources":["../../src/core/server/permissions.server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAG5D,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAuCvD;;;;;;;;;;GAUG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EACvC,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,MAAM,GAAG,IAAW,EAC/B,KAAK,GAAE,eAAwB,GAC9B,OAAO,CAAC,eAAe,CAAC,CAyB1B;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,IAAI;;;;EAK9E;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EACvC,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GACtC,OAAO,CAAC,OAAO,CAAC,CAKlB"}