@flowselections/admin-installatietechniek 1.0.2 → 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 +211 -106
- 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 -1
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +410 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +410 -0
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +410 -0
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +414 -0
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/admin.functions.d.ts +90380 -956
- package/dist-lib/lib/admin.functions.d.ts.map +1 -1
- package/dist-lib/lib/admin.functions.js +340 -131
- 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 +25 -10
- package/package.json +87 -87
|
@@ -3,80 +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
|
});
|
|
30
|
+
const roleIdInput = z.object({ roleId: z.string().uuid() });
|
|
26
31
|
const assignRoleInput = z.object({
|
|
27
32
|
userId: z.string().uuid(),
|
|
28
|
-
|
|
33
|
+
roleId: z.string().uuid(),
|
|
29
34
|
});
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
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([]),
|
|
33
46
|
});
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
47
|
+
const updateRoleInput = z.object({
|
|
48
|
+
roleId: z.string().uuid(),
|
|
49
|
+
name: z.string().trim().min(1).max(80),
|
|
50
|
+
permissions: rolePermissionsSchema.default([]),
|
|
38
51
|
});
|
|
39
52
|
const inviteUserInput = z.object({
|
|
40
53
|
email: z.string().trim().email().max(255),
|
|
41
54
|
displayName: z.string().trim().min(1).max(100),
|
|
42
|
-
|
|
55
|
+
roleId: z.string().uuid(),
|
|
43
56
|
});
|
|
44
57
|
const deleteUserInput = z.object({
|
|
45
58
|
userId: z.string().uuid(),
|
|
46
59
|
});
|
|
60
|
+
const updateUserNameInput = z.object({
|
|
61
|
+
userId: z.string().uuid(),
|
|
62
|
+
displayName: z.string().trim().min(1).max(100),
|
|
63
|
+
});
|
|
47
64
|
const createTestUserInput = z.object({
|
|
48
65
|
displayName: z.string().trim().min(1).max(100),
|
|
49
|
-
|
|
66
|
+
roleId: z.string().uuid(),
|
|
50
67
|
email: z.string().trim().email().max(255).optional(),
|
|
51
68
|
});
|
|
52
|
-
function
|
|
53
|
-
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;
|
|
54
96
|
}
|
|
55
97
|
export const getUsersWithRoles = createServerFn({ method: "GET" })
|
|
56
98
|
.middleware([requireSupabaseAuth])
|
|
57
99
|
.handler(async ({ context }) => {
|
|
58
100
|
const { isAdmin } = await requireUserAdmin(context);
|
|
59
101
|
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
60
|
-
const [{ data: profiles, error: profilesError }, { data: roles, error: rolesError }, { data: authList, error: authError }] = await Promise.all([
|
|
61
|
-
supabaseAdmin
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
.order("email", { ascending: true }),
|
|
65
|
-
supabaseAdmin
|
|
66
|
-
.from("user_roles")
|
|
67
|
-
.select("id, user_id, role")
|
|
68
|
-
.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 }),
|
|
69
106
|
supabaseAdmin.auth.admin.listUsers({ page: 1, perPage: 1000 }),
|
|
70
107
|
]);
|
|
71
108
|
if (profilesError)
|
|
72
109
|
throw profilesError;
|
|
110
|
+
if (assignmentsError)
|
|
111
|
+
throw assignmentsError;
|
|
73
112
|
if (rolesError)
|
|
74
113
|
throw rolesError;
|
|
75
114
|
if (authError)
|
|
76
115
|
throw authError;
|
|
77
116
|
const safeProfiles = Array.isArray(profiles) ? profiles : [];
|
|
117
|
+
const safeAssignments = Array.isArray(assignments) ? assignments : [];
|
|
78
118
|
const safeRoles = Array.isArray(roles) ? roles : [];
|
|
79
119
|
const safeAuthUsers = Array.isArray(authList?.users) ? authList.users : [];
|
|
120
|
+
const roleById = new Map(safeRoles.map((r) => [r.id, r]));
|
|
80
121
|
const confirmedById = new Map();
|
|
81
122
|
const isTestById = new Map();
|
|
82
123
|
for (const u of safeAuthUsers) {
|
|
@@ -85,21 +126,28 @@ export const getUsersWithRoles = createServerFn({ method: "GET" })
|
|
|
85
126
|
isTestById.set(u.id, Boolean(meta.is_test_account));
|
|
86
127
|
}
|
|
87
128
|
const rolesByUser = new Map();
|
|
88
|
-
for (const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
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);
|
|
92
138
|
}
|
|
93
139
|
return {
|
|
94
140
|
users: safeProfiles.map((profile) => ({
|
|
95
141
|
id: profile.id,
|
|
96
142
|
email: profile.email ?? "-",
|
|
97
143
|
displayName: profile.display_name ?? "-",
|
|
98
|
-
roles:
|
|
144
|
+
roles: rolesByUser.get(profile.id) ?? [],
|
|
99
145
|
emailConfirmed: confirmedById.get(profile.id) ?? false,
|
|
100
146
|
isTestAccount: isTestById.get(profile.id) ?? false,
|
|
101
147
|
})),
|
|
102
|
-
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 })),
|
|
103
151
|
isAdmin,
|
|
104
152
|
};
|
|
105
153
|
});
|
|
@@ -108,53 +156,207 @@ export const assignRole = createServerFn({ method: "POST" })
|
|
|
108
156
|
.inputValidator((data) => assignRoleInput.parse(data))
|
|
109
157
|
.handler(async ({ context, data }) => {
|
|
110
158
|
const { isAdmin } = await requireUserAdmin(context);
|
|
111
|
-
|
|
112
|
-
throw new Error("Alleen een admin kan de adminrol toekennen.");
|
|
113
|
-
}
|
|
114
|
-
// Schrijft via de user-scoped client: RLS op user_roles is nu de echte gate.
|
|
159
|
+
await assertMayUseRole(context.supabase, data.roleId, isAdmin);
|
|
115
160
|
const { data: existing, error: checkError } = await context.supabase
|
|
116
|
-
.from("
|
|
161
|
+
.from("role_assignments")
|
|
117
162
|
.select("id")
|
|
118
163
|
.eq("user_id", data.userId)
|
|
119
|
-
.eq("
|
|
164
|
+
.eq("role_id", data.roleId)
|
|
120
165
|
.maybeSingle();
|
|
121
166
|
if (checkError)
|
|
122
167
|
throw checkError;
|
|
123
|
-
if (existing)
|
|
124
|
-
throw new Error(
|
|
125
|
-
}
|
|
168
|
+
if (existing)
|
|
169
|
+
throw new Error("Gebruiker heeft deze rol al.");
|
|
126
170
|
const { error } = await context.supabase
|
|
127
|
-
.from("
|
|
128
|
-
.insert({ user_id: data.userId,
|
|
171
|
+
.from("role_assignments")
|
|
172
|
+
.insert({ user_id: data.userId, role_id: data.roleId });
|
|
129
173
|
if (error)
|
|
130
174
|
throw error;
|
|
131
175
|
return { ok: true };
|
|
132
176
|
});
|
|
133
177
|
export const removeRole = createServerFn({ method: "POST" })
|
|
134
178
|
.middleware([requireSupabaseAuth])
|
|
135
|
-
.inputValidator((data) =>
|
|
179
|
+
.inputValidator((data) => assignRoleInput.parse(data))
|
|
136
180
|
.handler(async ({ context, data }) => {
|
|
137
181
|
const { isAdmin } = await requireUserAdmin(context);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
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) {
|
|
142
184
|
throw new Error("Je kunt je eigen adminrol niet verwijderen.");
|
|
143
185
|
}
|
|
144
|
-
// Delete via user-scoped client (RLS gate). De DB-trigger
|
|
145
|
-
// `prevent_last_admin_delete` is het definitieve vangnet voor de laatste admin.
|
|
146
186
|
const { error } = await context.supabase
|
|
147
|
-
.from("
|
|
187
|
+
.from("role_assignments")
|
|
148
188
|
.delete()
|
|
149
189
|
.eq("user_id", data.userId)
|
|
150
|
-
.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);
|
|
151
353
|
if (error)
|
|
152
354
|
throw error;
|
|
153
355
|
return { ok: true };
|
|
154
356
|
});
|
|
155
357
|
/**
|
|
156
|
-
* Registreert de modules die de shell heeft geladen.
|
|
157
|
-
*
|
|
358
|
+
* Registreert de modules (en hun onderdelen) die de shell heeft geladen.
|
|
359
|
+
* Wat niet meer voorkomt blijft bestaan maar wordt op inactief gezet.
|
|
158
360
|
*/
|
|
159
361
|
export const syncModuleRegistry = createServerFn({ method: "POST" })
|
|
160
362
|
.middleware([requireSupabaseAuth])
|
|
@@ -174,7 +376,6 @@ export const syncModuleRegistry = createServerFn({ method: "POST" })
|
|
|
174
376
|
})), { onConflict: "module_id" });
|
|
175
377
|
if (error)
|
|
176
378
|
throw error;
|
|
177
|
-
// Modules van deze shell die niet meer geleverd worden → inactief.
|
|
178
379
|
if (data.shellId) {
|
|
179
380
|
const ids = modules.map((m) => m.id);
|
|
180
381
|
const { error: deactivateError } = await context.supabase
|
|
@@ -185,70 +386,71 @@ export const syncModuleRegistry = createServerFn({ method: "POST" })
|
|
|
185
386
|
if (deactivateError)
|
|
186
387
|
throw deactivateError;
|
|
187
388
|
}
|
|
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;
|
|
403
|
+
}
|
|
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
|
+
}
|
|
188
417
|
return { ok: true, count: modules.length };
|
|
189
418
|
});
|
|
190
|
-
export const getModulePermissions = createServerFn({ method: "GET" })
|
|
191
|
-
.middleware([requireSupabaseAuth])
|
|
192
|
-
.handler(async ({ context }) => {
|
|
193
|
-
await requireAdmin(context);
|
|
194
|
-
const [{ data, error }, { data: registry, error: registryError }] = await Promise.all([
|
|
195
|
-
context.supabase.from("module_permissions").select("role, module_id, permission"),
|
|
196
|
-
context.supabase
|
|
197
|
-
.from("module_registry")
|
|
198
|
-
.select("module_id, name, is_active")
|
|
199
|
-
.order("name", { ascending: true }),
|
|
200
|
-
]);
|
|
201
|
-
if (error)
|
|
202
|
-
throw error;
|
|
203
|
-
if (registryError)
|
|
204
|
-
throw registryError;
|
|
205
|
-
const safe = Array.isArray(data) ? data : [];
|
|
206
|
-
const safeRegistry = Array.isArray(registry) ? registry : [];
|
|
207
|
-
return {
|
|
208
|
-
roles: APP_ROLES,
|
|
209
|
-
modules: safeRegistry
|
|
210
|
-
.filter((row) => row.is_active)
|
|
211
|
-
.map((row) => ({
|
|
212
|
-
id: row.module_id,
|
|
213
|
-
name: row.name,
|
|
214
|
-
})),
|
|
215
|
-
permissions: safe.map((row) => ({
|
|
216
|
-
role: row.role,
|
|
217
|
-
moduleId: row.module_id,
|
|
218
|
-
permission: row.permission,
|
|
219
|
-
})),
|
|
220
|
-
};
|
|
221
|
-
});
|
|
222
419
|
export const getUserModulePermissions = createServerFn({ method: "POST" })
|
|
223
420
|
.middleware([requireSupabaseAuth])
|
|
224
421
|
.inputValidator((data) => userPermissionsInput.parse(data))
|
|
225
422
|
.handler(async ({ context, data }) => {
|
|
226
423
|
await requireUserAdmin(context);
|
|
227
|
-
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([
|
|
228
425
|
context.supabase
|
|
229
426
|
.from("user_module_permissions")
|
|
230
|
-
.select("module_id, permission")
|
|
427
|
+
.select("module_id, feature_id, permission")
|
|
231
428
|
.eq("user_id", data.userId),
|
|
232
|
-
context.supabase
|
|
233
|
-
|
|
234
|
-
.select("module_id, name, is_active")
|
|
235
|
-
.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 }),
|
|
236
431
|
]);
|
|
237
432
|
if (error)
|
|
238
433
|
throw error;
|
|
239
434
|
if (registryError)
|
|
240
435
|
throw registryError;
|
|
436
|
+
if (featuresError)
|
|
437
|
+
throw featuresError;
|
|
241
438
|
const safeOverrides = Array.isArray(overrides) ? overrides : [];
|
|
242
439
|
const safeRegistry = Array.isArray(registry) ? registry : [];
|
|
440
|
+
const safeFeatures = Array.isArray(featureRows) ? featureRows : [];
|
|
243
441
|
return {
|
|
244
442
|
modules: safeRegistry
|
|
245
443
|
.filter((row) => row.is_active)
|
|
246
444
|
.map((row) => ({
|
|
247
445
|
id: row.module_id,
|
|
248
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 })),
|
|
249
450
|
})),
|
|
250
451
|
overrides: safeOverrides.map((row) => ({
|
|
251
452
|
moduleId: row.module_id,
|
|
453
|
+
featureId: row.feature_id ?? "",
|
|
252
454
|
permission: row.permission,
|
|
253
455
|
})),
|
|
254
456
|
};
|
|
@@ -258,12 +460,14 @@ export const setUserModulePermission = createServerFn({ method: "POST" })
|
|
|
258
460
|
.inputValidator((data) => setUserPermissionInput.parse(data))
|
|
259
461
|
.handler(async ({ context, data }) => {
|
|
260
462
|
await requireUserAdmin(context);
|
|
463
|
+
const featureId = data.featureId ?? "";
|
|
261
464
|
if (data.permission === "inherit") {
|
|
262
465
|
const { error } = await context.supabase
|
|
263
466
|
.from("user_module_permissions")
|
|
264
467
|
.delete()
|
|
265
468
|
.eq("user_id", data.userId)
|
|
266
|
-
.eq("module_id", data.moduleId)
|
|
469
|
+
.eq("module_id", data.moduleId)
|
|
470
|
+
.eq("feature_id", featureId);
|
|
267
471
|
if (error)
|
|
268
472
|
throw error;
|
|
269
473
|
return { ok: true };
|
|
@@ -271,27 +475,10 @@ export const setUserModulePermission = createServerFn({ method: "POST" })
|
|
|
271
475
|
const { error } = await context.supabase.from("user_module_permissions").upsert({
|
|
272
476
|
user_id: data.userId,
|
|
273
477
|
module_id: data.moduleId,
|
|
478
|
+
feature_id: featureId,
|
|
274
479
|
permission: data.permission,
|
|
275
480
|
updated_at: new Date().toISOString(),
|
|
276
|
-
}, { onConflict: "user_id,module_id" });
|
|
277
|
-
if (error)
|
|
278
|
-
throw error;
|
|
279
|
-
return { ok: true };
|
|
280
|
-
});
|
|
281
|
-
export const setModulePermission = createServerFn({ method: "POST" })
|
|
282
|
-
.middleware([requireSupabaseAuth])
|
|
283
|
-
.inputValidator((data) => setPermissionInput.parse(data))
|
|
284
|
-
.handler(async ({ context, data }) => {
|
|
285
|
-
await requireAdmin(context);
|
|
286
|
-
// Upsert via user-scoped client — RLS write-policy op module_permissions
|
|
287
|
-
// vereist admin (via JWT-claim of has_role-fallback).
|
|
288
|
-
const { error } = await context.supabase
|
|
289
|
-
.from("module_permissions")
|
|
290
|
-
.upsert({
|
|
291
|
-
role: data.role,
|
|
292
|
-
module_id: data.moduleId,
|
|
293
|
-
permission: data.permission,
|
|
294
|
-
}, { onConflict: "role,module_id" });
|
|
481
|
+
}, { onConflict: "user_id,module_id,feature_id" });
|
|
295
482
|
if (error)
|
|
296
483
|
throw error;
|
|
297
484
|
return { ok: true };
|
|
@@ -301,9 +488,7 @@ export const inviteUser = createServerFn({ method: "POST" })
|
|
|
301
488
|
.inputValidator((data) => inviteUserInput.parse(data))
|
|
302
489
|
.handler(async ({ context, data }) => {
|
|
303
490
|
const { isAdmin } = await requireUserAdmin(context);
|
|
304
|
-
|
|
305
|
-
throw new Error("Alleen een admin kan een account met de adminrol aanmaken.");
|
|
306
|
-
}
|
|
491
|
+
await assertMayUseRole(context.supabase, data.roleId, isAdmin);
|
|
307
492
|
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
308
493
|
let redirectTo;
|
|
309
494
|
try {
|
|
@@ -316,10 +501,7 @@ export const inviteUser = createServerFn({ method: "POST" })
|
|
|
316
501
|
catch {
|
|
317
502
|
redirectTo = undefined;
|
|
318
503
|
}
|
|
319
|
-
const { data: invited, error: inviteError } = await supabaseAdmin.auth.admin.inviteUserByEmail(data.email, {
|
|
320
|
-
data: { display_name: data.displayName },
|
|
321
|
-
redirectTo,
|
|
322
|
-
});
|
|
504
|
+
const { data: invited, error: inviteError } = await supabaseAdmin.auth.admin.inviteUserByEmail(data.email, { data: { display_name: data.displayName }, redirectTo });
|
|
323
505
|
if (inviteError || !invited?.user) {
|
|
324
506
|
const msg = inviteError?.message ?? "Uitnodigen mislukt";
|
|
325
507
|
throw new Error(/already|exists|registered/i.test(msg) ? "Deze gebruiker bestaat al." : msg);
|
|
@@ -331,8 +513,8 @@ export const inviteUser = createServerFn({ method: "POST" })
|
|
|
331
513
|
if (profileError)
|
|
332
514
|
throw profileError;
|
|
333
515
|
const { error: roleError } = await supabaseAdmin
|
|
334
|
-
.from("
|
|
335
|
-
.upsert({ user_id: userId,
|
|
516
|
+
.from("role_assignments")
|
|
517
|
+
.upsert({ user_id: userId, role_id: data.roleId }, { onConflict: "user_id,role_id", ignoreDuplicates: true });
|
|
336
518
|
if (roleError)
|
|
337
519
|
throw roleError;
|
|
338
520
|
return { ok: true, userId };
|
|
@@ -347,13 +529,13 @@ export const deleteUser = createServerFn({ method: "POST" })
|
|
|
347
529
|
}
|
|
348
530
|
if (!isAdmin) {
|
|
349
531
|
const { data: targetRoles, error: targetError } = await context.supabase
|
|
350
|
-
.from("
|
|
351
|
-
.select("
|
|
532
|
+
.from("role_assignments")
|
|
533
|
+
.select("roles(key)")
|
|
352
534
|
.eq("user_id", data.userId);
|
|
353
535
|
if (targetError)
|
|
354
536
|
throw targetError;
|
|
355
537
|
const safeTargetRoles = Array.isArray(targetRoles) ? targetRoles : [];
|
|
356
|
-
if (safeTargetRoles.some((r) => r
|
|
538
|
+
if (safeTargetRoles.some((r) => r?.roles?.key === "admin")) {
|
|
357
539
|
throw new Error("Alleen een admin kan een adminaccount verwijderen.");
|
|
358
540
|
}
|
|
359
541
|
}
|
|
@@ -363,6 +545,37 @@ export const deleteUser = createServerFn({ method: "POST" })
|
|
|
363
545
|
throw error;
|
|
364
546
|
return { ok: true };
|
|
365
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
|
+
});
|
|
366
579
|
function generatePassword(length = 16) {
|
|
367
580
|
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%*";
|
|
368
581
|
const bytes = new Uint8Array(length);
|
|
@@ -377,12 +590,9 @@ export const createTestUser = createServerFn({ method: "POST" })
|
|
|
377
590
|
.inputValidator((data) => createTestUserInput.parse(data))
|
|
378
591
|
.handler(async ({ context, data }) => {
|
|
379
592
|
const { isAdmin } = await requireUserAdmin(context);
|
|
380
|
-
|
|
381
|
-
throw new Error("Alleen een admin kan een account met de adminrol aanmaken.");
|
|
382
|
-
}
|
|
593
|
+
await assertMayUseRole(context.supabase, data.roleId, isAdmin);
|
|
383
594
|
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
384
|
-
const email = data.email?.trim() ||
|
|
385
|
-
`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`;
|
|
386
596
|
const password = generatePassword(16);
|
|
387
597
|
const { data: created, error: createError } = await supabaseAdmin.auth.admin.createUser({
|
|
388
598
|
email,
|
|
@@ -395,15 +605,14 @@ export const createTestUser = createServerFn({ method: "POST" })
|
|
|
395
605
|
throw new Error(/already|exists|registered/i.test(msg) ? "Deze gebruiker bestaat al." : msg);
|
|
396
606
|
}
|
|
397
607
|
const userId = created.user.id;
|
|
398
|
-
// Trigger vult profiles al; toch upsert voor zekerheid (idempotent).
|
|
399
608
|
const { error: profileError } = await supabaseAdmin
|
|
400
609
|
.from("profiles")
|
|
401
610
|
.upsert({ id: userId, email, display_name: data.displayName }, { onConflict: "id" });
|
|
402
611
|
if (profileError)
|
|
403
612
|
throw profileError;
|
|
404
613
|
const { error: roleError } = await supabaseAdmin
|
|
405
|
-
.from("
|
|
406
|
-
.upsert({ user_id: userId,
|
|
614
|
+
.from("role_assignments")
|
|
615
|
+
.upsert({ user_id: userId, role_id: data.roleId }, { onConflict: "user_id,role_id", ignoreDuplicates: true });
|
|
407
616
|
if (roleError)
|
|
408
617
|
throw roleError;
|
|
409
618
|
return { ok: true, userId, email, password };
|