@flowselections/core 1.0.22 → 1.0.24

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 (134) hide show
  1. package/README.md +80 -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/roles.d.ts +10 -0
  6. package/dist/auth/roles.d.ts.map +1 -0
  7. package/dist/auth/roles.js +11 -0
  8. package/dist/auth/useAuth.d.ts +1 -1
  9. package/dist/auth/useAuth.d.ts.map +1 -1
  10. package/dist/auth/useAuth.js +1 -1
  11. package/dist/auth/useIsSuperadmin.d.ts +19 -0
  12. package/dist/auth/useIsSuperadmin.d.ts.map +1 -0
  13. package/dist/auth/useIsSuperadmin.js +16 -0
  14. package/dist/components/instellingen/NotificationSettingsCard.js +3 -3
  15. package/dist/components/layout/AuthenticatedLayout.d.ts +19 -10
  16. package/dist/components/layout/AuthenticatedLayout.d.ts.map +1 -1
  17. package/dist/components/layout/AuthenticatedLayout.js +43 -9
  18. package/dist/components/layout/BugsDialog.js +10 -10
  19. package/dist/components/layout/HandoverPage.d.ts +5 -1
  20. package/dist/components/layout/HandoverPage.d.ts.map +1 -1
  21. package/dist/components/layout/HandoverPage.js +24 -7
  22. package/dist/components/layout/Header.js +4 -4
  23. package/dist/components/layout/InstellingenPage.d.ts +1 -1
  24. package/dist/components/layout/InstellingenPage.d.ts.map +1 -1
  25. package/dist/components/layout/InstellingenPage.js +15 -7
  26. package/dist/components/layout/LoginPage.js +7 -7
  27. package/dist/components/layout/NotificationBell.js +4 -4
  28. package/dist/components/layout/ResetWachtwoordPage.d.ts +3 -1
  29. package/dist/components/layout/ResetWachtwoordPage.d.ts.map +1 -1
  30. package/dist/components/layout/ResetWachtwoordPage.js +10 -8
  31. package/dist/components/layout/Sidebar.d.ts +31 -7
  32. package/dist/components/layout/Sidebar.d.ts.map +1 -1
  33. package/dist/components/layout/Sidebar.js +33 -11
  34. package/dist/components/ui/accordion.js +1 -1
  35. package/dist/components/ui/alert-dialog.js +2 -2
  36. package/dist/components/ui/alert.js +1 -1
  37. package/dist/components/ui/avatar.js +1 -1
  38. package/dist/components/ui/badge.js +1 -1
  39. package/dist/components/ui/breadcrumb.js +1 -1
  40. package/dist/components/ui/button.js +1 -1
  41. package/dist/components/ui/calendar.d.ts +1 -1
  42. package/dist/components/ui/calendar.d.ts.map +1 -1
  43. package/dist/components/ui/calendar.js +2 -2
  44. package/dist/components/ui/card.js +1 -1
  45. package/dist/components/ui/carousel.d.ts +2 -2
  46. package/dist/components/ui/carousel.d.ts.map +1 -1
  47. package/dist/components/ui/carousel.js +2 -2
  48. package/dist/components/ui/chart.d.ts +10 -10
  49. package/dist/components/ui/chart.js +1 -1
  50. package/dist/components/ui/checkbox.js +1 -1
  51. package/dist/components/ui/command.js +2 -2
  52. package/dist/components/ui/context-menu.js +1 -1
  53. package/dist/components/ui/dialog.js +1 -1
  54. package/dist/components/ui/drawer.js +1 -1
  55. package/dist/components/ui/dropdown-menu.js +1 -1
  56. package/dist/components/ui/form.js +2 -2
  57. package/dist/components/ui/hover-card.js +1 -1
  58. package/dist/components/ui/icon-picker.js +3 -3
  59. package/dist/components/ui/input-otp.d.ts.map +1 -1
  60. package/dist/components/ui/input-otp.js +1 -1
  61. package/dist/components/ui/input.js +1 -1
  62. package/dist/components/ui/label.js +1 -1
  63. package/dist/components/ui/menubar.js +1 -1
  64. package/dist/components/ui/navigation-menu.js +1 -1
  65. package/dist/components/ui/pagination.d.ts +1 -1
  66. package/dist/components/ui/pagination.d.ts.map +1 -1
  67. package/dist/components/ui/pagination.js +2 -2
  68. package/dist/components/ui/popover.js +1 -1
  69. package/dist/components/ui/progress.js +1 -1
  70. package/dist/components/ui/radio-group.js +1 -1
  71. package/dist/components/ui/resizable.js +1 -1
  72. package/dist/components/ui/scroll-area.js +1 -1
  73. package/dist/components/ui/select.js +1 -1
  74. package/dist/components/ui/separator.js +1 -1
  75. package/dist/components/ui/sheet.js +1 -1
  76. package/dist/components/ui/sidebar.d.ts +2 -2
  77. package/dist/components/ui/sidebar.d.ts.map +1 -1
  78. package/dist/components/ui/sidebar.js +8 -8
  79. package/dist/components/ui/skeleton.js +1 -1
  80. package/dist/components/ui/slider.js +1 -1
  81. package/dist/components/ui/switch.js +1 -1
  82. package/dist/components/ui/table.js +1 -1
  83. package/dist/components/ui/tabs.js +1 -1
  84. package/dist/components/ui/textarea.js +1 -1
  85. package/dist/components/ui/toggle-group.js +2 -2
  86. package/dist/components/ui/toggle.js +1 -1
  87. package/dist/components/ui/tooltip.js +1 -1
  88. package/dist/extensions/ShellExtensions.d.ts +14 -1
  89. package/dist/extensions/ShellExtensions.d.ts.map +1 -1
  90. package/dist/extensions/ShellExtensions.js +21 -2
  91. package/dist/hooks/useLogoSrc.d.ts +7 -0
  92. package/dist/hooks/useLogoSrc.d.ts.map +1 -0
  93. package/dist/hooks/useLogoSrc.js +29 -0
  94. package/dist/index.d.ts +53 -44
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +42 -35
  97. package/dist/permissions/resolve.d.ts +22 -0
  98. package/dist/permissions/resolve.d.ts.map +1 -0
  99. package/dist/permissions/resolve.js +76 -0
  100. package/dist/permissions/store.d.ts +38 -0
  101. package/dist/permissions/store.d.ts.map +1 -0
  102. package/dist/permissions/store.js +180 -0
  103. package/dist/permissions/useModulePermission.d.ts +52 -32
  104. package/dist/permissions/useModulePermission.d.ts.map +1 -1
  105. package/dist/permissions/useModulePermission.js +72 -104
  106. package/dist/routes/index.d.ts +50 -0
  107. package/dist/routes/index.d.ts.map +1 -0
  108. package/dist/routes/index.js +63 -0
  109. package/dist/server/handover-handler.d.ts +24 -0
  110. package/dist/server/handover-handler.d.ts.map +1 -0
  111. package/dist/server/handover-handler.js +59 -0
  112. package/dist/server/index.d.ts +4 -1
  113. package/dist/server/index.d.ts.map +1 -1
  114. package/dist/server/index.js +3 -1
  115. package/dist/server/permissions.server.d.ts +14 -8
  116. package/dist/server/permissions.server.d.ts.map +1 -1
  117. package/dist/server/permissions.server.js +46 -9
  118. package/dist/shell/ShellContext.d.ts +26 -0
  119. package/dist/shell/ShellContext.d.ts.map +1 -0
  120. package/dist/shell/ShellContext.js +14 -0
  121. package/dist/styles.css +127 -0
  122. package/dist/supabase/auth-middleware.d.ts +16 -16
  123. package/dist/supabase/auth-middleware.d.ts.map +1 -1
  124. package/dist/supabase/client.d.ts +16 -16
  125. package/dist/supabase/client.d.ts.map +1 -1
  126. package/dist/supabase/client.server.d.ts +16 -16
  127. package/dist/supabase/client.server.d.ts.map +1 -1
  128. package/dist/types/module.d.ts +39 -4
  129. package/dist/types/module.d.ts.map +1 -1
  130. package/dist/types/module.js +33 -5
  131. package/package.json +14 -5
  132. package/dist/auth/handover.functions.d.ts +0 -16
  133. package/dist/auth/handover.functions.d.ts.map +0 -1
  134. package/dist/auth/handover.functions.js +0 -23
