@flowselections/core 1.0.21 → 1.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -0
- package/dist/auth/redirect.d.ts +52 -0
- package/dist/auth/redirect.d.ts.map +1 -0
- package/dist/auth/redirect.js +98 -0
- package/dist/auth/useAuth.d.ts +1 -1
- package/dist/auth/useAuth.d.ts.map +1 -1
- package/dist/auth/useIsSuperadmin.d.ts +19 -0
- package/dist/auth/useIsSuperadmin.d.ts.map +1 -0
- package/dist/auth/useIsSuperadmin.js +16 -0
- package/dist/components/layout/AuthenticatedLayout.d.ts +29 -0
- package/dist/components/layout/AuthenticatedLayout.d.ts.map +1 -0
- package/dist/components/layout/AuthenticatedLayout.js +49 -0
- package/dist/components/layout/HandoverPage.d.ts.map +1 -1
- package/dist/components/layout/HandoverPage.js +4 -9
- package/dist/components/layout/Header.js +1 -1
- package/dist/components/layout/InstellingenPage.d.ts.map +1 -1
- package/dist/components/layout/InstellingenPage.js +10 -2
- package/dist/components/layout/LoginPage.d.ts.map +1 -1
- package/dist/components/layout/LoginPage.js +6 -6
- package/dist/components/layout/ResetWachtwoordPage.d.ts +3 -1
- package/dist/components/layout/ResetWachtwoordPage.d.ts.map +1 -1
- package/dist/components/layout/ResetWachtwoordPage.js +5 -3
- package/dist/components/layout/Sidebar.d.ts +37 -3
- package/dist/components/layout/Sidebar.d.ts.map +1 -1
- package/dist/components/layout/Sidebar.js +52 -13
- package/dist/extensions/ShellExtensions.d.ts +14 -1
- package/dist/extensions/ShellExtensions.d.ts.map +1 -1
- package/dist/extensions/ShellExtensions.js +21 -2
- package/dist/hooks/useLogoSrc.d.ts +7 -0
- package/dist/hooks/useLogoSrc.d.ts.map +1 -0
- package/dist/hooks/useLogoSrc.js +29 -0
- package/dist/index.d.ts +20 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -3
- package/dist/permissions/resolve.d.ts +22 -0
- package/dist/permissions/resolve.d.ts.map +1 -0
- package/dist/permissions/resolve.js +76 -0
- package/dist/permissions/store.d.ts +38 -0
- package/dist/permissions/store.d.ts.map +1 -0
- package/dist/permissions/store.js +180 -0
- package/dist/permissions/useModulePermission.d.ts +52 -32
- package/dist/permissions/useModulePermission.d.ts.map +1 -1
- package/dist/permissions/useModulePermission.js +72 -104
- package/dist/routes/index.d.ts +48 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +63 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/permissions.server.d.ts +21 -8
- package/dist/server/permissions.server.d.ts.map +1 -1
- package/dist/server/permissions.server.js +54 -9
- package/dist/shell/ShellContext.d.ts +26 -0
- package/dist/shell/ShellContext.d.ts.map +1 -0
- package/dist/shell/ShellContext.js +14 -0
- package/dist/styles.css +127 -0
- package/dist/types/module.d.ts +39 -4
- package/dist/types/module.d.ts.map +1 -1
- package/dist/types/module.js +33 -5
- package/package.json +115 -107
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gedeelde rechten- en rollencache zonder React Query (sinds 1.0.23).
|
|
3
|
+
*
|
|
4
|
+
* De automatische paden (zijbalkfilter, route-guard, useIsSuperadmin) lezen
|
|
5
|
+
* hier via useSyncExternalStore. Werkt dus ook in shells zonder
|
|
6
|
+
* QueryClientProvider. Zichtbaarheid, geen beveiliging.
|
|
7
|
+
*/
|
|
8
|
+
import { useEffect, useSyncExternalStore } from "react";
|
|
9
|
+
import { supabase } from "../supabase/client.js";
|
|
10
|
+
import { normalizeRows } from "./resolve.js";
|
|
11
|
+
export const PERMISSION_TIMEOUT_MS = 3000;
|
|
12
|
+
const STALE_MS = 5 * 60 * 1000;
|
|
13
|
+
const defaultFetcher = async () => {
|
|
14
|
+
const { data, error } = await supabase.rpc("my_module_permissions");
|
|
15
|
+
if (error)
|
|
16
|
+
throw error;
|
|
17
|
+
return data;
|
|
18
|
+
};
|
|
19
|
+
const defaultRolesFetcher = async (userId) => {
|
|
20
|
+
const { data, error } = await supabase.from("user_roles").select("role").eq("user_id", userId);
|
|
21
|
+
if (error || !Array.isArray(data))
|
|
22
|
+
return [];
|
|
23
|
+
return data.map((r) => String(r.role));
|
|
24
|
+
};
|
|
25
|
+
let fetcher = defaultFetcher;
|
|
26
|
+
let rolesFetcher = defaultRolesFetcher;
|
|
27
|
+
let snapshot = { userId: null, status: "loading", rows: [], roles: [], rolesLoaded: false };
|
|
28
|
+
let fetchedAt = 0;
|
|
29
|
+
let inflight = null;
|
|
30
|
+
let inflightUser;
|
|
31
|
+
const listeners = new Set();
|
|
32
|
+
function set(next) {
|
|
33
|
+
snapshot = { ...snapshot, ...next };
|
|
34
|
+
listeners.forEach((l) => l());
|
|
35
|
+
}
|
|
36
|
+
export function subscribePermissions(listener) {
|
|
37
|
+
listeners.add(listener);
|
|
38
|
+
return () => listeners.delete(listener);
|
|
39
|
+
}
|
|
40
|
+
export function getPermissionSnapshot() {
|
|
41
|
+
return snapshot;
|
|
42
|
+
}
|
|
43
|
+
/** Rollen uit een JWT (claim `user_roles`). Geen verificatie: alleen UI. */
|
|
44
|
+
export function rolesFromAccessToken(token) {
|
|
45
|
+
if (!token)
|
|
46
|
+
return null;
|
|
47
|
+
try {
|
|
48
|
+
const part = token.split(".")[1];
|
|
49
|
+
if (!part)
|
|
50
|
+
return null;
|
|
51
|
+
const json = typeof atob === "function"
|
|
52
|
+
? atob(part.replace(/-/g, "+").replace(/_/g, "/"))
|
|
53
|
+
: Buffer.from(part, "base64").toString("utf8");
|
|
54
|
+
const claims = JSON.parse(json);
|
|
55
|
+
const roles = claims["user_roles"];
|
|
56
|
+
return Array.isArray(roles) ? roles.map(String) : null;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Laadt rechten (en rollen) voor `userId`. Dedupliceert en cachet 5 min.
|
|
64
|
+
* Na 3 s zonder antwoord gaat de status op `timeout` (fail-open); komt het
|
|
65
|
+
* antwoord later alsnog, dan wordt het `ready`.
|
|
66
|
+
*/
|
|
67
|
+
export function loadPermissions(userId, accessToken, force = false) {
|
|
68
|
+
if (!userId) {
|
|
69
|
+
fetchedAt = Date.now();
|
|
70
|
+
set({ userId: null, status: "ready", rows: [], roles: [], rolesLoaded: true });
|
|
71
|
+
return Promise.resolve();
|
|
72
|
+
}
|
|
73
|
+
const fresh = snapshot.userId === userId && snapshot.status === "ready" && Date.now() - fetchedAt < STALE_MS;
|
|
74
|
+
if (!force && fresh)
|
|
75
|
+
return Promise.resolve();
|
|
76
|
+
if (!force && inflight && inflightUser === userId)
|
|
77
|
+
return inflight;
|
|
78
|
+
if (snapshot.userId !== userId) {
|
|
79
|
+
const claimRoles = rolesFromAccessToken(accessToken);
|
|
80
|
+
set({ userId, status: "loading", rows: [], roles: claimRoles ?? [], rolesLoaded: !!claimRoles });
|
|
81
|
+
}
|
|
82
|
+
else if (snapshot.status !== "ready") {
|
|
83
|
+
set({ status: "loading" });
|
|
84
|
+
}
|
|
85
|
+
inflightUser = userId;
|
|
86
|
+
const timer = setTimeout(() => {
|
|
87
|
+
if (snapshot.userId === userId && snapshot.status === "loading")
|
|
88
|
+
set({ status: "timeout" });
|
|
89
|
+
}, PERMISSION_TIMEOUT_MS);
|
|
90
|
+
const p = (async () => {
|
|
91
|
+
const [perm, roles] = await Promise.allSettled([
|
|
92
|
+
fetcher(),
|
|
93
|
+
snapshot.rolesLoaded ? Promise.resolve(snapshot.roles) : rolesFetcher(userId),
|
|
94
|
+
]);
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
if (snapshot.userId !== userId)
|
|
97
|
+
return;
|
|
98
|
+
const roleList = roles.status === "fulfilled" ? roles.value : snapshot.roles;
|
|
99
|
+
if (perm.status === "fulfilled") {
|
|
100
|
+
fetchedAt = Date.now();
|
|
101
|
+
set({ status: "ready", rows: normalizeRows(perm.value), roles: roleList, rolesLoaded: true });
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// Ontbrekende RPC of andere fout: fail-open, geen retry.
|
|
105
|
+
set({ status: "error", rows: [], roles: roleList, rolesLoaded: true });
|
|
106
|
+
}
|
|
107
|
+
})().finally(() => {
|
|
108
|
+
if (inflight === p)
|
|
109
|
+
inflight = null;
|
|
110
|
+
});
|
|
111
|
+
inflight = p;
|
|
112
|
+
return p;
|
|
113
|
+
}
|
|
114
|
+
/** Wacht maximaal `ms` op rechten. Resolvet altijd (fail-open). */
|
|
115
|
+
export function waitForPermissions(userId, accessToken, ms = PERMISSION_TIMEOUT_MS) {
|
|
116
|
+
return new Promise((resolve) => {
|
|
117
|
+
const t = setTimeout(() => resolve(snapshot), ms);
|
|
118
|
+
loadPermissions(userId, accessToken).then(() => {
|
|
119
|
+
clearTimeout(t);
|
|
120
|
+
resolve(snapshot);
|
|
121
|
+
}, () => {
|
|
122
|
+
clearTimeout(t);
|
|
123
|
+
resolve(snapshot);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
let sessionWired = false;
|
|
128
|
+
/** Koppelt de store eenmalig aan de Supabase-sessie (alleen in de browser). */
|
|
129
|
+
export function ensureSessionWiring() {
|
|
130
|
+
if (sessionWired || typeof window === "undefined")
|
|
131
|
+
return;
|
|
132
|
+
sessionWired = true;
|
|
133
|
+
try {
|
|
134
|
+
supabase.auth.getSession().then(({ data }) => {
|
|
135
|
+
loadPermissions(data.session?.user.id ?? null, data.session?.access_token);
|
|
136
|
+
}, () => set({ status: "error" }));
|
|
137
|
+
supabase.auth.onAuthStateChange((event, session) => {
|
|
138
|
+
if (event === "SIGNED_OUT")
|
|
139
|
+
return void loadPermissions(null);
|
|
140
|
+
if (event === "SIGNED_IN" || event === "USER_UPDATED" || event === "TOKEN_REFRESHED") {
|
|
141
|
+
const id = session?.user.id ?? null;
|
|
142
|
+
const claim = rolesFromAccessToken(session?.access_token);
|
|
143
|
+
if (id && id === snapshot.userId && claim)
|
|
144
|
+
set({ roles: claim, rolesLoaded: true });
|
|
145
|
+
void loadPermissions(id, session?.access_token, event === "USER_UPDATED");
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
set({ status: "error", rolesLoaded: true });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** React-hook op de store. Werkt zonder QueryClientProvider. */
|
|
154
|
+
export function usePermissionSnapshot() {
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
ensureSessionWiring();
|
|
157
|
+
}, []);
|
|
158
|
+
// Server-snapshot = huidige snapshot: de _authenticated-tree draait met ssr:false.
|
|
159
|
+
return useSyncExternalStore(subscribePermissions, getPermissionSnapshot, getPermissionSnapshot);
|
|
160
|
+
}
|
|
161
|
+
// ─── Test- en preview-haken (niet in de hoofdbarrel) ──────────────────────────
|
|
162
|
+
export function __setPermissionFetchers(next) {
|
|
163
|
+
if (next.rows)
|
|
164
|
+
fetcher = next.rows;
|
|
165
|
+
if (next.roles)
|
|
166
|
+
rolesFetcher = next.roles;
|
|
167
|
+
}
|
|
168
|
+
export function __resetPermissionStore() {
|
|
169
|
+
fetcher = defaultFetcher;
|
|
170
|
+
rolesFetcher = defaultRolesFetcher;
|
|
171
|
+
snapshot = { userId: null, status: "loading", rows: [], roles: [], rolesLoaded: false };
|
|
172
|
+
fetchedAt = 0;
|
|
173
|
+
inflight = null;
|
|
174
|
+
inflightUser = undefined;
|
|
175
|
+
listeners.forEach((l) => l());
|
|
176
|
+
}
|
|
177
|
+
/** Markeert de wiring als gedaan (tests/preview met een mock). */
|
|
178
|
+
export function __disableSessionWiring() {
|
|
179
|
+
sessionWired = true;
|
|
180
|
+
}
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { FlowModule } from "../types/module.js";
|
|
1
3
|
import type { ModuleListTransform } from "../extensions/ShellExtensions.js";
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
moduleId: string;
|
|
5
|
-
featureId: string | null;
|
|
6
|
-
permission: PermissionLevel;
|
|
7
|
-
}
|
|
4
|
+
import { type ModulePermissionRow, type PermissionLevel } from "./resolve.js";
|
|
5
|
+
export type { PermissionLevel, ModulePermissionRow } from "./resolve.js";
|
|
8
6
|
/**
|
|
9
|
-
*
|
|
7
|
+
* Rechten van de ingelogde gebruiker via RPC `my_module_permissions()`.
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* Draait op de gedeelde store; gebruikt React Query alleen als er een
|
|
10
|
+
* QueryClientProvider is, anders een interne client — crasht nooit zonder
|
|
11
|
+
* provider. Fail-open: fout of ontbrekende RPC → lege lijst → default-allow.
|
|
12
|
+
* Zichtbaarheid, geen beveiliging.
|
|
14
13
|
*/
|
|
15
14
|
export declare function useMyModulePermissions(): {
|
|
15
|
+
data: ModulePermissionRow[] | undefined;
|
|
16
16
|
permissions: ModulePermissionRow[];
|
|
17
17
|
/** True zolang we niets weten; callers behandelen dit als allow. */
|
|
18
18
|
isUnknown: boolean;
|
|
19
|
-
|
|
19
|
+
status: import("./store.js").PermissionStatus;
|
|
20
20
|
error: Error;
|
|
21
21
|
isError: true;
|
|
22
22
|
isPending: false;
|
|
@@ -25,7 +25,6 @@ export declare function useMyModulePermissions(): {
|
|
|
25
25
|
isRefetchError: true;
|
|
26
26
|
isSuccess: false;
|
|
27
27
|
isPlaceholderData: false;
|
|
28
|
-
status: "error";
|
|
29
28
|
dataUpdatedAt: number;
|
|
30
29
|
errorUpdatedAt: number;
|
|
31
30
|
failureCount: number;
|
|
@@ -43,10 +42,11 @@ export declare function useMyModulePermissions(): {
|
|
|
43
42
|
fetchStatus: import("@tanstack/query-core").FetchStatus;
|
|
44
43
|
promise: Promise<ModulePermissionRow[]>;
|
|
45
44
|
} | {
|
|
45
|
+
data: ModulePermissionRow[] | undefined;
|
|
46
46
|
permissions: ModulePermissionRow[];
|
|
47
47
|
/** True zolang we niets weten; callers behandelen dit als allow. */
|
|
48
48
|
isUnknown: boolean;
|
|
49
|
-
|
|
49
|
+
status: import("./store.js").PermissionStatus;
|
|
50
50
|
error: null;
|
|
51
51
|
isError: false;
|
|
52
52
|
isPending: false;
|
|
@@ -55,7 +55,6 @@ export declare function useMyModulePermissions(): {
|
|
|
55
55
|
isRefetchError: false;
|
|
56
56
|
isSuccess: true;
|
|
57
57
|
isPlaceholderData: false;
|
|
58
|
-
status: "success";
|
|
59
58
|
dataUpdatedAt: number;
|
|
60
59
|
errorUpdatedAt: number;
|
|
61
60
|
failureCount: number;
|
|
@@ -73,10 +72,11 @@ export declare function useMyModulePermissions(): {
|
|
|
73
72
|
fetchStatus: import("@tanstack/query-core").FetchStatus;
|
|
74
73
|
promise: Promise<ModulePermissionRow[]>;
|
|
75
74
|
} | {
|
|
75
|
+
data: ModulePermissionRow[] | undefined;
|
|
76
76
|
permissions: ModulePermissionRow[];
|
|
77
77
|
/** True zolang we niets weten; callers behandelen dit als allow. */
|
|
78
78
|
isUnknown: boolean;
|
|
79
|
-
|
|
79
|
+
status: import("./store.js").PermissionStatus;
|
|
80
80
|
error: Error;
|
|
81
81
|
isError: true;
|
|
82
82
|
isPending: false;
|
|
@@ -85,7 +85,6 @@ export declare function useMyModulePermissions(): {
|
|
|
85
85
|
isRefetchError: false;
|
|
86
86
|
isSuccess: false;
|
|
87
87
|
isPlaceholderData: false;
|
|
88
|
-
status: "error";
|
|
89
88
|
dataUpdatedAt: number;
|
|
90
89
|
errorUpdatedAt: number;
|
|
91
90
|
failureCount: number;
|
|
@@ -103,10 +102,11 @@ export declare function useMyModulePermissions(): {
|
|
|
103
102
|
fetchStatus: import("@tanstack/query-core").FetchStatus;
|
|
104
103
|
promise: Promise<ModulePermissionRow[]>;
|
|
105
104
|
} | {
|
|
105
|
+
data: ModulePermissionRow[] | undefined;
|
|
106
106
|
permissions: ModulePermissionRow[];
|
|
107
107
|
/** True zolang we niets weten; callers behandelen dit als allow. */
|
|
108
108
|
isUnknown: boolean;
|
|
109
|
-
|
|
109
|
+
status: import("./store.js").PermissionStatus;
|
|
110
110
|
error: null;
|
|
111
111
|
isError: false;
|
|
112
112
|
isPending: true;
|
|
@@ -115,7 +115,6 @@ export declare function useMyModulePermissions(): {
|
|
|
115
115
|
isRefetchError: false;
|
|
116
116
|
isSuccess: false;
|
|
117
117
|
isPlaceholderData: false;
|
|
118
|
-
status: "pending";
|
|
119
118
|
dataUpdatedAt: number;
|
|
120
119
|
errorUpdatedAt: number;
|
|
121
120
|
failureCount: number;
|
|
@@ -133,10 +132,11 @@ export declare function useMyModulePermissions(): {
|
|
|
133
132
|
fetchStatus: import("@tanstack/query-core").FetchStatus;
|
|
134
133
|
promise: Promise<ModulePermissionRow[]>;
|
|
135
134
|
} | {
|
|
135
|
+
data: ModulePermissionRow[] | undefined;
|
|
136
136
|
permissions: ModulePermissionRow[];
|
|
137
137
|
/** True zolang we niets weten; callers behandelen dit als allow. */
|
|
138
138
|
isUnknown: boolean;
|
|
139
|
-
|
|
139
|
+
status: import("./store.js").PermissionStatus;
|
|
140
140
|
error: null;
|
|
141
141
|
isError: false;
|
|
142
142
|
isPending: true;
|
|
@@ -144,7 +144,6 @@ export declare function useMyModulePermissions(): {
|
|
|
144
144
|
isRefetchError: false;
|
|
145
145
|
isSuccess: false;
|
|
146
146
|
isPlaceholderData: false;
|
|
147
|
-
status: "pending";
|
|
148
147
|
dataUpdatedAt: number;
|
|
149
148
|
errorUpdatedAt: number;
|
|
150
149
|
failureCount: number;
|
|
@@ -163,10 +162,11 @@ export declare function useMyModulePermissions(): {
|
|
|
163
162
|
fetchStatus: import("@tanstack/query-core").FetchStatus;
|
|
164
163
|
promise: Promise<ModulePermissionRow[]>;
|
|
165
164
|
} | {
|
|
165
|
+
data: ModulePermissionRow[] | undefined;
|
|
166
166
|
permissions: ModulePermissionRow[];
|
|
167
167
|
/** True zolang we niets weten; callers behandelen dit als allow. */
|
|
168
168
|
isUnknown: boolean;
|
|
169
|
-
|
|
169
|
+
status: import("./store.js").PermissionStatus;
|
|
170
170
|
isError: false;
|
|
171
171
|
error: null;
|
|
172
172
|
isPending: false;
|
|
@@ -175,7 +175,6 @@ export declare function useMyModulePermissions(): {
|
|
|
175
175
|
isRefetchError: false;
|
|
176
176
|
isSuccess: true;
|
|
177
177
|
isPlaceholderData: true;
|
|
178
|
-
status: "success";
|
|
179
178
|
dataUpdatedAt: number;
|
|
180
179
|
errorUpdatedAt: number;
|
|
181
180
|
failureCount: number;
|
|
@@ -194,8 +193,8 @@ export declare function useMyModulePermissions(): {
|
|
|
194
193
|
promise: Promise<ModulePermissionRow[]>;
|
|
195
194
|
};
|
|
196
195
|
/**
|
|
197
|
-
* Rechtenniveau voor één module (optioneel één feature). Tijdens laden
|
|
198
|
-
*
|
|
196
|
+
* Rechtenniveau voor één module (optioneel één feature). Tijdens laden, bij
|
|
197
|
+
* een fout en voor admin/superadmin: `edit`. Zichtbaarheid, geen beveiliging.
|
|
199
198
|
*/
|
|
200
199
|
export declare function useModulePermission(moduleId: string, featureId?: string | null): {
|
|
201
200
|
level: PermissionLevel;
|
|
@@ -204,14 +203,35 @@ export declare function useModulePermission(moduleId: string, featureId?: string
|
|
|
204
203
|
isLoading: boolean;
|
|
205
204
|
};
|
|
206
205
|
/**
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
206
|
+
* Sinds 1.0.23. `allowed` = niveau ≥ `level` (standaard `view`). Laden → true.
|
|
207
|
+
* Zichtbaarheid, geen beveiliging.
|
|
208
|
+
*/
|
|
209
|
+
export declare function useCan(moduleId: string, featureId?: string | null, level?: PermissionLevel): {
|
|
210
|
+
allowed: boolean;
|
|
211
|
+
level: PermissionLevel;
|
|
212
|
+
isLoading: boolean;
|
|
213
|
+
};
|
|
214
|
+
export interface CanProps {
|
|
215
|
+
module: string;
|
|
216
|
+
feature?: string | null;
|
|
217
|
+
level?: PermissionLevel;
|
|
218
|
+
fallback?: ReactNode;
|
|
219
|
+
children: ReactNode;
|
|
220
|
+
}
|
|
221
|
+
/** Sinds 1.0.23. Rendert children als `useCan` toestaat. Zichtbaarheid, geen beveiliging. */
|
|
222
|
+
export declare function Can({ module, feature, level, fallback, children }: CanProps): import("react").FunctionComponentElement<import("react").FragmentProps>;
|
|
223
|
+
/**
|
|
224
|
+
* ModuleListTransform die zijbalkitems met `none` weglaat. Laden/fout/admin →
|
|
225
|
+
* ongewijzigd. Zichtbaarheid, geen beveiliging.
|
|
215
226
|
*/
|
|
216
227
|
export declare function useModulePermissionTransform(): ModuleListTransform;
|
|
228
|
+
/**
|
|
229
|
+
* Sinds 1.0.23. Zijbalkfilter met laadstatus: `loading` = eerste laden (toon
|
|
230
|
+
* skeleton); na fout of 3 s time-out de volledige lijst.
|
|
231
|
+
* Zichtbaarheid, geen beveiliging.
|
|
232
|
+
*/
|
|
233
|
+
export declare function usePermissionFilteredModules(modules: FlowModule[], enabled?: boolean): {
|
|
234
|
+
modules: FlowModule[];
|
|
235
|
+
loading: boolean;
|
|
236
|
+
};
|
|
217
237
|
//# sourceMappingURL=useModulePermission.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useModulePermission.d.ts","sourceRoot":"","sources":["../../src/core/permissions/useModulePermission.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useModulePermission.d.ts","sourceRoot":"","sources":["../../src/core/permissions/useModulePermission.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAIL,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACrB,MAAM,cAAc,CAAC;AAGtB,YAAY,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAiBzE;;;;;;;GAOG;AACH,wBAAgB,sBAAsB;;;IAuBlC,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAApE,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAApE,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAApE,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAApE,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAApE,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;EAIvE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;;;;;EAW9E;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,GAAE,eAAwB;;;;EAGlG;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,6FAA6F;AAC7F,wBAAgB,GAAG,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,KAAc,EAAE,QAAe,EAAE,QAAQ,EAAE,EAAE,QAAQ,2EAG3F;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,IAAI,mBAAmB,CASlE;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,OAAO,UAAO;;;EAQjF"}
|
|
@@ -1,137 +1,105 @@
|
|
|
1
|
-
import { useCallback } from "react";
|
|
2
|
-
import { useQuery } from "@tanstack/react-query";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
if (
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return "edit";
|
|
1
|
+
import { createElement, Fragment, useCallback, useContext } from "react";
|
|
2
|
+
import { QueryClient, QueryClientContext, useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { filterModules, RANK, resolveLevel, } from "./resolve.js";
|
|
4
|
+
import { getPermissionSnapshot, loadPermissions, usePermissionSnapshot } from "./store.js";
|
|
5
|
+
let fallbackClient = null;
|
|
6
|
+
function getFallbackClient() {
|
|
7
|
+
if (!fallbackClient)
|
|
8
|
+
fallbackClient = new QueryClient();
|
|
9
|
+
return fallbackClient;
|
|
11
10
|
}
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
return [];
|
|
15
|
-
return data.map((raw) => {
|
|
16
|
-
const row = (raw ?? {});
|
|
17
|
-
const rawModule = (row["module_id"] ?? row["moduleId"] ?? "");
|
|
18
|
-
const rawFeature = (row["feature_id"] ?? row["featureId"] ?? null);
|
|
19
|
-
return {
|
|
20
|
-
moduleId: navKey(String(rawModule ?? "")),
|
|
21
|
-
featureId: rawFeature ? String(rawFeature) : null,
|
|
22
|
-
permission: normalizeLevel(row["permission"] ?? row["level"] ?? row["permission_level"]),
|
|
23
|
-
};
|
|
24
|
-
});
|
|
11
|
+
function rolesAreAdmin(roles) {
|
|
12
|
+
return roles.includes("admin") || roles.includes("superadmin");
|
|
25
13
|
}
|
|
26
|
-
/**
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
const code = e.code ?? "";
|
|
30
|
-
const message = (e.message ?? "").toLowerCase();
|
|
31
|
-
return (code === "42883" ||
|
|
32
|
-
code === "PGRST202" ||
|
|
33
|
-
code === "PGRST302" ||
|
|
34
|
-
e.status === 404 ||
|
|
35
|
-
message.includes("does not exist") ||
|
|
36
|
-
message.includes("could not find the function") ||
|
|
37
|
-
message.includes("schema cache"));
|
|
14
|
+
/** Snapshot → bekend? Laden = onbekend; fout/time-out = bekend (leeg → allow). */
|
|
15
|
+
function isKnown(s) {
|
|
16
|
+
return s.status !== "loading";
|
|
38
17
|
}
|
|
39
18
|
/**
|
|
40
|
-
*
|
|
19
|
+
* Rechten van de ingelogde gebruiker via RPC `my_module_permissions()`.
|
|
41
20
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
21
|
+
* Draait op de gedeelde store; gebruikt React Query alleen als er een
|
|
22
|
+
* QueryClientProvider is, anders een interne client — crasht nooit zonder
|
|
23
|
+
* provider. Fail-open: fout of ontbrekende RPC → lege lijst → default-allow.
|
|
24
|
+
* Zichtbaarheid, geen beveiliging.
|
|
45
25
|
*/
|
|
46
26
|
export function useMyModulePermissions() {
|
|
27
|
+
const snap = usePermissionSnapshot();
|
|
28
|
+
const ctxClient = useContext(QueryClientContext);
|
|
47
29
|
const query = useQuery({
|
|
48
|
-
queryKey: ["my-module-permissions"],
|
|
30
|
+
queryKey: ["my-module-permissions", snap.userId],
|
|
49
31
|
queryFn: async () => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (isMissingRpc(error))
|
|
53
|
-
return [];
|
|
54
|
-
// Ook andere fouten mogen niet blokkeren: fail-open, geen retry.
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
|
-
return normalizeRows(data);
|
|
32
|
+
await loadPermissions(getPermissionSnapshot().userId);
|
|
33
|
+
return getPermissionSnapshot().rows;
|
|
58
34
|
},
|
|
59
|
-
|
|
35
|
+
enabled: snap.status !== "loading",
|
|
36
|
+
initialData: isKnown(snap) ? snap.rows : undefined,
|
|
60
37
|
retry: false,
|
|
61
38
|
refetchOnWindowFocus: false,
|
|
62
39
|
staleTime: 5 * 60 * 1000,
|
|
63
|
-
});
|
|
40
|
+
}, ctxClient ?? getFallbackClient());
|
|
64
41
|
return {
|
|
65
42
|
...query,
|
|
66
|
-
|
|
43
|
+
data: isKnown(snap) ? snap.rows : query.data,
|
|
44
|
+
permissions: isKnown(snap) ? snap.rows : [],
|
|
67
45
|
/** True zolang we niets weten; callers behandelen dit als allow. */
|
|
68
|
-
isUnknown:
|
|
46
|
+
isUnknown: !isKnown(snap),
|
|
47
|
+
status: snap.status,
|
|
69
48
|
};
|
|
70
49
|
}
|
|
71
|
-
function resolveLevel(rows, moduleId, featureId) {
|
|
72
|
-
const key = navKey(moduleId);
|
|
73
|
-
const forModule = rows.filter((r) => r.moduleId === key);
|
|
74
|
-
// Geen rij voor deze module → default-allow.
|
|
75
|
-
if (forModule.length === 0)
|
|
76
|
-
return "edit";
|
|
77
|
-
if (featureId) {
|
|
78
|
-
const feature = forModule.find((r) => r.featureId === featureId);
|
|
79
|
-
if (feature)
|
|
80
|
-
return feature.permission;
|
|
81
|
-
}
|
|
82
|
-
const moduleLevel = forModule.find((r) => !r.featureId);
|
|
83
|
-
if (moduleLevel)
|
|
84
|
-
return moduleLevel.permission;
|
|
85
|
-
// Alleen feature-rijen bekend: hoogste feature-recht bepaalt moduletoegang.
|
|
86
|
-
if (forModule.some((r) => r.permission === "edit"))
|
|
87
|
-
return "edit";
|
|
88
|
-
if (forModule.some((r) => r.permission === "view"))
|
|
89
|
-
return "view";
|
|
90
|
-
return "none";
|
|
91
|
-
}
|
|
92
50
|
/**
|
|
93
|
-
* Rechtenniveau voor één module (optioneel één feature). Tijdens laden
|
|
94
|
-
*
|
|
51
|
+
* Rechtenniveau voor één module (optioneel één feature). Tijdens laden, bij
|
|
52
|
+
* een fout en voor admin/superadmin: `edit`. Zichtbaarheid, geen beveiliging.
|
|
95
53
|
*/
|
|
96
54
|
export function useModulePermission(moduleId, featureId) {
|
|
97
|
-
const
|
|
98
|
-
const
|
|
55
|
+
const snap = usePermissionSnapshot();
|
|
56
|
+
const unknown = !isKnown(snap);
|
|
57
|
+
const level = unknown || rolesAreAdmin(snap.roles) ? "edit" : resolveLevel(snap.rows, moduleId, featureId);
|
|
99
58
|
return {
|
|
100
59
|
level,
|
|
101
60
|
canView: level === "view" || level === "edit",
|
|
102
61
|
canEdit: level === "edit",
|
|
103
|
-
isLoading:
|
|
62
|
+
isLoading: unknown,
|
|
104
63
|
};
|
|
105
64
|
}
|
|
106
65
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
66
|
+
* Sinds 1.0.23. `allowed` = niveau ≥ `level` (standaard `view`). Laden → true.
|
|
67
|
+
* Zichtbaarheid, geen beveiliging.
|
|
68
|
+
*/
|
|
69
|
+
export function useCan(moduleId, featureId, level = "view") {
|
|
70
|
+
const p = useModulePermission(moduleId, featureId);
|
|
71
|
+
return { allowed: RANK[p.level] >= RANK[level], level: p.level, isLoading: p.isLoading };
|
|
72
|
+
}
|
|
73
|
+
/** Sinds 1.0.23. Rendert children als `useCan` toestaat. Zichtbaarheid, geen beveiliging. */
|
|
74
|
+
export function Can({ module, feature, level = "view", fallback = null, children }) {
|
|
75
|
+
const { allowed } = useCan(module, feature ?? null, level);
|
|
76
|
+
return createElement(Fragment, null, allowed ? children : fallback);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* ModuleListTransform die zijbalkitems met `none` weglaat. Laden/fout/admin →
|
|
80
|
+
* ongewijzigd. Zichtbaarheid, geen beveiliging.
|
|
115
81
|
*/
|
|
116
82
|
export function useModulePermissionTransform() {
|
|
117
|
-
const
|
|
83
|
+
const snap = usePermissionSnapshot();
|
|
118
84
|
return useCallback((modules) => {
|
|
119
|
-
if (
|
|
85
|
+
if (!isKnown(snap) || rolesAreAdmin(snap.roles))
|
|
120
86
|
return modules;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
87
|
+
return filterModules(modules, snap.rows);
|
|
88
|
+
}, [snap]);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Sinds 1.0.23. Zijbalkfilter met laadstatus: `loading` = eerste laden (toon
|
|
92
|
+
* skeleton); na fout of 3 s time-out de volledige lijst.
|
|
93
|
+
* Zichtbaarheid, geen beveiliging.
|
|
94
|
+
*/
|
|
95
|
+
export function usePermissionFilteredModules(modules, enabled = true) {
|
|
96
|
+
const snap = usePermissionSnapshot();
|
|
97
|
+
if (!enabled)
|
|
98
|
+
return { modules, loading: false };
|
|
99
|
+
if (snap.status === "loading") {
|
|
100
|
+
return { modules, loading: !rolesAreAdmin(snap.roles) };
|
|
101
|
+
}
|
|
102
|
+
if (rolesAreAdmin(snap.roles) || snap.status !== "ready")
|
|
103
|
+
return { modules, loading: false };
|
|
104
|
+
return { modules: filterModules(modules, snap.rows), loading: false };
|
|
137
105
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface SystemRouteOptions {
|
|
2
|
+
/** Pad als er geen geldig `?redirect` is (na inloggen, handover, reset). */
|
|
3
|
+
landing: string;
|
|
4
|
+
/** Pad van de inlogpagina. Standaard "/login". */
|
|
5
|
+
loginPath?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface LoginSearch {
|
|
8
|
+
redirect?: string;
|
|
9
|
+
}
|
|
10
|
+
/** `/login`: ingelogd → `?redirect` (alleen same-site) wint van `landing`. */
|
|
11
|
+
export declare function loginRoute({ landing }: SystemRouteOptions): {
|
|
12
|
+
validateSearch: (search: Record<string, unknown>) => LoginSearch;
|
|
13
|
+
beforeLoad: ({ context, search }: {
|
|
14
|
+
context: unknown;
|
|
15
|
+
search: LoginSearch;
|
|
16
|
+
}) => void;
|
|
17
|
+
head: () => {
|
|
18
|
+
meta: {
|
|
19
|
+
title: string;
|
|
20
|
+
}[];
|
|
21
|
+
};
|
|
22
|
+
component: () => import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
};
|
|
24
|
+
/** `/reset-wachtwoord`: de core-pagina, daarna naar `landing`. */
|
|
25
|
+
export declare function resetPasswordRoute({ landing, loginPath }: SystemRouteOptions): {
|
|
26
|
+
head: () => {
|
|
27
|
+
meta: {
|
|
28
|
+
title: string;
|
|
29
|
+
}[];
|
|
30
|
+
};
|
|
31
|
+
component: () => import("react/jsx-runtime").JSX.Element;
|
|
32
|
+
};
|
|
33
|
+
/** `/`: redirect in beforeLoad, dus er wordt niets gerenderd (geen flits). */
|
|
34
|
+
export declare function landingRoute({ landing }: Pick<SystemRouteOptions, "landing">): {
|
|
35
|
+
beforeLoad: () => never;
|
|
36
|
+
component: () => null;
|
|
37
|
+
};
|
|
38
|
+
/** `/auth/handover`: client-only, want de sessie staat alleen in localStorage. */
|
|
39
|
+
export declare function handoverRoute({ landing, loginPath }: SystemRouteOptions): {
|
|
40
|
+
ssr: false;
|
|
41
|
+
head: () => {
|
|
42
|
+
meta: {
|
|
43
|
+
title: string;
|
|
44
|
+
}[];
|
|
45
|
+
};
|
|
46
|
+
component: () => import("react/jsx-runtime").JSX.Element;
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/routes/index.tsx"],"names":[],"mappings":"AAeA,MAAM,WAAW,kBAAkB;IACjC,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAMD,8EAA8E;AAC9E,wBAAgB,UAAU,CAAC,EAAE,OAAO,EAAE,EAAE,kBAAkB;6BAE7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,WAAW;sCAE5B;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,WAAW,CAAA;KAAE;;;;;;;EAU9E;AAED,kEAAkE;AAClE,wBAAgB,kBAAkB,CAAC,EAAE,OAAO,EAAE,SAAoB,EAAE,EAAE,kBAAkB;;;;;;;EAOvF;AAED,8EAA8E;AAC9E,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE,IAAI,CAAC,kBAAkB,EAAE,SAAS,CAAC;sBAEzD,KAAK;;EAOxB;AAED,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,EAAE,OAAO,EAAE,SAAoB,EAAE,EAAE,kBAAkB;;;;;;;;EAQlF"}
|