@flowselections/admin-installatietechniek 1.0.3 → 1.0.5
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 +4 -0
- package/dist-lib/components/AdminForInstallatieTechniek.d.ts.map +1 -1
- package/dist-lib/components/AdminForInstallatieTechniek.js +223 -197
- 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 +11 -4
- package/dist-lib/hooks/useModulePermission.d.ts.map +1 -1
- package/dist-lib/hooks/useModulePermission.js +33 -26
- package/dist-lib/index.d.ts +3 -3
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +2 -1
- 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 +978 -133
- 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 +978 -133
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +978 -133
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +987 -137
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/admin.functions.d.ts +78644 -2655
- package/dist-lib/lib/admin.functions.d.ts.map +1 -1
- package/dist-lib/lib/admin.functions.js +351 -174
- package/dist-lib/lib/moduleDiscovery.d.ts +7 -2
- package/dist-lib/lib/moduleDiscovery.d.ts.map +1 -1
- package/dist-lib/lib/moduleDiscovery.js +26 -11
- 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 +1 -1
- package/dist-lib/hooks/useModuleOrder.d.ts +0 -30
- package/dist-lib/hooks/useModuleOrder.d.ts.map +0 -1
- package/dist-lib/hooks/useModuleOrder.js +0 -99
|
@@ -3,83 +3,121 @@ import { getRequestHost } from "@tanstack/react-start/server";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { requireSupabaseAuth } from "../integrations/supabase/auth-middleware";
|
|
5
5
|
import { requireAdmin, requireUserAdmin } from "../lib/admin.server";
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const featureSchema = z.object({
|
|
7
|
+
id: z.string().trim().min(1).max(200),
|
|
8
|
+
name: z.string().trim().min(1).max(200),
|
|
9
|
+
});
|
|
9
10
|
const syncModulesInput = z.object({
|
|
10
11
|
shellId: z.string().trim().min(1).max(100).optional(),
|
|
11
12
|
modules: z
|
|
12
13
|
.array(z.object({
|
|
13
14
|
id: z.string().trim().min(1).max(100),
|
|
14
15
|
name: z.string().trim().min(1).max(120),
|
|
16
|
+
features: z.array(featureSchema).max(200).optional(),
|
|
15
17
|
}))
|
|
16
18
|
.max(200),
|
|
17
19
|
});
|
|
20
|
+
const permissionSchema = z.enum(["none", "view", "edit"]);
|
|
18
21
|
const setUserPermissionInput = z.object({
|
|
19
22
|
userId: z.string().uuid(),
|
|
20
23
|
moduleId: z.string().min(1),
|
|
24
|
+
featureId: z.string().max(200).default(""),
|
|
21
25
|
permission: z.union([permissionSchema, z.literal("inherit")]),
|
|
22
26
|
});
|
|
23
27
|
const userPermissionsInput = z.object({
|
|
24
28
|
userId: z.string().uuid(),
|
|
25
29
|
});
|
|
26
|
-
const
|
|
27
|
-
moduleIds: z.array(z.string().trim().min(1).max(100)).max(200),
|
|
28
|
-
});
|
|
30
|
+
const roleIdInput = z.object({ roleId: z.string().uuid() });
|
|
29
31
|
const assignRoleInput = z.object({
|
|
30
32
|
userId: z.string().uuid(),
|
|
31
|
-
|
|
33
|
+
roleId: z.string().uuid(),
|
|
32
34
|
});
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
const rolePermissionsSchema = z
|
|
36
|
+
.array(z.object({
|
|
37
|
+
moduleId: z.string().trim().min(1).max(100),
|
|
38
|
+
featureId: z.string().trim().max(200).default(""),
|
|
39
|
+
canView: z.boolean(),
|
|
40
|
+
canEdit: z.boolean(),
|
|
41
|
+
}))
|
|
42
|
+
.max(2000);
|
|
43
|
+
const createRoleInput = z.object({
|
|
44
|
+
name: z.string().trim().min(1).max(80),
|
|
45
|
+
permissions: rolePermissionsSchema.default([]),
|
|
36
46
|
});
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
const updateRoleInput = z.object({
|
|
48
|
+
roleId: z.string().uuid(),
|
|
49
|
+
name: z.string().trim().min(1).max(80),
|
|
50
|
+
permissions: rolePermissionsSchema.default([]),
|
|
41
51
|
});
|
|
42
52
|
const inviteUserInput = z.object({
|
|
43
53
|
email: z.string().trim().email().max(255),
|
|
44
54
|
displayName: z.string().trim().min(1).max(100),
|
|
45
|
-
|
|
55
|
+
roleId: z.string().uuid(),
|
|
46
56
|
});
|
|
47
57
|
const deleteUserInput = z.object({
|
|
48
58
|
userId: z.string().uuid(),
|
|
49
59
|
});
|
|
60
|
+
const updateUserNameInput = z.object({
|
|
61
|
+
userId: z.string().uuid(),
|
|
62
|
+
displayName: z.string().trim().min(1).max(100),
|
|
63
|
+
});
|
|
50
64
|
const createTestUserInput = z.object({
|
|
51
65
|
displayName: z.string().trim().min(1).max(100),
|
|
52
|
-
|
|
66
|
+
roleId: z.string().uuid(),
|
|
53
67
|
email: z.string().trim().email().max(255).optional(),
|
|
54
68
|
});
|
|
55
|
-
function
|
|
56
|
-
return
|
|
69
|
+
function slugify(value) {
|
|
70
|
+
return (value
|
|
71
|
+
.toLowerCase()
|
|
72
|
+
.normalize("NFD")
|
|
73
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
74
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
75
|
+
.replace(/^-+|-+$/g, "")
|
|
76
|
+
.slice(0, 60) || `rol-${Date.now()}`);
|
|
77
|
+
}
|
|
78
|
+
async function fetchRole(supabase, roleId) {
|
|
79
|
+
const { data, error } = await supabase
|
|
80
|
+
.from("roles")
|
|
81
|
+
.select("id, key, name, is_protected")
|
|
82
|
+
.eq("id", roleId)
|
|
83
|
+
.maybeSingle();
|
|
84
|
+
if (error)
|
|
85
|
+
throw error;
|
|
86
|
+
if (!data)
|
|
87
|
+
throw new Error("Rol bestaat niet.");
|
|
88
|
+
return data;
|
|
89
|
+
}
|
|
90
|
+
async function assertMayUseRole(supabase, roleId, isAdmin) {
|
|
91
|
+
const role = await fetchRole(supabase, roleId);
|
|
92
|
+
if (!isAdmin && role.key === "admin") {
|
|
93
|
+
throw new Error("Alleen een admin kan de adminrol gebruiken.");
|
|
94
|
+
}
|
|
95
|
+
return role;
|
|
57
96
|
}
|
|
58
97
|
export const getUsersWithRoles = createServerFn({ method: "GET" })
|
|
59
98
|
.middleware([requireSupabaseAuth])
|
|
60
99
|
.handler(async ({ context }) => {
|
|
61
100
|
const { isAdmin } = await requireUserAdmin(context);
|
|
62
101
|
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
63
|
-
const [{ data: profiles, error: profilesError }, { data: roles, error: rolesError }, { data: authList, error: authError }] = await Promise.all([
|
|
64
|
-
supabaseAdmin
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
.order("email", { ascending: true }),
|
|
68
|
-
supabaseAdmin
|
|
69
|
-
.from("user_roles")
|
|
70
|
-
.select("id, user_id, role")
|
|
71
|
-
.order("created_at", { ascending: false }),
|
|
102
|
+
const [{ data: profiles, error: profilesError }, { data: assignments, error: assignmentsError }, { data: roles, error: rolesError }, { data: authList, error: authError },] = await Promise.all([
|
|
103
|
+
supabaseAdmin.from("profiles").select("id, email, display_name").order("email", { ascending: true }),
|
|
104
|
+
supabaseAdmin.from("role_assignments").select("user_id, role_id"),
|
|
105
|
+
supabaseAdmin.from("roles").select("id, key, name, is_protected").order("name", { ascending: true }),
|
|
72
106
|
supabaseAdmin.auth.admin.listUsers({ page: 1, perPage: 1000 }),
|
|
73
107
|
]);
|
|
74
108
|
if (profilesError)
|
|
75
109
|
throw profilesError;
|
|
110
|
+
if (assignmentsError)
|
|
111
|
+
throw assignmentsError;
|
|
76
112
|
if (rolesError)
|
|
77
113
|
throw rolesError;
|
|
78
114
|
if (authError)
|
|
79
115
|
throw authError;
|
|
80
116
|
const safeProfiles = Array.isArray(profiles) ? profiles : [];
|
|
117
|
+
const safeAssignments = Array.isArray(assignments) ? assignments : [];
|
|
81
118
|
const safeRoles = Array.isArray(roles) ? roles : [];
|
|
82
119
|
const safeAuthUsers = Array.isArray(authList?.users) ? authList.users : [];
|
|
120
|
+
const roleById = new Map(safeRoles.map((r) => [r.id, r]));
|
|
83
121
|
const confirmedById = new Map();
|
|
84
122
|
const isTestById = new Map();
|
|
85
123
|
for (const u of safeAuthUsers) {
|
|
@@ -88,21 +126,28 @@ export const getUsersWithRoles = createServerFn({ method: "GET" })
|
|
|
88
126
|
isTestById.set(u.id, Boolean(meta.is_test_account));
|
|
89
127
|
}
|
|
90
128
|
const rolesByUser = new Map();
|
|
91
|
-
for (const
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
129
|
+
for (const a of safeAssignments) {
|
|
130
|
+
const role = roleById.get(a.role_id);
|
|
131
|
+
if (!role)
|
|
132
|
+
continue;
|
|
133
|
+
const list = rolesByUser.get(a.user_id) ?? [];
|
|
134
|
+
if (!list.some((r) => r.id === role.id)) {
|
|
135
|
+
list.push({ id: role.id, key: role.key, name: role.name });
|
|
136
|
+
}
|
|
137
|
+
rolesByUser.set(a.user_id, list);
|
|
95
138
|
}
|
|
96
139
|
return {
|
|
97
140
|
users: safeProfiles.map((profile) => ({
|
|
98
141
|
id: profile.id,
|
|
99
142
|
email: profile.email ?? "-",
|
|
100
143
|
displayName: profile.display_name ?? "-",
|
|
101
|
-
roles:
|
|
144
|
+
roles: rolesByUser.get(profile.id) ?? [],
|
|
102
145
|
emailConfirmed: confirmedById.get(profile.id) ?? false,
|
|
103
146
|
isTestAccount: isTestById.get(profile.id) ?? false,
|
|
104
147
|
})),
|
|
105
|
-
availableRoles:
|
|
148
|
+
availableRoles: safeRoles
|
|
149
|
+
.filter((r) => isAdmin || r.key !== "admin")
|
|
150
|
+
.map((r) => ({ id: r.id, key: r.key, name: r.name, isProtected: r.is_protected })),
|
|
106
151
|
isAdmin,
|
|
107
152
|
};
|
|
108
153
|
});
|
|
@@ -111,53 +156,208 @@ export const assignRole = createServerFn({ method: "POST" })
|
|
|
111
156
|
.inputValidator((data) => assignRoleInput.parse(data))
|
|
112
157
|
.handler(async ({ context, data }) => {
|
|
113
158
|
const { isAdmin } = await requireUserAdmin(context);
|
|
114
|
-
|
|
115
|
-
throw new Error("Alleen een admin kan de adminrol toekennen.");
|
|
116
|
-
}
|
|
117
|
-
// Schrijft via de user-scoped client: RLS op user_roles is nu de echte gate.
|
|
159
|
+
await assertMayUseRole(context.supabase, data.roleId, isAdmin);
|
|
118
160
|
const { data: existing, error: checkError } = await context.supabase
|
|
119
|
-
.from("
|
|
161
|
+
.from("role_assignments")
|
|
120
162
|
.select("id")
|
|
121
163
|
.eq("user_id", data.userId)
|
|
122
|
-
.eq("
|
|
164
|
+
.eq("role_id", data.roleId)
|
|
123
165
|
.maybeSingle();
|
|
124
166
|
if (checkError)
|
|
125
167
|
throw checkError;
|
|
126
|
-
if (existing)
|
|
127
|
-
throw new Error(
|
|
128
|
-
}
|
|
168
|
+
if (existing)
|
|
169
|
+
throw new Error("Gebruiker heeft deze rol al.");
|
|
129
170
|
const { error } = await context.supabase
|
|
130
|
-
.from("
|
|
131
|
-
.insert({ user_id: data.userId,
|
|
171
|
+
.from("role_assignments")
|
|
172
|
+
.insert({ user_id: data.userId, role_id: data.roleId });
|
|
132
173
|
if (error)
|
|
133
174
|
throw error;
|
|
134
175
|
return { ok: true };
|
|
135
176
|
});
|
|
136
177
|
export const removeRole = createServerFn({ method: "POST" })
|
|
137
178
|
.middleware([requireSupabaseAuth])
|
|
138
|
-
.inputValidator((data) =>
|
|
179
|
+
.inputValidator((data) => assignRoleInput.parse(data))
|
|
139
180
|
.handler(async ({ context, data }) => {
|
|
140
181
|
const { isAdmin } = await requireUserAdmin(context);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
if (data.role === "admin" && data.userId === context.userId) {
|
|
182
|
+
const role = await assertMayUseRole(context.supabase, data.roleId, isAdmin);
|
|
183
|
+
if (role.key === "admin" && data.userId === context.userId) {
|
|
145
184
|
throw new Error("Je kunt je eigen adminrol niet verwijderen.");
|
|
146
185
|
}
|
|
147
|
-
// Delete via user-scoped client (RLS gate). De DB-trigger
|
|
148
|
-
// `prevent_last_admin_delete` is het definitieve vangnet voor de laatste admin.
|
|
149
186
|
const { error } = await context.supabase
|
|
150
|
-
.from("
|
|
187
|
+
.from("role_assignments")
|
|
151
188
|
.delete()
|
|
152
189
|
.eq("user_id", data.userId)
|
|
153
|
-
.eq("
|
|
190
|
+
.eq("role_id", data.roleId);
|
|
191
|
+
if (error)
|
|
192
|
+
throw error;
|
|
193
|
+
return { ok: true };
|
|
194
|
+
});
|
|
195
|
+
/** Alle rollen met hun rechten, plus de geregistreerde modules en onderdelen. */
|
|
196
|
+
export const getRoles = createServerFn({ method: "GET" })
|
|
197
|
+
.middleware([requireSupabaseAuth])
|
|
198
|
+
.handler(async ({ context }) => {
|
|
199
|
+
await requireUserAdmin(context);
|
|
200
|
+
const [{ data: roles, error: rolesError }, { data: perms, error: permsError }, { data: registry, error: registryError }, { data: featureRows, error: featuresError },] = await Promise.all([
|
|
201
|
+
context.supabase.from("roles").select("id, key, name, is_protected").order("name", { ascending: true }),
|
|
202
|
+
context.supabase.from("role_permissions").select("role_id, module_id, feature_id, can_view, can_edit"),
|
|
203
|
+
context.supabase.from("module_registry").select("module_id, name, is_active, enforce").order("name", { ascending: true }),
|
|
204
|
+
context.supabase.from("module_features").select("module_id, feature_id, name, is_active").order("name", { ascending: true }),
|
|
205
|
+
]);
|
|
206
|
+
if (rolesError)
|
|
207
|
+
throw rolesError;
|
|
208
|
+
if (permsError)
|
|
209
|
+
throw permsError;
|
|
210
|
+
if (registryError)
|
|
211
|
+
throw registryError;
|
|
212
|
+
if (featuresError)
|
|
213
|
+
throw featuresError;
|
|
214
|
+
const safeRoles = Array.isArray(roles) ? roles : [];
|
|
215
|
+
const safePerms = Array.isArray(perms) ? perms : [];
|
|
216
|
+
const safeRegistry = Array.isArray(registry) ? registry : [];
|
|
217
|
+
const safeFeatures = Array.isArray(featureRows) ? featureRows : [];
|
|
218
|
+
return {
|
|
219
|
+
roles: safeRoles.map((r) => ({
|
|
220
|
+
id: r.id,
|
|
221
|
+
key: r.key,
|
|
222
|
+
name: r.name,
|
|
223
|
+
isProtected: r.is_protected,
|
|
224
|
+
})),
|
|
225
|
+
modules: safeRegistry
|
|
226
|
+
.filter((m) => m.is_active)
|
|
227
|
+
.map((m) => ({
|
|
228
|
+
id: m.module_id,
|
|
229
|
+
name: m.name,
|
|
230
|
+
enforce: Boolean(m.enforce),
|
|
231
|
+
features: safeFeatures
|
|
232
|
+
.filter((f) => f.is_active && f.module_id === m.module_id)
|
|
233
|
+
.map((f) => ({ id: f.feature_id, name: f.name })),
|
|
234
|
+
})),
|
|
235
|
+
permissions: safePerms.map((p) => ({
|
|
236
|
+
roleId: p.role_id,
|
|
237
|
+
moduleId: p.module_id,
|
|
238
|
+
featureId: p.feature_id ?? "",
|
|
239
|
+
canView: Boolean(p.can_view),
|
|
240
|
+
canEdit: Boolean(p.can_edit),
|
|
241
|
+
})),
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
async function writeRolePermissions(supabase, roleId, permissions) {
|
|
245
|
+
const { error: delError } = await supabase.from("role_permissions").delete().eq("role_id", roleId);
|
|
246
|
+
if (delError)
|
|
247
|
+
throw delError;
|
|
248
|
+
const rows = permissions
|
|
249
|
+
.filter((p) => p.canView || p.canEdit)
|
|
250
|
+
.map((p) => ({
|
|
251
|
+
role_id: roleId,
|
|
252
|
+
module_id: p.moduleId,
|
|
253
|
+
feature_id: p.featureId ?? "",
|
|
254
|
+
can_view: p.canView || p.canEdit,
|
|
255
|
+
can_edit: p.canEdit,
|
|
256
|
+
updated_at: new Date().toISOString(),
|
|
257
|
+
}));
|
|
258
|
+
if (rows.length === 0)
|
|
259
|
+
return;
|
|
260
|
+
const { error } = await supabase.from("role_permissions").insert(rows);
|
|
261
|
+
if (error)
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
export const createRole = createServerFn({ method: "POST" })
|
|
265
|
+
.middleware([requireSupabaseAuth])
|
|
266
|
+
.inputValidator((data) => createRoleInput.parse(data))
|
|
267
|
+
.handler(async ({ context, data }) => {
|
|
268
|
+
await requireAdmin(context);
|
|
269
|
+
const baseKey = slugify(data.name);
|
|
270
|
+
let key = baseKey;
|
|
271
|
+
for (let i = 2; i < 50; i++) {
|
|
272
|
+
const { data: taken, error } = await context.supabase
|
|
273
|
+
.from("roles")
|
|
274
|
+
.select("id")
|
|
275
|
+
.eq("key", key)
|
|
276
|
+
.maybeSingle();
|
|
277
|
+
if (error)
|
|
278
|
+
throw error;
|
|
279
|
+
if (!taken)
|
|
280
|
+
break;
|
|
281
|
+
key = `${baseKey}-${i}`;
|
|
282
|
+
}
|
|
283
|
+
const { data: created, error } = await context.supabase
|
|
284
|
+
.from("roles")
|
|
285
|
+
.insert({ key, name: data.name, is_protected: false })
|
|
286
|
+
.select("id")
|
|
287
|
+
.single();
|
|
288
|
+
if (error)
|
|
289
|
+
throw error;
|
|
290
|
+
await writeRolePermissions(context.supabase, created.id, data.permissions);
|
|
291
|
+
return { ok: true, roleId: created.id };
|
|
292
|
+
});
|
|
293
|
+
export const updateRole = createServerFn({ method: "POST" })
|
|
294
|
+
.middleware([requireSupabaseAuth])
|
|
295
|
+
.inputValidator((data) => updateRoleInput.parse(data))
|
|
296
|
+
.handler(async ({ context, data }) => {
|
|
297
|
+
await requireAdmin(context);
|
|
298
|
+
const { error } = await context.supabase
|
|
299
|
+
.from("roles")
|
|
300
|
+
.update({ name: data.name, updated_at: new Date().toISOString() })
|
|
301
|
+
.eq("id", data.roleId);
|
|
302
|
+
if (error)
|
|
303
|
+
throw error;
|
|
304
|
+
await writeRolePermissions(context.supabase, data.roleId, data.permissions);
|
|
305
|
+
return { ok: true };
|
|
306
|
+
});
|
|
307
|
+
export const duplicateRole = createServerFn({ method: "POST" })
|
|
308
|
+
.middleware([requireSupabaseAuth])
|
|
309
|
+
.inputValidator((data) => roleIdInput.parse(data))
|
|
310
|
+
.handler(async ({ context, data }) => {
|
|
311
|
+
await requireAdmin(context);
|
|
312
|
+
const role = await fetchRole(context.supabase, data.roleId);
|
|
313
|
+
const { data: perms, error: permsError } = await context.supabase
|
|
314
|
+
.from("role_permissions")
|
|
315
|
+
.select("module_id, feature_id, can_view, can_edit")
|
|
316
|
+
.eq("role_id", data.roleId);
|
|
317
|
+
if (permsError)
|
|
318
|
+
throw permsError;
|
|
319
|
+
const name = `${role.name} (kopie)`;
|
|
320
|
+
const baseKey = slugify(name);
|
|
321
|
+
let key = baseKey;
|
|
322
|
+
for (let i = 2; i < 50; i++) {
|
|
323
|
+
const { data: taken } = await context.supabase.from("roles").select("id").eq("key", key).maybeSingle();
|
|
324
|
+
if (!taken)
|
|
325
|
+
break;
|
|
326
|
+
key = `${baseKey}-${i}`;
|
|
327
|
+
}
|
|
328
|
+
const { data: created, error } = await context.supabase
|
|
329
|
+
.from("roles")
|
|
330
|
+
.insert({ key, name, is_protected: false })
|
|
331
|
+
.select("id")
|
|
332
|
+
.single();
|
|
333
|
+
if (error)
|
|
334
|
+
throw error;
|
|
335
|
+
const safePerms = Array.isArray(perms) ? perms : [];
|
|
336
|
+
await writeRolePermissions(context.supabase, created.id, safePerms.map((p) => ({
|
|
337
|
+
moduleId: p.module_id,
|
|
338
|
+
featureId: p.feature_id ?? "",
|
|
339
|
+
canView: Boolean(p.can_view),
|
|
340
|
+
canEdit: Boolean(p.can_edit),
|
|
341
|
+
})));
|
|
342
|
+
return { ok: true, roleId: created.id };
|
|
343
|
+
});
|
|
344
|
+
export const deleteRole = createServerFn({ method: "POST" })
|
|
345
|
+
.middleware([requireSupabaseAuth])
|
|
346
|
+
.inputValidator((data) => roleIdInput.parse(data))
|
|
347
|
+
.handler(async ({ context, data }) => {
|
|
348
|
+
await requireAdmin(context);
|
|
349
|
+
const role = await fetchRole(context.supabase, data.roleId);
|
|
350
|
+
if (role.is_protected) {
|
|
351
|
+
throw new Error("Deze rol is beschermd en kan niet verwijderd worden.");
|
|
352
|
+
}
|
|
353
|
+
const { error } = await context.supabase.from("roles").delete().eq("id", data.roleId);
|
|
154
354
|
if (error)
|
|
155
355
|
throw error;
|
|
156
356
|
return { ok: true };
|
|
157
357
|
});
|
|
158
358
|
/**
|
|
159
|
-
* Registreert de modules die de shell heeft geladen.
|
|
160
|
-
*
|
|
359
|
+
* Registreert de modules (en hun onderdelen) die de shell heeft geladen.
|
|
360
|
+
* Wat niet meer voorkomt blijft bestaan maar wordt op inactief gezet.
|
|
161
361
|
*/
|
|
162
362
|
export const syncModuleRegistry = createServerFn({ method: "POST" })
|
|
163
363
|
.middleware([requireSupabaseAuth])
|
|
@@ -177,7 +377,6 @@ export const syncModuleRegistry = createServerFn({ method: "POST" })
|
|
|
177
377
|
})), { onConflict: "module_id" });
|
|
178
378
|
if (error)
|
|
179
379
|
throw error;
|
|
180
|
-
// Modules van deze shell die niet meer geleverd worden → inactief.
|
|
181
380
|
if (data.shellId) {
|
|
182
381
|
const ids = modules.map((m) => m.id);
|
|
183
382
|
const { error: deactivateError } = await context.supabase
|
|
@@ -188,117 +387,88 @@ export const syncModuleRegistry = createServerFn({ method: "POST" })
|
|
|
188
387
|
if (deactivateError)
|
|
189
388
|
throw deactivateError;
|
|
190
389
|
}
|
|
390
|
+
// Onderdelen (subpagina's) synchroniseren.
|
|
391
|
+
const featureRows = modules.flatMap((m) => (m.features ?? []).map((f) => ({
|
|
392
|
+
module_id: m.id,
|
|
393
|
+
feature_id: f.id,
|
|
394
|
+
name: f.name,
|
|
395
|
+
is_active: true,
|
|
396
|
+
last_seen_at: now,
|
|
397
|
+
})));
|
|
398
|
+
if (featureRows.length > 0) {
|
|
399
|
+
const { error: featError } = await context.supabase
|
|
400
|
+
.from("module_features")
|
|
401
|
+
.upsert(featureRows, { onConflict: "module_id,feature_id" });
|
|
402
|
+
if (featError)
|
|
403
|
+
throw featError;
|
|
404
|
+
}
|
|
405
|
+
for (const m of modules) {
|
|
406
|
+
const ids = (m.features ?? []).map((f) => f.id);
|
|
407
|
+
let query = context.supabase
|
|
408
|
+
.from("module_features")
|
|
409
|
+
.update({ is_active: false })
|
|
410
|
+
.eq("module_id", m.id);
|
|
411
|
+
if (ids.length > 0) {
|
|
412
|
+
query = query.not("feature_id", "in", `(${ids.map((i) => `"${i}"`).join(",")})`);
|
|
413
|
+
}
|
|
414
|
+
const { error: deactivateFeatError } = await query;
|
|
415
|
+
if (deactivateFeatError)
|
|
416
|
+
throw deactivateFeatError;
|
|
417
|
+
}
|
|
191
418
|
return { ok: true, count: modules.length };
|
|
192
419
|
});
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
*/
|
|
197
|
-
export const getModuleOrder = createServerFn({ method: "GET" })
|
|
198
|
-
.middleware([requireSupabaseAuth])
|
|
199
|
-
.handler(async ({ context }) => {
|
|
200
|
-
const { data, error } = await context.supabase
|
|
201
|
-
.from("module_registry")
|
|
202
|
-
.select("module_id, name, sort_order, is_active")
|
|
203
|
-
.eq("is_active", true);
|
|
204
|
-
if (error)
|
|
205
|
-
throw error;
|
|
206
|
-
const safe = Array.isArray(data) ? data : [];
|
|
207
|
-
// Rijen zonder sort_order blijven `null`: de client bepaalt hun plek op
|
|
208
|
-
// basis van de bronvolgorde van de shell (geen alfabetische fallback).
|
|
209
|
-
return {
|
|
210
|
-
modules: safe
|
|
211
|
-
.map((row) => ({
|
|
212
|
-
id: row.module_id,
|
|
213
|
-
name: row.name,
|
|
214
|
-
sortOrder: typeof row.sort_order === "number" ? row.sort_order : null,
|
|
215
|
-
}))
|
|
216
|
-
.sort((a, b) => {
|
|
217
|
-
const ao = a.sortOrder ?? Number.MAX_SAFE_INTEGER;
|
|
218
|
-
const bo = b.sortOrder ?? Number.MAX_SAFE_INTEGER;
|
|
219
|
-
return ao - bo;
|
|
220
|
-
}),
|
|
221
|
-
};
|
|
420
|
+
const setModuleEnforceInput = z.object({
|
|
421
|
+
moduleId: z.string().trim().min(1).max(100),
|
|
422
|
+
enforce: z.boolean(),
|
|
222
423
|
});
|
|
223
|
-
|
|
224
|
-
export const setModuleOrder = createServerFn({ method: "POST" })
|
|
424
|
+
export const setModuleEnforce = createServerFn({ method: "POST" })
|
|
225
425
|
.middleware([requireSupabaseAuth])
|
|
226
|
-
.inputValidator((data) =>
|
|
426
|
+
.inputValidator((data) => setModuleEnforceInput.parse(data))
|
|
227
427
|
.handler(async ({ context, data }) => {
|
|
228
428
|
await requireAdmin(context);
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
.update({ sort_order: i })
|
|
234
|
-
.eq("module_id", ids[i]);
|
|
235
|
-
if (error)
|
|
236
|
-
throw error;
|
|
237
|
-
}
|
|
238
|
-
return { ok: true, count: ids.length };
|
|
239
|
-
});
|
|
240
|
-
export const getModulePermissions = createServerFn({ method: "GET" })
|
|
241
|
-
.middleware([requireSupabaseAuth])
|
|
242
|
-
.handler(async ({ context }) => {
|
|
243
|
-
await requireAdmin(context);
|
|
244
|
-
const [{ data, error }, { data: registry, error: registryError }] = await Promise.all([
|
|
245
|
-
context.supabase.from("module_permissions").select("role, module_id, permission"),
|
|
246
|
-
context.supabase
|
|
247
|
-
.from("module_registry")
|
|
248
|
-
.select("module_id, name, is_active")
|
|
249
|
-
.order("name", { ascending: true }),
|
|
250
|
-
]);
|
|
429
|
+
const { error } = await context.supabase
|
|
430
|
+
.from("module_registry")
|
|
431
|
+
.update({ enforce: data.enforce })
|
|
432
|
+
.eq("module_id", data.moduleId);
|
|
251
433
|
if (error)
|
|
252
434
|
throw error;
|
|
253
|
-
|
|
254
|
-
throw registryError;
|
|
255
|
-
const safe = Array.isArray(data) ? data : [];
|
|
256
|
-
const safeRegistry = Array.isArray(registry) ? registry : [];
|
|
257
|
-
return {
|
|
258
|
-
roles: APP_ROLES,
|
|
259
|
-
modules: safeRegistry
|
|
260
|
-
.filter((row) => row.is_active)
|
|
261
|
-
.map((row) => ({
|
|
262
|
-
id: row.module_id,
|
|
263
|
-
name: row.name,
|
|
264
|
-
})),
|
|
265
|
-
permissions: safe.map((row) => ({
|
|
266
|
-
role: row.role,
|
|
267
|
-
moduleId: row.module_id,
|
|
268
|
-
permission: row.permission,
|
|
269
|
-
})),
|
|
270
|
-
};
|
|
435
|
+
return { ok: true };
|
|
271
436
|
});
|
|
272
437
|
export const getUserModulePermissions = createServerFn({ method: "POST" })
|
|
273
438
|
.middleware([requireSupabaseAuth])
|
|
274
439
|
.inputValidator((data) => userPermissionsInput.parse(data))
|
|
275
440
|
.handler(async ({ context, data }) => {
|
|
276
441
|
await requireUserAdmin(context);
|
|
277
|
-
const [{ data: overrides, error }, { data: registry, error: registryError }] = await Promise.all([
|
|
442
|
+
const [{ data: overrides, error }, { data: registry, error: registryError }, { data: featureRows, error: featuresError },] = await Promise.all([
|
|
278
443
|
context.supabase
|
|
279
444
|
.from("user_module_permissions")
|
|
280
|
-
.select("module_id, permission")
|
|
445
|
+
.select("module_id, feature_id, permission")
|
|
281
446
|
.eq("user_id", data.userId),
|
|
282
|
-
context.supabase
|
|
283
|
-
|
|
284
|
-
.select("module_id, name, is_active")
|
|
285
|
-
.order("name", { ascending: true }),
|
|
447
|
+
context.supabase.from("module_registry").select("module_id, name, is_active").order("name", { ascending: true }),
|
|
448
|
+
context.supabase.from("module_features").select("module_id, feature_id, name, is_active").order("name", { ascending: true }),
|
|
286
449
|
]);
|
|
287
450
|
if (error)
|
|
288
451
|
throw error;
|
|
289
452
|
if (registryError)
|
|
290
453
|
throw registryError;
|
|
454
|
+
if (featuresError)
|
|
455
|
+
throw featuresError;
|
|
291
456
|
const safeOverrides = Array.isArray(overrides) ? overrides : [];
|
|
292
457
|
const safeRegistry = Array.isArray(registry) ? registry : [];
|
|
458
|
+
const safeFeatures = Array.isArray(featureRows) ? featureRows : [];
|
|
293
459
|
return {
|
|
294
460
|
modules: safeRegistry
|
|
295
461
|
.filter((row) => row.is_active)
|
|
296
462
|
.map((row) => ({
|
|
297
463
|
id: row.module_id,
|
|
298
464
|
name: row.name,
|
|
465
|
+
features: safeFeatures
|
|
466
|
+
.filter((f) => f.is_active && f.module_id === row.module_id)
|
|
467
|
+
.map((f) => ({ id: f.feature_id, name: f.name })),
|
|
299
468
|
})),
|
|
300
469
|
overrides: safeOverrides.map((row) => ({
|
|
301
470
|
moduleId: row.module_id,
|
|
471
|
+
featureId: row.feature_id ?? "",
|
|
302
472
|
permission: row.permission,
|
|
303
473
|
})),
|
|
304
474
|
};
|
|
@@ -308,12 +478,14 @@ export const setUserModulePermission = createServerFn({ method: "POST" })
|
|
|
308
478
|
.inputValidator((data) => setUserPermissionInput.parse(data))
|
|
309
479
|
.handler(async ({ context, data }) => {
|
|
310
480
|
await requireUserAdmin(context);
|
|
481
|
+
const featureId = data.featureId ?? "";
|
|
311
482
|
if (data.permission === "inherit") {
|
|
312
483
|
const { error } = await context.supabase
|
|
313
484
|
.from("user_module_permissions")
|
|
314
485
|
.delete()
|
|
315
486
|
.eq("user_id", data.userId)
|
|
316
|
-
.eq("module_id", data.moduleId)
|
|
487
|
+
.eq("module_id", data.moduleId)
|
|
488
|
+
.eq("feature_id", featureId);
|
|
317
489
|
if (error)
|
|
318
490
|
throw error;
|
|
319
491
|
return { ok: true };
|
|
@@ -321,27 +493,10 @@ export const setUserModulePermission = createServerFn({ method: "POST" })
|
|
|
321
493
|
const { error } = await context.supabase.from("user_module_permissions").upsert({
|
|
322
494
|
user_id: data.userId,
|
|
323
495
|
module_id: data.moduleId,
|
|
496
|
+
feature_id: featureId,
|
|
324
497
|
permission: data.permission,
|
|
325
498
|
updated_at: new Date().toISOString(),
|
|
326
|
-
}, { onConflict: "user_id,module_id" });
|
|
327
|
-
if (error)
|
|
328
|
-
throw error;
|
|
329
|
-
return { ok: true };
|
|
330
|
-
});
|
|
331
|
-
export const setModulePermission = createServerFn({ method: "POST" })
|
|
332
|
-
.middleware([requireSupabaseAuth])
|
|
333
|
-
.inputValidator((data) => setPermissionInput.parse(data))
|
|
334
|
-
.handler(async ({ context, data }) => {
|
|
335
|
-
await requireAdmin(context);
|
|
336
|
-
// Upsert via user-scoped client — RLS write-policy op module_permissions
|
|
337
|
-
// vereist admin (via JWT-claim of has_role-fallback).
|
|
338
|
-
const { error } = await context.supabase
|
|
339
|
-
.from("module_permissions")
|
|
340
|
-
.upsert({
|
|
341
|
-
role: data.role,
|
|
342
|
-
module_id: data.moduleId,
|
|
343
|
-
permission: data.permission,
|
|
344
|
-
}, { onConflict: "role,module_id" });
|
|
499
|
+
}, { onConflict: "user_id,module_id,feature_id" });
|
|
345
500
|
if (error)
|
|
346
501
|
throw error;
|
|
347
502
|
return { ok: true };
|
|
@@ -351,9 +506,7 @@ export const inviteUser = createServerFn({ method: "POST" })
|
|
|
351
506
|
.inputValidator((data) => inviteUserInput.parse(data))
|
|
352
507
|
.handler(async ({ context, data }) => {
|
|
353
508
|
const { isAdmin } = await requireUserAdmin(context);
|
|
354
|
-
|
|
355
|
-
throw new Error("Alleen een admin kan een account met de adminrol aanmaken.");
|
|
356
|
-
}
|
|
509
|
+
await assertMayUseRole(context.supabase, data.roleId, isAdmin);
|
|
357
510
|
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
358
511
|
let redirectTo;
|
|
359
512
|
try {
|
|
@@ -366,10 +519,7 @@ export const inviteUser = createServerFn({ method: "POST" })
|
|
|
366
519
|
catch {
|
|
367
520
|
redirectTo = undefined;
|
|
368
521
|
}
|
|
369
|
-
const { data: invited, error: inviteError } = await supabaseAdmin.auth.admin.inviteUserByEmail(data.email, {
|
|
370
|
-
data: { display_name: data.displayName },
|
|
371
|
-
redirectTo,
|
|
372
|
-
});
|
|
522
|
+
const { data: invited, error: inviteError } = await supabaseAdmin.auth.admin.inviteUserByEmail(data.email, { data: { display_name: data.displayName }, redirectTo });
|
|
373
523
|
if (inviteError || !invited?.user) {
|
|
374
524
|
const msg = inviteError?.message ?? "Uitnodigen mislukt";
|
|
375
525
|
throw new Error(/already|exists|registered/i.test(msg) ? "Deze gebruiker bestaat al." : msg);
|
|
@@ -381,8 +531,8 @@ export const inviteUser = createServerFn({ method: "POST" })
|
|
|
381
531
|
if (profileError)
|
|
382
532
|
throw profileError;
|
|
383
533
|
const { error: roleError } = await supabaseAdmin
|
|
384
|
-
.from("
|
|
385
|
-
.upsert({ user_id: userId,
|
|
534
|
+
.from("role_assignments")
|
|
535
|
+
.upsert({ user_id: userId, role_id: data.roleId }, { onConflict: "user_id,role_id", ignoreDuplicates: true });
|
|
386
536
|
if (roleError)
|
|
387
537
|
throw roleError;
|
|
388
538
|
return { ok: true, userId };
|
|
@@ -397,13 +547,13 @@ export const deleteUser = createServerFn({ method: "POST" })
|
|
|
397
547
|
}
|
|
398
548
|
if (!isAdmin) {
|
|
399
549
|
const { data: targetRoles, error: targetError } = await context.supabase
|
|
400
|
-
.from("
|
|
401
|
-
.select("
|
|
550
|
+
.from("role_assignments")
|
|
551
|
+
.select("roles(key)")
|
|
402
552
|
.eq("user_id", data.userId);
|
|
403
553
|
if (targetError)
|
|
404
554
|
throw targetError;
|
|
405
555
|
const safeTargetRoles = Array.isArray(targetRoles) ? targetRoles : [];
|
|
406
|
-
if (safeTargetRoles.some((r) => r
|
|
556
|
+
if (safeTargetRoles.some((r) => r?.roles?.key === "admin")) {
|
|
407
557
|
throw new Error("Alleen een admin kan een adminaccount verwijderen.");
|
|
408
558
|
}
|
|
409
559
|
}
|
|
@@ -413,6 +563,37 @@ export const deleteUser = createServerFn({ method: "POST" })
|
|
|
413
563
|
throw error;
|
|
414
564
|
return { ok: true };
|
|
415
565
|
});
|
|
566
|
+
export const updateUserName = createServerFn({ method: "POST" })
|
|
567
|
+
.middleware([requireSupabaseAuth])
|
|
568
|
+
.inputValidator((data) => updateUserNameInput.parse(data))
|
|
569
|
+
.handler(async ({ context, data }) => {
|
|
570
|
+
const { isAdmin } = await requireUserAdmin(context);
|
|
571
|
+
if (!isAdmin && data.userId !== context.userId) {
|
|
572
|
+
const { data: targetRoles, error: targetError } = await context.supabase
|
|
573
|
+
.from("role_assignments")
|
|
574
|
+
.select("roles(key)")
|
|
575
|
+
.eq("user_id", data.userId);
|
|
576
|
+
if (targetError)
|
|
577
|
+
throw targetError;
|
|
578
|
+
const safeTargetRoles = Array.isArray(targetRoles) ? targetRoles : [];
|
|
579
|
+
if (safeTargetRoles.some((r) => r?.roles?.key === "admin")) {
|
|
580
|
+
throw new Error("Alleen een admin kan een adminaccount aanpassen.");
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
584
|
+
const { error: authError } = await supabaseAdmin.auth.admin.updateUserById(data.userId, {
|
|
585
|
+
user_metadata: { display_name: data.displayName },
|
|
586
|
+
});
|
|
587
|
+
if (authError)
|
|
588
|
+
throw authError;
|
|
589
|
+
const { error } = await supabaseAdmin
|
|
590
|
+
.from("profiles")
|
|
591
|
+
.update({ display_name: data.displayName })
|
|
592
|
+
.eq("id", data.userId);
|
|
593
|
+
if (error)
|
|
594
|
+
throw error;
|
|
595
|
+
return { ok: true };
|
|
596
|
+
});
|
|
416
597
|
function generatePassword(length = 16) {
|
|
417
598
|
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%*";
|
|
418
599
|
const bytes = new Uint8Array(length);
|
|
@@ -427,12 +608,9 @@ export const createTestUser = createServerFn({ method: "POST" })
|
|
|
427
608
|
.inputValidator((data) => createTestUserInput.parse(data))
|
|
428
609
|
.handler(async ({ context, data }) => {
|
|
429
610
|
const { isAdmin } = await requireUserAdmin(context);
|
|
430
|
-
|
|
431
|
-
throw new Error("Alleen een admin kan een account met de adminrol aanmaken.");
|
|
432
|
-
}
|
|
611
|
+
await assertMayUseRole(context.supabase, data.roleId, isAdmin);
|
|
433
612
|
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
434
|
-
const email = data.email?.trim() ||
|
|
435
|
-
`test-${Date.now()}-${Math.floor(Math.random() * 1000)}@test.local`;
|
|
613
|
+
const email = data.email?.trim() || `test-${Date.now()}-${Math.floor(Math.random() * 1000)}@test.local`;
|
|
436
614
|
const password = generatePassword(16);
|
|
437
615
|
const { data: created, error: createError } = await supabaseAdmin.auth.admin.createUser({
|
|
438
616
|
email,
|
|
@@ -445,15 +623,14 @@ export const createTestUser = createServerFn({ method: "POST" })
|
|
|
445
623
|
throw new Error(/already|exists|registered/i.test(msg) ? "Deze gebruiker bestaat al." : msg);
|
|
446
624
|
}
|
|
447
625
|
const userId = created.user.id;
|
|
448
|
-
// Trigger vult profiles al; toch upsert voor zekerheid (idempotent).
|
|
449
626
|
const { error: profileError } = await supabaseAdmin
|
|
450
627
|
.from("profiles")
|
|
451
628
|
.upsert({ id: userId, email, display_name: data.displayName }, { onConflict: "id" });
|
|
452
629
|
if (profileError)
|
|
453
630
|
throw profileError;
|
|
454
631
|
const { error: roleError } = await supabaseAdmin
|
|
455
|
-
.from("
|
|
456
|
-
.upsert({ user_id: userId,
|
|
632
|
+
.from("role_assignments")
|
|
633
|
+
.upsert({ user_id: userId, role_id: data.roleId }, { onConflict: "user_id,role_id", ignoreDuplicates: true });
|
|
457
634
|
if (roleError)
|
|
458
635
|
throw roleError;
|
|
459
636
|
return { ok: true, userId, email, password };
|