package/README.md CHANGED
@@ -4,6 +4,25 @@ Fundament-module: authenticatie, shell-layout (sidebar/header), instellingen en
4
4
  gedeelde UI-componenten. Elke module heeft core als `peerDependency`; de shell
5
5
  bepaalt welke core-versie draait.
6
6
 
7
+ ## Gebruik 1.0.23 niet
8
+
9
+ **Core 1.0.23 breekt de clientbuild van elke shell** (`"Readable"/"AsyncLocalStorage" is not exported by "__vite-browser-external"`): de handoverpagina trok via een serverfunctie servercode in de clientbundel. Gebruik **1.0.24 of hoger**.
10
+
11
+ Vanaf 1.0.24 wisselt `HandoverPage` de code in via een server-route in de shell. Het dashboard genereert daarvoor `src/routes/api/public/auth/handover.ts`:
12
+
13
+ ```ts
14
+ import { createFileRoute } from "@tanstack/react-router";
15
+ import { handoverExchangeHandler } from "@flowselections/core/server";
16
+
17
+ export const Route = createFileRoute("/api/public/auth/handover")({
18
+ server: { handlers: { POST: handoverExchangeHandler() } },
19
+ });
20
+ ```
21
+
22
+ Ander pad? Geef `exchangePath` mee aan `HandoverPage` of `handoverRoute`. Zonder dit bestand mislukt alleen de handover (neutrale foutmelding); `/login` blijft werken.
23
+
24
+ `build:pkg` controleert met `scripts/check-client-graph.mjs` dat de hoofdbarrel en `./routes` geen servercode importeren. `npm run verify:consumer` bouwt een minimale consumer-app tegen het gepakte pakket.
25
+
7
26
  ## Modulecontract
8
27
 
