@flowselections/admin-installatietechniek 1.0.3 → 1.0.4
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 +212 -197
- package/dist-lib/hooks/useModulePermission.d.ts +4 -3
- package/dist-lib/hooks/useModulePermission.d.ts.map +1 -1
- package/dist-lib/hooks/useModulePermission.js +10 -6
- package/dist-lib/index.d.ts +1 -3
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +0 -1
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +407 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +407 -0
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +407 -0
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +411 -0
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/admin.functions.d.ts +48897 -1136
- package/dist-lib/lib/admin.functions.d.ts.map +1 -1
- package/dist-lib/lib/admin.functions.js +340 -181
- 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/package.json +87 -87
- 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,207 @@ 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").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
|
+
features: safeFeatures
|
|
231
|
+
.filter((f) => f.is_active && f.module_id === m.module_id)
|
|
232
|
+
.map((f) => ({ id: f.feature_id, name: f.name })),
|
|
233
|
+
})),
|
|
234
|
+
permissions: safePerms.map((p) => ({
|
|
235
|
+
roleId: p.role_id,
|
|
236
|
+
moduleId: p.module_id,
|
|
237
|
+
featureId: p.feature_id ?? "",
|
|
238
|
+
canView: Boolean(p.can_view),
|
|
239
|
+
canEdit: Boolean(p.can_edit),
|
|
240
|
+
})),
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
async function writeRolePermissions(supabase, roleId, permissions) {
|
|
244
|
+
const { error: delError } = await supabase.from("role_permissions").delete().eq("role_id", roleId);
|
|
245
|
+
if (delError)
|
|
246
|
+
throw delError;
|
|
247
|
+
const rows = permissions
|
|
248
|
+
.filter((p) => p.canView || p.canEdit)
|
|
249
|
+
.map((p) => ({
|
|
250
|
+
role_id: roleId,
|
|
251
|
+
module_id: p.moduleId,
|
|
252
|
+
feature_id: p.featureId ?? "",
|
|
253
|
+
can_view: p.canView || p.canEdit,
|
|
254
|
+
can_edit: p.canEdit,
|
|
255
|
+
updated_at: new Date().toISOString(),
|
|
256
|
+
}));
|
|
257
|
+
if (rows.length === 0)
|
|
258
|
+
return;
|
|
259
|
+
const { error } = await supabase.from("role_permissions").insert(rows);
|
|
260
|
+
if (error)
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
export const createRole = createServerFn({ method: "POST" })
|
|
264
|
+
.middleware([requireSupabaseAuth])
|
|
265
|
+
.inputValidator((data) => createRoleInput.parse(data))
|
|
266
|
+
.handler(async ({ context, data }) => {
|
|
267
|
+
await requireAdmin(context);
|
|
268
|
+
const baseKey = slugify(data.name);
|
|
269
|
+
let key = baseKey;
|
|
270
|
+
for (let i = 2; i < 50; i++) {
|
|
271
|
+
const { data: taken, error } = await context.supabase
|
|
272
|
+
.from("roles")
|
|
273
|
+
.select("id")
|
|
274
|
+
.eq("key", key)
|
|
275
|
+
.maybeSingle();
|
|
276
|
+
if (error)
|
|
277
|
+
throw error;
|
|
278
|
+
if (!taken)
|
|
279
|
+
break;
|
|
280
|
+
key = `${baseKey}-${i}`;
|
|
281
|
+
}
|
|
282
|
+
const { data: created, error } = await context.supabase
|
|
283
|
+
.from("roles")
|
|
284
|
+
.insert({ key, name: data.name, is_protected: false })
|
|
285
|
+
.select("id")
|
|
286
|
+
.single();
|
|
287
|
+
if (error)
|
|
288
|
+
throw error;
|
|
289
|
+
await writeRolePermissions(context.supabase, created.id, data.permissions);
|
|
290
|
+
return { ok: true, roleId: created.id };
|
|
291
|
+
});
|
|
292
|
+
export const updateRole = createServerFn({ method: "POST" })
|
|
293
|
+
.middleware([requireSupabaseAuth])
|
|
294
|
+
.inputValidator((data) => updateRoleInput.parse(data))
|
|
295
|
+
.handler(async ({ context, data }) => {
|
|
296
|
+
await requireAdmin(context);
|
|
297
|
+
const { error } = await context.supabase
|
|
298
|
+
.from("roles")
|
|
299
|
+
.update({ name: data.name, updated_at: new Date().toISOString() })
|
|
300
|
+
.eq("id", data.roleId);
|
|
301
|
+
if (error)
|
|
302
|
+
throw error;
|
|
303
|
+
await writeRolePermissions(context.supabase, data.roleId, data.permissions);
|
|
304
|
+
return { ok: true };
|
|
305
|
+
});
|
|
306
|
+
export const duplicateRole = createServerFn({ method: "POST" })
|
|
307
|
+
.middleware([requireSupabaseAuth])
|
|
308
|
+
.inputValidator((data) => roleIdInput.parse(data))
|
|
309
|
+
.handler(async ({ context, data }) => {
|
|
310
|
+
await requireAdmin(context);
|
|
311
|
+
const role = await fetchRole(context.supabase, data.roleId);
|
|
312
|
+
const { data: perms, error: permsError } = await context.supabase
|
|
313
|
+
.from("role_permissions")
|
|
314
|
+
.select("module_id, feature_id, can_view, can_edit")
|
|
315
|
+
.eq("role_id", data.roleId);
|
|
316
|
+
if (permsError)
|
|
317
|
+
throw permsError;
|
|
318
|
+
const name = `${role.name} (kopie)`;
|
|
319
|
+
const baseKey = slugify(name);
|
|
320
|
+
let key = baseKey;
|
|
321
|
+
for (let i = 2; i < 50; i++) {
|
|
322
|
+
const { data: taken } = await context.supabase.from("roles").select("id").eq("key", key).maybeSingle();
|
|
323
|
+
if (!taken)
|
|
324
|
+
break;
|
|
325
|
+
key = `${baseKey}-${i}`;
|
|
326
|
+
}
|
|
327
|
+
const { data: created, error } = await context.supabase
|
|
328
|
+
.from("roles")
|
|
329
|
+
.insert({ key, name, is_protected: false })
|
|
330
|
+
.select("id")
|
|
331
|
+
.single();
|
|
332
|
+
if (error)
|
|
333
|
+
throw error;
|
|
334
|
+
const safePerms = Array.isArray(perms) ? perms : [];
|
|
335
|
+
await writeRolePermissions(context.supabase, created.id, safePerms.map((p) => ({
|
|
336
|
+
moduleId: p.module_id,
|
|
337
|
+
featureId: p.feature_id ?? "",
|
|
338
|
+
canView: Boolean(p.can_view),
|
|
339
|
+
canEdit: Boolean(p.can_edit),
|
|
340
|
+
})));
|
|
341
|
+
return { ok: true, roleId: created.id };
|
|
342
|
+
});
|
|
343
|
+
export const deleteRole = createServerFn({ method: "POST" })
|
|
344
|
+
.middleware([requireSupabaseAuth])
|
|
345
|
+
.inputValidator((data) => roleIdInput.parse(data))
|
|
346
|
+
.handler(async ({ context, data }) => {
|
|
347
|
+
await requireAdmin(context);
|
|
348
|
+
const role = await fetchRole(context.supabase, data.roleId);
|
|
349
|
+
if (role.is_protected) {
|
|
350
|
+
throw new Error("Deze rol is beschermd en kan niet verwijderd worden.");
|
|
351
|
+
}
|
|
352
|
+
const { error } = await context.supabase.from("roles").delete().eq("id", data.roleId);
|
|
154
353
|
if (error)
|
|
155
354
|
throw error;
|
|
156
355
|
return { ok: true };
|
|
157
356
|
});
|
|
158
357
|
/**
|
|
159
|
-
* Registreert de modules die de shell heeft geladen.
|
|
160
|
-
*
|
|
358
|
+
* Registreert de modules (en hun onderdelen) die de shell heeft geladen.
|
|
359
|
+
* Wat niet meer voorkomt blijft bestaan maar wordt op inactief gezet.
|
|
161
360
|
*/
|
|
162
361
|
export const syncModuleRegistry = createServerFn({ method: "POST" })
|
|
163
362
|
.middleware([requireSupabaseAuth])
|
|
@@ -177,7 +376,6 @@ export const syncModuleRegistry = createServerFn({ method: "POST" })
|
|
|
177
376
|
})), { onConflict: "module_id" });
|
|
178
377
|
if (error)
|
|
179
378
|
throw error;
|
|
180
|
-
// Modules van deze shell die niet meer geleverd worden → inactief.
|
|
181
379
|
if (data.shellId) {
|
|
182
380
|
const ids = modules.map((m) => m.id);
|
|
183
381
|
const { error: deactivateError } = await context.supabase
|
|
@@ -188,117 +386,71 @@ export const syncModuleRegistry = createServerFn({ method: "POST" })
|
|
|
188
386
|
if (deactivateError)
|
|
189
387
|
throw deactivateError;
|
|
190
388
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
};
|
|
222
|
-
});
|
|
223
|
-
/** Slaat de volgorde op: index in de aangeleverde lijst wordt sort_order. */
|
|
224
|
-
export const setModuleOrder = createServerFn({ method: "POST" })
|
|
225
|
-
.middleware([requireSupabaseAuth])
|
|
226
|
-
.inputValidator((data) => moduleOrderInput.parse(data))
|
|
227
|
-
.handler(async ({ context, data }) => {
|
|
228
|
-
await requireAdmin(context);
|
|
229
|
-
const ids = Array.isArray(data.moduleIds) ? data.moduleIds : [];
|
|
230
|
-
for (let i = 0; i < ids.length; i++) {
|
|
231
|
-
const { error } = await context.supabase
|
|
232
|
-
.from("module_registry")
|
|
233
|
-
.update({ sort_order: i })
|
|
234
|
-
.eq("module_id", ids[i]);
|
|
235
|
-
if (error)
|
|
236
|
-
throw error;
|
|
389
|
+
// Onderdelen (subpagina's) synchroniseren.
|
|
390
|
+
const featureRows = modules.flatMap((m) => (m.features ?? []).map((f) => ({
|
|
391
|
+
module_id: m.id,
|
|
392
|
+
feature_id: f.id,
|
|
393
|
+
name: f.name,
|
|
394
|
+
is_active: true,
|
|
395
|
+
last_seen_at: now,
|
|
396
|
+
})));
|
|
397
|
+
if (featureRows.length > 0) {
|
|
398
|
+
const { error: featError } = await context.supabase
|
|
399
|
+
.from("module_features")
|
|
400
|
+
.upsert(featureRows, { onConflict: "module_id,feature_id" });
|
|
401
|
+
if (featError)
|
|
402
|
+
throw featError;
|
|
237
403
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
throw error;
|
|
253
|
-
if (registryError)
|
|
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
|
-
};
|
|
404
|
+
for (const m of modules) {
|
|
405
|
+
const ids = (m.features ?? []).map((f) => f.id);
|
|
406
|
+
let query = context.supabase
|
|
407
|
+
.from("module_features")
|
|
408
|
+
.update({ is_active: false })
|
|
409
|
+
.eq("module_id", m.id);
|
|
410
|
+
if (ids.length > 0) {
|
|
411
|
+
query = query.not("feature_id", "in", `(${ids.map((i) => `"${i}"`).join(",")})`);
|
|
412
|
+
}
|
|
413
|
+
const { error: deactivateFeatError } = await query;
|
|
414
|
+
if (deactivateFeatError)
|
|
415
|
+
throw deactivateFeatError;
|
|
416
|
+
}
|
|
417
|
+
return { ok: true, count: modules.length };
|
|
271
418
|
});
|
|
272
419
|
export const getUserModulePermissions = createServerFn({ method: "POST" })
|
|
273
420
|
.middleware([requireSupabaseAuth])
|
|
274
421
|
.inputValidator((data) => userPermissionsInput.parse(data))
|
|
275
422
|
.handler(async ({ context, data }) => {
|
|
276
423
|
await requireUserAdmin(context);
|
|
277
|
-
const [{ data: overrides, error }, { data: registry, error: registryError }] = await Promise.all([
|
|
424
|
+
const [{ data: overrides, error }, { data: registry, error: registryError }, { data: featureRows, error: featuresError },] = await Promise.all([
|
|
278
425
|
context.supabase
|
|
279
426
|
.from("user_module_permissions")
|
|
280
|
-
.select("module_id, permission")
|
|
427
|
+
.select("module_id, feature_id, permission")
|
|
281
428
|
.eq("user_id", data.userId),
|
|
282
|
-
context.supabase
|
|
283
|
-
|
|
284
|
-
.select("module_id, name, is_active")
|
|
285
|
-
.order("name", { ascending: true }),
|
|
429
|
+
context.supabase.from("module_registry").select("module_id, name, is_active").order("name", { ascending: true }),
|
|
430
|
+
context.supabase.from("module_features").select("module_id, feature_id, name, is_active").order("name", { ascending: true }),
|
|
286
431
|
]);
|
|
287
432
|
if (error)
|
|
288
433
|
throw error;
|
|
289
434
|
if (registryError)
|
|
290
435
|
throw registryError;
|
|
436
|
+
if (featuresError)
|
|
437
|
+
throw featuresError;
|
|
291
438
|
const safeOverrides = Array.isArray(overrides) ? overrides : [];
|
|
292
439
|
const safeRegistry = Array.isArray(registry) ? registry : [];
|
|
440
|
+
const safeFeatures = Array.isArray(featureRows) ? featureRows : [];
|
|
293
441
|
return {
|
|
294
442
|
modules: safeRegistry
|
|
295
443
|
.filter((row) => row.is_active)
|
|
296
444
|
.map((row) => ({
|
|
297
445
|
id: row.module_id,
|
|
298
446
|
name: row.name,
|
|
447
|
+
features: safeFeatures
|
|
448
|
+
.filter((f) => f.is_active && f.module_id === row.module_id)
|
|
449
|
+
.map((f) => ({ id: f.feature_id, name: f.name })),
|
|
299
450
|
})),
|
|
300
451
|
overrides: safeOverrides.map((row) => ({
|
|
301
452
|
moduleId: row.module_id,
|
|
453
|
+
featureId: row.feature_id ?? "",
|
|
302
454
|
permission: row.permission,
|
|
303
455
|
})),
|
|
304
456
|
};
|
|
@@ -308,12 +460,14 @@ export const setUserModulePermission = createServerFn({ method: "POST" })
|
|
|
308
460
|
.inputValidator((data) => setUserPermissionInput.parse(data))
|
|
309
461
|
.handler(async ({ context, data }) => {
|
|
310
462
|
await requireUserAdmin(context);
|
|
463
|
+
const featureId = data.featureId ?? "";
|
|
311
464
|
if (data.permission === "inherit") {
|
|
312
465
|
const { error } = await context.supabase
|
|
313
466
|
.from("user_module_permissions")
|
|
314
467
|
.delete()
|
|
315
468
|
.eq("user_id", data.userId)
|
|
316
|
-
.eq("module_id", data.moduleId)
|
|
469
|
+
.eq("module_id", data.moduleId)
|
|
470
|
+
.eq("feature_id", featureId);
|
|
317
471
|
if (error)
|
|
318
472
|
throw error;
|
|
319
473
|
return { ok: true };
|
|
@@ -321,27 +475,10 @@ export const setUserModulePermission = createServerFn({ method: "POST" })
|
|
|
321
475
|
const { error } = await context.supabase.from("user_module_permissions").upsert({
|
|
322
476
|
user_id: data.userId,
|
|
323
477
|
module_id: data.moduleId,
|
|
478
|
+
feature_id: featureId,
|
|
324
479
|
permission: data.permission,
|
|
325
480
|
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" });
|
|
481
|
+
}, { onConflict: "user_id,module_id,feature_id" });
|
|
345
482
|
if (error)
|
|
346
483
|
throw error;
|
|
347
484
|
return { ok: true };
|
|
@@ -351,9 +488,7 @@ export const inviteUser = createServerFn({ method: "POST" })
|
|
|
351
488
|
.inputValidator((data) => inviteUserInput.parse(data))
|
|
352
489
|
.handler(async ({ context, data }) => {
|
|
353
490
|
const { isAdmin } = await requireUserAdmin(context);
|
|
354
|
-
|
|
355
|
-
throw new Error("Alleen een admin kan een account met de adminrol aanmaken.");
|
|
356
|
-
}
|
|
491
|
+
await assertMayUseRole(context.supabase, data.roleId, isAdmin);
|
|
357
492
|
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
358
493
|
let redirectTo;
|
|
359
494
|
try {
|
|
@@ -366,10 +501,7 @@ export const inviteUser = createServerFn({ method: "POST" })
|
|
|
366
501
|
catch {
|
|
367
502
|
redirectTo = undefined;
|
|
368
503
|
}
|
|
369
|
-
const { data: invited, error: inviteError } = await supabaseAdmin.auth.admin.inviteUserByEmail(data.email, {
|
|
370
|
-
data: { display_name: data.displayName },
|
|
371
|
-
redirectTo,
|
|
372
|
-
});
|
|
504
|
+
const { data: invited, error: inviteError } = await supabaseAdmin.auth.admin.inviteUserByEmail(data.email, { data: { display_name: data.displayName }, redirectTo });
|
|
373
505
|
if (inviteError || !invited?.user) {
|
|
374
506
|
const msg = inviteError?.message ?? "Uitnodigen mislukt";
|
|
375
507
|
throw new Error(/already|exists|registered/i.test(msg) ? "Deze gebruiker bestaat al." : msg);
|
|
@@ -381,8 +513,8 @@ export const inviteUser = createServerFn({ method: "POST" })
|
|
|
381
513
|
if (profileError)
|
|
382
514
|
throw profileError;
|
|
383
515
|
const { error: roleError } = await supabaseAdmin
|
|
384
|
-
.from("
|
|
385
|
-
.upsert({ user_id: userId,
|
|
516
|
+
.from("role_assignments")
|
|
517
|
+
.upsert({ user_id: userId, role_id: data.roleId }, { onConflict: "user_id,role_id", ignoreDuplicates: true });
|
|
386
518
|
if (roleError)
|
|
387
519
|
throw roleError;
|
|
388
520
|
return { ok: true, userId };
|
|
@@ -397,13 +529,13 @@ export const deleteUser = createServerFn({ method: "POST" })
|
|
|
397
529
|
}
|
|
398
530
|
if (!isAdmin) {
|
|
399
531
|
const { data: targetRoles, error: targetError } = await context.supabase
|
|
400
|
-
.from("
|
|
401
|
-
.select("
|
|
532
|
+
.from("role_assignments")
|
|
533
|
+
.select("roles(key)")
|
|
402
534
|
.eq("user_id", data.userId);
|
|
403
535
|
if (targetError)
|
|
404
536
|
throw targetError;
|
|
405
537
|
const safeTargetRoles = Array.isArray(targetRoles) ? targetRoles : [];
|
|
406
|
-
if (safeTargetRoles.some((r) => r
|
|
538
|
+
if (safeTargetRoles.some((r) => r?.roles?.key === "admin")) {
|
|
407
539
|
throw new Error("Alleen een admin kan een adminaccount verwijderen.");
|
|
408
540
|
}
|
|
409
541
|
}
|
|
@@ -413,6 +545,37 @@ export const deleteUser = createServerFn({ method: "POST" })
|
|
|
413
545
|
throw error;
|
|
414
546
|
return { ok: true };
|
|
415
547
|
});
|
|
548
|
+
export const updateUserName = createServerFn({ method: "POST" })
|
|
549
|
+
.middleware([requireSupabaseAuth])
|
|
550
|
+
.inputValidator((data) => updateUserNameInput.parse(data))
|
|
551
|
+
.handler(async ({ context, data }) => {
|
|
552
|
+
const { isAdmin } = await requireUserAdmin(context);
|
|
553
|
+
if (!isAdmin && data.userId !== context.userId) {
|
|
554
|
+
const { data: targetRoles, error: targetError } = await context.supabase
|
|
555
|
+
.from("role_assignments")
|
|
556
|
+
.select("roles(key)")
|
|
557
|
+
.eq("user_id", data.userId);
|
|
558
|
+
if (targetError)
|
|
559
|
+
throw targetError;
|
|
560
|
+
const safeTargetRoles = Array.isArray(targetRoles) ? targetRoles : [];
|
|
561
|
+
if (safeTargetRoles.some((r) => r?.roles?.key === "admin")) {
|
|
562
|
+
throw new Error("Alleen een admin kan een adminaccount aanpassen.");
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
566
|
+
const { error: authError } = await supabaseAdmin.auth.admin.updateUserById(data.userId, {
|
|
567
|
+
user_metadata: { display_name: data.displayName },
|
|
568
|
+
});
|
|
569
|
+
if (authError)
|
|
570
|
+
throw authError;
|
|
571
|
+
const { error } = await supabaseAdmin
|
|
572
|
+
.from("profiles")
|
|
573
|
+
.update({ display_name: data.displayName })
|
|
574
|
+
.eq("id", data.userId);
|
|
575
|
+
if (error)
|
|
576
|
+
throw error;
|
|
577
|
+
return { ok: true };
|
|
578
|
+
});
|
|
416
579
|
function generatePassword(length = 16) {
|
|
417
580
|
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%*";
|
|
418
581
|
const bytes = new Uint8Array(length);
|
|
@@ -427,12 +590,9 @@ export const createTestUser = createServerFn({ method: "POST" })
|
|
|
427
590
|
.inputValidator((data) => createTestUserInput.parse(data))
|
|
428
591
|
.handler(async ({ context, data }) => {
|
|
429
592
|
const { isAdmin } = await requireUserAdmin(context);
|
|
430
|
-
|
|
431
|
-
throw new Error("Alleen een admin kan een account met de adminrol aanmaken.");
|
|
432
|
-
}
|
|
593
|
+
await assertMayUseRole(context.supabase, data.roleId, isAdmin);
|
|
433
594
|
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`;
|
|
595
|
+
const email = data.email?.trim() || `test-${Date.now()}-${Math.floor(Math.random() * 1000)}@test.local`;
|
|
436
596
|
const password = generatePassword(16);
|
|
437
597
|
const { data: created, error: createError } = await supabaseAdmin.auth.admin.createUser({
|
|
438
598
|
email,
|
|
@@ -445,15 +605,14 @@ export const createTestUser = createServerFn({ method: "POST" })
|
|
|
445
605
|
throw new Error(/already|exists|registered/i.test(msg) ? "Deze gebruiker bestaat al." : msg);
|
|
446
606
|
}
|
|
447
607
|
const userId = created.user.id;
|
|
448
|
-
// Trigger vult profiles al; toch upsert voor zekerheid (idempotent).
|
|
449
608
|
const { error: profileError } = await supabaseAdmin
|
|
450
609
|
.from("profiles")
|
|
451
610
|
.upsert({ id: userId, email, display_name: data.displayName }, { onConflict: "id" });
|
|
452
611
|
if (profileError)
|
|
453
612
|
throw profileError;
|
|
454
613
|
const { error: roleError } = await supabaseAdmin
|
|
455
|
-
.from("
|
|
456
|
-
.upsert({ user_id: userId,
|
|
614
|
+
.from("role_assignments")
|
|
615
|
+
.upsert({ user_id: userId, role_id: data.roleId }, { onConflict: "user_id,role_id", ignoreDuplicates: true });
|
|
457
616
|
if (roleError)
|
|
458
617
|
throw roleError;
|
|
459
618
|
return { ok: true, userId, email, password };
|