@flowselections/core 1.0.16 → 1.0.19

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 (41) hide show
  1. package/README.md +113 -0
  2. package/dist/auth/handover.functions.d.ts +16 -0
  3. package/dist/auth/handover.functions.d.ts.map +1 -0
  4. package/dist/auth/handover.functions.js +23 -0
  5. package/dist/components/layout/HandoverPage.d.ts +8 -0
  6. package/dist/components/layout/HandoverPage.d.ts.map +1 -0
  7. package/dist/components/layout/HandoverPage.js +70 -0
  8. package/dist/components/layout/LoginPage.d.ts +2 -1
  9. package/dist/components/layout/LoginPage.d.ts.map +1 -1
  10. package/dist/components/layout/LoginPage.js +23 -4
  11. package/dist/components/layout/Sidebar.d.ts +1 -1
  12. package/dist/components/layout/Sidebar.d.ts.map +1 -1
  13. package/dist/components/layout/Sidebar.js +4 -1
  14. package/dist/extensions/ModuleGuardSlot.d.ts +10 -0
  15. package/dist/extensions/ModuleGuardSlot.d.ts.map +1 -0
  16. package/dist/extensions/ModuleGuardSlot.js +13 -0
  17. package/dist/extensions/ShellExtensions.d.ts +31 -0
  18. package/dist/extensions/ShellExtensions.d.ts.map +1 -0
  19. package/dist/extensions/ShellExtensions.js +45 -0
  20. package/dist/index.d.ts +10 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +8 -0
  23. package/dist/permissions/ModuleAccessGuard.d.ts +19 -0
  24. package/dist/permissions/ModuleAccessGuard.d.ts.map +1 -0
  25. package/dist/permissions/ModuleAccessGuard.js +20 -0
  26. package/dist/permissions/useModulePermission.d.ts +217 -0
  27. package/dist/permissions/useModulePermission.d.ts.map +1 -0
  28. package/dist/permissions/useModulePermission.js +137 -0
  29. package/dist/server/index.d.ts +4 -0
  30. package/dist/server/index.d.ts.map +1 -1
  31. package/dist/server/index.js +4 -0
  32. package/dist/server/permissions.server.d.ts +16 -0
  33. package/dist/server/permissions.server.d.ts.map +1 -0
  34. package/dist/server/permissions.server.js +41 -0
  35. package/dist/server/provider-token.d.ts +17 -0
  36. package/dist/server/provider-token.d.ts.map +1 -1
  37. package/dist/server/provider-token.js +53 -6
  38. package/dist/types/module.d.ts +23 -0
  39. package/dist/types/module.d.ts.map +1 -1
  40. package/dist/types/module.js +24 -1
  41. package/package.json +6 -3
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # @flowselections/core
2
+
3
+ Fundament-module: authenticatie, shell-layout (sidebar/header), instellingen en de
4
+ gedeelde UI-componenten. Elke module heeft core als `peerDependency`; de shell
5
+ bepaalt welke core-versie draait.
6
+
7
+ ## Modulecontract
8
+
9
+ ```ts
10
+ interface FlowModule {
11
+ id: string; // pakket-id (npm), NIET de externe sleutel
12
+ name: string;
13
+ version: string;
14
+ nav: NavItem;
15
+ settingsCards: SettingsCard[];
16
+ registryKey?: string; // optionele expliciete externe sleutel
17
+ }
18
+ ```
19
+
20
+ ### De modulesleutel
21
+
22
+ Core matcht extern (module_registry, rechten) **altijd** op `moduleKey(mod)` en
23
+ nooit op `mod.id` — `id` is een pakket-id, terwijl de registry route-slugs bevat.
24
+
25
+ `moduleKey()`:
26
+
27
+ 1. `registryKey` als die is opgegeven;
28
+ 2. anders het eerste padsegment van `nav.href`;
29
+ 3. **afsluitende underscores worden gestript.**
30
+
31
+ Regel 3 is een expliciete beslissing: een afsluitende underscore in een routepad
32
+ (`/voorraad_/detail`) is een TanStack un-nesting-conventie en geen betekenisvol
33
+ segment. Zonder strippen krijgt dezelfde module twee sleutels (`voorraad` en
34
+ `voorraad_`) en verdwijnt hij half uit de zijbalk. Er is gekozen voor strippen in
35
+ plaats van `registryKey` verplicht maken, omdat dat laatste bestaande modules
36
+ stil zou breken. Wil je afwijken, zet dan `registryKey` expliciet.
37
+
38
+ Core kent geen enkele module- of domeinnaam; de regel is volledig generiek.
39
+
40
+ ## Shell-extensiepunten
41
+
42
+ Alles optioneel. Zonder provider gedraagt een shell zich exact zoals voorheen:
43
+ default-allow, code-volgorde.
44
+
45
+ ```tsx
46
+ // src/routes/_authenticated.tsx
47
+ import { ShellExtensionsProvider, ModuleGuardSlot, Sidebar, useModulePermissionTransform } from "@flowselections/core";
48
+ import { useModuleOrderTransform } from "@flowselections/sidebar-ordening-installatie-techniek";
49
+
50
+ <ShellExtensionsProvider
51
+ useModuleList={[useModulePermissionTransform, useModuleOrderTransform]}
52
+ >
53
+ <Sidebar auth={auth} modules={modules} />
54
+ <ModuleGuardSlot>
55
+ <Outlet />
56
+ </ModuleGuardSlot>
57
+ </ShellExtensionsProvider>
58
+ ```
59
+
60
+ Pipeline-volgorde is de array-volgorde: **eerst rechten filteren, dan sorteren.**
61
+ Meegegeven hooks moeten stabiele referenties zijn (module-scope functies, geen
62
+ inline lambdas), zodat de hook-volgorde tussen renders nooit verandert.
63
+
64
+ Bij laden, een ontbrekende tabel/RPC of een fout in een transform komt de lijst
65
+ **ongewijzigd** terug. Een lege zijbalk is nooit een geldige uitkomst.
66
+
67
+ ### Parent/child-gedrag in de zijbalk
68
+
69
+ - Groep waarvan **alle** kinderen wegvallen → groep wordt niet gerenderd.
70
+ - Parent-sleutel geweigerd maar een kind toegestaan → groep blijft staan met
71
+ alleen de toegestane kinderen. `NavItem.href` is op een groep een uitklapknop,
72
+ geen echte bestemming, dus de parent-sleutel wordt voor groepen niet gebruikt
73
+ om zichtbaarheid te bepalen.
74
+ - Platte items worden op hun eigen sleutel beoordeeld.
75
+
76
+ ## Rechten (leeskant)
77
+
78
+ - `useMyModulePermissions()` / `useModulePermission(moduleId, featureId?)` — React
79
+ Query op RPC `my_module_permissions()`, `queryKey: ["my-module-permissions"]`.
80
+ - `useModulePermissionTransform()` — laat zijbalkitems met `none` weg.
81
+ - `ModuleAccessGuard` — pad-gebaseerde guard (module-id uit het eerste
82
+ URL-segment).
83
+ - `requireModulePermission()` — **server-only**, alleen via
84
+ `@flowselections/core/server`.
85
+
86
+ Core leest of schrijft **geen enkele tabel** uit het rechtenschema. Alleen de twee
87
+ RPC's `my_module_permissions()` en `effective_module_permission()`. Het schema
88
+ blijft eigendom van de admin-add-on.
89
+
90
+ ### ModuleAccessGuard is fail-open — met opzet
91
+
92
+ `ModuleAccessGuard` en `useModulePermission` laten door bij:
93
+
94
+ - laden (eerste render, eerste login);
95
+ - een ontbrekende RPC (add-on niet geïnstalleerd op die database — PostgREST
96
+ 404 / Postgres 42883);
97
+ - elke andere fout in de query;
98
+ - een module zonder rij in `module_registry` (default-allow).
99
+
100
+ Dit is een bewuste keuze: zichtbaarheid is geen beveiliging, en een dichte guard
101
+ zou bij elke laadfout of ontbrekende add-on de hele app onbruikbaar maken.
102
+
103
+ **De echte afscherming is `requireModulePermission()` server-side plus RLS/policies
104
+ op de database.** Lees deze guard niet als beveiligingslaag.
105
+
106
+ De permissie-query heeft `retry: false`: een ontbrekende RPC mag nooit een
107
+ retry-storm veroorzaken.
108
+
109
+ ## Supabase
110
+
111
+ De browser-client is env-gedreven en lazy (`VITE_SUPABASE_URL` /
112
+ `VITE_SUPABASE_PUBLISHABLE_KEY`, met `SUPABASE_*` fallback voor SSR). Er staat
113
+ geen projectref of key in de code, ook niet als fallback.
@@ -0,0 +1,16 @@
1
+ export interface HandoverExchangeResult {
2
+ accessToken: string;
3
+ refreshToken: string;
4
+ }
5
+ /**
6
+ * Wisselt een eenmalige handover-code in voor een Supabase-sessie.
7
+ *
8
+ * Fail-closed: bij elke fout gooit deze functie en krijgt de client alleen een
9
+ * generieke foutmelding. De code wordt nooit gelogd.
10
+ */
11
+ export declare const exchangeHandover: import("@tanstack/start-client-core").RequiredFetcher<undefined, (data: {
12
+ code: string;
13
+ }) => {
14
+ code: string;
15
+ }, Promise<HandoverExchangeResult>>;
16
+ //# sourceMappingURL=handover.functions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handover.functions.d.ts","sourceRoot":"","sources":["../../src/core/auth/handover.functions.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,0EACJ;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE;;mCASrC,CAAC"}
@@ -0,0 +1,23 @@
1
+ import { createServerFn } from "@tanstack/react-start";
2
+ // Top-level import van het server-only bestand: de Start-compiler haalt de
3
+ // handler-body (en dus deze aanroep) uit de clientbundel. Met een testbuild
4
+ // gecontroleerd: het central-auth-secret en het exchange-pad komen niet in de
5
+ // clientchunks voor.
6
+ import { exchangeHandoverCode } from "../server/provider-token.js";
7
+ /**
8
+ * Wisselt een eenmalige handover-code in voor een Supabase-sessie.
9
+ *
10
+ * Fail-closed: bij elke fout gooit deze functie en krijgt de client alleen een
11
+ * generieke foutmelding. De code wordt nooit gelogd.
12
+ */
13
+ export const exchangeHandover = createServerFn({ method: "POST" })
14
+ .inputValidator((data) => {
15
+ if (!data || typeof data.code !== "string" || data.code.trim() === "") {
16
+ throw new Error("invalid_request");
17
+ }
18
+ return { code: data.code };
19
+ })
20
+ .handler(async ({ data }) => {
21
+ const session = await exchangeHandoverCode(data.code);
22
+ return { accessToken: session.accessToken, refreshToken: session.refreshToken };
23
+ });
@@ -0,0 +1,8 @@
1
+ export interface HandoverPageProps {
2
+ /** Waar de gebruiker na een geslaagde handover naartoe gaat. */
3
+ redirectTo?: string;
4
+ /** Pad van de (centrale) inlogpagina voor de foutknop. */
5
+ loginPath?: string;
6
+ }
7
+ export declare function HandoverPage({ redirectTo, loginPath }: HandoverPageProps): import("react/jsx-runtime").JSX.Element;
8
+ //# sourceMappingURL=HandoverPage.d.ts.map
@@ -0,0 +1 @@
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;CACpB;AAID,wBAAgB,YAAY,CAAC,EAAE,UAAgB,EAAE,SAAoB,EAAE,EAAE,iBAAiB,2CA6FzF"}
@@ -0,0 +1,70 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
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 { exchangeHandover } from "../../auth/handover.functions.js";
8
+ const FOUTMELDING = "Inloggen is niet gelukt. Probeer het opnieuw vanaf de inlogpagina.";
9
+ export function HandoverPage({ redirectTo = "/", loginPath = "/login" }) {
10
+ const router = useRouter();
11
+ const [failed, setFailed] = useState(false);
12
+ const started = useRef(false);
13
+ /** Wacht tot de auth-context de nieuwe sessie kent, zodat de route-gate niet terugstuurt. */
14
+ const waitForAuthContext = async () => {
15
+ for (let i = 0; i < 60; i++) {
16
+ const auth = router.options.context?.auth;
17
+ if (auth?.isAuthenticated)
18
+ return true;
19
+ await new Promise((r) => setTimeout(r, 25));
20
+ }
21
+ return false;
22
+ };
23
+ useEffect(() => {
24
+ // React 19 mount kan in dev dubbel vuren; een code is eenmalig bruikbaar.
25
+ if (started.current)
26
+ return;
27
+ started.current = true;
28
+ (async () => {
29
+ const code = new URLSearchParams(window.location.search).get("code");
30
+ if (!code) {
31
+ setFailed(true);
32
+ return;
33
+ }
34
+ // Code direct uit de adresbalk halen: hij mag niet in de geschiedenis
35
+ // of in een referer blijven staan.
36
+ const cleanUrl = window.location.pathname + window.location.hash;
37
+ window.history.replaceState(null, "", cleanUrl);
38
+ try {
39
+ // Eerst een eventuele bestaande sessie beëindigen, anders belandt
40
+ // iemand in de omgeving van de vorige gebruiker.
41
+ const { data } = await supabase.auth.getSession();
42
+ if (data.session)
43
+ await supabase.auth.signOut();
44
+ const result = await exchangeHandover({ data: { code } });
45
+ const { error } = await supabase.auth.setSession({
46
+ access_token: result.accessToken,
47
+ refresh_token: result.refreshToken,
48
+ });
49
+ if (error) {
50
+ setFailed(true);
51
+ return;
52
+ }
53
+ await waitForAuthContext();
54
+ // Nooit window.location.href: een volledige pagina-load laat de
55
+ // server (zonder sessie) eerst naar /login redirecten.
56
+ await router.navigate({ to: redirectTo, replace: true });
57
+ }
58
+ catch {
59
+ setFailed(true);
60
+ }
61
+ })();
62
+ // eslint-disable-next-line react-hooks/exhaustive-deps
63
+ }, []);
64
+ if (failed) {
65
+ return (_jsx("div", { className: "min-h-screen flex items-center justify-center bg-background p-4", children: _jsxs(Card, { className: "w-full max-w-md", children: [_jsxs(CardHeader, { className: "text-center", children: [_jsx(CardTitle, { className: "text-2xl", children: "Inloggen mislukt" }), _jsx(CardDescription, { children: FOUTMELDING })] }), _jsx(CardFooter, { children: _jsx(Button, { className: "w-full", onClick: () => {
66
+ window.location.href = loginPath;
67
+ }, children: "Naar de inlogpagina" }) })] }) }));
68
+ }
69
+ return (_jsx("div", { className: "min-h-screen flex items-center justify-center bg-background p-4", children: _jsx(Card, { className: "w-full max-w-md", children: _jsxs(CardContent, { className: "flex flex-col items-center gap-4 py-10", children: [_jsx("div", { className: "animate-spin rounded-full h-8 w-8 border-b-2 border-primary" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Je wordt aangemeld\u2026" })] }) }) }));
70
+ }
@@ -1,5 +1,6 @@
1
1
  interface LoginPageProps {
2
- redirectTo: string;
2
+ /** Pad waarnaar wordt genavigeerd na succesvol inloggen. */
3
+ redirectTo?: string;
3
4
  }
4
5
  export declare function LoginPage({ redirectTo }: LoginPageProps): import("react/jsx-runtime").JSX.Element;
5
6
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"LoginPage.d.ts","sourceRoot":"","sources":["../../../src/core/components/layout/LoginPage.tsx"],"names":[],"mappings":"AASA,UAAU,cAAc;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,SAAS,CAAC,EAAE,UAAU,EAAE,EAAE,cAAc,2CA4GvD"}
1
+ {"version":3,"file":"LoginPage.d.ts","sourceRoot":"","sources":["../../../src/core/components/layout/LoginPage.tsx"],"names":[],"mappings":"AAUA,UAAU,cAAc;IACtB,4DAA4D;IAC5D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,SAAS,CAAC,EAAE,UAA4B,EAAE,EAAE,cAAc,2CAgIzE"}
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
+ import { useRouter } from "@tanstack/react-router";
3
4
  import { Button } from "../ui/button";
4
5
  import { Input } from "../ui/input";
5
6
  import { Label } from "../ui/label";
@@ -7,13 +8,24 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
7
8
  import { toast } from "sonner";
8
9
  import { loginSchema } from "../../lib/validationSchemas";
9
10
  import { supabase } from "../../supabase/client";
10
- export function LoginPage({ redirectTo }) {
11
+ export function LoginPage({ redirectTo = "/instellingen" }) {
12
+ const router = useRouter();
11
13
  const [email, setEmail] = useState("");
12
14
  const [password, setPassword] = useState("");
13
15
  const [isLoading, setIsLoading] = useState(false);
14
16
  const [showForgot, setShowForgot] = useState(false);
15
17
  const [forgotEmail, setForgotEmail] = useState("");
16
18
  const [forgotLoading, setForgotLoading] = useState(false);
19
+ /** Wacht tot de auth-context de nieuwe sessie kent, zodat de route-gate niet terugstuurt. */
20
+ const waitForAuthContext = async () => {
21
+ for (let i = 0; i < 60; i++) {
22
+ const auth = router.options.context?.auth;
23
+ if (auth?.isAuthenticated)
24
+ return true;
25
+ await new Promise((r) => setTimeout(r, 25));
26
+ }
27
+ return false;
28
+ };
17
29
  const handleSubmit = async (e) => {
18
30
  e.preventDefault();
19
31
  const validation = loginSchema.safeParse({ email, password });
@@ -23,13 +35,20 @@ export function LoginPage({ redirectTo }) {
23
35
  }
24
36
  setIsLoading(true);
25
37
  const { error } = await supabase.auth.signInWithPassword({ email, password });
26
- setIsLoading(false);
27
38
  if (error) {
39
+ setIsLoading(false);
28
40
  toast.error("Controleer je e-mailadres en wachtwoord.");
41
+ return;
29
42
  }
30
- else {
43
+ // Client-side navigatie zonder volledige herlaad: dat voorkomt de flits
44
+ // waarbij de server (zonder sessie) eerst naar /login terugstuurt.
45
+ const ready = await waitForAuthContext();
46
+ if (!ready) {
31
47
  window.location.href = redirectTo;
48
+ return;
32
49
  }
50
+ await router.navigate({ to: redirectTo, replace: true });
51
+ setIsLoading(false);
33
52
  };
34
53
  const handleForgotPassword = async (e) => {
35
54
  e.preventDefault();
@@ -53,5 +72,5 @@ export function LoginPage({ redirectTo }) {
53
72
  if (showForgot) {
54
73
  return (_jsx("div", { className: "min-h-screen flex items-center justify-center bg-background p-4", children: _jsxs(Card, { className: "w-full max-w-md", children: [_jsxs(CardHeader, { className: "text-center", children: [_jsx(CardTitle, { className: "text-2xl", children: "Wachtwoord vergeten" }), _jsx(CardDescription, { children: "Vul je e-mailadres in om een reset-link te ontvangen" })] }), _jsxs("form", { onSubmit: handleForgotPassword, children: [_jsx(CardContent, { className: "space-y-4", children: _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "forgot-email", children: "E-mailadres" }), _jsx(Input, { id: "forgot-email", type: "email", placeholder: "naam@bedrijf.nl", value: forgotEmail, onChange: (e) => setForgotEmail(e.target.value), required: true, maxLength: 255 })] }) }), _jsxs(CardFooter, { className: "flex flex-col gap-4", children: [_jsx(Button, { type: "submit", className: "w-full", disabled: forgotLoading, children: forgotLoading ? "Versturen..." : "Reset-link versturen" }), _jsx("button", { type: "button", onClick: () => setShowForgot(false), className: "text-sm text-primary hover:underline", children: "Terug naar inloggen" })] })] })] }) }));
55
74
  }
56
- return (_jsx("div", { className: "min-h-screen flex items-center justify-center bg-background p-4", children: _jsxs(Card, { className: "w-full max-w-md", children: [_jsxs(CardHeader, { className: "text-center", children: [_jsx(CardTitle, { className: "text-2xl", children: "Inloggen" }), _jsx(CardDescription, { children: "Voer je gegevens in om toegang te krijgen tot het systeem" })] }), _jsxs("form", { onSubmit: handleSubmit, children: [_jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "email", children: "E-mailadres" }), _jsx(Input, { id: "email", type: "email", placeholder: "naam@bedrijf.nl", value: email, onChange: (e) => setEmail(e.target.value), required: true, maxLength: 255 })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "password", children: "Wachtwoord" }), _jsx(Input, { id: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", type: "password", value: password, onChange: (e) => setPassword(e.target.value), required: true, maxLength: 128 })] })] }), _jsxs(CardFooter, { className: "flex flex-col gap-4", children: [_jsx(Button, { type: "submit", className: "w-full", disabled: isLoading, children: isLoading ? "Bezig met inloggen..." : "Inloggen" }), _jsx("button", { type: "button", onClick: () => setShowForgot(true), className: "text-sm text-muted-foreground hover:text-primary hover:underline", children: "Wachtwoord vergeten?" })] })] })] }) }));
75
+ return (_jsx("div", { className: "min-h-screen flex items-center justify-center bg-background p-4", children: _jsxs(Card, { className: "w-full max-w-md", children: [_jsxs(CardHeader, { className: "text-center", children: [_jsx(CardTitle, { className: "text-2xl", children: "Inloggen" }), _jsx(CardDescription, { children: "Voer je gegevens in om toegang te krijgen tot het systeem" })] }), _jsxs("form", { onSubmit: handleSubmit, children: [_jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "email", children: "E-mailadres" }), _jsx(Input, { id: "email", type: "email", placeholder: "naam@bedrijf.nl", value: email, onChange: (e) => setEmail(e.target.value), required: true, maxLength: 255 })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "password", children: "Wachtwoord" }), _jsx(Input, { id: "password", type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", value: password, onChange: (e) => setPassword(e.target.value), required: true, maxLength: 128 })] })] }), _jsxs(CardFooter, { className: "flex flex-col gap-4", children: [_jsx(Button, { type: "submit", className: "w-full", disabled: isLoading, children: isLoading ? "Bezig met inloggen..." : "Inloggen" }), _jsx("button", { type: "button", onClick: () => setShowForgot(true), className: "text-sm text-muted-foreground hover:text-primary hover:underline", children: "Wachtwoord vergeten?" })] })] })] }) }));
57
76
  }
@@ -1,5 +1,5 @@
1
1
  import type { AuthContext } from "../../auth/useAuth";
2
- import type { FlowModule } from "../../types/module";
2
+ import type { FlowModule } from "../../types/module.js";
3
3
  interface SidebarProps {
4
4
  auth: AuthContext;
5
5
  modules: FlowModule[];
@@ -1 +1 @@
1
- {"version":3,"file":"Sidebar.d.ts","sourceRoot":"","sources":["../../../src/core/components/layout/Sidebar.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAW,MAAM,oBAAoB,CAAC;AAI9D,UAAU,YAAY;IACpB,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,UAAU,EAAE,CAAC;CACvB;AAkFD,wBAAgB,OAAO,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,YAAY,2CAkDtD"}
1
+ {"version":3,"file":"Sidebar.d.ts","sourceRoot":"","sources":["../../../src/core/components/layout/Sidebar.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAW,MAAM,uBAAuB,CAAC;AAMjE,UAAU,YAAY;IACpB,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,UAAU,EAAE,CAAC;CACvB;AAkFD,wBAAgB,OAAO,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,YAAY,2CAqDtD"}
@@ -3,6 +3,7 @@ import { useState } from "react";
3
3
  import { Link, useLocation } from "@tanstack/react-router";
4
4
  import { Settings, LogOut, ChevronDown } from "lucide-react";
5
5
  import { cn } from "../../lib/utils";
6
+ import { useModuleListTransform } from "../../extensions/ShellExtensions.js";
6
7
  // ─── Single flat nav link ─────────────────────────────────────────────────────
7
8
  function SidebarLink({ item, pathname, indent = false, }) {
8
9
  const isActive = pathname === item.href;
@@ -22,8 +23,10 @@ function SidebarGroup({ item, pathname, }) {
22
23
  export function Sidebar({ auth, modules }) {
23
24
  const location = useLocation();
24
25
  const pathname = location.pathname;
26
+ // Zonder ShellExtensionsProvider komt de lijst ongewijzigd terug (code-volgorde).
27
+ const visibleModules = useModuleListTransform(modules);
25
28
  const handleSignOut = async () => {
26
29
  await auth.signOut();
27
30
  };
28
- return (_jsx("aside", { className: "fixed left-0 top-0 z-40 h-screen w-64 bg-sidebar", children: _jsxs("div", { className: "flex h-full flex-col", children: [_jsx("div", { className: "px-12 py-0 border-b border-sidebar-border", children: _jsx("img", { src: "/logo.png", alt: "Logo", className: "w-full h-auto object-contain" }) }), _jsx("nav", { className: "flex-1 space-y-1 px-3 py-4 overflow-y-auto sidebar-scroll", children: modules.map((mod) => mod.nav.children?.length ? (_jsx(SidebarGroup, { item: mod.nav, pathname: pathname }, mod.id)) : (_jsx(SidebarLink, { item: mod.nav, pathname: pathname }, mod.id))) }), _jsxs("div", { className: "border-t border-sidebar-border px-3 py-4", children: [_jsx(SidebarLink, { item: { label: "Instellingen", href: "/instellingen", icon: Settings }, pathname: pathname }), _jsxs("button", { onClick: handleSignOut, className: "flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors text-sidebar-muted hover:bg-sidebar-accent hover:text-sidebar-accent-foreground mt-1 cursor-pointer", children: [_jsx(LogOut, { className: "h-5 w-5" }), "Uitloggen"] })] })] }) }));
31
+ return (_jsx("aside", { className: "fixed left-0 top-0 z-40 h-screen w-64 bg-sidebar", children: _jsxs("div", { className: "flex h-full flex-col", children: [_jsx("div", { className: "px-12 py-0 border-b border-sidebar-border", children: _jsx("img", { src: "/logo.png", alt: "Logo", className: "w-full h-auto object-contain" }) }), _jsx("nav", { className: "flex-1 space-y-1 px-3 py-4 overflow-y-auto sidebar-scroll", children: visibleModules.map((mod) => mod.nav.children?.length ? (_jsx(SidebarGroup, { item: mod.nav, pathname: pathname }, mod.id)) : (_jsx(SidebarLink, { item: mod.nav, pathname: pathname }, mod.id))) }), _jsxs("div", { className: "border-t border-sidebar-border px-3 py-4", children: [_jsx(SidebarLink, { item: { label: "Instellingen", href: "/instellingen", icon: Settings }, pathname: pathname }), _jsxs("button", { onClick: handleSignOut, className: "flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors text-sidebar-muted hover:bg-sidebar-accent hover:text-sidebar-accent-foreground mt-1 cursor-pointer", children: [_jsx(LogOut, { className: "h-5 w-5" }), "Uitloggen"] })] })] }) }));
29
32
  }
@@ -0,0 +1,10 @@
1
+ import type { ReactNode } from "react";
2
+ /**
3
+ * Rendert standaard alleen zijn children. Is er via `ShellExtensionsProvider`
4
+ * een `guard` meegegeven, dan wordt die eromheen gebruikt. Zo kan een shell een
5
+ * eigen guard inprikken zonder dat core een module hoeft te kennen.
6
+ */
7
+ export declare function ModuleGuardSlot({ children }: {
8
+ children: ReactNode;
9
+ }): import("react/jsx-runtime").JSX.Element;
10
+ //# sourceMappingURL=ModuleGuardSlot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModuleGuardSlot.d.ts","sourceRoot":"","sources":["../../src/core/extensions/ModuleGuardSlot.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,2CAIpE"}
@@ -0,0 +1,13 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useShellGuard } from "./ShellExtensions.js";
3
+ /**
4
+ * Rendert standaard alleen zijn children. Is er via `ShellExtensionsProvider`
5
+ * een `guard` meegegeven, dan wordt die eromheen gebruikt. Zo kan een shell een
6
+ * eigen guard inprikken zonder dat core een module hoeft te kennen.
7
+ */
8
+ export function ModuleGuardSlot({ children }) {
9
+ const Guard = useShellGuard();
10
+ if (!Guard)
11
+ return _jsx(_Fragment, { children: children });
12
+ return _jsx(Guard, { children: children });
13
+ }
@@ -0,0 +1,31 @@
1
+ import type { ComponentType, ReactNode } from "react";
2
+ import type { FlowModule } from "../types/module.js";
3
+ /** Transformeert de modulelijst (filteren en/of sorteren). Puur, geen side effects. */
4
+ export type ModuleListTransform = (modules: FlowModule[]) => FlowModule[];
5
+ export type UseModuleListTransform = () => ModuleListTransform;
6
+ export interface ShellExtensionsProviderProps {
7
+ /**
8
+ * Eén of meer hooks die elk een ModuleListTransform teruggeven.
9
+ *
10
+ * Contract: geef stabiele referenties mee (module-scope functies, geen inline
11
+ * lambdas). De array-volgorde is de pipeline-volgorde: eerst rechten filteren,
12
+ * daarna sorteren.
13
+ */
14
+ useModuleList?: UseModuleListTransform | UseModuleListTransform[];
15
+ /** Optionele guard-component die `ModuleGuardSlot` rendert rond zijn children. */
16
+ guard?: ComponentType<{
17
+ children: ReactNode;
18
+ }>;
19
+ children: ReactNode;
20
+ }
21
+ export declare function ShellExtensionsProvider({ useModuleList, guard, children, }: ShellExtensionsProviderProps): import("react/jsx-runtime").JSX.Element;
22
+ export declare function useShellGuard(): ComponentType<{
23
+ children: ReactNode;
24
+ }> | undefined;
25
+ /**
26
+ * Past de geregistreerde transforms toe. Zonder provider, tijdens laden of bij
27
+ * een fout in een transform komt de lijst ONGEWIJZIGD terug — nooit een lege
28
+ * zijbalk.
29
+ */
30
+ export declare function useModuleListTransform(modules: FlowModule[]): FlowModule[];
31
+ //# sourceMappingURL=ShellExtensions.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useMemo } from "react";
3
+ const ShellExtensionsContext = createContext({ transforms: [] });
4
+ export function ShellExtensionsProvider({ useModuleList, guard, children, }) {
5
+ const transforms = useMemo(() => (Array.isArray(useModuleList) ? useModuleList : useModuleList ? [useModuleList] : []),
6
+ // Bewust op de referentie(s): de hooks moeten stabiel zijn, zodat de
7
+ // hook-volgorde tussen renders nooit verandert.
8
+ // eslint-disable-next-line react-hooks/exhaustive-deps
9
+ [useModuleList]);
10
+ const value = useMemo(() => ({ transforms, guard }), [transforms, guard]);
11
+ return _jsx(ShellExtensionsContext.Provider, { value: value, children: children });
12
+ }
13
+ export function useShellGuard() {
14
+ return useContext(ShellExtensionsContext).guard;
15
+ }
16
+ /**
17
+ * Past de geregistreerde transforms toe. Zonder provider, tijdens laden of bij
18
+ * een fout in een transform komt de lijst ONGEWIJZIGD terug — nooit een lege
19
+ * zijbalk.
20
+ */
21
+ export function useModuleListTransform(modules) {
22
+ const { transforms } = useContext(ShellExtensionsContext);
23
+ // Onvoorwaardelijk en in vaste volgorde: geen hook-order-problemen.
24
+ const applied = [];
25
+ for (const useTransform of transforms) {
26
+ try {
27
+ applied.push(useTransform());
28
+ }
29
+ catch {
30
+ applied.push((list) => list);
31
+ }
32
+ }
33
+ let result = modules;
34
+ for (const transform of applied) {
35
+ try {
36
+ const next = transform(result);
37
+ if (Array.isArray(next))
38
+ result = next;
39
+ }
40
+ catch {
41
+ // fail-open: lijst ongewijzigd laten
42
+ }
43
+ }
44
+ return result;
45
+ }
package/dist/index.d.ts CHANGED
@@ -3,6 +3,8 @@ export type { AuthContext, AuthState, AuthActions, AppRole } from "./auth/useAut
3
3
  export { supabase } from "./supabase/client";
4
4
  export type { Database } from "./supabase/types";
5
5
  export { LoginPage } from "./components/layout/LoginPage";
6
+ export { HandoverPage } from "./components/layout/HandoverPage";
7
+ export type { HandoverPageProps } from "./components/layout/HandoverPage";
6
8
  export { ResetWachtwoordPage } from "./components/layout/ResetWachtwoordPage";
7
9
  export { Header } from "./components/layout/Header";
8
10
  export { InstellingenPage } from "./components/layout/InstellingenPage";
@@ -31,4 +33,12 @@ export { validateFileUpload, ALLOWED_FILE_TYPES, loginSchema } from "./lib/valid
31
33
  export { useIsMobile } from "./hooks/use-mobile";
32
34
  export { useHuisstijl } from "./hooks/useHuisstijl";
33
35
  export type { FlowModule, NavItem, SettingsCard } from "./types/module";
36
+ export { moduleKey, navKey } from "./types/module.js";
37
+ export { ShellExtensionsProvider, useModuleListTransform } from "./extensions/ShellExtensions.js";
38
+ export type { ModuleListTransform, UseModuleListTransform, ShellExtensionsProviderProps, } from "./extensions/ShellExtensions.js";
39
+ export { ModuleGuardSlot } from "./extensions/ModuleGuardSlot.js";
40
+ export { useMyModulePermissions, useModulePermission, useModulePermissionTransform, } from "./permissions/useModulePermission.js";
41
+ export type { PermissionLevel, ModulePermissionRow } from "./permissions/useModulePermission.js";
42
+ export { ModuleAccessGuard } from "./permissions/ModuleAccessGuard.js";
43
+ export type { ModuleAccessGuardProps } from "./permissions/ModuleAccessGuard.js";
34
44
  //# 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
@@ -4,6 +4,7 @@ export { useAuth, useAuthState } from "./auth/useAuth";
4
4
  export { supabase } from "./supabase/client";
5
5
  // Layout
6
6
  export { LoginPage } from "./components/layout/LoginPage";
7
+ export { HandoverPage } from "./components/layout/HandoverPage";
7
8
  export { ResetWachtwoordPage } from "./components/layout/ResetWachtwoordPage";
8
9
  export { Header } from "./components/layout/Header";
9
10
  export { InstellingenPage } from "./components/layout/InstellingenPage";
@@ -33,3 +34,10 @@ export { cn } from "./lib/utils";
33
34
  export { validateFileUpload, ALLOWED_FILE_TYPES, loginSchema } from "./lib/validationSchemas";
34
35
  export { useIsMobile } from "./hooks/use-mobile";
35
36
  export { useHuisstijl } from "./hooks/useHuisstijl";
37
+ export { moduleKey, navKey } from "./types/module.js";
38
+ // Shell-extensiepunten (allemaal optioneel; zonder provider ongewijzigd gedrag)
39
+ export { ShellExtensionsProvider, useModuleListTransform } from "./extensions/ShellExtensions.js";
40
+ export { ModuleGuardSlot } from "./extensions/ModuleGuardSlot.js";
41
+ // Rechten (leeskant; fail-open, zie README)
42
+ export { useMyModulePermissions, useModulePermission, useModulePermissionTransform, } from "./permissions/useModulePermission.js";
43
+ export { ModuleAccessGuard } from "./permissions/ModuleAccessGuard.js";
@@ -0,0 +1,19 @@
1
+ import type { ReactNode } from "react";
2
+ export interface ModuleAccessGuardProps {
3
+ children: ReactNode;
4
+ /** Wat te tonen bij `none`. Default: een neutrale melding. */
5
+ fallback?: ReactNode;
6
+ /** Overschrijf de module-id; standaard afgeleid uit het eerste URL-segment. */
7
+ moduleId?: string;
8
+ featureId?: string | null;
9
+ }
10
+ /**
11
+ * Pad-gebaseerde guard.
12
+ *
13
+ * FAIL-OPEN, met opzet: tijdens laden, bij een ontbrekende RPC of bij elke
14
+ * fout laat deze guard door. Modules zonder rij in het rechtenschema zijn
15
+ * default-allow. Dit is UX (menu's en pagina's verbergen), GEEN beveiliging —
16
+ * de echte afscherming is `requireModulePermission` server-side plus RLS.
17
+ */
18
+ export declare function ModuleAccessGuard({ children, fallback, moduleId, featureId, }: ModuleAccessGuardProps): import("react/jsx-runtime").JSX.Element;
19
+ //# sourceMappingURL=ModuleAccessGuard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModuleAccessGuard.d.ts","sourceRoot":"","sources":["../../src/core/permissions/ModuleAccessGuard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAKvC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,SAAS,CAAC;IACpB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,SAAS,GACV,EAAE,sBAAsB,2CAqBxB"}
@@ -0,0 +1,20 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useLocation } from "@tanstack/react-router";
3
+ import { navKey } from "../types/module.js";
4
+ import { useModulePermission } from "./useModulePermission.js";
5
+ /**
6
+ * Pad-gebaseerde guard.
7
+ *
8
+ * FAIL-OPEN, met opzet: tijdens laden, bij een ontbrekende RPC of bij elke
9
+ * fout laat deze guard door. Modules zonder rij in het rechtenschema zijn
10
+ * default-allow. Dit is UX (menu's en pagina's verbergen), GEEN beveiliging —
11
+ * de echte afscherming is `requireModulePermission` server-side plus RLS.
12
+ */
13
+ export function ModuleAccessGuard({ children, fallback, moduleId, featureId, }) {
14
+ const location = useLocation();
15
+ const key = moduleId ?? navKey(location.pathname);
16
+ const { canView } = useModulePermission(key, featureId ?? null);
17
+ if (canView)
18
+ return _jsx(_Fragment, { children: children });
19
+ return (_jsx(_Fragment, { children: fallback ?? (_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." })] }) })) }));
20
+ }