@flowselections/admin-installatietechniek 1.0.4 → 1.0.6
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/dist-lib/components/AdminForInstallatieTechniek.d.ts.map +1 -1
- package/dist-lib/components/AdminForInstallatieTechniek.js +166 -9
- package/dist-lib/components/ModuleAccessGuard.d.ts +15 -0
- package/dist-lib/components/ModuleAccessGuard.d.ts.map +1 -0
- package/dist-lib/components/ModuleAccessGuard.js +40 -0
- package/dist-lib/hooks/useModulePermission.d.ts +10 -3
- package/dist-lib/hooks/useModulePermission.d.ts.map +1 -1
- package/dist-lib/hooks/useModulePermission.js +26 -23
- package/dist-lib/index.d.ts +2 -0
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +2 -0
- package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/auth-attacher.js +5 -1
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +1126 -155
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/auth-middleware.js +21 -7
- package/dist-lib/integrations/supabase/client.d.ts +1126 -155
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +1126 -155
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +1148 -154
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +3 -0
- package/dist-lib/lib/admin.functions.d.ts +171971 -2043
- package/dist-lib/lib/admin.functions.d.ts.map +1 -1
- package/dist-lib/lib/admin.functions.js +292 -16
- package/dist-lib/lib/permission-types.d.ts +7 -0
- package/dist-lib/lib/permission-types.d.ts.map +1 -0
- package/dist-lib/lib/permission-types.js +1 -0
- package/dist-lib/lib/permissions.server.d.ts +14 -0
- package/dist-lib/lib/permissions.server.d.ts.map +1 -0
- package/dist-lib/lib/permissions.server.js +26 -0
- package/package.json +87 -87
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminForInstallatieTechniek.d.ts","sourceRoot":"","sources":["../../src/components/AdminForInstallatieTechniek.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AdminForInstallatieTechniek.d.ts","sourceRoot":"","sources":["../../src/components/AdminForInstallatieTechniek.tsx"],"names":[],"mappings":"AAgGA,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,gCAAgC;IAC/C,8EAA8E;IAC9E,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC7B,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA2FD,wBAAgB,2BAA2B,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,GAAE,gCAAqC,+BAyjDtG"}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useState } from "react";
|
|
3
|
-
import { useAuth, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Badge, Button, Card, CardContent, CardHeader, CardTitle, Checkbox, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, toast, } from "@flowselections/core";
|
|
4
|
-
import { ChevronDown, ChevronRight, Copy, FlaskConical, MoreVertical, Plus, Pencil, Shield, UserPlus, UserX, } from "lucide-react";
|
|
5
|
-
import { assignRole, createRole, createTestUser, deleteRole, deleteUser, duplicateRole, getRoles, getUserModulePermissions, getUsersWithRoles, inviteUser, removeRole, setUserModulePermission, syncModuleRegistry, updateRole, updateUserName, } from "../lib/admin.functions";
|
|
3
|
+
import { useAuth, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Badge, Button, Card, CardContent, CardHeader, CardTitle, Checkbox, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, toast, } from "@flowselections/core";
|
|
4
|
+
import { Building2, ChevronDown, ChevronRight, Copy, FlaskConical, MoreVertical, Plus, Pencil, Shield, UserPlus, UserX, } from "lucide-react";
|
|
5
|
+
import { assignRole, createRole, createProfile, createTestUser, deleteProfile, deleteRole, deleteUser, duplicateProfile, duplicateRole, getProfiles, getRoles, getUserModulePermissions, getUsersWithRoles, inviteUser, removeRole, setActiveProfile, setModuleEnforce, setUserModulePermission, syncModuleRegistry, updateProfile, updateRole, updateUserName, } from "../lib/admin.functions";
|
|
6
6
|
import { useDiscoveredModules } from "../lib/moduleDiscovery";
|
|
7
7
|
import { supabase } from "../integrations/supabase/client";
|
|
8
|
+
const ACTIVE_PROFILE_STORAGE_KEY = "flowselections.admin.activeProfileId";
|
|
8
9
|
const USER_PERMISSION_LABELS = {
|
|
9
10
|
inherit: "Volg rol",
|
|
10
11
|
none: "Geen toegang",
|
|
@@ -66,6 +67,17 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
66
67
|
const [searchQuery, setSearchQuery] = useState("");
|
|
67
68
|
const [roleFilter, setRoleFilter] = useState("all");
|
|
68
69
|
const [sortBy, setSortBy] = useState("name-asc");
|
|
70
|
+
const [orgProfiles, setOrgProfiles] = useState([]);
|
|
71
|
+
const [tenants, setTenants] = useState([]);
|
|
72
|
+
const [profilesLoading, setProfilesLoading] = useState(false);
|
|
73
|
+
const [activeProfileId, setActiveProfileId] = useState(() => {
|
|
74
|
+
if (typeof window === "undefined")
|
|
75
|
+
return "";
|
|
76
|
+
return window.localStorage.getItem(ACTIVE_PROFILE_STORAGE_KEY) ?? "";
|
|
77
|
+
});
|
|
78
|
+
const [profileEditor, setProfileEditor] = useState(null);
|
|
79
|
+
const [profileSaving, setProfileSaving] = useState(false);
|
|
80
|
+
const [profileDeleteTarget, setProfileDeleteTarget] = useState(null);
|
|
69
81
|
const [permUserId, setPermUserId] = useState("");
|
|
70
82
|
const [userPermModules, setUserPermModules] = useState([]);
|
|
71
83
|
const [userPermMap, setUserPermMap] = useState({});
|
|
@@ -82,7 +94,9 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
82
94
|
const loadUsers = async () => {
|
|
83
95
|
setLoading(true);
|
|
84
96
|
try {
|
|
85
|
-
const result = await getUsersWithRoles(
|
|
97
|
+
const result = await getUsersWithRoles({
|
|
98
|
+
data: activeProfileId ? { profileId: activeProfileId } : {},
|
|
99
|
+
});
|
|
86
100
|
setUsers(Array.isArray(result?.users) ? result.users : []);
|
|
87
101
|
const roleList = Array.isArray(result?.availableRoles) ? result.availableRoles : [];
|
|
88
102
|
setAvailableRoles(roleList);
|
|
@@ -100,7 +114,9 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
100
114
|
const loadRoles = async () => {
|
|
101
115
|
setRolesLoading(true);
|
|
102
116
|
try {
|
|
103
|
-
const result = await getRoles(
|
|
117
|
+
const result = await getRoles({
|
|
118
|
+
data: activeProfileId ? { profileId: activeProfileId } : {},
|
|
119
|
+
});
|
|
104
120
|
setRoles(Array.isArray(result?.roles) ? result.roles : []);
|
|
105
121
|
setRoleModules(Array.isArray(result?.modules) ? result.modules : []);
|
|
106
122
|
setRolePermissions(Array.isArray(result?.permissions) ? result.permissions : []);
|
|
@@ -112,6 +128,28 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
112
128
|
setRolesLoading(false);
|
|
113
129
|
}
|
|
114
130
|
};
|
|
131
|
+
const loadProfiles = async () => {
|
|
132
|
+
if (!isAdmin)
|
|
133
|
+
return;
|
|
134
|
+
setProfilesLoading(true);
|
|
135
|
+
try {
|
|
136
|
+
const result = await getProfiles();
|
|
137
|
+
const list = Array.isArray(result?.profiles) ? result.profiles : [];
|
|
138
|
+
setOrgProfiles(list);
|
|
139
|
+
setTenants(Array.isArray(result?.tenants) ? result.tenants : []);
|
|
140
|
+
setActiveProfileId((current) => {
|
|
141
|
+
if (current && list.some((p) => p.id === current))
|
|
142
|
+
return current;
|
|
143
|
+
return list.find((p) => p.isDefault)?.id ?? list[0]?.id ?? "";
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
toast.error(getErrorMessage(error, "Fout bij het laden van bedrijfsprofielen"));
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
setProfilesLoading(false);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
115
153
|
const loadUserPermissions = async (userId) => {
|
|
116
154
|
if (!userId) {
|
|
117
155
|
setUserPermModules([]);
|
|
@@ -157,6 +195,7 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
157
195
|
}
|
|
158
196
|
if (cancelled)
|
|
159
197
|
return;
|
|
198
|
+
loadProfiles();
|
|
160
199
|
loadUsers();
|
|
161
200
|
loadRoles();
|
|
162
201
|
})();
|
|
@@ -165,6 +204,19 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
165
204
|
};
|
|
166
205
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
167
206
|
}, [canAccess, isAdmin, shellModulesKey, effectiveShellId]);
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (typeof window === "undefined")
|
|
209
|
+
return;
|
|
210
|
+
if (activeProfileId)
|
|
211
|
+
window.localStorage.setItem(ACTIVE_PROFILE_STORAGE_KEY, activeProfileId);
|
|
212
|
+
}, [activeProfileId]);
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
if (!canAccess || !activeProfileId)
|
|
215
|
+
return;
|
|
216
|
+
loadUsers();
|
|
217
|
+
loadRoles();
|
|
218
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
219
|
+
}, [activeProfileId]);
|
|
168
220
|
useEffect(() => {
|
|
169
221
|
if (!canAccess)
|
|
170
222
|
return;
|
|
@@ -188,6 +240,17 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
188
240
|
toast.error(getErrorMessage(error, "Opslaan mislukt"));
|
|
189
241
|
}
|
|
190
242
|
};
|
|
243
|
+
const handleEnforceChange = async (moduleId, enforce) => {
|
|
244
|
+
setRoleModules((prev) => prev.map((m) => (m.id === moduleId ? { ...m, enforce } : m)));
|
|
245
|
+
try {
|
|
246
|
+
await setModuleEnforce({ data: { moduleId, enforce } });
|
|
247
|
+
toast.success(`Module ${enforce ? "afgedwongen" : "niet langer afgedwongen"}`);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
setRoleModules((prev) => prev.map((m) => (m.id === moduleId ? { ...m, enforce: !enforce } : m)));
|
|
251
|
+
toast.error(getErrorMessage(error, "Afdwingen wijzigen mislukt"));
|
|
252
|
+
}
|
|
253
|
+
};
|
|
191
254
|
const handleAssign = async (userId, roleId) => {
|
|
192
255
|
setSubmitting(true);
|
|
193
256
|
try {
|
|
@@ -308,7 +371,12 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
308
371
|
setInviting(true);
|
|
309
372
|
try {
|
|
310
373
|
await inviteUser({
|
|
311
|
-
data: {
|
|
374
|
+
data: {
|
|
375
|
+
email: inviteEmail.trim(),
|
|
376
|
+
displayName: inviteName.trim(),
|
|
377
|
+
roleId: inviteRole,
|
|
378
|
+
...(activeProfileId ? { profileId: activeProfileId } : {}),
|
|
379
|
+
},
|
|
312
380
|
});
|
|
313
381
|
toast.success(`Uitnodiging verstuurd naar ${inviteEmail.trim()}`);
|
|
314
382
|
setInviteOpen(false);
|
|
@@ -367,7 +435,11 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
367
435
|
setTestCreating(true);
|
|
368
436
|
try {
|
|
369
437
|
const result = await createTestUser({
|
|
370
|
-
data: {
|
|
438
|
+
data: {
|
|
439
|
+
displayName: testName.trim(),
|
|
440
|
+
roleId: testRole,
|
|
441
|
+
...(activeProfileId ? { profileId: activeProfileId } : {}),
|
|
442
|
+
},
|
|
371
443
|
});
|
|
372
444
|
setTestCredentials({ email: result.email, password: result.password });
|
|
373
445
|
setTestOpen(false);
|
|
@@ -391,6 +463,91 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
391
463
|
toast.error("Kopiëren mislukt");
|
|
392
464
|
}
|
|
393
465
|
};
|
|
466
|
+
const openProfileEditor = (profile) => {
|
|
467
|
+
setProfileEditor({
|
|
468
|
+
profileId: profile?.id ?? null,
|
|
469
|
+
name: profile?.name ?? "",
|
|
470
|
+
tenantId: profile?.tenantId ?? "",
|
|
471
|
+
roleIds: profile?.roleIds ?? [],
|
|
472
|
+
moduleIds: profile?.moduleIds ?? [],
|
|
473
|
+
});
|
|
474
|
+
};
|
|
475
|
+
const toggleProfileValue = (field, value) => {
|
|
476
|
+
setProfileEditor((state) => {
|
|
477
|
+
if (!state)
|
|
478
|
+
return state;
|
|
479
|
+
const list = state[field];
|
|
480
|
+
return {
|
|
481
|
+
...state,
|
|
482
|
+
[field]: list.includes(value) ? list.filter((v) => v !== value) : [...list, value],
|
|
483
|
+
};
|
|
484
|
+
});
|
|
485
|
+
};
|
|
486
|
+
const handleSaveProfile = async () => {
|
|
487
|
+
if (!profileEditor || !profileEditor.name.trim())
|
|
488
|
+
return;
|
|
489
|
+
setProfileSaving(true);
|
|
490
|
+
try {
|
|
491
|
+
const payload = {
|
|
492
|
+
name: profileEditor.name.trim(),
|
|
493
|
+
tenantId: profileEditor.tenantId ? profileEditor.tenantId : null,
|
|
494
|
+
roleIds: profileEditor.roleIds,
|
|
495
|
+
moduleIds: profileEditor.moduleIds,
|
|
496
|
+
};
|
|
497
|
+
if (profileEditor.profileId) {
|
|
498
|
+
await updateProfile({ data: { profileId: profileEditor.profileId, ...payload } });
|
|
499
|
+
toast.success("Bedrijfsprofiel bijgewerkt");
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
await createProfile({ data: payload });
|
|
503
|
+
toast.success("Bedrijfsprofiel aangemaakt");
|
|
504
|
+
}
|
|
505
|
+
setProfileEditor(null);
|
|
506
|
+
await loadProfiles();
|
|
507
|
+
await Promise.all([loadUsers(), loadRoles()]);
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
toast.error(getErrorMessage(error, "Opslaan mislukt"));
|
|
511
|
+
}
|
|
512
|
+
finally {
|
|
513
|
+
setProfileSaving(false);
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
const handleDuplicateProfile = async (profile) => {
|
|
517
|
+
try {
|
|
518
|
+
await duplicateProfile({ data: { profileId: profile.id } });
|
|
519
|
+
toast.success("Bedrijfsprofiel gedupliceerd");
|
|
520
|
+
await loadProfiles();
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
toast.error(getErrorMessage(error, "Dupliceren mislukt"));
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
const handleDeleteProfile = async () => {
|
|
527
|
+
if (!profileDeleteTarget)
|
|
528
|
+
return;
|
|
529
|
+
try {
|
|
530
|
+
await deleteProfile({ data: { profileId: profileDeleteTarget.id } });
|
|
531
|
+
toast.success("Bedrijfsprofiel verwijderd");
|
|
532
|
+
setProfileDeleteTarget(null);
|
|
533
|
+
await loadProfiles();
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
toast.error(getErrorMessage(error, "Verwijderen mislukt"));
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
const handleSetActiveProfile = async (profile) => {
|
|
540
|
+
try {
|
|
541
|
+
await setActiveProfile({ data: { profileId: profile.id } });
|
|
542
|
+
setActiveProfileId(profile.id);
|
|
543
|
+
toast.success(`${profile.name} is nu het standaardprofiel`);
|
|
544
|
+
await loadProfiles();
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
toast.error(getErrorMessage(error, "Instellen mislukt"));
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
const allRolesForProfile = roles.length > 0 ? roles : availableRoles;
|
|
394
551
|
const filteredUsers = useMemo(() => {
|
|
395
552
|
const query = searchQuery.trim().toLowerCase();
|
|
396
553
|
const list = users.filter((u) => {
|
|
@@ -422,7 +579,7 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
422
579
|
if (!canAccess) {
|
|
423
580
|
return (_jsx("main", { className: "p-6", children: _jsx(Card, { children: _jsx(CardContent, { className: "pt-6", children: _jsx("p", { className: "text-muted-foreground", children: "Je hebt geen toegang tot deze pagina." }) }) }) }));
|
|
424
581
|
}
|
|
425
|
-
return (_jsxs("main", { className: "p-6 space-y-6", children: [_jsxs(Card, { children: [_jsx(CardHeader, { className: "space-y-4", children: _jsxs("div", { className: "flex flex-row items-start justify-between gap-4", children: [_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Shield, { className: "h-5 w-5 text-primary" }), _jsx(CardTitle, { children: "Gebruikers & rollen" })] }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Hier vind je alle gebruikers die toegang hebben en zie je welke rol zij hebben. Ook beheer je hier alle beschikbare rollen." })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [isAdmin && activeTab === "roles" && (_jsxs(Button, { size: "sm", onClick: () => openRoleEditor(), children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), "Nieuwe rol"] })), activeTab === "users" && (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setTestOpen(true), children: [_jsx(FlaskConical, { className: "h-4 w-4 mr-2" }), "Testaccount"] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setInviteOpen(true), children: [_jsx(UserPlus, { className: "h-4 w-4 mr-2" }), "Account aanmaken"] })] }))] })] }) }), _jsx(CardContent, { children: _jsxs(Tabs, { value: activeTab, onValueChange: setActiveTab, children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "users", children: "Gebruikers" }), _jsx(TabsTrigger, { value: "roles", children: "Rollen" }), _jsx(TabsTrigger, { value: "user-permissions", children: "Rechten per gebruiker" })] }), _jsxs(TabsContent, { value: "users", className: "mt-4 space-y-6", children: [_jsxs("div", { className: "flex flex-col gap-4 sm:flex-row sm:items-end", children: [_jsxs("div", { className: "flex-1 space-y-2", children: [_jsx(Label, { className: "text-sm font-medium", children: "Zoeken" }), _jsx(Input, { placeholder: "Zoek op naam of e-mail", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value) })] }), _jsxs("div", { className: "w-full sm:w-48 space-y-2", children: [_jsx(Label, { className: "text-sm font-medium", children: "Filter op rol" }), _jsxs(Select, { value: roleFilter, onValueChange: setRoleFilter, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "Alle rollen" }), availableRoles.map((role) => (_jsx(SelectItem, { value: role.id, children: role.name }, role.id))), _jsx(SelectItem, { value: "none", children: "Geen rollen" })] })] })] }), _jsxs("div", { className: "w-full sm:w-48 space-y-2", children: [_jsx(Label, { className: "text-sm font-medium", children: "Sorteren" }), _jsxs(Select, { value: sortBy, onValueChange: (value) => setSortBy(value), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "name-asc", children: "Naam A-Z" }), _jsx(SelectItem, { value: "name-desc", children: "Naam Z-A" }), _jsx(SelectItem, { value: "role-asc", children: "Rol A-Z" }), _jsx(SelectItem, { value: "role-desc", children: "Rol Z-A" })] })] })] })] }), loading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Laden..." })) : filteredUsers.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: hasActiveFilters
|
|
582
|
+
return (_jsxs("main", { className: "p-6 space-y-6", children: [_jsxs(Card, { children: [_jsx(CardHeader, { className: "space-y-4", children: _jsxs("div", { className: "flex flex-row items-start justify-between gap-4", children: [_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Shield, { className: "h-5 w-5 text-primary" }), _jsx(CardTitle, { children: "Gebruikers & rollen" })] }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Hier vind je alle gebruikers die toegang hebben en zie je welke rol zij hebben. Ook beheer je hier alle beschikbare rollen." })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [isAdmin && orgProfiles.length > 0 && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Building2, { className: "h-4 w-4 text-muted-foreground" }), _jsxs(Select, { value: activeProfileId, onValueChange: setActiveProfileId, children: [_jsx(SelectTrigger, { className: "w-56", children: _jsx(SelectValue, { placeholder: "Bedrijfsprofiel" }) }), _jsx(SelectContent, { children: orgProfiles.map((profile) => (_jsx(SelectItem, { value: profile.id, children: profile.name }, profile.id))) })] })] })), isAdmin && activeTab === "profiles" && (_jsxs(Button, { size: "sm", onClick: () => openProfileEditor(), children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), "Nieuw profiel"] })), isAdmin && activeTab === "roles" && (_jsxs(Button, { size: "sm", onClick: () => openRoleEditor(), children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), "Nieuwe rol"] })), activeTab === "users" && (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setTestOpen(true), children: [_jsx(FlaskConical, { className: "h-4 w-4 mr-2" }), "Testaccount"] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setInviteOpen(true), children: [_jsx(UserPlus, { className: "h-4 w-4 mr-2" }), "Account aanmaken"] })] }))] })] }) }), _jsx(CardContent, { children: _jsxs(Tabs, { value: activeTab, onValueChange: setActiveTab, children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "users", children: "Gebruikers" }), _jsx(TabsTrigger, { value: "roles", children: "Rollen" }), _jsx(TabsTrigger, { value: "user-permissions", children: "Rechten per gebruiker" }), _jsx(TabsTrigger, { value: "modules", children: "Modules" }), isAdmin && _jsx(TabsTrigger, { value: "profiles", children: "Profielen" })] }), _jsxs(TabsContent, { value: "users", className: "mt-4 space-y-6", children: [_jsxs("div", { className: "flex flex-col gap-4 sm:flex-row sm:items-end", children: [_jsxs("div", { className: "flex-1 space-y-2", children: [_jsx(Label, { className: "text-sm font-medium", children: "Zoeken" }), _jsx(Input, { placeholder: "Zoek op naam of e-mail", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value) })] }), _jsxs("div", { className: "w-full sm:w-48 space-y-2", children: [_jsx(Label, { className: "text-sm font-medium", children: "Filter op rol" }), _jsxs(Select, { value: roleFilter, onValueChange: setRoleFilter, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "Alle rollen" }), availableRoles.map((role) => (_jsx(SelectItem, { value: role.id, children: role.name }, role.id))), _jsx(SelectItem, { value: "none", children: "Geen rollen" })] })] })] }), _jsxs("div", { className: "w-full sm:w-48 space-y-2", children: [_jsx(Label, { className: "text-sm font-medium", children: "Sorteren" }), _jsxs(Select, { value: sortBy, onValueChange: (value) => setSortBy(value), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "name-asc", children: "Naam A-Z" }), _jsx(SelectItem, { value: "name-desc", children: "Naam Z-A" }), _jsx(SelectItem, { value: "role-asc", children: "Rol A-Z" }), _jsx(SelectItem, { value: "role-desc", children: "Rol Z-A" })] })] })] })] }), loading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Laden..." })) : filteredUsers.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: hasActiveFilters
|
|
426
583
|
? "Geen gebruikers gevonden met de huidige filters."
|
|
427
584
|
: "Geen gebruikers gevonden." })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Gebruiker" }), _jsx(TableHead, { children: "Rollen" }), _jsx(TableHead, { className: "w-16" })] }) }), _jsx(TableBody, { children: filteredUsers.map((u) => {
|
|
428
585
|
const manageable = canManageUser(u);
|
|
@@ -439,7 +596,7 @@ export function AdminForInstallatieTechniek({ modules, shellId } = {}) {
|
|
|
439
596
|
rows.push(_jsxs(TableRow, { children: [_jsx(TableCell, { className: "pl-8 text-sm text-muted-foreground", children: f.name }), _jsx(TableCell, { children: _jsxs(Select, { value: userPermMap[permCellKey(mod.id, f.id)] ?? "inherit", onValueChange: (v) => handleUserPermChange(mod.id, f.id, v), children: [_jsx(SelectTrigger, { className: "w-48", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: Object.keys(USER_PERMISSION_LABELS).map((p) => (_jsx(SelectItem, { value: p, children: USER_PERMISSION_LABELS[p] }, p))) })] }) })] }, `${mod.id}-${f.id}`));
|
|
440
597
|
}
|
|
441
598
|
return rows;
|
|
442
|
-
}) })] }))] })] }) })] }), _jsx(Dialog, { open: editor !== null, onOpenChange: (open) => !open && !editorSaving && setEditor(null), children: _jsxs(DialogContent, { className: "max-w-3xl", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: editor?.roleId ? "Rol bewerken" : "Rol toevoegen" }), _jsx(DialogDescription, { children: "Maak hier een rol aan en wijs de gewenste rechten toe. Vink je een onderdeel niet aan? Dan heeft de gebruiker geen toegang tot (een deel van) die pagina." })] }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "role-name", children: "Naam rol" }), _jsx(Input, { id: "role-name", value: editor?.name ?? "", placeholder: "Vul een naam voor de rol in.", onChange: (e) => setEditor((state) => (state ? { ...state, name: e.target.value } : state)), disabled: editorSaving })] }), _jsxs("div", { className: "max-h-[45vh] overflow-y-auto rounded-md border", children: [_jsxs("div", { className: "flex items-center justify-end gap-8 border-b px-4 py-2 text-sm font-medium", children: [_jsx("span", { className: "w-16 text-center", children: "Lezen" }), _jsx("span", { className: "w-16 text-center", children: "Bewerken" })] }), roleModules.length === 0 ? (_jsx("p", { className: "p-4 text-sm text-muted-foreground", children: "Er zijn nog geen modules geregistreerd voor deze shell." })) : (roleModules.map((mod) => {
|
|
599
|
+
}) })] }))] }), _jsxs(TabsContent, { value: "modules", className: "mt-4 space-y-6", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Schakel \"Toegang afdwingen\" in voor een module om de rollen en gebruikersrechten actief te laten controleren. Modules zonder deze schakelaar blijven voor iedereen toegankelijk (default-allow)." }), roleModules.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Er zijn nog geen modules geregistreerd." })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Module" }), _jsx(TableHead, { className: "w-48", children: "Toegang afdwingen" })] }) }), _jsx(TableBody, { children: roleModules.map((mod) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-medium", children: mod.name }), _jsx(TableCell, { children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Switch, { id: `enforce-${mod.id}`, checked: mod.enforce ?? false, onCheckedChange: (checked) => handleEnforceChange(mod.id, checked), disabled: submitting || !isAdmin }), _jsx(Label, { htmlFor: `enforce-${mod.id}`, className: "text-sm font-normal", children: (mod.enforce ?? false) ? "Ingeschakeld" : "Uitgeschakeld" })] }) })] }, mod.id))) })] }))] }), isAdmin && (_jsxs(TabsContent, { value: "profiles", className: "mt-4 space-y-6", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Een bedrijfsprofiel bundelt de rollen en modules van \u00E9\u00E9n organisatie. Het gekozen profiel bepaalt welke medewerkers en rollen je hierboven ziet." }), profilesLoading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Laden..." })) : orgProfiles.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Er zijn nog geen bedrijfsprofielen." })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Profiel" }), _jsx(TableHead, { children: "Bedrijf" }), _jsx(TableHead, { className: "w-24", children: "Rollen" }), _jsx(TableHead, { className: "w-24", children: "Modules" }), _jsx(TableHead, { className: "w-28", children: "Gebruikers" }), _jsx(TableHead, { className: "w-16 text-right", children: "Acties" })] }) }), _jsx(TableBody, { children: orgProfiles.map((profile) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-medium", children: _jsxs("div", { className: "flex items-center gap-2", children: [profile.name, profile.isDefault && _jsx(Badge, { variant: "secondary", children: "Standaard" })] }) }), _jsx(TableCell, { className: "text-muted-foreground", children: profile.tenantName ?? "-" }), _jsx(TableCell, { children: profile.roleIds.length }), _jsx(TableCell, { children: profile.moduleIds.length }), _jsx(TableCell, { children: profile.userCount }), _jsx(TableCell, { className: "text-right", children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", children: _jsx(MoreVertical, { className: "h-4 w-4" }) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsxs(DropdownMenuItem, { onClick: () => openProfileEditor(profile), children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), "Bewerken"] }), _jsxs(DropdownMenuItem, { onClick: () => handleDuplicateProfile(profile), children: [_jsx(Copy, { className: "h-4 w-4 mr-2" }), "Dupliceren"] }), !profile.isDefault && (_jsxs(DropdownMenuItem, { onClick: () => handleSetActiveProfile(profile), children: [_jsx(Shield, { className: "h-4 w-4 mr-2" }), "Als standaard instellen"] })), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "text-destructive", disabled: profile.isDefault, onClick: () => setProfileDeleteTarget(profile), children: [_jsx(UserX, { className: "h-4 w-4 mr-2" }), "Verwijderen"] })] })] }) })] }, profile.id))) })] }))] }))] }) })] }), _jsx(Dialog, { open: profileEditor !== null, onOpenChange: (open) => !open && !profileSaving && setProfileEditor(null), children: _jsxs(DialogContent, { className: "max-w-2xl", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: profileEditor?.profileId ? "Bedrijfsprofiel bewerken" : "Bedrijfsprofiel toevoegen" }), _jsx(DialogDescription, { children: "Koppel het profiel aan een bedrijf en kies welke rollen en modules erbij horen." })] }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "profile-name", children: "Naam profiel" }), _jsx(Input, { id: "profile-name", value: profileEditor?.name ?? "", placeholder: "Bijvoorbeeld: Installatie Techniek BV", onChange: (e) => setProfileEditor((state) => (state ? { ...state, name: e.target.value } : state)), disabled: profileSaving })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Bedrijf" }), _jsxs(Select, { value: profileEditor?.tenantId ? profileEditor.tenantId : "none", onValueChange: (value) => setProfileEditor((state) => state ? { ...state, tenantId: value === "none" ? "" : value } : state), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies een bedrijf" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "none", children: "Geen bedrijf (alle gebruikers)" }), tenants.map((tenant) => (_jsx(SelectItem, { value: tenant.id, children: tenant.name }, tenant.id)))] })] })] }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Rollen in dit profiel" }), _jsx("div", { className: "max-h-56 space-y-2 overflow-y-auto rounded-md border p-3", children: allRolesForProfile.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Geen rollen beschikbaar." })) : (allRolesForProfile.map((role) => (_jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx(Checkbox, { checked: profileEditor?.roleIds.includes(role.id) ?? false, onCheckedChange: () => toggleProfileValue("roleIds", role.id), disabled: profileSaving }), role.name] }, role.id)))) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Modules in dit profiel" }), _jsx("div", { className: "max-h-56 space-y-2 overflow-y-auto rounded-md border p-3", children: roleModules.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Geen modules geregistreerd." })) : (roleModules.map((mod) => (_jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx(Checkbox, { checked: profileEditor?.moduleIds.includes(mod.id) ?? false, onCheckedChange: () => toggleProfileValue("moduleIds", mod.id), disabled: profileSaving }), mod.name] }, mod.id)))) })] })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setProfileEditor(null), disabled: profileSaving, children: "Annuleren" }), _jsx(Button, { onClick: handleSaveProfile, disabled: profileSaving || !profileEditor?.name.trim(), children: profileSaving ? "Opslaan..." : "Opslaan" })] })] }) }), _jsx(AlertDialog, { open: profileDeleteTarget !== null, onOpenChange: (open) => !open && setProfileDeleteTarget(null), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Bedrijfsprofiel verwijderen?" }), _jsxs(AlertDialogDescription, { children: ["Weet je zeker dat je \"", profileDeleteTarget?.name, "\" wilt verwijderen? Rollen en gebruikers blijven bestaan, alleen de bundeling verdwijnt."] })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { onClick: handleDeleteProfile, children: "Verwijderen" })] })] }) }), _jsx(Dialog, { open: editor !== null, onOpenChange: (open) => !open && !editorSaving && setEditor(null), children: _jsxs(DialogContent, { className: "max-w-3xl", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: editor?.roleId ? "Rol bewerken" : "Rol toevoegen" }), _jsx(DialogDescription, { children: "Maak hier een rol aan en wijs de gewenste rechten toe. Vink je een onderdeel niet aan? Dan heeft de gebruiker geen toegang tot (een deel van) die pagina." })] }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "role-name", children: "Naam rol" }), _jsx(Input, { id: "role-name", value: editor?.name ?? "", placeholder: "Vul een naam voor de rol in.", onChange: (e) => setEditor((state) => (state ? { ...state, name: e.target.value } : state)), disabled: editorSaving })] }), _jsxs("div", { className: "max-h-[45vh] overflow-y-auto rounded-md border", children: [_jsxs("div", { className: "flex items-center justify-end gap-8 border-b px-4 py-2 text-sm font-medium", children: [_jsx("span", { className: "w-16 text-center", children: "Lezen" }), _jsx("span", { className: "w-16 text-center", children: "Bewerken" })] }), roleModules.length === 0 ? (_jsx("p", { className: "p-4 text-sm text-muted-foreground", children: "Er zijn nog geen modules geregistreerd voor deze shell." })) : (roleModules.map((mod) => {
|
|
443
600
|
const cell = editor?.cells[permCellKey(mod.id, "")] ?? {
|
|
444
601
|
canView: false,
|
|
445
602
|
canEdit: false,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface ModuleAccessGuardProps {
|
|
2
|
+
children: React.ReactNode;
|
|
3
|
+
featureId?: string;
|
|
4
|
+
fallback?: React.ReactNode;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Rendert alleen kinderen als de ingelogde gebruiker toegang heeft tot de
|
|
8
|
+
* huidige module. Tijdens laden een skeleton; bij onvoldoende recht een nette
|
|
9
|
+
* "geen toegang"-kaart. Modules die niet geregistreerd zijn worden altijd
|
|
10
|
+
* toegestaan (default-allow), zodat bestaande shells zonder aanpassingen
|
|
11
|
+
* blijven werken.
|
|
12
|
+
*/
|
|
13
|
+
export declare function ModuleAccessGuard({ children, featureId, fallback }: ModuleAccessGuardProps): import("react").JSX.Element;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=ModuleAccessGuard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModuleAccessGuard.d.ts","sourceRoot":"","sources":["../../src/components/ModuleAccessGuard.tsx"],"names":[],"mappings":"AAKA,UAAU,sBAAsB;IAC9B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAYD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,QAAQ,EAAE,SAAc,EAAE,QAAQ,EAAE,EAAE,sBAAsB,+BAwC/F"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRouterState } from "@tanstack/react-router";
|
|
3
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@flowselections/core";
|
|
4
|
+
import { ShieldAlert } from "lucide-react";
|
|
5
|
+
import { useModulePermission } from "../hooks/useModulePermission";
|
|
6
|
+
const EXCLUDED_SEGMENTS = new Set(["admin-installatietechniek", "instellingen"]);
|
|
7
|
+
function deriveModuleId(pathname) {
|
|
8
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
9
|
+
const first = segments[0];
|
|
10
|
+
if (!first || first.startsWith("$") || first.startsWith("_"))
|
|
11
|
+
return null;
|
|
12
|
+
if (EXCLUDED_SEGMENTS.has(first))
|
|
13
|
+
return null;
|
|
14
|
+
return first;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Rendert alleen kinderen als de ingelogde gebruiker toegang heeft tot de
|
|
18
|
+
* huidige module. Tijdens laden een skeleton; bij onvoldoende recht een nette
|
|
19
|
+
* "geen toegang"-kaart. Modules die niet geregistreerd zijn worden altijd
|
|
20
|
+
* toegestaan (default-allow), zodat bestaande shells zonder aanpassingen
|
|
21
|
+
* blijven werken.
|
|
22
|
+
*/
|
|
23
|
+
export function ModuleAccessGuard({ children, featureId = "", fallback }) {
|
|
24
|
+
const pathname = useRouterState({ select: (s) => s.location.pathname });
|
|
25
|
+
const moduleId = deriveModuleId(pathname);
|
|
26
|
+
// Als we geen module kunnen afleiden (bijv. admin of instellingen), toon
|
|
27
|
+
// altijd de inhoud.
|
|
28
|
+
if (!moduleId)
|
|
29
|
+
return _jsx(_Fragment, { children: children });
|
|
30
|
+
const { permission, loading, canView } = useModulePermission(moduleId, featureId);
|
|
31
|
+
if (loading) {
|
|
32
|
+
return (_jsxs("div", { className: "flex-1 p-6", children: [_jsx("div", { className: "h-6 w-48 animate-pulse rounded bg-muted mb-4" }), _jsx("div", { className: "h-32 animate-pulse rounded bg-muted" })] }));
|
|
33
|
+
}
|
|
34
|
+
if (!canView) {
|
|
35
|
+
if (fallback)
|
|
36
|
+
return _jsx(_Fragment, { children: fallback });
|
|
37
|
+
return (_jsx("div", { className: "flex-1 flex items-center justify-center p-6", children: _jsxs(Card, { className: "max-w-md w-full", children: [_jsxs(CardHeader, { className: "flex flex-row items-center gap-3", children: [_jsx(ShieldAlert, { className: "h-6 w-6 text-destructive" }), _jsx(CardTitle, { children: "Geen toegang" })] }), _jsx(CardContent, { children: _jsx("p", { className: "text-sm text-muted-foreground", children: "Je hebt geen toegang tot deze module. Neem contact op met een beheerder als je denkt dat dit niet klopt." }) })] }) }));
|
|
38
|
+
}
|
|
39
|
+
return _jsx(_Fragment, { children: children });
|
|
40
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import type { ModulePermission } from "../lib/permission-types";
|
|
2
|
+
export type { ModulePermission };
|
|
2
3
|
/**
|
|
3
4
|
* Haalt de effectieve rechten van de ingelogde gebruiker op voor alle
|
|
4
5
|
* geregistreerde modules en onderdelen. Persoonlijke uitzonderingen winnen
|
|
5
|
-
* van rolrechten.
|
|
6
|
+
* van rolrechten. Gedeelde cache via React Query; één keer per sessie.
|
|
6
7
|
*/
|
|
7
8
|
export declare function useMyModulePermissions(): {
|
|
8
|
-
permissions: Record<string, ModulePermission
|
|
9
|
+
permissions: NoInfer<Record<string, ModulePermission>>;
|
|
9
10
|
loading: boolean;
|
|
10
11
|
};
|
|
11
12
|
/** Effectief recht voor één module of onderdeel: "none" | "view" | "edit". */
|
|
@@ -15,4 +16,10 @@ export declare function useModulePermission(moduleId: string, featureId?: string
|
|
|
15
16
|
canEdit: boolean;
|
|
16
17
|
loading: boolean;
|
|
17
18
|
};
|
|
19
|
+
/** Invalidatie-aanhaker voor auth-wijzigingen. */
|
|
20
|
+
export declare function invalidateModulePermissions(queryClient: {
|
|
21
|
+
invalidateQueries: (options: {
|
|
22
|
+
queryKey: string[];
|
|
23
|
+
}) => Promise<void>;
|
|
24
|
+
}): Promise<void>;
|
|
18
25
|
//# sourceMappingURL=useModulePermission.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useModulePermission.d.ts","sourceRoot":"","sources":["../../src/hooks/useModulePermission.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useModulePermission.d.ts","sourceRoot":"","sources":["../../src/hooks/useModulePermission.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,YAAY,EAAE,gBAAgB,EAAE,CAAC;AAMjC;;;;GAIG;AACH,wBAAgB,sBAAsB;;;EAmBrC;AAED,8EAA8E;AAC9E,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,SAAK;;;;;EAgBnE;AAED,kDAAkD;AAClD,wBAAgB,2BAA2B,CAAC,WAAW,EAAE;IACvD,iBAAiB,EAAE,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvE,iBAEA"}
|
|
@@ -1,42 +1,41 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
2
|
import { supabase } from "../integrations/supabase/client";
|
|
3
3
|
const cellKey = (moduleId, featureId) => `${moduleId}::${featureId}`;
|
|
4
|
+
const permissionQueryKey = ["my-module-permissions"];
|
|
4
5
|
/**
|
|
5
6
|
* Haalt de effectieve rechten van de ingelogde gebruiker op voor alle
|
|
6
7
|
* geregistreerde modules en onderdelen. Persoonlijke uitzonderingen winnen
|
|
7
|
-
* van rolrechten.
|
|
8
|
+
* van rolrechten. Gedeelde cache via React Query; één keer per sessie.
|
|
8
9
|
*/
|
|
9
10
|
export function useMyModulePermissions() {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
let cancelled = false;
|
|
14
|
-
(async () => {
|
|
11
|
+
const { data, isLoading } = useQuery({
|
|
12
|
+
queryKey: permissionQueryKey,
|
|
13
|
+
queryFn: async () => {
|
|
15
14
|
const { data, error } = await supabase.rpc("my_module_permissions");
|
|
16
|
-
if (
|
|
17
|
-
|
|
15
|
+
if (error)
|
|
16
|
+
throw error;
|
|
18
17
|
const rows = Array.isArray(data)
|
|
19
18
|
? data
|
|
20
19
|
: [];
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
map[cellKey(row.module_id, row.feature_id ?? "")] = row.permission;
|
|
25
|
-
}
|
|
26
|
-
setPermissions(map);
|
|
20
|
+
const map = {};
|
|
21
|
+
for (const row of rows) {
|
|
22
|
+
map[cellKey(row.module_id, row.feature_id ?? "")] = row.permission;
|
|
27
23
|
}
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}, []);
|
|
34
|
-
return { permissions, loading };
|
|
24
|
+
return map;
|
|
25
|
+
},
|
|
26
|
+
staleTime: 5 * 60 * 1000,
|
|
27
|
+
});
|
|
28
|
+
return { permissions: data ?? {}, loading: isLoading };
|
|
35
29
|
}
|
|
36
30
|
/** Effectief recht voor één module of onderdeel: "none" | "view" | "edit". */
|
|
37
31
|
export function useModulePermission(moduleId, featureId = "") {
|
|
38
32
|
const { permissions, loading } = useMyModulePermissions();
|
|
39
|
-
const
|
|
33
|
+
const exact = permissions[cellKey(moduleId, featureId)];
|
|
34
|
+
const moduleFallback = permissions[cellKey(moduleId, "")];
|
|
35
|
+
// Niet geregistreerde modules zijn default toegestaan (edit) zodat
|
|
36
|
+
// bestaande shells blijven werken totdat een beheerder expliciet rechten
|
|
37
|
+
// instelt en enforce aanzet.
|
|
38
|
+
const permission = exact ?? moduleFallback ?? "edit";
|
|
40
39
|
return {
|
|
41
40
|
permission,
|
|
42
41
|
canView: permission === "view" || permission === "edit",
|
|
@@ -44,3 +43,7 @@ export function useModulePermission(moduleId, featureId = "") {
|
|
|
44
43
|
loading,
|
|
45
44
|
};
|
|
46
45
|
}
|
|
46
|
+
/** Invalidatie-aanhaker voor auth-wijzigingen. */
|
|
47
|
+
export function invalidateModulePermissions(queryClient) {
|
|
48
|
+
return queryClient.invalidateQueries({ queryKey: permissionQueryKey });
|
|
49
|
+
}
|
package/dist-lib/index.d.ts
CHANGED
|
@@ -6,5 +6,7 @@ export { useModulePermission, useMyModulePermissions } from './hooks/useModulePe
|
|
|
6
6
|
export type { ModulePermission } from './hooks/useModulePermission';
|
|
7
7
|
export { useDiscoveredModules } from './lib/moduleDiscovery';
|
|
8
8
|
export type { DiscoveredModule, DiscoveredFeature } from './lib/moduleDiscovery';
|
|
9
|
+
export { ModuleAccessGuard } from './components/ModuleAccessGuard';
|
|
10
|
+
export { requireModulePermission } from './lib/permissions.server';
|
|
9
11
|
export declare const adminInstallatietechniekModule: FlowModule;
|
|
10
12
|
//# sourceMappingURL=index.d.ts.map
|
package/dist-lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,2BAA2B,EAAE,MAAM,0CAA0C,CAAC;AACvF,YAAY,EAAE,gCAAgC,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AACnH,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAC1F,YAAY,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,2BAA2B,EAAE,MAAM,0CAA0C,CAAC;AACvF,YAAY,EAAE,gCAAgC,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AACnH,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAC1F,YAAY,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAInE,eAAO,MAAM,8BAA8B,EAAE,UAY5C,CAAC"}
|
package/dist-lib/index.js
CHANGED
|
@@ -6,6 +6,8 @@ export * from './_core-safelist';
|
|
|
6
6
|
export { AdminForInstallatieTechniek } from './components/AdminForInstallatieTechniek';
|
|
7
7
|
export { useModulePermission, useMyModulePermissions } from './hooks/useModulePermission';
|
|
8
8
|
export { useDiscoveredModules } from './lib/moduleDiscovery';
|
|
9
|
+
export { ModuleAccessGuard } from './components/ModuleAccessGuard';
|
|
10
|
+
export { requireModulePermission } from './lib/permissions.server';
|
|
9
11
|
export const adminInstallatietechniekModule = {
|
|
10
12
|
id: "admin-installatietechniek",
|
|
11
13
|
name: "Admin Installatietechniek",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-attacher.d.ts","sourceRoot":"","sources":["../../../src/integrations/supabase/auth-attacher.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,kBAAkB,
|
|
1
|
+
{"version":3,"file":"auth-attacher.d.ts","sourceRoot":"","sources":["../../../src/integrations/supabase/auth-attacher.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,kBAAkB,mHAY9B,CAAA"}
|
|
@@ -7,6 +7,10 @@ export const attachSupabaseAuth = createMiddleware({ type: 'function' }).client(
|
|
|
7
7
|
const { data } = await supabase.auth.getSession();
|
|
8
8
|
const token = data.session?.access_token;
|
|
9
9
|
return next({
|
|
10
|
-
|
|
10
|
+
// `x-sb-auth` is een fallback: sommige preview-proxies overschrijven de
|
|
11
|
+
// Authorization-header, waardoor de server een ongeldig token zag.
|
|
12
|
+
headers: token
|
|
13
|
+
? { Authorization: `Bearer ${token}`, 'x-sb-auth': token }
|
|
14
|
+
: {},
|
|
11
15
|
});
|
|
12
16
|
});
|