@flowselections/core 1.0.15 → 1.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -0
- package/dist/components/layout/LoginPage.d.ts +2 -1
- package/dist/components/layout/LoginPage.d.ts.map +1 -1
- package/dist/components/layout/LoginPage.js +23 -4
- package/dist/components/layout/Sidebar.d.ts +1 -1
- package/dist/components/layout/Sidebar.d.ts.map +1 -1
- package/dist/components/layout/Sidebar.js +4 -1
- package/dist/extensions/ModuleGuardSlot.d.ts +10 -0
- package/dist/extensions/ModuleGuardSlot.d.ts.map +1 -0
- package/dist/extensions/ModuleGuardSlot.js +13 -0
- package/dist/extensions/ShellExtensions.d.ts +31 -0
- package/dist/extensions/ShellExtensions.d.ts.map +1 -0
- package/dist/extensions/ShellExtensions.js +45 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/permissions/ModuleAccessGuard.d.ts +19 -0
- package/dist/permissions/ModuleAccessGuard.d.ts.map +1 -0
- package/dist/permissions/ModuleAccessGuard.js +20 -0
- package/dist/permissions/useModulePermission.d.ts +217 -0
- package/dist/permissions/useModulePermission.d.ts.map +1 -0
- package/dist/permissions/useModulePermission.js +137 -0
- package/dist/server/index.d.ts +4 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +4 -1
- package/dist/server/permissions.server.d.ts +16 -0
- package/dist/server/permissions.server.d.ts.map +1 -0
- package/dist/server/permissions.server.js +41 -0
- package/dist/server/provider-token.d.ts +13 -0
- package/dist/server/provider-token.d.ts.map +1 -1
- package/dist/server/provider-token.js +129 -44
- package/dist/types/module.d.ts +23 -0
- package/dist/types/module.d.ts.map +1 -1
- package/dist/types/module.js +24 -1
- 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.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LoginPage.d.ts","sourceRoot":"","sources":["../../../src/core/components/layout/LoginPage.tsx"],"names":[],"mappings":"
|
|
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
|
-
|
|
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",
|
|
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 +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,
|
|
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:
|
|
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
|
@@ -31,4 +31,12 @@ export { validateFileUpload, ALLOWED_FILE_TYPES, loginSchema } from "./lib/valid
|
|
|
31
31
|
export { useIsMobile } from "./hooks/use-mobile";
|
|
32
32
|
export { useHuisstijl } from "./hooks/useHuisstijl";
|
|
33
33
|
export type { FlowModule, NavItem, SettingsCard } from "./types/module";
|
|
34
|
+
export { moduleKey, navKey } from "./types/module.js";
|
|
35
|
+
export { ShellExtensionsProvider, useModuleListTransform } from "./extensions/ShellExtensions.js";
|
|
36
|
+
export type { ModuleListTransform, UseModuleListTransform, ShellExtensionsProviderProps, } from "./extensions/ShellExtensions.js";
|
|
37
|
+
export { ModuleGuardSlot } from "./extensions/ModuleGuardSlot.js";
|
|
38
|
+
export { useMyModulePermissions, useModulePermission, useModulePermissionTransform, } from "./permissions/useModulePermission.js";
|
|
39
|
+
export type { PermissionLevel, ModulePermissionRow } from "./permissions/useModulePermission.js";
|
|
40
|
+
export { ModuleAccessGuard } from "./permissions/ModuleAccessGuard.js";
|
|
41
|
+
export type { ModuleAccessGuardProps } from "./permissions/ModuleAccessGuard.js";
|
|
34
42
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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
|
@@ -33,3 +33,10 @@ export { cn } from "./lib/utils";
|
|
|
33
33
|
export { validateFileUpload, ALLOWED_FILE_TYPES, loginSchema } from "./lib/validationSchemas";
|
|
34
34
|
export { useIsMobile } from "./hooks/use-mobile";
|
|
35
35
|
export { useHuisstijl } from "./hooks/useHuisstijl";
|
|
36
|
+
export { moduleKey, navKey } from "./types/module.js";
|
|
37
|
+
// Shell-extensiepunten (allemaal optioneel; zonder provider ongewijzigd gedrag)
|
|
38
|
+
export { ShellExtensionsProvider, useModuleListTransform } from "./extensions/ShellExtensions.js";
|
|
39
|
+
export { ModuleGuardSlot } from "./extensions/ModuleGuardSlot.js";
|
|
40
|
+
// Rechten (leeskant; fail-open, zie README)
|
|
41
|
+
export { useMyModulePermissions, useModulePermission, useModulePermissionTransform, } from "./permissions/useModulePermission.js";
|
|
42
|
+
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
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
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
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Leest de rechten van de ingelogde gebruiker via RPC `my_module_permissions()`.
|
|
10
|
+
*
|
|
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.
|
|
14
|
+
*/
|
|
15
|
+
export declare function useMyModulePermissions(): {
|
|
16
|
+
permissions: ModulePermissionRow[];
|
|
17
|
+
/** True zolang we niets weten; callers behandelen dit als allow. */
|
|
18
|
+
isUnknown: boolean;
|
|
19
|
+
data: ModulePermissionRow[];
|
|
20
|
+
error: Error;
|
|
21
|
+
isError: true;
|
|
22
|
+
isPending: false;
|
|
23
|
+
isLoading: false;
|
|
24
|
+
isLoadingError: false;
|
|
25
|
+
isRefetchError: true;
|
|
26
|
+
isSuccess: false;
|
|
27
|
+
isPlaceholderData: false;
|
|
28
|
+
status: "error";
|
|
29
|
+
dataUpdatedAt: number;
|
|
30
|
+
errorUpdatedAt: number;
|
|
31
|
+
failureCount: number;
|
|
32
|
+
failureReason: Error | null;
|
|
33
|
+
errorUpdateCount: number;
|
|
34
|
+
isFetched: boolean;
|
|
35
|
+
isFetchedAfterMount: boolean;
|
|
36
|
+
isFetching: boolean;
|
|
37
|
+
isInitialLoading: boolean;
|
|
38
|
+
isPaused: boolean;
|
|
39
|
+
isRefetching: boolean;
|
|
40
|
+
isStale: boolean;
|
|
41
|
+
isEnabled: boolean;
|
|
42
|
+
refetch: (options?: import("@tanstack/query-core").RefetchOptions) => Promise<import("@tanstack/query-core").QueryObserverResult<ModulePermissionRow[], Error>>;
|
|
43
|
+
fetchStatus: import("@tanstack/query-core").FetchStatus;
|
|
44
|
+
promise: Promise<ModulePermissionRow[]>;
|
|
45
|
+
} | {
|
|
46
|
+
permissions: ModulePermissionRow[];
|
|
47
|
+
/** True zolang we niets weten; callers behandelen dit als allow. */
|
|
48
|
+
isUnknown: boolean;
|
|
49
|
+
data: ModulePermissionRow[];
|
|
50
|
+
error: null;
|
|
51
|
+
isError: false;
|
|
52
|
+
isPending: false;
|
|
53
|
+
isLoading: false;
|
|
54
|
+
isLoadingError: false;
|
|
55
|
+
isRefetchError: false;
|
|
56
|
+
isSuccess: true;
|
|
57
|
+
isPlaceholderData: false;
|
|
58
|
+
status: "success";
|
|
59
|
+
dataUpdatedAt: number;
|
|
60
|
+
errorUpdatedAt: number;
|
|
61
|
+
failureCount: number;
|
|
62
|
+
failureReason: Error | null;
|
|
63
|
+
errorUpdateCount: number;
|
|
64
|
+
isFetched: boolean;
|
|
65
|
+
isFetchedAfterMount: boolean;
|
|
66
|
+
isFetching: boolean;
|
|
67
|
+
isInitialLoading: boolean;
|
|
68
|
+
isPaused: boolean;
|
|
69
|
+
isRefetching: boolean;
|
|
70
|
+
isStale: boolean;
|
|
71
|
+
isEnabled: boolean;
|
|
72
|
+
refetch: (options?: import("@tanstack/query-core").RefetchOptions) => Promise<import("@tanstack/query-core").QueryObserverResult<ModulePermissionRow[], Error>>;
|
|
73
|
+
fetchStatus: import("@tanstack/query-core").FetchStatus;
|
|
74
|
+
promise: Promise<ModulePermissionRow[]>;
|
|
75
|
+
} | {
|
|
76
|
+
permissions: ModulePermissionRow[];
|
|
77
|
+
/** True zolang we niets weten; callers behandelen dit als allow. */
|
|
78
|
+
isUnknown: boolean;
|
|
79
|
+
data: undefined;
|
|
80
|
+
error: Error;
|
|
81
|
+
isError: true;
|
|
82
|
+
isPending: false;
|
|
83
|
+
isLoading: false;
|
|
84
|
+
isLoadingError: true;
|
|
85
|
+
isRefetchError: false;
|
|
86
|
+
isSuccess: false;
|
|
87
|
+
isPlaceholderData: false;
|
|
88
|
+
status: "error";
|
|
89
|
+
dataUpdatedAt: number;
|
|
90
|
+
errorUpdatedAt: number;
|
|
91
|
+
failureCount: number;
|
|
92
|
+
failureReason: Error | null;
|
|
93
|
+
errorUpdateCount: number;
|
|
94
|
+
isFetched: boolean;
|
|
95
|
+
isFetchedAfterMount: boolean;
|
|
96
|
+
isFetching: boolean;
|
|
97
|
+
isInitialLoading: boolean;
|
|
98
|
+
isPaused: boolean;
|
|
99
|
+
isRefetching: boolean;
|
|
100
|
+
isStale: boolean;
|
|
101
|
+
isEnabled: boolean;
|
|
102
|
+
refetch: (options?: import("@tanstack/query-core").RefetchOptions) => Promise<import("@tanstack/query-core").QueryObserverResult<ModulePermissionRow[], Error>>;
|
|
103
|
+
fetchStatus: import("@tanstack/query-core").FetchStatus;
|
|
104
|
+
promise: Promise<ModulePermissionRow[]>;
|
|
105
|
+
} | {
|
|
106
|
+
permissions: ModulePermissionRow[];
|
|
107
|
+
/** True zolang we niets weten; callers behandelen dit als allow. */
|
|
108
|
+
isUnknown: boolean;
|
|
109
|
+
data: undefined;
|
|
110
|
+
error: null;
|
|
111
|
+
isError: false;
|
|
112
|
+
isPending: true;
|
|
113
|
+
isLoading: true;
|
|
114
|
+
isLoadingError: false;
|
|
115
|
+
isRefetchError: false;
|
|
116
|
+
isSuccess: false;
|
|
117
|
+
isPlaceholderData: false;
|
|
118
|
+
status: "pending";
|
|
119
|
+
dataUpdatedAt: number;
|
|
120
|
+
errorUpdatedAt: number;
|
|
121
|
+
failureCount: number;
|
|
122
|
+
failureReason: Error | null;
|
|
123
|
+
errorUpdateCount: number;
|
|
124
|
+
isFetched: boolean;
|
|
125
|
+
isFetchedAfterMount: boolean;
|
|
126
|
+
isFetching: boolean;
|
|
127
|
+
isInitialLoading: boolean;
|
|
128
|
+
isPaused: boolean;
|
|
129
|
+
isRefetching: boolean;
|
|
130
|
+
isStale: boolean;
|
|
131
|
+
isEnabled: boolean;
|
|
132
|
+
refetch: (options?: import("@tanstack/query-core").RefetchOptions) => Promise<import("@tanstack/query-core").QueryObserverResult<ModulePermissionRow[], Error>>;
|
|
133
|
+
fetchStatus: import("@tanstack/query-core").FetchStatus;
|
|
134
|
+
promise: Promise<ModulePermissionRow[]>;
|
|
135
|
+
} | {
|
|
136
|
+
permissions: ModulePermissionRow[];
|
|
137
|
+
/** True zolang we niets weten; callers behandelen dit als allow. */
|
|
138
|
+
isUnknown: boolean;
|
|
139
|
+
data: undefined;
|
|
140
|
+
error: null;
|
|
141
|
+
isError: false;
|
|
142
|
+
isPending: true;
|
|
143
|
+
isLoadingError: false;
|
|
144
|
+
isRefetchError: false;
|
|
145
|
+
isSuccess: false;
|
|
146
|
+
isPlaceholderData: false;
|
|
147
|
+
status: "pending";
|
|
148
|
+
dataUpdatedAt: number;
|
|
149
|
+
errorUpdatedAt: number;
|
|
150
|
+
failureCount: number;
|
|
151
|
+
failureReason: Error | null;
|
|
152
|
+
errorUpdateCount: number;
|
|
153
|
+
isFetched: boolean;
|
|
154
|
+
isFetchedAfterMount: boolean;
|
|
155
|
+
isFetching: boolean;
|
|
156
|
+
isLoading: boolean;
|
|
157
|
+
isInitialLoading: boolean;
|
|
158
|
+
isPaused: boolean;
|
|
159
|
+
isRefetching: boolean;
|
|
160
|
+
isStale: boolean;
|
|
161
|
+
isEnabled: boolean;
|
|
162
|
+
refetch: (options?: import("@tanstack/query-core").RefetchOptions) => Promise<import("@tanstack/query-core").QueryObserverResult<ModulePermissionRow[], Error>>;
|
|
163
|
+
fetchStatus: import("@tanstack/query-core").FetchStatus;
|
|
164
|
+
promise: Promise<ModulePermissionRow[]>;
|
|
165
|
+
} | {
|
|
166
|
+
permissions: ModulePermissionRow[];
|
|
167
|
+
/** True zolang we niets weten; callers behandelen dit als allow. */
|
|
168
|
+
isUnknown: boolean;
|
|
169
|
+
data: ModulePermissionRow[];
|
|
170
|
+
isError: false;
|
|
171
|
+
error: null;
|
|
172
|
+
isPending: false;
|
|
173
|
+
isLoading: false;
|
|
174
|
+
isLoadingError: false;
|
|
175
|
+
isRefetchError: false;
|
|
176
|
+
isSuccess: true;
|
|
177
|
+
isPlaceholderData: true;
|
|
178
|
+
status: "success";
|
|
179
|
+
dataUpdatedAt: number;
|
|
180
|
+
errorUpdatedAt: number;
|
|
181
|
+
failureCount: number;
|
|
182
|
+
failureReason: Error | null;
|
|
183
|
+
errorUpdateCount: number;
|
|
184
|
+
isFetched: boolean;
|
|
185
|
+
isFetchedAfterMount: boolean;
|
|
186
|
+
isFetching: boolean;
|
|
187
|
+
isInitialLoading: boolean;
|
|
188
|
+
isPaused: boolean;
|
|
189
|
+
isRefetching: boolean;
|
|
190
|
+
isStale: boolean;
|
|
191
|
+
isEnabled: boolean;
|
|
192
|
+
refetch: (options?: import("@tanstack/query-core").RefetchOptions) => Promise<import("@tanstack/query-core").QueryObserverResult<ModulePermissionRow[], Error>>;
|
|
193
|
+
fetchStatus: import("@tanstack/query-core").FetchStatus;
|
|
194
|
+
promise: Promise<ModulePermissionRow[]>;
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Rechtenniveau voor één module (optioneel één feature). Tijdens laden en bij
|
|
198
|
+
* elke fout is het antwoord `edit` (fail-open).
|
|
199
|
+
*/
|
|
200
|
+
export declare function useModulePermission(moduleId: string, featureId?: string | null): {
|
|
201
|
+
level: PermissionLevel;
|
|
202
|
+
canView: boolean;
|
|
203
|
+
canEdit: boolean;
|
|
204
|
+
isLoading: boolean;
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
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.
|
|
215
|
+
*/
|
|
216
|
+
export declare function useModulePermissionTransform(): ModuleListTransform;
|
|
217
|
+
//# sourceMappingURL=useModulePermission.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,137 @@
|
|
|
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";
|
|
11
|
+
}
|
|
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
|
+
});
|
|
25
|
+
}
|
|
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"));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Leest de rechten van de ingelogde gebruiker via RPC `my_module_permissions()`.
|
|
41
|
+
*
|
|
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.
|
|
45
|
+
*/
|
|
46
|
+
export function useMyModulePermissions() {
|
|
47
|
+
const query = useQuery({
|
|
48
|
+
queryKey: ["my-module-permissions"],
|
|
49
|
+
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);
|
|
58
|
+
},
|
|
59
|
+
// GEEN retry: een ontbrekende RPC mag nooit een retry-storm veroorzaken.
|
|
60
|
+
retry: false,
|
|
61
|
+
refetchOnWindowFocus: false,
|
|
62
|
+
staleTime: 5 * 60 * 1000,
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
...query,
|
|
66
|
+
permissions: query.data ?? [],
|
|
67
|
+
/** True zolang we niets weten; callers behandelen dit als allow. */
|
|
68
|
+
isUnknown: query.isLoading || query.data === undefined,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
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
|
+
/**
|
|
93
|
+
* Rechtenniveau voor één module (optioneel één feature). Tijdens laden en bij
|
|
94
|
+
* elke fout is het antwoord `edit` (fail-open).
|
|
95
|
+
*/
|
|
96
|
+
export function useModulePermission(moduleId, featureId) {
|
|
97
|
+
const { permissions, isUnknown } = useMyModulePermissions();
|
|
98
|
+
const level = isUnknown ? "edit" : resolveLevel(permissions, moduleId, featureId);
|
|
99
|
+
return {
|
|
100
|
+
level,
|
|
101
|
+
canView: level === "view" || level === "edit",
|
|
102
|
+
canEdit: level === "edit",
|
|
103
|
+
isLoading: isUnknown,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
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.
|
|
115
|
+
*/
|
|
116
|
+
export function useModulePermissionTransform() {
|
|
117
|
+
const { permissions, isUnknown } = useMyModulePermissions();
|
|
118
|
+
return useCallback((modules) => {
|
|
119
|
+
if (isUnknown || permissions.length === 0)
|
|
120
|
+
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]);
|
|
137
|
+
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
export { getProviderToken } from "./provider-token.js";
|
|
1
|
+
export { getProviderToken, listProviderAccounts, revokeProviderAccount } from "./provider-token.js";
|
|
2
|
+
export type { ProviderAccount } from "./provider-token.js";
|
|
2
3
|
export { CentralAuthConfigError, ProviderNotConnectedError, ProviderUpstreamError, CentralAuthUnavailableError, AmbiguousAccountError, } from "./provider-token.js";
|
|
4
|
+
export { requireModulePermission } from "./permissions.server.js";
|
|
5
|
+
export type { PermissionLevel } from "./permissions.server.js";
|
|
3
6
|
//# 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,MAAM,qBAAqB,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,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"}
|
package/dist/server/index.js
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
export { getProviderToken } from "./provider-token.js";
|
|
1
|
+
export { getProviderToken, listProviderAccounts, revokeProviderAccount } from "./provider-token.js";
|
|
2
2
|
export { CentralAuthConfigError, ProviderNotConnectedError, ProviderUpstreamError, CentralAuthUnavailableError, AmbiguousAccountError, } from "./provider-token.js";
|
|
3
|
+
// Server-only rechtencheck. Uitsluitend via het "./server" subpath, nooit via
|
|
4
|
+
// de hoofdbarrel.
|
|
5
|
+
export { requireModulePermission } from "./permissions.server.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
export type PermissionLevel = "none" | "view" | "edit";
|
|
3
|
+
/**
|
|
4
|
+
* Server-side rechtencheck via RPC `effective_module_permission()`.
|
|
5
|
+
*
|
|
6
|
+
* Bedoeld om te combineren met `requireSupabaseAuth`: geef de authenticated
|
|
7
|
+
* client uit `context.supabase` mee.
|
|
8
|
+
*
|
|
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.
|
|
14
|
+
*/
|
|
15
|
+
export declare function requireModulePermission(supabase: SupabaseClient<any, any, any>, moduleId: string, featureId?: string | null, level?: PermissionLevel): Promise<PermissionLevel>;
|
|
16
|
+
//# sourceMappingURL=permissions.server.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const RANK = { none: 0, view: 1, edit: 2 };
|
|
2
|
+
function isMissingRpc(error) {
|
|
3
|
+
const e = (error ?? {});
|
|
4
|
+
const code = e.code ?? "";
|
|
5
|
+
const message = (e.message ?? "").toLowerCase();
|
|
6
|
+
return (code === "42883" ||
|
|
7
|
+
code === "PGRST202" ||
|
|
8
|
+
code === "PGRST302" ||
|
|
9
|
+
e.status === 404 ||
|
|
10
|
+
message.includes("does not exist") ||
|
|
11
|
+
message.includes("could not find the function"));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Server-side rechtencheck via RPC `effective_module_permission()`.
|
|
15
|
+
*
|
|
16
|
+
* Bedoeld om te combineren met `requireSupabaseAuth`: geef de authenticated
|
|
17
|
+
* client uit `context.supabase` mee.
|
|
18
|
+
*
|
|
19
|
+
* - Ontbreekt de RPC (rechten-add-on niet geïnstalleerd) → toegestaan
|
|
20
|
+
* (default-allow), zodat bestaande shells blijven werken.
|
|
21
|
+
* - Onvoldoende rechten → gooit een `Response` met status 403.
|
|
22
|
+
*
|
|
23
|
+
* Core raakt géén enkele tabel uit het rechtenschema; alleen deze RPC.
|
|
24
|
+
*/
|
|
25
|
+
export async function requireModulePermission(supabase, moduleId, featureId = null, level = "view") {
|
|
26
|
+
const { data, error } = await supabase.rpc("effective_module_permission", {
|
|
27
|
+
_module_id: moduleId,
|
|
28
|
+
_feature_id: featureId,
|
|
29
|
+
});
|
|
30
|
+
if (error) {
|
|
31
|
+
if (isMissingRpc(error))
|
|
32
|
+
return "edit";
|
|
33
|
+
throw new Response("Rechtencheck mislukt", { status: 500 });
|
|
34
|
+
}
|
|
35
|
+
const raw = typeof data === "string" ? data.toLowerCase() : "";
|
|
36
|
+
const effective = raw === "none" || raw === "view" || raw === "edit" ? raw : "edit";
|
|
37
|
+
if (RANK[effective] < RANK[level]) {
|
|
38
|
+
throw new Response("Forbidden: geen rechten voor deze module", { status: 403 });
|
|
39
|
+
}
|
|
40
|
+
return effective;
|
|
41
|
+
}
|
|
@@ -13,6 +13,13 @@ export declare class CentralAuthUnavailableError extends Error {
|
|
|
13
13
|
export declare class AmbiguousAccountError extends Error {
|
|
14
14
|
constructor(message: string);
|
|
15
15
|
}
|
|
16
|
+
export interface ProviderAccount {
|
|
17
|
+
accountKey: string;
|
|
18
|
+
environment: "staging" | "live";
|
|
19
|
+
status: string;
|
|
20
|
+
createdAt: Date;
|
|
21
|
+
updatedAt: Date;
|
|
22
|
+
}
|
|
16
23
|
export declare function getProviderToken(provider: string, opts?: {
|
|
17
24
|
environment?: "staging" | "live";
|
|
18
25
|
accountKey?: string;
|
|
@@ -20,4 +27,10 @@ export declare function getProviderToken(provider: string, opts?: {
|
|
|
20
27
|
accessToken: string;
|
|
21
28
|
expiresAt: Date;
|
|
22
29
|
}>;
|
|
30
|
+
export declare function listProviderAccounts(provider: string, opts?: {
|
|
31
|
+
environment?: "staging" | "live";
|
|
32
|
+
}): Promise<ProviderAccount[]>;
|
|
33
|
+
export declare function revokeProviderAccount(provider: string, accountKey: string, opts?: {
|
|
34
|
+
environment?: "staging" | "live";
|
|
35
|
+
}): Promise<void>;
|
|
23
36
|
//# sourceMappingURL=provider-token.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider-token.d.ts","sourceRoot":"","sources":["../../src/core/server/provider-token.ts"],"names":[],"mappings":"AAAA,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,2BAA4B,SAAQ,KAAK;gBACxC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;
|
|
1
|
+
{"version":3,"file":"provider-token.d.ts","sourceRoot":"","sources":["../../src/core/server/provider-token.ts"],"names":[],"mappings":"AAAA,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,2BAA4B,SAAQ,KAAK;gBACxC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAOD,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,SAAS,GAAG,MAAM,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB;AA4LD,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/D,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAE,CAAC,CA6BnD;AAED,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,SAAS,GAAG,MAAM,CAAA;CAAE,GAC1C,OAAO,CAAC,eAAe,EAAE,CAAC,CA4C5B;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,SAAS,GAAG,MAAM,CAAA;CAAE,GAC1C,OAAO,CAAC,IAAI,CAAC,CA8Bf"}
|
|
@@ -30,6 +30,10 @@ export class AmbiguousAccountError extends Error {
|
|
|
30
30
|
}
|
|
31
31
|
const TIMEOUT_MS = 10000;
|
|
32
32
|
const EXPIRY_BUFFER_MS = 60000;
|
|
33
|
+
// Module-private by design: getProviderToken and revokeProviderAccount share
|
|
34
|
+
// this Map (revoke invalidates entries it never wrote itself), so it must
|
|
35
|
+
// never be exposed or passed around — the two functions' coupling through it
|
|
36
|
+
// is intentional, but only within this file.
|
|
33
37
|
const cache = new Map();
|
|
34
38
|
function isFresh(token) {
|
|
35
39
|
return token.expiresAt.getTime() - EXPIRY_BUFFER_MS > Date.now();
|
|
@@ -37,55 +41,24 @@ function isFresh(token) {
|
|
|
37
41
|
function logAuthFailure(provider, environment, status) {
|
|
38
42
|
console.error("[central-auth] provider token request failed", { provider, environment, status });
|
|
39
43
|
}
|
|
40
|
-
|
|
44
|
+
function getCentralAuthConfig() {
|
|
41
45
|
const baseUrl = process.env.CENTRAL_AUTH_URL;
|
|
42
46
|
const secret = process.env.CENTRAL_AUTH_SECRET;
|
|
43
47
|
if (!baseUrl || !secret) {
|
|
44
48
|
throw new CentralAuthConfigError("Missing CENTRAL_AUTH_URL or CENTRAL_AUTH_SECRET environment variable.");
|
|
45
49
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
response = await fetch(url, {
|
|
59
|
-
method: "POST",
|
|
60
|
-
headers: {
|
|
61
|
-
Authorization: `Bearer ${secret}`,
|
|
62
|
-
"Content-Type": "application/json",
|
|
63
|
-
},
|
|
64
|
-
body: JSON.stringify(requestBody),
|
|
65
|
-
signal: controller.signal,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
logAuthFailure(provider, environment, timedOut ? "timeout" : "network");
|
|
70
|
-
throw new CentralAuthUnavailableError(`Central auth request failed (provider=${provider}, environment=${environment}, reason=${timedOut ? "timeout" : "network"}).`);
|
|
71
|
-
}
|
|
72
|
-
finally {
|
|
73
|
-
clearTimeout(timeout);
|
|
74
|
-
}
|
|
75
|
-
if (response.ok) {
|
|
76
|
-
let body;
|
|
77
|
-
try {
|
|
78
|
-
body = (await response.json());
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
logAuthFailure(provider, environment, response.status);
|
|
82
|
-
throw new CentralAuthUnavailableError(`Central auth returned an unparseable response (provider=${provider}, environment=${environment}, status=${response.status}).`);
|
|
83
|
-
}
|
|
84
|
-
return {
|
|
85
|
-
accessToken: body.access_token,
|
|
86
|
-
expiresAt: new Date(body.expires_at),
|
|
87
|
-
};
|
|
88
|
-
}
|
|
50
|
+
return { baseUrl, secret };
|
|
51
|
+
}
|
|
52
|
+
// segment is always one of the three literals above, never caller input —
|
|
53
|
+
// the URL path must never be built from anything a caller supplies.
|
|
54
|
+
function buildCentralUrl(baseUrl, segment, provider, query) {
|
|
55
|
+
const base = `${baseUrl.replace(/\/+$/, "")}/api/${segment}/${encodeURIComponent(provider)}`;
|
|
56
|
+
if (!query)
|
|
57
|
+
return base;
|
|
58
|
+
const qs = new URLSearchParams(query).toString();
|
|
59
|
+
return qs ? `${base}?${qs}` : base;
|
|
60
|
+
}
|
|
61
|
+
async function mapCentralError(response, provider, environment, allowAmbiguousAccount) {
|
|
89
62
|
let errorCode;
|
|
90
63
|
try {
|
|
91
64
|
const text = await response.text();
|
|
@@ -98,6 +71,14 @@ async function fetchProviderToken(provider, environment, accountKey) {
|
|
|
98
71
|
}
|
|
99
72
|
logAuthFailure(provider, environment, response.status);
|
|
100
73
|
if (response.status === 400 && errorCode === "ambiguous_account") {
|
|
74
|
+
if (!allowAmbiguousAccount) {
|
|
75
|
+
// Only /api/token/ can legitimately return ambiguous_account. Any other
|
|
76
|
+
// endpoint returning it is an unexpected/malformed response from
|
|
77
|
+
// central, not a real ambiguity the caller can resolve — surfacing it
|
|
78
|
+
// as AmbiguousAccountError here would hand the caller a message about
|
|
79
|
+
// "multiple accounts" for an endpoint that never asked to disambiguate.
|
|
80
|
+
throw new CentralAuthUnavailableError(`Central auth returned an unexpected ambiguous_account error (provider=${provider}, environment=${environment}, status=400).`);
|
|
81
|
+
}
|
|
101
82
|
throw new AmbiguousAccountError(`Central auth reports multiple connected accounts for provider "${provider}"; accountKey is required to disambiguate (provider=${provider}, environment=${environment}).`);
|
|
102
83
|
}
|
|
103
84
|
if (response.status === 401 || errorCode === "unauthorized") {
|
|
@@ -116,6 +97,58 @@ async function fetchProviderToken(provider, environment, accountKey) {
|
|
|
116
97
|
}
|
|
117
98
|
throw new CentralAuthUnavailableError(`Central auth unavailable (provider=${provider}, environment=${environment}, status=${response.status}).`);
|
|
118
99
|
}
|
|
100
|
+
async function requestCentral(segment, provider, environment, init, allowAmbiguousAccount) {
|
|
101
|
+
const { baseUrl, secret } = getCentralAuthConfig();
|
|
102
|
+
const url = buildCentralUrl(baseUrl, segment, provider, init.query);
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
let timedOut = false;
|
|
105
|
+
const timeout = setTimeout(() => {
|
|
106
|
+
timedOut = true;
|
|
107
|
+
controller.abort();
|
|
108
|
+
}, TIMEOUT_MS);
|
|
109
|
+
let response;
|
|
110
|
+
try {
|
|
111
|
+
response = await fetch(url, {
|
|
112
|
+
method: init.method,
|
|
113
|
+
headers: {
|
|
114
|
+
Authorization: `Bearer ${secret}`,
|
|
115
|
+
...(init.body !== undefined ? { "Content-Type": "application/json" } : {}),
|
|
116
|
+
},
|
|
117
|
+
...(init.body !== undefined ? { body: JSON.stringify(init.body) } : {}),
|
|
118
|
+
signal: controller.signal,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
logAuthFailure(provider, environment, timedOut ? "timeout" : "network");
|
|
123
|
+
throw new CentralAuthUnavailableError(`Central auth request failed (provider=${provider}, environment=${environment}, reason=${timedOut ? "timeout" : "network"}).`);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
clearTimeout(timeout);
|
|
127
|
+
}
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
await mapCentralError(response, provider, environment, allowAmbiguousAccount);
|
|
130
|
+
}
|
|
131
|
+
return response;
|
|
132
|
+
}
|
|
133
|
+
async function fetchProviderToken(provider, environment, accountKey) {
|
|
134
|
+
const requestBody = { environment };
|
|
135
|
+
if (accountKey !== undefined)
|
|
136
|
+
requestBody.account_key = accountKey;
|
|
137
|
+
const response = await requestCentral("token", provider, environment, { method: "POST", body: requestBody },
|
|
138
|
+
/* allowAmbiguousAccount */ true);
|
|
139
|
+
let body;
|
|
140
|
+
try {
|
|
141
|
+
body = (await response.json());
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
logAuthFailure(provider, environment, response.status);
|
|
145
|
+
throw new CentralAuthUnavailableError(`Central auth returned an unparseable response (provider=${provider}, environment=${environment}, status=${response.status}).`);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
accessToken: body.access_token,
|
|
149
|
+
expiresAt: new Date(body.expires_at),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
119
152
|
export async function getProviderToken(provider, opts) {
|
|
120
153
|
const environment = opts?.environment ?? "live";
|
|
121
154
|
// Moet central's normalisatie in oauth-authcode.server.ts spiegelen (trim + toLowerCase).
|
|
@@ -146,3 +179,55 @@ export async function getProviderToken(provider, opts) {
|
|
|
146
179
|
});
|
|
147
180
|
return promise;
|
|
148
181
|
}
|
|
182
|
+
export async function listProviderAccounts(provider, opts) {
|
|
183
|
+
// Filter: only sent to central when the caller explicitly asked for one
|
|
184
|
+
// environment. Omitting it must return accounts across all environments —
|
|
185
|
+
// defaulting it here would silently narrow every unfiltered call to "live".
|
|
186
|
+
const environmentFilter = opts?.environment;
|
|
187
|
+
// Context for error messages/logging only, never the query filter above.
|
|
188
|
+
// Defaults to "live" purely so log lines read "environment=live" instead of
|
|
189
|
+
// "environment=" when no filter was requested.
|
|
190
|
+
const logEnvironment = environmentFilter ?? "live";
|
|
191
|
+
const response = await requestCentral("accounts", provider, logEnvironment, { method: "GET", query: environmentFilter ? { environment: environmentFilter } : undefined },
|
|
192
|
+
/* allowAmbiguousAccount */ false);
|
|
193
|
+
let body;
|
|
194
|
+
try {
|
|
195
|
+
body = (await response.json());
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
logAuthFailure(provider, logEnvironment, response.status);
|
|
199
|
+
throw new CentralAuthUnavailableError(`Central auth returned an unparseable response (provider=${provider}, environment=${logEnvironment}, status=${response.status}).`);
|
|
200
|
+
}
|
|
201
|
+
// "revoked" rows are intentionally kept — the caller decides what to do with them.
|
|
202
|
+
return body.accounts.map((account) => ({
|
|
203
|
+
accountKey: account.account_key,
|
|
204
|
+
environment: account.environment,
|
|
205
|
+
status: account.status,
|
|
206
|
+
createdAt: new Date(account.created_at),
|
|
207
|
+
updatedAt: new Date(account.updated_at),
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
export async function revokeProviderAccount(provider, accountKey, opts) {
|
|
211
|
+
const environment = opts?.environment ?? "live";
|
|
212
|
+
// Same normalisation as getProviderToken — a caller revoking "INFO@X.nl"
|
|
213
|
+
// must invalidate the exact cache entries getProviderToken would have used.
|
|
214
|
+
const normalizedAccountKey = accountKey.trim().toLowerCase();
|
|
215
|
+
if (!normalizedAccountKey) {
|
|
216
|
+
throw new CentralAuthConfigError("revokeProviderAccount requires a non-empty accountKey.");
|
|
217
|
+
}
|
|
218
|
+
await requestCentral("revoke", provider, environment, { method: "POST", body: { account_key: normalizedAccountKey, environment } },
|
|
219
|
+
/* allowAmbiguousAccount */ false);
|
|
220
|
+
// Cache-invalidatie na een geslaagde revoke: verwijder ALLE entries met
|
|
221
|
+
// prefix `${provider}:${environment}:`, niet alleen die van dit accountKey.
|
|
222
|
+
// Reden: de entry zonder accountKey (`${provider}:${environment}:`)
|
|
223
|
+
// resolveert naar "de enige actieve mailbox" — revoke je die mailbox, dan
|
|
224
|
+
// blijft een geldig token voor een ingetrokken account tot een uur in de
|
|
225
|
+
// cache staan. Revoke verandert bovendien de ambiguity-resolutie voor
|
|
226
|
+
// toekomstige aanroepen zonder accountKey. Alleen uitvoeren als de
|
|
227
|
+
// revoke-call zelf slaagde (requestCentral gooit hierboven bij een fout).
|
|
228
|
+
const prefix = `${provider}:${environment}:`;
|
|
229
|
+
for (const key of cache.keys()) {
|
|
230
|
+
if (key.startsWith(prefix))
|
|
231
|
+
cache.delete(key);
|
|
232
|
+
}
|
|
233
|
+
}
|
package/dist/types/module.d.ts
CHANGED
|
@@ -16,5 +16,28 @@ export interface FlowModule {
|
|
|
16
16
|
version: string;
|
|
17
17
|
nav: NavItem;
|
|
18
18
|
settingsCards: SettingsCard[];
|
|
19
|
+
/**
|
|
20
|
+
* Optionele expliciete sleutel waarmee deze module extern (module_registry,
|
|
21
|
+
* rechten) wordt geïdentificeerd. Zet deze wanneer de afgeleide sleutel niet
|
|
22
|
+
* gelijk is aan de registry-slug.
|
|
23
|
+
*/
|
|
24
|
+
registryKey?: string;
|
|
19
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* De externe modulesleutel.
|
|
28
|
+
*
|
|
29
|
+
* Regel (generiek, core kent geen enkele module- of domeinnaam):
|
|
30
|
+
* 1. `registryKey` als die is opgegeven;
|
|
31
|
+
* 2. anders het eerste padsegment van `nav.href`;
|
|
32
|
+
* 3. afsluitende underscores worden gestript.
|
|
33
|
+
*
|
|
34
|
+
* Waarom (3): een afsluitende underscore in een routepad is een TanStack
|
|
35
|
+
* un-nesting-conventie (`/voorraad_/detail`), geen betekenisvol segment. Zonder
|
|
36
|
+
* strippen levert dezelfde module twee sleutels op (`voorraad` en `voorraad_`)
|
|
37
|
+
* en verdwijnt hij half uit de zijbalk. Strippen is gekozen boven "registryKey
|
|
38
|
+
* verplicht maken", omdat dat laatste bestaande modules stil zou breken.
|
|
39
|
+
*/
|
|
40
|
+
export declare function moduleKey(mod: FlowModule): string;
|
|
41
|
+
/** Zelfde afleidingsregel, maar voor een los pad (nav-children, URL-pathname). */
|
|
42
|
+
export declare function navKey(href: string): string;
|
|
20
43
|
//# sourceMappingURL=module.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../../src/core/types/module.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,CAAC;CACxC;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,aAAa,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,OAAO,CAAC;IACb,aAAa,EAAE,YAAY,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../../src/core/types/module.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,CAAC;CACxC;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,aAAa,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,OAAO,CAAC;IACb,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAGjD;AAED,kFAAkF;AAClF,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG3C"}
|
package/dist/types/module.js
CHANGED
|
@@ -1 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* De externe modulesleutel.
|
|
3
|
+
*
|
|
4
|
+
* Regel (generiek, core kent geen enkele module- of domeinnaam):
|
|
5
|
+
* 1. `registryKey` als die is opgegeven;
|
|
6
|
+
* 2. anders het eerste padsegment van `nav.href`;
|
|
7
|
+
* 3. afsluitende underscores worden gestript.
|
|
8
|
+
*
|
|
9
|
+
* Waarom (3): een afsluitende underscore in een routepad is een TanStack
|
|
10
|
+
* un-nesting-conventie (`/voorraad_/detail`), geen betekenisvol segment. Zonder
|
|
11
|
+
* strippen levert dezelfde module twee sleutels op (`voorraad` en `voorraad_`)
|
|
12
|
+
* en verdwijnt hij half uit de zijbalk. Strippen is gekozen boven "registryKey
|
|
13
|
+
* verplicht maken", omdat dat laatste bestaande modules stil zou breken.
|
|
14
|
+
*/
|
|
15
|
+
export function moduleKey(mod) {
|
|
16
|
+
if (mod.registryKey)
|
|
17
|
+
return mod.registryKey;
|
|
18
|
+
return navKey(mod.nav.href);
|
|
19
|
+
}
|
|
20
|
+
/** Zelfde afleidingsregel, maar voor een los pad (nav-children, URL-pathname). */
|
|
21
|
+
export function navKey(href) {
|
|
22
|
+
const segment = href.split("?")[0].split("#")[0].split("/").filter(Boolean)[0] ?? "";
|
|
23
|
+
return segment.replace(/_+$/, "");
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowselections/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.18",
|
|
4
4
|
"private": false,
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"type": "module",
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
"default": "./dist/server/index.js"
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
|
-
"files": [
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
20
22
|
"scripts": {
|
|
21
23
|
"dev": "vite dev",
|
|
22
24
|
"build": "vite build",
|
|
@@ -70,6 +72,7 @@
|
|
|
70
72
|
"embla-carousel-react": "^8.6.0",
|
|
71
73
|
"input-otp": "^1.4.2",
|
|
72
74
|
"lucide-react": "^0.575.0",
|
|
75
|
+
"nitro": "3.0.260603-beta",
|
|
73
76
|
"react": "^19.2.0",
|
|
74
77
|
"react-barcode": "^1.6.1",
|
|
75
78
|
"react-day-picker": "^9.14.0",
|
|
@@ -88,7 +91,7 @@
|
|
|
88
91
|
},
|
|
89
92
|
"devDependencies": {
|
|
90
93
|
"@eslint/js": "^9.32.0",
|
|
91
|
-
"@lovable.dev/vite-tanstack-config": "
|
|
94
|
+
"@lovable.dev/vite-tanstack-config": "2.13.1",
|
|
92
95
|
"@types/node": "^22.16.5",
|
|
93
96
|
"@types/react": "^19.2.0",
|
|
94
97
|
"@types/react-dom": "^19.2.0",
|