9
28
  ```ts
@@ -111,3 +130,64 @@ retry-storm veroorzaken.
111
130
  De browser-client is env-gedreven en lazy (`VITE_SUPABASE_URL` /
112
131
  `VITE_SUPABASE_PUBLISHABLE_KEY`, met `SUPABASE_*` fallback voor SSR). Er staat
113
132
  geen projectref of key in de code, ook niet als fallback.
133
+
134
+ ## Rechten in 1.0.23
135
+
136
+ > **Zichtbaarheid, geen beveiliging.** Alles hieronder verbergt of toont UI. De
137
+ > data wordt afgeschermd door RLS en serverfuncties (`requireModulePermission`).
138
+
139
+ - **Automatisch via `AuthenticatedLayout` + `requireAuth`** — het gegenereerde
140
+ `_authenticated.tsx` hoeft niet te veranderen.
141
+ - Zijbalk: eerst het rechtenfilter, dan extensie-transforms (ordening). Uit met
142
+ `permissions={false}`. Tijdens het eerste laden een skeleton; na een fout of
143
+ 3 s time-out de volledige lijst (fail-open).
144
+ - `requireAuth` geeft `{ moduleAccess }` terug uit `beforeLoad` (context wordt
145
+ niet gemuteerd). Bij `none` toont de layout "Geen toegang". Wacht max. 3 s.
146
+ - Werkt zonder `QueryClientProvider` (gedeelde store, `useSyncExternalStore`).
147
+ - `useCan(module, feature?, level?)` / `<Can>` — niveaus `none < view < edit`.
148
+ - `NavItem.featureId` (anders `featureKey(href)`); groepen zonder zichtbare
149
+ kinderen verdwijnen, tenzij de module een eigen link heeft.
150
+ - `SettingsCard.moduleId/featureId`: kaarten erven het recht van hun module.
151
+ - Footer: `ShellExtensionsProvider useFooter` kan Instellingen verbergen;
152
+ Uitloggen is nooit te verbergen.
153
+ - `useIsSuperadmin()` (claim `user_roles`, terugval tabel); server:
154
+ `getRoleFlags(claims)`, `isSuperadminServer()`. Nooit voor moduletoegang.
155
+ - `useShell()` / `useShellModules()` — ruwe modulelijst + `shellId`.
156
+ - Admin en superadmin zien altijd alles.
157
+
158
+ ### Gedragswijziging `requireModulePermission`
159
+
160
+ **Vóór 1.0.23 liet deze functie alles door**: de RPC werd zonder `_user_id`
161
+ aangeroepen, werd niet gevonden, en dat gold als "add-on ontbreekt" →
162
+ toegestaan. Sinds 1.0.23 roept hij de 3-argumentversie aan; `_user_id` komt
163
+ uitsluitend uit het geverifieerde token van het verzoek, moduleniveau is
164
+ `_feature_id ''`. Zonder geldige sessie: 401. Controleer serverfuncties die
165
+ erop leunen: ze kunnen nu echt 403 geven.
166
+
167
+ ### Huisstijl
168
+
169
+ Tokens staan op één plek: `src/core/styles.css`, ook als
170
+ `@flowselections/core/styles.css`. Logo: `logoSrc`-prop, anders `logo` uit
171
+ `/huisstijl.json`, anders `/logo.png`.
172
+
173
+ ## Systeemroutes (`@flowselections/core/routes`, vanaf 1.0.23)
174
+
175
+ Shells houden per systeemroute een dun omhulsel; core levert de opties:
176
+
177
+ ```tsx
178
+ // src/routes/login.tsx
179
+ import { createFileRoute } from "@tanstack/react-router";
180
+ import { loginRoute } from "@flowselections/core/routes";
181
+ export const Route = createFileRoute("/login")(loginRoute({ landing: "/voorraad" }));
182
+ ```
183
+
184
+ Ook: `resetPasswordRoute({ landing })` (`/reset-wachtwoord`), `landingRoute({ landing })` (`/`),
185
+ `handoverRoute({ landing })` (`/auth/handover`, `ssr: false`). Bij `loginRoute` wint een veilig `?redirect`
186
+ (same-site pad) van `landing`, ook als je al ingelogd bent. Shells met eigen routebestanden blijven werken.
187
+
188
+ ## Publiceren
189
+
190
+ `vite build` (preview/hosting) schrijft verplicht naar `dist/` en overschrijft dan het pakket. Daarom:
191
+ `prepublishOnly` draait `build:pkg`, dat `dist/` eerst leegmaakt, opnieuw bouwt en met
192
+ `scripts/check-pkg.mjs` controleert dat `dist/` precies het pakket bevat. `npm publish` publiceert dus nooit
193
+ een preview-build.
@@ -1,3 +1,4 @@
1
+ import { type PermissionLevel } from "../permissions/resolve.js";
1
2
  /**
2
3
  * Accepteert alleen een same-site pad: begint met "/", niet met "//",
3
4
  * geen backslash, geen schema en geen stuurtekens. Anders `fallback`.
@@ -12,12 +13,40 @@ export interface RequireAuthArgs {
12
13
  context: unknown;
13
14
  location: {
14
15
  href: string;
16
+ pathname?: string;
15
17
  };
16
18
  }
19
+ /** Sinds 1.0.23: wat `requireAuth` aan de routecontext toevoegt. */
20
+ export interface ModuleAccessContext {
21
+ moduleAccess: PermissionLevel;
22
+ }
23
+ /**
24
+ * Sinds 1.0.23. Moduletoegang voor een pad. Wacht max. 3 s; fout, time-out,
25
+ * vrijgesteld pad of geen modulerij → `edit` (fail-open).
26
+ * Zichtbaarheid, geen beveiliging.
27
+ */
28
+ export declare function resolveModuleAccess(path: string, opts?: {
29
+ userId?: string | null;
30
+ accessToken?: string | null;
31
+ roles?: string[];
32
+ }): Promise<PermissionLevel>;
33
+ /** Sinds 1.0.23. `true` tenzij het niveau `none` is. Zichtbaarheid, geen beveiliging. */
34
+ export declare function canAccessModule(path: string, opts?: {
35
+ userId?: string | null;
36
+ accessToken?: string | null;
37
+ roles?: string[];
38
+ }): Promise<boolean>;
17
39
  /**
18
- * Guard voor de `_authenticated`-route (gebruik in beforeLoad, met ssr: false).
19
- * Zolang auth laadt: geen redirect; de root invalideert de router zodra auth
20
- * geladen is, waarna deze guard opnieuw draait.
40
+ * Guard voor de `_authenticated`-route (beforeLoad, met ssr: false).
41
+ *
42
+ * - Zolang auth laadt: niets (de root invalideert de router daarna).
43
+ * - Geen sessie: synchrone redirect naar `loginPath` met `?redirect=`.
44
+ * - Sinds 1.0.23: geeft `{ moduleAccess }` terug (TanStack voegt dat samen in
45
+ * de routecontext; de context wordt niet gemuteerd). Bij `none` rendert
46
+ * AuthenticatedLayout "Geen toegang". Fail-open na 3 s of bij een fout.
47
+ *
48
+ * Zichtbaarheid, geen beveiliging: data wordt afgeschermd door RLS en
49
+ * serverfuncties.
21
50
  */
22
- export declare function requireAuth({ context, location }: RequireAuthArgs, loginPath?: string): void;
51
+ export declare function requireAuth({ context, location }: RequireAuthArgs, loginPath?: string): void | ModuleAccessContext | Promise<ModuleAccessContext>;
23
52
  //# sourceMappingURL=redirect.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"redirect.d.ts","sourceRoot":"","sources":["../../src/core/auth/redirect.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAezE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,eAAe,EAAE,SAAS,SAAW,GAAG,IAAI,CAI9F"}
1
+ {"version":3,"file":"redirect.d.ts","sourceRoot":"","sources":["../../src/core/auth/redirect.ts"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAG5F;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAazE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/C;AAED,oEAAoE;AACpE,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,eAAe,CAAC;CAC/B;AAUD;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CAAO,GACnF,OAAO,CAAC,eAAe,CAAC,CAM1B;AAED,yFAAyF;AACzF,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAC/E,OAAO,CAAC,OAAO,CAAC,CAElB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CACzB,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,eAAe,EACtC,SAAS,SAAW,GACnB,IAAI,GAAG,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAuB3D"}
@@ -1,4 +1,7 @@
1
1
  import { redirect } from "@tanstack/react-router";
2
+ import { navKey } from "../types/module.js";
3
+ import { EXEMPT_KEYS, resolveLevel } from "../permissions/resolve.js";
4
+ import { getPermissionSnapshot, waitForPermissions } from "../permissions/store.js";
2
5
  /**
3
6
  * Accepteert alleen een same-site pad: begint met "/", niet met "//",
4
7
  * geen backslash, geen schema en geen stuurtekens. Anders `fallback`.
@@ -13,8 +16,6 @@ export function safeRedirectPath(value, fallback) {
13
16
  // eslint-disable-next-line no-control-regex
14
17
  if (/[\u0000-\u001f\u007f]/.test(value))
15
18
  return fallback;
16
- // Schema vóór het eerste "/", "?" of "#" (bijv. "/javascript:..." is ok, "javascript:" niet;
17
- // dat laatste valt al af op de "/"-regel, dit vangt varianten als "/\t//").
18
19
  try {
19
20
  const url = new URL(value, "http://same.invalid");
20
21
  if (url.origin !== "http://same.invalid")
@@ -34,14 +35,64 @@ export function readRedirectParam(fallback) {
34
35
  return fallback;
35
36
  return safeRedirectPath(new URLSearchParams(window.location.search).get("redirect"), fallback);
36
37
  }
38
+ function pathOf(location) {
39
+ return location.pathname ?? location.href.split("?")[0].split("#")[0];
40
+ }
41
+ function isAdminRoles(roles) {
42
+ return Array.isArray(roles) && (roles.includes("admin") || roles.includes("superadmin"));
43
+ }
44
+ /**
45
+ * Sinds 1.0.23. Moduletoegang voor een pad. Wacht max. 3 s; fout, time-out,
46
+ * vrijgesteld pad of geen modulerij → `edit` (fail-open).
47
+ * Zichtbaarheid, geen beveiliging.
48
+ */
49
+ export async function resolveModuleAccess(path, opts = {}) {
50
+ const key = navKey(path);
51
+ if (EXEMPT_KEYS.has(key) || isAdminRoles(opts.roles))
52
+ return "edit";
53
+ const snap = await waitForPermissions(opts.userId ?? null, opts.accessToken);
54
+ if (snap.status !== "ready" || isAdminRoles(snap.roles))
55
+ return "edit";
56
+ return resolveLevel(snap.rows, key);
57
+ }
58
+ /** Sinds 1.0.23. `true` tenzij het niveau `none` is. Zichtbaarheid, geen beveiliging. */
59
+ export async function canAccessModule(path, opts) {
60
+ return (await resolveModuleAccess(path, opts)) !== "none";
61
+ }
37
62
  /**
38
- * Guard voor de `_authenticated`-route (gebruik in beforeLoad, met ssr: false).
39
- * Zolang auth laadt: geen redirect; de root invalideert de router zodra auth
40
- * geladen is, waarna deze guard opnieuw draait.
63
+ * Guard voor de `_authenticated`-route (beforeLoad, met ssr: false).
64
+ *
65
+ * - Zolang auth laadt: niets (de root invalideert de router daarna).
66
+ * - Geen sessie: synchrone redirect naar `loginPath` met `?redirect=`.
67
+ * - Sinds 1.0.23: geeft `{ moduleAccess }` terug (TanStack voegt dat samen in
68
+ * de routecontext; de context wordt niet gemuteerd). Bij `none` rendert
69
+ * AuthenticatedLayout "Geen toegang". Fail-open na 3 s of bij een fout.
70
+ *
71
+ * Zichtbaarheid, geen beveiliging: data wordt afgeschermd door RLS en
72
+ * serverfuncties.
41
73
  */
42
74
  export function requireAuth({ context, location }, loginPath = "/login") {
43
75
  const auth = context?.auth;
44
- if (!auth || auth.loading || auth.isAuthenticated)
76
+ if (!auth || auth.loading)
45
77
  return;
46
- throw redirect({ to: loginPath, search: { redirect: location.href } });
78
+ if (!auth.isAuthenticated) {
79
+ throw redirect({ to: loginPath, search: { redirect: location.href } });
80
+ }
81
+ const path = pathOf(location);
82
+ const key = navKey(path);
83
+ const roles = Array.isArray(auth.roles) ? auth.roles : [];
84
+ if (EXEMPT_KEYS.has(key) || isAdminRoles(roles))
85
+ return { moduleAccess: "edit" };
86
+ const userId = auth.user?.id ?? null;
87
+ const snap = getPermissionSnapshot();
88
+ if (snap.userId === userId && snap.status !== "loading") {
89
+ // Synchroon antwoord als de cache al bekend is (geen wachttijd bij navigatie).
90
+ const level = snap.status === "ready" && !isAdminRoles(snap.roles) ? resolveLevel(snap.rows, key) : "edit";
91
+ return { moduleAccess: level };
92
+ }
93
+ return resolveModuleAccess(path, {
94
+ userId,
95
+ accessToken: auth.session?.access_token ?? null,
96
+ roles,
97
+ }).then((moduleAccess) => ({ moduleAccess }));
47
98
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Sinds 1.0.23. Rolvlaggen uit geverifieerde claims (bijv. `context.claims` uit
3
+ * requireSupabaseAuth). superadmin ⇒ admin. Zichtbaarheid, geen beveiliging.
4
+ */
5
+ export declare function getRoleFlags(claims: Record<string, unknown> | undefined | null): {
6
+ roles: string[];
7
+ isSuperadmin: boolean;
8
+ isAdmin: boolean;
9
+ };
10
+ //# sourceMappingURL=roles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles.d.ts","sourceRoot":"","sources":["../../src/core/auth/roles.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,IAAI;;;;EAK9E"}
@@ -0,0 +1,11 @@
1
+ // Pure rollogica, zonder server- of browserimports: veilig voor client én server.
2
+ /**
3
+ * Sinds 1.0.23. Rolvlaggen uit geverifieerde claims (bijv. `context.claims` uit
4
+ * requireSupabaseAuth). superadmin ⇒ admin. Zichtbaarheid, geen beveiliging.
5
+ */
6
+ export function getRoleFlags(claims) {
7
+ const raw = claims?.["user_roles"];
8
+ const roles = Array.isArray(raw) ? raw.map(String) : [];
9
+ const isSuperadmin = roles.includes("superadmin");
10
+ return { roles, isSuperadmin, isAdmin: isSuperadmin || roles.includes("admin") };
11
+ }
@@ -1,5 +1,5 @@
1
1
  import { User, Session } from "@supabase/supabase-js";
2
- export type AppRole = "admin" | "manager" | "warehouse" | "sales" | "developer";
2
+ export type AppRole = "admin" | "manager" | "warehouse" | "sales" | "developer" | "superadmin" | "administratie" | "ai_admin" | "monteur" | "planner" | "receptie";
3
3
  export interface AuthState {
4
4
  user: User | null;
5
5
  session: Session | null;
@@ -1 +1 @@
1
- {"version":3,"file":"useAuth.d.ts","sourceRoot":"","sources":["../../src/core/auth/useAuth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAGtD,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,SAAS,GAAG,WAAW,GAAG,OAAO,GAAG,WAAW,CAAC;AAEhF,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,OAAO,CAAC;IACzB,OAAO,EAAE;QAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;CACvE;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAC9E,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IACnE,YAAY,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC;AAED,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,WAAW,CAAC;AAElD,wBAAgB,YAAY,IAAI,WAAW,CAsE1C;AACD,wBAAgB,OAAO,IAAI,WAAW,CAErC"}
1
+ {"version":3,"file":"useAuth.d.ts","sourceRoot":"","sources":["../../src/core/auth/useAuth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAGtD,MAAM,MAAM,OAAO,GACf,OAAO,GAAG,SAAS,GAAG,WAAW,GAAG,OAAO,GAAG,WAAW,GACzD,YAAY,GAAG,eAAe,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;AAErF,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,OAAO,CAAC;IACzB,OAAO,EAAE;QAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;CACvE;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAC9E,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IACnE,YAAY,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC;AAED,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,WAAW,CAAC;AAElD,wBAAgB,YAAY,IAAI,WAAW,CAsE1C;AACD,wBAAgB,OAAO,IAAI,WAAW,CAErC"}
@@ -1,5 +1,5 @@
1
1
  import { useState, useEffect, useCallback } from "react";
2
- import { supabase } from "../supabase/client";
2
+ import { supabase } from "../supabase/client.js";
3
3
  export function useAuthState() {
4
4
  const [user, setUser] = useState(null);
5
5
  const [session, setSession] = useState(null);
@@ -0,0 +1,19 @@
1
+ /** Pure: superadmin ⇒ admin (contract §4). */
2
+ export declare function roleFlags(roles: string[]): {
3
+ isSuperadmin: boolean;
4
+ isAdmin: boolean;
5
+ roles: string[];
6
+ };
7
+ /**
8
+ * Sinds 1.0.23. Rollen uit de JWT-claim `user_roles`, terugval op tabel
9
+ * `user_roles`. Nooit gebruiken voor moduletoegang (daarvoor
10
+ * `my_module_permissions`). Werkt zonder QueryClientProvider.
11
+ * Zichtbaarheid, geen beveiliging.
12
+ */
13
+ export declare function useIsSuperadmin(): {
14
+ isLoading: boolean;
15
+ isSuperadmin: boolean;
16
+ isAdmin: boolean;
17
+ roles: string[];
18
+ };
19
+ //# sourceMappingURL=useIsSuperadmin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useIsSuperadmin.d.ts","sourceRoot":"","sources":["../../src/core/auth/useIsSuperadmin.ts"],"names":[],"mappings":"AAEA,8CAA8C;AAC9C,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE;;;;EAGxC;AAED;;;;;GAKG;AACH,wBAAgB,eAAe;;;;;EAG9B"}
@@ -0,0 +1,16 @@
1
+ import { usePermissionSnapshot } from "../permissions/store.js";
2
+ /** Pure: superadmin ⇒ admin (contract §4). */
3
+ export function roleFlags(roles) {
4
+ const isSuperadmin = roles.includes("superadmin");
5
+ return { isSuperadmin, isAdmin: isSuperadmin || roles.includes("admin"), roles };
6
+ }
7
+ /**
8
+ * Sinds 1.0.23. Rollen uit de JWT-claim `user_roles`, terugval op tabel
9
+ * `user_roles`. Nooit gebruiken voor moduletoegang (daarvoor
10
+ * `my_module_permissions`). Werkt zonder QueryClientProvider.
11
+ * Zichtbaarheid, geen beveiliging.
12
+ */
13
+ export function useIsSuperadmin() {
14
+ const snap = usePermissionSnapshot();
15
+ return { ...roleFlags(snap.roles), isLoading: !snap.rolesLoaded };
16
+ }
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useEffect } from "react";
3
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
4
- import { Switch } from "../ui/switch";
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card.js";
4
+ import { Switch } from "../ui/switch.js";
5
5
  import { Bell, Loader2 } from "lucide-react";
6
- import { supabase } from "../../supabase/client";
6
+ import { supabase } from "../../supabase/client.js";
7
7
  import { toast } from "sonner";
8
8
  export function NotificationSettingsCard() {
9
9
  const [prefs, setPrefs] = useState({ email_notifications: true });
@@ -1,20 +1,29 @@
1
1
  import type { ReactNode } from "react";
2
- import type { SidebarProps } from "./Sidebar";
3
- /**
4
- * Alle Sidebar-props (auth, modules, shellId, ...) plus optionele children.
5
- * Zo verliest een shell bij migratie naar AuthenticatedLayout geen enkele
6
- * prop die hij voorheen rechtstreeks aan <Sidebar /> gaf.
7
- */
2
+ import type { SidebarProps } from "./Sidebar.js";
8
3
  export type AuthenticatedLayoutProps = SidebarProps & {
9
4
  /** Optioneel; standaard rendert de layout <Outlet />. */
10
5
  children?: ReactNode;
11
6
  /** Toon de gedeelde paginakop. Standaard ingeschakeld. */
12
7
  header?: boolean;
13
8
  };
9
+ type MatchLike = {
10
+ staticData?: unknown;
11
+ context?: unknown;
12
+ };
13
+ /** Sinds 1.0.23: diepste match met een niet-lege titel. */
14
+ export declare function pickTitle(matches: MatchLike[]): string;
15
+ /** Sinds 1.0.23: `moduleAccess` uit de diepste routecontext die hem heeft. */
16
+ export declare function pickModuleAccess(matches: MatchLike[]): string | undefined;
17
+ export declare function NoAccess(): import("react/jsx-runtime").JSX.Element;
14
18
  /**
15
- * Zijbalk + content-offset + mobiele topbalk. Desktop: ml-64 (gelijk aan de
16
- * oude shell-layout). Mobiel: geen marge, pt-14 onder de topbalk; de sticky
17
- * Header schuift via --core-topbar-h onder de topbalk.
19
+ * Zijbalk + content-offset + mobiele topbalk + paginakop.
20
+ *
21
+ * Sinds 1.0.23: filtert de zijbalk standaard op rechten (eerst rechten, dan
22
+ * extensie-transforms; uit met `permissions={false}`), toont "Geen toegang"
23
+ * als `requireAuth` `moduleAccess: "none"` teruggaf, en levert `useShell()`.
24
+ * Zichtbaarheid, geen beveiliging: data wordt afgeschermd door RLS en
25
+ * serverfuncties.
18
26
  */
19
- export declare function AuthenticatedLayout({ children, header, ...sidebarProps }: AuthenticatedLayoutProps): import("react/jsx-runtime").JSX.Element;
27
+ export declare function AuthenticatedLayout({ children, header, permissions, ...sidebarProps }: AuthenticatedLayoutProps): import("react/jsx-runtime").JSX.Element;
28
+ export {};
20
29
  //# sourceMappingURL=AuthenticatedLayout.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AuthenticatedLayout.d.ts","sourceRoot":"","sources":["../../../src/core/components/layout/AuthenticatedLayout.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAG9C;;;;GAIG;AACH,MAAM,MAAM,wBAAwB,GAAG,YAAY,GAAG;IACpD,yDAAyD;IACzD,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,QAAQ,EAAE,MAAa,EAAE,GAAG,YAAY,EAAE,EAAE,wBAAwB,2CAkBzG"}
1
+ {"version":3,"file":"AuthenticatedLayout.d.ts","sourceRoot":"","sources":["../../../src/core/components/layout/AuthenticatedLayout.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAIjD,MAAM,MAAM,wBAAwB,GAAG,YAAY,GAAG;IACpD,yDAAyD;IACzD,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,KAAK,SAAS,GAAG;IAAE,UAAU,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE7D,2DAA2D;AAC3D,wBAAgB,SAAS,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,MAAM,CAMtD;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,MAAM,GAAG,SAAS,CAMzE;AAED,wBAAgB,QAAQ,4CAWvB;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,EACR,MAAa,EACb,WAAkB,EAClB,GAAG,YAAY,EAChB,EAAE,wBAAwB,2CA6B1B"}
@@ -1,15 +1,49 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
2
3
  import { Outlet, useMatches } from "@tanstack/react-router";
3
- import { Sidebar } from "./Sidebar";
4
- import { Header } from "./Header";
4
+ import { Sidebar, useSidebarModules } from "./Sidebar.js";
5
+ import { Header } from "./Header.js";
6
+ import { ShellContext } from "../../shell/ShellContext.js";
7
+ /** Sinds 1.0.23: diepste match met een niet-lege titel. */
8
+ export function pickTitle(matches) {
9
+ for (let i = matches.length - 1; i >= 0; i--) {
10
+ const t = matches[i].staticData?.title;
11
+ if (typeof t === "string" && t.trim() !== "")
12
+ return t;
13
+ }
14
+ return "";
15
+ }
16
+ /** Sinds 1.0.23: `moduleAccess` uit de diepste routecontext die hem heeft. */
17
+ export function pickModuleAccess(matches) {
18
+ for (let i = matches.length - 1; i >= 0; i--) {
19
+ const v = matches[i].context?.moduleAccess;
20
+ if (typeof v === "string")
21
+ return v;
22
+ }
23
+ return undefined;
24
+ }
25
+ export function NoAccess() {
26
+ return (_jsx("div", { className: "flex min-h-[60vh] items-center justify-center p-8", children: _jsxs("div", { className: "max-w-md text-center", children: [_jsx("h1", { className: "text-lg font-semibold text-foreground", children: "Geen toegang" }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: "Je hebt geen rechten voor deze module. Vraag een beheerder om toegang." })] }) }));
27
+ }
5
28
  /**
6
- * Zijbalk + content-offset + mobiele topbalk. Desktop: ml-64 (gelijk aan de
7
- * oude shell-layout). Mobiel: geen marge, pt-14 onder de topbalk; de sticky
8
- * Header schuift via --core-topbar-h onder de topbalk.
29
+ * Zijbalk + content-offset + mobiele topbalk + paginakop.
30
+ *
31
+ * Sinds 1.0.23: filtert de zijbalk standaard op rechten (eerst rechten, dan
32
+ * extensie-transforms; uit met `permissions={false}`), toont "Geen toegang"
33
+ * als `requireAuth` `moduleAccess: "none"` teruggaf, en levert `useShell()`.
34
+ * Zichtbaarheid, geen beveiliging: data wordt afgeschermd door RLS en
35
+ * serverfuncties.
9
36
  */
10
- export function AuthenticatedLayout({ children, header = true, ...sidebarProps }) {
37
+ export function AuthenticatedLayout({ children, header = true, permissions = true, ...sidebarProps }) {
11
38
  const matches = useMatches();
12
- const titleMatch = [...matches].reverse().find((match) => typeof match.staticData?.title === "string");
13
- const title = titleMatch?.staticData?.title ?? "";
14
- return (_jsxs("div", { className: "min-h-screen bg-background [--core-topbar-h:3.5rem] md:[--core-topbar-h:0px]", children: [_jsx(Sidebar, { ...sidebarProps }), _jsxs("div", { className: "flex min-h-dvh flex-col pt-14 md:ml-64 md:pt-0", children: [header && _jsx(Header, { title: title }), _jsx("main", { className: "flex-1", children: children ?? _jsx(Outlet, {}) })] })] }));
39
+ const title = pickTitle(matches);
40
+ const access = pickModuleAccess(matches);
41
+ const { visibleModules, loading } = useSidebarModules(sidebarProps.modules, permissions);
42
+ const shell = useMemo(() => ({
43
+ shellId: sidebarProps.shellId,
44
+ modules: sidebarProps.modules,
45
+ visibleModules,
46
+ permissionsLoading: loading,
47
+ }), [sidebarProps.shellId, sidebarProps.modules, visibleModules, loading]);
48
+ return (_jsx(ShellContext.Provider, { value: shell, children: _jsxs("div", { className: "min-h-screen bg-background [--core-topbar-h:3.5rem] md:[--core-topbar-h:0px]", children: [_jsx(Sidebar, { ...sidebarProps, permissions: permissions, visibleModules: visibleModules, loading: loading }), _jsxs("div", { className: "flex min-h-dvh flex-col pt-14 md:ml-64 md:pt-0", children: [header && _jsx(Header, { title: title }), _jsx("main", { className: "flex-1", children: permissions && access === "none" ? _jsx(NoAccess, {}) : (children ?? _jsx(Outlet, {})) })] })] }) }));
15
49
  }
@@ -1,17 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState, useEffect, useRef } from "react";
3
- import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
4
- import { Button } from "../ui/button";
5
- import { Input } from "../ui/input";
6
- import { Textarea } from "../ui/textarea";
7
- import { Label } from "../ui/label";
8
- import { Badge } from "../ui/badge";
9
- import { Select, SelectContent, SelectItem, SelectTrigger } from "../ui/select";
10
- import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
3
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog.js";
4
+ import { Button } from "../ui/button.js";
5
+ import { Input } from "../ui/input.js";
6
+ import { Textarea } from "../ui/textarea.js";
7
+ import { Label } from "../ui/label.js";
8
+ import { Badge } from "../ui/badge.js";
9
+ import { Select, SelectContent, SelectItem, SelectTrigger } from "../ui/select.js";
10
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs.js";
11
11
  import { Plus, Loader2, CheckCircle, Trash2, Paperclip, X, FileText } from "lucide-react";
12
- import { supabase } from "../../supabase/client";
12
+ import { supabase } from "../../supabase/client.js";
13
13
  import { toast } from "sonner";
14
- import { bugReportSchema, validateFileUpload, ALLOWED_FILE_TYPES } from "../../lib/validationSchemas";
14
+ import { bugReportSchema, validateFileUpload, ALLOWED_FILE_TYPES } from "../../lib/validationSchemas.js";
15
15
  const statusConfig = {
16
16
  open: { label: "Open", variant: "destructive" },
17
17
  in_progress: { label: "In behandeling", variant: "default" },
@@ -3,6 +3,10 @@ export interface HandoverPageProps {
3
3
  redirectTo?: string;
4
4
  /** Pad van de (centrale) inlogpagina voor de foutknop. */
5
5
  loginPath?: string;
6
+ /** Server-route die de code inwisselt. Standaard "/api/public/auth/handover". */
7
+ exchangePath?: string;
6
8
  }
7
- export declare function HandoverPage({ redirectTo, loginPath }: HandoverPageProps): import("react/jsx-runtime").JSX.Element;
9
+ /** Sinds 1.0.24: standaardpad van de handover-server-route in de shell. */
10
+ export declare const DEFAULT_HANDOVER_EXCHANGE_PATH = "/api/public/auth/handover";
11
+ export declare function HandoverPage({ redirectTo, loginPath, exchangePath, }: HandoverPageProps): import("react/jsx-runtime").JSX.Element;
8
12
  //# sourceMappingURL=HandoverPage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"HandoverPage.d.ts","sourceRoot":"","sources":["../../../src/core/components/layout/HandoverPage.tsx"],"names":[],"mappings":"AAQA,MAAM,WAAW,iBAAiB;IAChC,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAID,wBAAgB,YAAY,CAAC,EAAE,UAAgB,EAAE,SAAoB,EAAE,EAAE,iBAAiB,2CA0FzF"}
1
+ {"version":3,"file":"HandoverPage.d.ts","sourceRoot":"","sources":["../../../src/core/components/layout/HandoverPage.tsx"],"names":[],"mappings":"AAOA,MAAM,WAAW,iBAAiB;IAChC,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iFAAiF;IACjF,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,2EAA2E;AAC3E,eAAO,MAAM,8BAA8B,8BAA8B,CAAC;AAoB1E,wBAAgB,YAAY,CAAC,EAC3B,UAAgB,EAChB,SAAoB,EACpB,YAA6C,GAC9C,EAAE,iBAAiB,2CA0FnB"}
@@ -1,13 +1,30 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from "react";
3
3
  import { useRouter } from "@tanstack/react-router";
4
- import { Button } from "../ui/button";
5
- import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "../ui/card";
6
- import { supabase } from "../../supabase/client";
7
- import { readRedirectParam } from "../../auth/redirect";
8
- import { exchangeHandover } from "../../auth/handover.functions.js";
4
+ import { Button } from "../ui/button.js";
5
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "../ui/card.js";
6
+ import { supabase } from "../../supabase/client.js";
7
+ import { readRedirectParam } from "../../auth/redirect.js";
8
+ /** Sinds 1.0.24: standaardpad van de handover-server-route in de shell. */
9
+ export const DEFAULT_HANDOVER_EXCHANGE_PATH = "/api/public/auth/handover";
10
+ async function exchangeHandover(exchangePath, code) {
11
+ const res = await fetch(exchangePath, {
12
+ method: "POST",
13
+ headers: { "content-type": "application/json" },
14
+ credentials: "same-origin",
15
+ cache: "no-store",
16
+ body: JSON.stringify({ code }),
17
+ });
18
+ if (!res.ok)
19
+ throw new Error("handover_failed");
20
+ const body = (await res.json());
21
+ if (typeof body.accessToken !== "string" || typeof body.refreshToken !== "string") {
22
+ throw new Error("handover_failed");
23
+ }
24
+ return { accessToken: body.accessToken, refreshToken: body.refreshToken };
25
+ }
9
26
  const FOUTMELDING = "Inloggen is niet gelukt. Probeer het opnieuw vanaf de inlogpagina.";
10
- export function HandoverPage({ redirectTo = "/", loginPath = "/login" }) {
27
+ export function HandoverPage({ redirectTo = "/", loginPath = "/login", exchangePath = DEFAULT_HANDOVER_EXCHANGE_PATH, }) {
11
28
  const router = useRouter();
12
29
  const [failed, setFailed] = useState(false);
13
30
  const started = useRef(false);
@@ -39,7 +56,7 @@ export function HandoverPage({ redirectTo = "/", loginPath = "/login" }) {
39
56
  const cleanUrl = window.location.pathname + window.location.hash;
40
57
  window.history.replaceState(null, "", cleanUrl);
41
58
  try {
42
- const result = await exchangeHandover({ data: { code } });
59
+ const result = await exchangeHandover(exchangePath, code);
43
60
  const { error } = await supabase.auth.setSession({
44
61
  access_token: result.accessToken,
45
62
  refresh_token: result.refreshToken,
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
3
  import { Bug, Search } from "lucide-react";
4
- import { Input } from "../ui/input";
5
- import { Button } from "../ui/button";
6
- import { BugsDialog } from "./BugsDialog";
7
- import { NotificationBell } from "./NotificationBell";
4
+ import { Input } from "../ui/input.js";
5
+ import { Button } from "../ui/button.js";
6
+ import { BugsDialog } from "./BugsDialog.js";
7
+ import { NotificationBell } from "./NotificationBell.js";
8
8
  export function Header({ title }) {
9
9
  const [bugsDialogOpen, setBugsDialogOpen] = useState(false);
10
10
  return (_jsxs(_Fragment, { children: [_jsxs("header", { className: "sticky top-[var(--core-topbar-h,0px)] md:top-0 z-30 flex h-16 items-center justify-between border-b border-border bg-card px-6", children: [_jsx("h1", { className: "text-xl font-semibold text-foreground", children: title }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsxs("div", { className: "relative hidden md:block", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { placeholder: "Zoeken...", className: "w-64 pl-9" })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setBugsDialogOpen(true), children: [_jsx(Bug, { className: "mr-2 h-4 w-4" }), "Bugs melden"] }), _jsx(NotificationBell, {})] })] }), _jsx(BugsDialog, { open: bugsDialogOpen, onOpenChange: setBugsDialogOpen })] }));
@@ -1,4 +1,4 @@
1
- import type { FlowModule } from "../../types/module";
1
+ import type { FlowModule } from "../../types/module.js";
2
2
  export declare function InstellingenPage({ modules }: {
3
3
  modules: FlowModule[];
4
4
  }): import("react/jsx-runtime").JSX.Element;
@@ -1 +1 @@
1
- {"version":3,"file":"InstellingenPage.d.ts","sourceRoot":"","sources":["../../../src/core/components/layout/InstellingenPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAgB,MAAM,oBAAoB,CAAC;AAqInE,wBAAgB,gBAAgB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,UAAU,EAAE,CAAA;CAAE,2CAWtE"}
1
+ {"version":3,"file":"InstellingenPage.d.ts","sourceRoot":"","sources":["../../../src/core/components/layout/InstellingenPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAgB,MAAM,uBAAuB,CAAC;AAwItE,wBAAgB,gBAAgB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,UAAU,EAAE,CAAA;CAAE,2CAsBtE"}
@@ -1,13 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { moduleKey } from "../../types/module.js";
3
+ import { resolveLevel } from "../../permissions/resolve.js";
4
+ import { usePermissionSnapshot } from "../../permissions/store.js";
2
5
  import { useState } from "react";
3
- import { Button } from "../ui/button";
4
- import { Input } from "../ui/input";
5
- import { Label } from "../ui/label";
6
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
7
- import { supabase } from "../../supabase/client";
6
+ import { Button } from "../ui/button.js";
7
+ import { Input } from "../ui/input.js";
8
+ import { Label } from "../ui/label.js";
9
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card.js";
10
+ import { supabase } from "../../supabase/client.js";
8
11
  import { toast } from "sonner";
9
12
  import { Loader2, User } from "lucide-react";
10
- import { NotificationSettingsCard } from "../instellingen/NotificationSettingsCard";
13
+ import { NotificationSettingsCard } from "../instellingen/NotificationSettingsCard.js";
11
14
  // ─── Profile Card ─────────────────────────────────────────────────────────────
12
15
  function ProfileCard() {
13
16
  const [displayName, setDisplayName] = useState("");
@@ -72,7 +75,12 @@ const coreCards = [
72
75
  ];
73
76
  // ─── Main export ──────────────────────────────────────────────────────────────
74
77
  export function InstellingenPage({ modules }) {
75
- const moduleCards = modules.flatMap((m) => m.settingsCards);
78
+ // Sinds 1.0.23: modulekaarten erven het recht van hun module (of featureId).
79
+ // Core-kaarten altijd. Laden/fout/admin → tonen. Zichtbaarheid, geen beveiliging.
80
+ const snap = usePermissionSnapshot();
81
+ const filterOn = snap.status === "ready" && !snap.roles.includes("admin") && !snap.roles.includes("superadmin");
82
+ const moduleCards = modules.flatMap((m) => m.settingsCards.filter((card) => !filterOn ||
83
+ resolveLevel(snap.rows, card.moduleId ?? moduleKey(m), card.featureId ?? null) !== "none"));
76
84
  const allCards = [...coreCards, ...moduleCards].sort((a, b) => a.order - b.order);
77
85
  return (_jsx("div", { className: "p-6", children: _jsx("div", { className: "grid gap-6 lg:grid-cols-2", children: allCards.map((card, i) => _jsx(card.component, {}, i)) }) }));
78
86
  }