@flowselections/admin-installatietechniek 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-lib/components/AdminForInstallatieTechniek.d.ts.map +1 -1
- package/dist-lib/components/AdminForInstallatieTechniek.js +117 -215
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +18907 -9909
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +18907 -9909
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +18907 -9909
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +19312 -10201
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +1 -0
- package/dist-lib/lib/admin.functions.d.ts +168505 -127289
- package/dist-lib/lib/admin.functions.d.ts.map +1 -1
- package/dist-lib/lib/admin.functions.js +132 -445
- package/dist-lib/lib/admin.server.d.ts +20 -3
- package/dist-lib/lib/admin.server.d.ts.map +1 -1
- package/dist-lib/lib/admin.server.js +101 -3
- package/dist-lib/lib/serverErrors.d.ts +7 -0
- package/dist-lib/lib/serverErrors.d.ts.map +1 -0
- package/dist-lib/lib/serverErrors.js +29 -0
- package/package.json +4 -4
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createServerFn } from "@tanstack/react-start";
|
|
2
|
-
import { getRequestHost } from "@tanstack/react-start/server";
|
|
3
2
|
import { z } from "zod";
|
|
4
3
|
import { requireSupabaseAuth } from "../integrations/supabase/auth-middleware";
|
|
5
|
-
import {
|
|
4
|
+
import { normalizeServerErrors } from "../lib/serverErrors";
|
|
5
|
+
import { isFullAdminRoleKey, requireAdmin, requireUserAdmin, resolveAppBaseUrl, verifyAddressWithGoogle } from "../lib/admin.server";
|
|
6
6
|
const featureSchema = z.object({
|
|
7
7
|
id: z.string().trim().min(1).max(200),
|
|
8
8
|
name: z.string().trim().min(1).max(200),
|
|
@@ -31,7 +31,6 @@ const roleIdInput = z.object({ roleId: z.string().uuid() });
|
|
|
31
31
|
const assignRoleInput = z.object({
|
|
32
32
|
userId: z.string().uuid(),
|
|
33
33
|
roleId: z.string().uuid(),
|
|
34
|
-
profileId: z.string().uuid().optional(),
|
|
35
34
|
});
|
|
36
35
|
const rolePermissionsSchema = z
|
|
37
36
|
.array(z.object({
|
|
@@ -50,11 +49,33 @@ const updateRoleInput = z.object({
|
|
|
50
49
|
name: z.string().trim().min(1).max(80),
|
|
51
50
|
permissions: rolePermissionsSchema.default([]),
|
|
52
51
|
});
|
|
52
|
+
const CONTACT_REQUIRED_ROLE_KEYS = ["monteur", "planner", "administratie"];
|
|
53
|
+
const phoneRegex = /^[0-9+()\-\s]{8,25}$/;
|
|
54
|
+
const optionalAddress = z.string().trim().max(200).optional().default("");
|
|
55
|
+
const optionalPhone = z
|
|
56
|
+
.string()
|
|
57
|
+
.trim()
|
|
58
|
+
.max(25)
|
|
59
|
+
.optional()
|
|
60
|
+
.default("")
|
|
61
|
+
.refine((v) => v === "" || phoneRegex.test(v), { message: "Ongeldig telefoonnummer" });
|
|
62
|
+
function assertContactForRole(roleKey, address, _phone) {
|
|
63
|
+
if (!roleKey || !CONTACT_REQUIRED_ROLE_KEYS.includes(roleKey))
|
|
64
|
+
return;
|
|
65
|
+
if (address.trim().length < 5)
|
|
66
|
+
throw new Error("Adres is verplicht voor deze rol (minimaal 5 tekens).");
|
|
67
|
+
}
|
|
53
68
|
const inviteUserInput = z.object({
|
|
54
69
|
email: z.string().trim().email().max(255),
|
|
55
70
|
displayName: z.string().trim().min(1).max(100),
|
|
56
71
|
roleId: z.string().uuid(),
|
|
57
|
-
|
|
72
|
+
address: optionalAddress,
|
|
73
|
+
phone: optionalPhone,
|
|
74
|
+
});
|
|
75
|
+
const updateUserContactInput = z.object({
|
|
76
|
+
userId: z.string().uuid(),
|
|
77
|
+
address: optionalAddress,
|
|
78
|
+
phone: optionalPhone,
|
|
58
79
|
});
|
|
59
80
|
const deleteUserInput = z.object({
|
|
60
81
|
userId: z.string().uuid(),
|
|
@@ -67,7 +88,6 @@ const createTestUserInput = z.object({
|
|
|
67
88
|
displayName: z.string().trim().min(1).max(100),
|
|
68
89
|
roleId: z.string().uuid(),
|
|
69
90
|
email: z.string().trim().email().max(255).optional(),
|
|
70
|
-
profileId: z.string().uuid().optional(),
|
|
71
91
|
});
|
|
72
92
|
function slugify(value) {
|
|
73
93
|
return (value
|
|
@@ -92,95 +112,30 @@ async function fetchRole(supabase, roleId) {
|
|
|
92
112
|
}
|
|
93
113
|
async function assertMayUseRole(supabase, roleId, isAdmin) {
|
|
94
114
|
const role = await fetchRole(supabase, roleId);
|
|
95
|
-
if (!isAdmin && role.key
|
|
115
|
+
if (!isAdmin && isFullAdminRoleKey(role.key)) {
|
|
96
116
|
throw new Error("Alleen een admin kan de adminrol gebruiken.");
|
|
97
117
|
}
|
|
98
118
|
return role;
|
|
99
119
|
}
|
|
100
|
-
const profileScopeInput = z
|
|
101
|
-
.object({ profileId: z.string().uuid().optional() })
|
|
102
|
-
.optional();
|
|
103
120
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
121
|
+
* Eén database = één organisatie. Bestaat er precies één tenant, dan is dat
|
|
122
|
+
* de tenant voor nieuwe gebruikers; anders wordt er niets gezet.
|
|
106
123
|
*/
|
|
107
|
-
async function
|
|
108
|
-
|
|
109
|
-
return null;
|
|
110
|
-
const { data, error } = await supabase
|
|
111
|
-
.from("profiles_org")
|
|
112
|
-
.select("id, name, tenant_id, is_default")
|
|
113
|
-
.eq("id", profileId)
|
|
114
|
-
.maybeSingle();
|
|
124
|
+
async function resolveSingleTenantId(admin) {
|
|
125
|
+
const { data, error } = await admin.from("tenants").select("id").limit(2);
|
|
115
126
|
if (error)
|
|
116
127
|
throw error;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const [{ data: roleRows }, { data: moduleRows }] = await Promise.all([
|
|
120
|
-
supabase.from("profile_roles").select("role_id").eq("profile_id", data.id),
|
|
121
|
-
supabase.from("profile_modules").select("module_id").eq("profile_id", data.id),
|
|
122
|
-
]);
|
|
123
|
-
return {
|
|
124
|
-
id: data.id,
|
|
125
|
-
name: data.name,
|
|
126
|
-
tenantId: (data.tenant_id ?? null),
|
|
127
|
-
roleIds: (Array.isArray(roleRows) ? roleRows : []).map((r) => r.role_id),
|
|
128
|
-
moduleIds: (Array.isArray(moduleRows) ? moduleRows : []).map((m) => m.module_id),
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Bepaalt op welk bedrijfsprofiel deze aanvraag draait.
|
|
133
|
-
* - Admin: vrije keuze (leeg = overzicht met alles).
|
|
134
|
-
* - Niet-admin (administratie): altijd vastgezet op het eigen profiel,
|
|
135
|
-
* afgeleid uit de tenant van de gebruiker of anders uit de profielen
|
|
136
|
-
* waaraan een van zijn rollen gekoppeld is.
|
|
137
|
-
*/
|
|
138
|
-
async function resolveEffectiveProfileId(admin, userId, requestedProfileId, isAdmin) {
|
|
139
|
-
if (isAdmin)
|
|
140
|
-
return { profileId: requestedProfileId, locked: false };
|
|
141
|
-
const { data: me } = await admin
|
|
142
|
-
.from("profiles")
|
|
143
|
-
.select("tenant_id")
|
|
144
|
-
.eq("id", userId)
|
|
145
|
-
.maybeSingle();
|
|
146
|
-
if (me?.tenant_id) {
|
|
147
|
-
const { data: byTenant } = await admin
|
|
148
|
-
.from("profiles_org")
|
|
149
|
-
.select("id")
|
|
150
|
-
.eq("tenant_id", me.tenant_id)
|
|
151
|
-
.limit(1);
|
|
152
|
-
const first = Array.isArray(byTenant) ? byTenant[0] : null;
|
|
153
|
-
if (first?.id)
|
|
154
|
-
return { profileId: first.id, locked: true };
|
|
155
|
-
}
|
|
156
|
-
const { data: myRoles } = await admin
|
|
157
|
-
.from("role_assignments")
|
|
158
|
-
.select("role_id")
|
|
159
|
-
.eq("user_id", userId);
|
|
160
|
-
const roleIds = (Array.isArray(myRoles) ? myRoles : []).map((r) => r.role_id);
|
|
161
|
-
if (roleIds.length > 0) {
|
|
162
|
-
const { data: links } = await admin
|
|
163
|
-
.from("profile_roles")
|
|
164
|
-
.select("profile_id")
|
|
165
|
-
.in("role_id", roleIds)
|
|
166
|
-
.limit(1);
|
|
167
|
-
const link = Array.isArray(links) ? links[0] : null;
|
|
168
|
-
if (link?.profile_id)
|
|
169
|
-
return { profileId: link.profile_id, locked: true };
|
|
170
|
-
}
|
|
171
|
-
return { profileId: undefined, locked: true };
|
|
128
|
+
const rows = Array.isArray(data) ? data : [];
|
|
129
|
+
return rows.length === 1 ? rows[0].id : null;
|
|
172
130
|
}
|
|
173
131
|
export const getUsersWithRoles = createServerFn({ method: "POST" })
|
|
174
|
-
.middleware([requireSupabaseAuth])
|
|
175
|
-
.
|
|
176
|
-
.handler(async ({ context, data }) => {
|
|
132
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
133
|
+
.handler(async ({ context }) => {
|
|
177
134
|
const { isAdmin } = await requireUserAdmin(context);
|
|
178
135
|
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
179
|
-
const effective = await resolveEffectiveProfileId(supabaseAdmin, context.userId, data?.profileId, isAdmin);
|
|
180
|
-
const scope = await loadProfileScope(supabaseAdmin, effective.profileId);
|
|
181
136
|
const profilesQuery = supabaseAdmin
|
|
182
137
|
.from("profiles")
|
|
183
|
-
.select("id, email, display_name, tenant_id")
|
|
138
|
+
.select("id, email, display_name, tenant_id, address, phone")
|
|
184
139
|
.order("email", { ascending: true });
|
|
185
140
|
const [{ data: profiles, error: profilesError }, { data: assignments, error: assignmentsError }, { data: roles, error: rolesError }, { data: authList, error: authError },] = await Promise.all([
|
|
186
141
|
profilesQuery,
|
|
@@ -199,15 +154,15 @@ export const getUsersWithRoles = createServerFn({ method: "POST" })
|
|
|
199
154
|
const safeProfiles = Array.isArray(profiles) ? profiles : [];
|
|
200
155
|
const safeAssignments = Array.isArray(assignments) ? assignments : [];
|
|
201
156
|
const allRoles = Array.isArray(roles) ? roles : [];
|
|
202
|
-
const safeRoles = scope && scope.roleIds.length > 0
|
|
203
|
-
? allRoles.filter((r) => scope.roleIds.includes(r.id))
|
|
204
|
-
: allRoles;
|
|
205
157
|
const safeAuthUsers = Array.isArray(authList?.users) ? authList.users : [];
|
|
206
158
|
const roleById = new Map(allRoles.map((r) => [r.id, r]));
|
|
207
159
|
const confirmedById = new Map();
|
|
208
160
|
const isTestById = new Map();
|
|
161
|
+
const authEmailById = new Map();
|
|
209
162
|
for (const u of safeAuthUsers) {
|
|
210
163
|
confirmedById.set(u.id, Boolean(u.email_confirmed_at));
|
|
164
|
+
if (u.email)
|
|
165
|
+
authEmailById.set(u.id, u.email);
|
|
211
166
|
const meta = (u.user_metadata ?? {});
|
|
212
167
|
isTestById.set(u.id, Boolean(meta.is_test_account));
|
|
213
168
|
}
|
|
@@ -222,56 +177,25 @@ export const getUsersWithRoles = createServerFn({ method: "POST" })
|
|
|
222
177
|
}
|
|
223
178
|
rolesByUser.set(a.user_id, list);
|
|
224
179
|
}
|
|
225
|
-
// Binnen een profiel horen gebruikers erbij via de tenant OF via een rol
|
|
226
|
-
// die aan het profiel gekoppeld is. Zonder profiel: iedereen.
|
|
227
|
-
const scopedRoleIds = new Set(scope?.roleIds ?? []);
|
|
228
|
-
const usersViaRole = new Set();
|
|
229
|
-
if (scope && scopedRoleIds.size > 0) {
|
|
230
|
-
for (const a of safeAssignments) {
|
|
231
|
-
if (scopedRoleIds.has(a.role_id))
|
|
232
|
-
usersViaRole.add(a.user_id);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
// Vastgezette gebruiker zonder gekoppeld profiel: niets tonen.
|
|
236
|
-
const noProfileLock = effective.locked && !scope;
|
|
237
|
-
const scopedProfiles = noProfileLock
|
|
238
|
-
? []
|
|
239
|
-
: !scope
|
|
240
|
-
? safeProfiles
|
|
241
|
-
: safeProfiles.filter((p) => {
|
|
242
|
-
const viaTenant = Boolean(scope.tenantId) && p.tenant_id === scope.tenantId;
|
|
243
|
-
return viaTenant || usersViaRole.has(p.id);
|
|
244
|
-
});
|
|
245
180
|
return {
|
|
246
|
-
users:
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
: viaTenant && viaRole
|
|
259
|
-
? "both"
|
|
260
|
-
: viaTenant
|
|
261
|
-
? "tenant"
|
|
262
|
-
: "role",
|
|
263
|
-
};
|
|
264
|
-
}),
|
|
265
|
-
availableRoles: (noProfileLock ? [] : safeRoles)
|
|
266
|
-
.filter((r) => isAdmin || r.key !== "admin")
|
|
181
|
+
users: safeProfiles.map((profile) => ({
|
|
182
|
+
id: profile.id,
|
|
183
|
+
email: profile.email ?? authEmailById.get(profile.id) ?? "-",
|
|
184
|
+
displayName: profile.display_name ?? "-",
|
|
185
|
+
address: typeof profile.address === "string" ? profile.address : "",
|
|
186
|
+
phone: typeof profile.phone === "string" ? profile.phone : "",
|
|
187
|
+
roles: rolesByUser.get(profile.id) ?? [],
|
|
188
|
+
emailConfirmed: confirmedById.get(profile.id) ?? false,
|
|
189
|
+
isTestAccount: isTestById.get(profile.id) ?? false,
|
|
190
|
+
})),
|
|
191
|
+
availableRoles: allRoles
|
|
192
|
+
.filter((r) => isAdmin || !isFullAdminRoleKey(r.key))
|
|
267
193
|
.map((r) => ({ id: r.id, key: r.key, name: r.name, isProtected: r.is_protected })),
|
|
268
194
|
isAdmin,
|
|
269
|
-
profileLocked: effective.locked,
|
|
270
|
-
activeProfile: scope ? { id: scope.id, name: scope.name, tenantId: scope.tenantId } : null,
|
|
271
195
|
};
|
|
272
196
|
});
|
|
273
197
|
export const assignRole = createServerFn({ method: "POST" })
|
|
274
|
-
.middleware([requireSupabaseAuth])
|
|
198
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
275
199
|
.inputValidator((data) => assignRoleInput.parse(data))
|
|
276
200
|
.handler(async ({ context, data }) => {
|
|
277
201
|
const { isAdmin } = await requireUserAdmin(context);
|
|
@@ -291,23 +215,21 @@ export const assignRole = createServerFn({ method: "POST" })
|
|
|
291
215
|
.insert({ user_id: data.userId, role_id: data.roleId });
|
|
292
216
|
if (error)
|
|
293
217
|
throw error;
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
.is("tenant_id", null);
|
|
305
|
-
}
|
|
218
|
+
// Eén organisatie per database: bestaat er precies één tenant, koppel de
|
|
219
|
+
// gebruiker daaraan als zijn tenant_id nog leeg is.
|
|
220
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
221
|
+
const tenantId = await resolveSingleTenantId(supabaseAdmin);
|
|
222
|
+
if (tenantId) {
|
|
223
|
+
await supabaseAdmin
|
|
224
|
+
.from("profiles")
|
|
225
|
+
.update({ tenant_id: tenantId })
|
|
226
|
+
.eq("id", data.userId)
|
|
227
|
+
.is("tenant_id", null);
|
|
306
228
|
}
|
|
307
229
|
return { ok: true };
|
|
308
230
|
});
|
|
309
231
|
export const removeRole = createServerFn({ method: "POST" })
|
|
310
|
-
.middleware([requireSupabaseAuth])
|
|
232
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
311
233
|
.inputValidator((data) => assignRoleInput.parse(data))
|
|
312
234
|
.handler(async ({ context, data }) => {
|
|
313
235
|
const { isAdmin } = await requireUserAdmin(context);
|
|
@@ -326,13 +248,9 @@ export const removeRole = createServerFn({ method: "POST" })
|
|
|
326
248
|
});
|
|
327
249
|
/** Alle rollen met hun rechten, plus de geregistreerde modules en onderdelen. */
|
|
328
250
|
export const getRoles = createServerFn({ method: "POST" })
|
|
329
|
-
.middleware([requireSupabaseAuth])
|
|
330
|
-
.
|
|
331
|
-
|
|
332
|
-
const { isAdmin } = await requireUserAdmin(context);
|
|
333
|
-
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
334
|
-
const effective = await resolveEffectiveProfileId(supabaseAdmin, context.userId, data?.profileId, isAdmin);
|
|
335
|
-
const scope = await loadProfileScope(supabaseAdmin, effective.profileId);
|
|
251
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
252
|
+
.handler(async ({ context }) => {
|
|
253
|
+
await requireUserAdmin(context);
|
|
336
254
|
const [{ data: roles, error: rolesError }, { data: perms, error: permsError }, { data: registry, error: registryError }, { data: featureRows, error: featuresError },] = await Promise.all([
|
|
337
255
|
context.supabase.from("roles").select("id, key, name, is_protected").order("name", { ascending: true }),
|
|
338
256
|
context.supabase.from("role_permissions").select("role_id, module_id, feature_id, can_view, can_edit"),
|
|
@@ -347,23 +265,21 @@ export const getRoles = createServerFn({ method: "POST" })
|
|
|
347
265
|
throw registryError;
|
|
348
266
|
if (featuresError)
|
|
349
267
|
throw featuresError;
|
|
350
|
-
const
|
|
351
|
-
const allRoles = noProfileLock ? [] : Array.isArray(roles) ? roles : [];
|
|
352
|
-
const safeRoles = scope && scope.roleIds.length > 0
|
|
353
|
-
? allRoles.filter((r) => scope.roleIds.includes(r.id))
|
|
354
|
-
: allRoles;
|
|
268
|
+
const safeRoles = Array.isArray(roles) ? roles : [];
|
|
355
269
|
const safePerms = Array.isArray(perms) ? perms : [];
|
|
356
|
-
const
|
|
357
|
-
const safeRegistry = scope && scope.moduleIds.length > 0
|
|
358
|
-
? allRegistry.filter((m) => scope.moduleIds.includes(m.module_id))
|
|
359
|
-
: allRegistry;
|
|
270
|
+
const safeRegistry = Array.isArray(registry) ? registry : [];
|
|
360
271
|
const safeFeatures = Array.isArray(featureRows) ? featureRows : [];
|
|
272
|
+
// Rollen waarvan de sleutel niet in de app_role-lijst voorkomt: die worden
|
|
273
|
+
// door de sync-trigger stil overgeslagen en werken nergens.
|
|
274
|
+
const { data: unmappedRows } = await context.supabase.rpc("roles_without_enum_mapping");
|
|
275
|
+
const unmappedKeys = new Set((Array.isArray(unmappedRows) ? unmappedRows : []).map((r) => r?.key));
|
|
361
276
|
return {
|
|
362
277
|
roles: safeRoles.map((r) => ({
|
|
363
278
|
id: r.id,
|
|
364
279
|
key: r.key,
|
|
365
280
|
name: r.name,
|
|
366
281
|
isProtected: r.is_protected,
|
|
282
|
+
unmapped: unmappedKeys.has(r.key),
|
|
367
283
|
})),
|
|
368
284
|
modules: safeRegistry
|
|
369
285
|
.filter((m) => m.is_active)
|
|
@@ -382,8 +298,6 @@ export const getRoles = createServerFn({ method: "POST" })
|
|
|
382
298
|
canView: Boolean(p.can_view),
|
|
383
299
|
canEdit: Boolean(p.can_edit),
|
|
384
300
|
})),
|
|
385
|
-
profileLocked: effective.locked,
|
|
386
|
-
activeProfile: scope ? { id: scope.id, name: scope.name, tenantId: scope.tenantId } : null,
|
|
387
301
|
};
|
|
388
302
|
});
|
|
389
303
|
async function writeRolePermissions(supabase, roleId, permissions) {
|
|
@@ -407,7 +321,7 @@ async function writeRolePermissions(supabase, roleId, permissions) {
|
|
|
407
321
|
throw error;
|
|
408
322
|
}
|
|
409
323
|
export const createRole = createServerFn({ method: "POST" })
|
|
410
|
-
.middleware([requireSupabaseAuth])
|
|
324
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
411
325
|
.inputValidator((data) => createRoleInput.parse(data))
|
|
412
326
|
.handler(async ({ context, data }) => {
|
|
413
327
|
await requireAdmin(context);
|
|
@@ -436,7 +350,7 @@ export const createRole = createServerFn({ method: "POST" })
|
|
|
436
350
|
return { ok: true, roleId: created.id };
|
|
437
351
|
});
|
|
438
352
|
export const updateRole = createServerFn({ method: "POST" })
|
|
439
|
-
.middleware([requireSupabaseAuth])
|
|
353
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
440
354
|
.inputValidator((data) => updateRoleInput.parse(data))
|
|
441
355
|
.handler(async ({ context, data }) => {
|
|
442
356
|
await requireAdmin(context);
|
|
@@ -450,7 +364,7 @@ export const updateRole = createServerFn({ method: "POST" })
|
|
|
450
364
|
return { ok: true };
|
|
451
365
|
});
|
|
452
366
|
export const duplicateRole = createServerFn({ method: "POST" })
|
|
453
|
-
.middleware([requireSupabaseAuth])
|
|
367
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
454
368
|
.inputValidator((data) => roleIdInput.parse(data))
|
|
455
369
|
.handler(async ({ context, data }) => {
|
|
456
370
|
await requireAdmin(context);
|
|
@@ -487,7 +401,7 @@ export const duplicateRole = createServerFn({ method: "POST" })
|
|
|
487
401
|
return { ok: true, roleId: created.id };
|
|
488
402
|
});
|
|
489
403
|
export const deleteRole = createServerFn({ method: "POST" })
|
|
490
|
-
.middleware([requireSupabaseAuth])
|
|
404
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
491
405
|
.inputValidator((data) => roleIdInput.parse(data))
|
|
492
406
|
.handler(async ({ context, data }) => {
|
|
493
407
|
await requireAdmin(context);
|
|
@@ -505,7 +419,7 @@ export const deleteRole = createServerFn({ method: "POST" })
|
|
|
505
419
|
* Wat niet meer voorkomt blijft bestaan maar wordt op inactief gezet.
|
|
506
420
|
*/
|
|
507
421
|
export const syncModuleRegistry = createServerFn({ method: "POST" })
|
|
508
|
-
.middleware([requireSupabaseAuth])
|
|
422
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
509
423
|
.inputValidator((data) => syncModulesInput.parse(data))
|
|
510
424
|
.handler(async ({ context, data }) => {
|
|
511
425
|
await requireAdmin(context);
|
|
@@ -577,7 +491,7 @@ const setModuleEnforceInput = z.object({
|
|
|
577
491
|
enforce: z.boolean(),
|
|
578
492
|
});
|
|
579
493
|
export const setModuleEnforce = createServerFn({ method: "POST" })
|
|
580
|
-
.middleware([requireSupabaseAuth])
|
|
494
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
581
495
|
.inputValidator((data) => setModuleEnforceInput.parse(data))
|
|
582
496
|
.handler(async ({ context, data }) => {
|
|
583
497
|
await requireAdmin(context);
|
|
@@ -590,7 +504,7 @@ export const setModuleEnforce = createServerFn({ method: "POST" })
|
|
|
590
504
|
return { ok: true };
|
|
591
505
|
});
|
|
592
506
|
export const getUserModulePermissions = createServerFn({ method: "POST" })
|
|
593
|
-
.middleware([requireSupabaseAuth])
|
|
507
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
594
508
|
.inputValidator((data) => userPermissionsInput.parse(data))
|
|
595
509
|
.handler(async ({ context, data }) => {
|
|
596
510
|
await requireUserAdmin(context);
|
|
@@ -629,7 +543,7 @@ export const getUserModulePermissions = createServerFn({ method: "POST" })
|
|
|
629
543
|
};
|
|
630
544
|
});
|
|
631
545
|
export const setUserModulePermission = createServerFn({ method: "POST" })
|
|
632
|
-
.middleware([requireSupabaseAuth])
|
|
546
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
633
547
|
.inputValidator((data) => setUserPermissionInput.parse(data))
|
|
634
548
|
.handler(async ({ context, data }) => {
|
|
635
549
|
await requireUserAdmin(context);
|
|
@@ -657,35 +571,36 @@ export const setUserModulePermission = createServerFn({ method: "POST" })
|
|
|
657
571
|
return { ok: true };
|
|
658
572
|
});
|
|
659
573
|
export const inviteUser = createServerFn({ method: "POST" })
|
|
660
|
-
.middleware([requireSupabaseAuth])
|
|
574
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
661
575
|
.inputValidator((data) => inviteUserInput.parse(data))
|
|
662
576
|
.handler(async ({ context, data }) => {
|
|
663
577
|
const { isAdmin } = await requireUserAdmin(context);
|
|
664
578
|
await assertMayUseRole(context.supabase, data.roleId, isAdmin);
|
|
579
|
+
const { data: roleRow, error: roleLookupError } = await context.supabase
|
|
580
|
+
.from("roles")
|
|
581
|
+
.select("key")
|
|
582
|
+
.eq("id", data.roleId)
|
|
583
|
+
.maybeSingle();
|
|
584
|
+
if (roleLookupError)
|
|
585
|
+
throw roleLookupError;
|
|
586
|
+
assertContactForRole(roleRow?.key ?? null, data.address, data.phone);
|
|
587
|
+
const verifiedAddress = await verifyAddressWithGoogle(data.address ?? "");
|
|
665
588
|
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
666
|
-
|
|
667
|
-
try {
|
|
668
|
-
const host = getRequestHost();
|
|
669
|
-
if (host) {
|
|
670
|
-
const proto = host.startsWith("localhost") ? "http" : "https";
|
|
671
|
-
redirectTo = `${proto}://${host}/reset-wachtwoord`;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
catch {
|
|
675
|
-
redirectTo = undefined;
|
|
676
|
-
}
|
|
589
|
+
const redirectTo = `${resolveAppBaseUrl()}/reset-wachtwoord`;
|
|
677
590
|
const { data: invited, error: inviteError } = await supabaseAdmin.auth.admin.inviteUserByEmail(data.email, { data: { display_name: data.displayName }, redirectTo });
|
|
678
591
|
if (inviteError || !invited?.user) {
|
|
679
592
|
const msg = inviteError?.message ?? "Uitnodigen mislukt";
|
|
680
593
|
throw new Error(/already|exists|registered/i.test(msg) ? "Deze gebruiker bestaat al." : msg);
|
|
681
594
|
}
|
|
682
595
|
const userId = invited.user.id;
|
|
683
|
-
const
|
|
596
|
+
const tenantId = await resolveSingleTenantId(supabaseAdmin);
|
|
684
597
|
const { error: profileError } = await supabaseAdmin.from("profiles").upsert({
|
|
685
598
|
id: userId,
|
|
686
599
|
email: data.email,
|
|
687
600
|
display_name: data.displayName,
|
|
688
|
-
|
|
601
|
+
address: verifiedAddress ? verifiedAddress : null,
|
|
602
|
+
phone: data.phone ? data.phone.trim() : null,
|
|
603
|
+
...(tenantId ? { tenant_id: tenantId } : {}),
|
|
689
604
|
}, { onConflict: "id" });
|
|
690
605
|
if (profileError)
|
|
691
606
|
throw profileError;
|
|
@@ -697,7 +612,7 @@ export const inviteUser = createServerFn({ method: "POST" })
|
|
|
697
612
|
return { ok: true, userId };
|
|
698
613
|
});
|
|
699
614
|
export const deleteUser = createServerFn({ method: "POST" })
|
|
700
|
-
.middleware([requireSupabaseAuth])
|
|
615
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
701
616
|
.inputValidator((data) => deleteUserInput.parse(data))
|
|
702
617
|
.handler(async ({ context, data }) => {
|
|
703
618
|
const { isAdmin } = await requireUserAdmin(context);
|
|
@@ -712,7 +627,7 @@ export const deleteUser = createServerFn({ method: "POST" })
|
|
|
712
627
|
if (targetError)
|
|
713
628
|
throw targetError;
|
|
714
629
|
const safeTargetRoles = Array.isArray(targetRoles) ? targetRoles : [];
|
|
715
|
-
if (safeTargetRoles.some((r) => r?.roles?.key
|
|
630
|
+
if (safeTargetRoles.some((r) => isFullAdminRoleKey(r?.roles?.key))) {
|
|
716
631
|
throw new Error("Alleen een admin kan een adminaccount verwijderen.");
|
|
717
632
|
}
|
|
718
633
|
}
|
|
@@ -723,7 +638,7 @@ export const deleteUser = createServerFn({ method: "POST" })
|
|
|
723
638
|
return { ok: true };
|
|
724
639
|
});
|
|
725
640
|
export const updateUserName = createServerFn({ method: "POST" })
|
|
726
|
-
.middleware([requireSupabaseAuth])
|
|
641
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
727
642
|
.inputValidator((data) => updateUserNameInput.parse(data))
|
|
728
643
|
.handler(async ({ context, data }) => {
|
|
729
644
|
const { isAdmin } = await requireUserAdmin(context);
|
|
@@ -735,7 +650,7 @@ export const updateUserName = createServerFn({ method: "POST" })
|
|
|
735
650
|
if (targetError)
|
|
736
651
|
throw targetError;
|
|
737
652
|
const safeTargetRoles = Array.isArray(targetRoles) ? targetRoles : [];
|
|
738
|
-
if (safeTargetRoles.some((r) => r?.roles?.key
|
|
653
|
+
if (safeTargetRoles.some((r) => isFullAdminRoleKey(r?.roles?.key))) {
|
|
739
654
|
throw new Error("Alleen een admin kan een adminaccount aanpassen.");
|
|
740
655
|
}
|
|
741
656
|
}
|
|
@@ -753,6 +668,36 @@ export const updateUserName = createServerFn({ method: "POST" })
|
|
|
753
668
|
throw error;
|
|
754
669
|
return { ok: true };
|
|
755
670
|
});
|
|
671
|
+
export const updateUserContact = createServerFn({ method: "POST" })
|
|
672
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
673
|
+
.inputValidator((data) => updateUserContactInput.parse(data))
|
|
674
|
+
.handler(async ({ context, data }) => {
|
|
675
|
+
await requireUserAdmin(context);
|
|
676
|
+
const { data: targetRoles, error: targetError } = await context.supabase
|
|
677
|
+
.from("role_assignments")
|
|
678
|
+
.select("roles(key)")
|
|
679
|
+
.eq("user_id", data.userId);
|
|
680
|
+
if (targetError)
|
|
681
|
+
throw targetError;
|
|
682
|
+
const safeTargetRoles = Array.isArray(targetRoles) ? targetRoles : [];
|
|
683
|
+
const keys = safeTargetRoles
|
|
684
|
+
.map((r) => r?.roles?.key)
|
|
685
|
+
.filter((k) => typeof k === "string");
|
|
686
|
+
const requiredKey = keys.find((k) => CONTACT_REQUIRED_ROLE_KEYS.includes(k)) ?? null;
|
|
687
|
+
assertContactForRole(requiredKey, data.address, data.phone);
|
|
688
|
+
const verifiedAddress = await verifyAddressWithGoogle(data.address ?? "");
|
|
689
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
690
|
+
const { error } = await supabaseAdmin
|
|
691
|
+
.from("profiles")
|
|
692
|
+
.update({
|
|
693
|
+
address: verifiedAddress ? verifiedAddress : null,
|
|
694
|
+
phone: data.phone ? data.phone.trim() : null,
|
|
695
|
+
})
|
|
696
|
+
.eq("id", data.userId);
|
|
697
|
+
if (error)
|
|
698
|
+
throw error;
|
|
699
|
+
return { ok: true };
|
|
700
|
+
});
|
|
756
701
|
function generatePassword(length = 16) {
|
|
757
702
|
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%*";
|
|
758
703
|
const bytes = new Uint8Array(length);
|
|
@@ -763,7 +708,7 @@ function generatePassword(length = 16) {
|
|
|
763
708
|
return out;
|
|
764
709
|
}
|
|
765
710
|
export const createTestUser = createServerFn({ method: "POST" })
|
|
766
|
-
.middleware([requireSupabaseAuth])
|
|
711
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
767
712
|
.inputValidator((data) => createTestUserInput.parse(data))
|
|
768
713
|
.handler(async ({ context, data }) => {
|
|
769
714
|
const { isAdmin } = await requireUserAdmin(context);
|
|
@@ -782,12 +727,12 @@ export const createTestUser = createServerFn({ method: "POST" })
|
|
|
782
727
|
throw new Error(/already|exists|registered/i.test(msg) ? "Deze gebruiker bestaat al." : msg);
|
|
783
728
|
}
|
|
784
729
|
const userId = created.user.id;
|
|
785
|
-
const
|
|
730
|
+
const tenantId = await resolveSingleTenantId(supabaseAdmin);
|
|
786
731
|
const { error: profileError } = await supabaseAdmin.from("profiles").upsert({
|
|
787
732
|
id: userId,
|
|
788
733
|
email,
|
|
789
734
|
display_name: data.displayName,
|
|
790
|
-
...(
|
|
735
|
+
...(tenantId ? { tenant_id: tenantId } : {}),
|
|
791
736
|
}, { onConflict: "id" });
|
|
792
737
|
if (profileError)
|
|
793
738
|
throw profileError;
|
|
@@ -798,261 +743,3 @@ export const createTestUser = createServerFn({ method: "POST" })
|
|
|
798
743
|
throw roleError;
|
|
799
744
|
return { ok: true, userId, email, password };
|
|
800
745
|
});
|
|
801
|
-
// ============================================================================
|
|
802
|
-
// Bedrijfsprofielen: bundelen rollen + modules per organisatie (tenant)
|
|
803
|
-
// ============================================================================
|
|
804
|
-
const profileIdInput = z.object({ profileId: z.string().uuid() });
|
|
805
|
-
const profileBodySchema = {
|
|
806
|
-
name: z.string().trim().min(1).max(120),
|
|
807
|
-
tenantId: z.string().uuid().nullable().optional(),
|
|
808
|
-
/** Naam van het bedrijf; maakt een nieuw bedrijf aan of hernoemt het gekozen bedrijf. */
|
|
809
|
-
tenantName: z.string().trim().max(120).optional(),
|
|
810
|
-
roleIds: z.array(z.string().uuid()).max(200).default([]),
|
|
811
|
-
moduleIds: z.array(z.string().trim().min(1).max(100)).max(200).default([]),
|
|
812
|
-
};
|
|
813
|
-
const createProfileInput = z.object(profileBodySchema);
|
|
814
|
-
const updateProfileInput = z.object({ profileId: z.string().uuid(), ...profileBodySchema });
|
|
815
|
-
/**
|
|
816
|
-
* Bepaalt het tenant-id voor een profiel:
|
|
817
|
-
* - bestaand bedrijf + gewijzigde naam -> hernoemen
|
|
818
|
-
* - geen bedrijf + naam -> nieuw bedrijf aanmaken
|
|
819
|
-
*/
|
|
820
|
-
async function resolveTenant(supabase, tenantId, tenantName) {
|
|
821
|
-
const name = (tenantName ?? "").trim();
|
|
822
|
-
if (tenantId) {
|
|
823
|
-
if (name) {
|
|
824
|
-
const { data: current, error: readError } = await supabase
|
|
825
|
-
.from("tenants")
|
|
826
|
-
.select("name")
|
|
827
|
-
.eq("id", tenantId)
|
|
828
|
-
.maybeSingle();
|
|
829
|
-
if (readError)
|
|
830
|
-
throw readError;
|
|
831
|
-
if (current && current.name !== name) {
|
|
832
|
-
const { error } = await supabase
|
|
833
|
-
.from("tenants")
|
|
834
|
-
.update({ name, updated_at: new Date().toISOString() })
|
|
835
|
-
.eq("id", tenantId);
|
|
836
|
-
if (error)
|
|
837
|
-
throw error;
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
return tenantId;
|
|
841
|
-
}
|
|
842
|
-
if (!name)
|
|
843
|
-
return null;
|
|
844
|
-
const { data: created, error } = await supabase
|
|
845
|
-
.from("tenants")
|
|
846
|
-
.insert({ name })
|
|
847
|
-
.select("id")
|
|
848
|
-
.single();
|
|
849
|
-
if (error)
|
|
850
|
-
throw error;
|
|
851
|
-
return created.id;
|
|
852
|
-
}
|
|
853
|
-
async function writeProfileLinks(supabase, profileId, roleIds, moduleIds) {
|
|
854
|
-
const { error: delRoles } = await supabase.from("profile_roles").delete().eq("profile_id", profileId);
|
|
855
|
-
if (delRoles)
|
|
856
|
-
throw delRoles;
|
|
857
|
-
const { error: delModules } = await supabase.from("profile_modules").delete().eq("profile_id", profileId);
|
|
858
|
-
if (delModules)
|
|
859
|
-
throw delModules;
|
|
860
|
-
const uniqueRoles = Array.from(new Set(roleIds));
|
|
861
|
-
if (uniqueRoles.length > 0) {
|
|
862
|
-
const { error } = await supabase
|
|
863
|
-
.from("profile_roles")
|
|
864
|
-
.insert(uniqueRoles.map((roleId) => ({ profile_id: profileId, role_id: roleId })));
|
|
865
|
-
if (error)
|
|
866
|
-
throw error;
|
|
867
|
-
}
|
|
868
|
-
const uniqueModules = Array.from(new Set(moduleIds));
|
|
869
|
-
if (uniqueModules.length > 0) {
|
|
870
|
-
const { error } = await supabase
|
|
871
|
-
.from("profile_modules")
|
|
872
|
-
.insert(uniqueModules.map((moduleId) => ({ profile_id: profileId, module_id: moduleId })));
|
|
873
|
-
if (error)
|
|
874
|
-
throw error;
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
/** Alle bedrijfsprofielen met hun rollen, modules en tellingen. */
|
|
878
|
-
export const getProfiles = createServerFn({ method: "GET" })
|
|
879
|
-
.middleware([requireSupabaseAuth])
|
|
880
|
-
.handler(async ({ context }) => {
|
|
881
|
-
const { isAdmin } = await requireUserAdmin(context);
|
|
882
|
-
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
883
|
-
const effective = await resolveEffectiveProfileId(supabaseAdmin, context.userId, undefined, isAdmin);
|
|
884
|
-
const [{ data: profilesOrg, error: profilesError }, { data: profileRoles, error: profileRolesError }, { data: profileModules, error: profileModulesError }, { data: tenants, error: tenantsError },] = await Promise.all([
|
|
885
|
-
context.supabase
|
|
886
|
-
.from("profiles_org")
|
|
887
|
-
.select("id, name, tenant_id, is_default")
|
|
888
|
-
.order("name", { ascending: true }),
|
|
889
|
-
context.supabase.from("profile_roles").select("profile_id, role_id"),
|
|
890
|
-
context.supabase.from("profile_modules").select("profile_id, module_id"),
|
|
891
|
-
// `tenants` is via RLS beperkt tot de eigen tenant; namen tonen mag hier
|
|
892
|
-
// omdat de aanvrager al is geverifieerd als admin/administratie.
|
|
893
|
-
supabaseAdmin.from("tenants").select("id, name").order("name", { ascending: true }),
|
|
894
|
-
]);
|
|
895
|
-
if (profilesError)
|
|
896
|
-
throw profilesError;
|
|
897
|
-
if (profileRolesError)
|
|
898
|
-
throw profileRolesError;
|
|
899
|
-
if (profileModulesError)
|
|
900
|
-
throw profileModulesError;
|
|
901
|
-
if (tenantsError)
|
|
902
|
-
throw tenantsError;
|
|
903
|
-
const { data: userRows, error: userError } = await supabaseAdmin
|
|
904
|
-
.from("profiles")
|
|
905
|
-
.select("id, tenant_id");
|
|
906
|
-
if (userError)
|
|
907
|
-
throw userError;
|
|
908
|
-
const allProfilesOrg = Array.isArray(profilesOrg) ? profilesOrg : [];
|
|
909
|
-
// Niet-admin: alleen het eigen (vastgezette) profiel.
|
|
910
|
-
const safeProfiles = effective.locked
|
|
911
|
-
? allProfilesOrg.filter((p) => p.id === effective.profileId)
|
|
912
|
-
: allProfilesOrg;
|
|
913
|
-
const safeProfileRoles = Array.isArray(profileRoles) ? profileRoles : [];
|
|
914
|
-
const safeProfileModules = Array.isArray(profileModules) ? profileModules : [];
|
|
915
|
-
const safeTenants = Array.isArray(tenants) ? tenants : [];
|
|
916
|
-
const safeUsers = Array.isArray(userRows) ? userRows : [];
|
|
917
|
-
const usersByTenant = new Map();
|
|
918
|
-
for (const u of safeUsers) {
|
|
919
|
-
if (!u.tenant_id)
|
|
920
|
-
continue;
|
|
921
|
-
usersByTenant.set(u.tenant_id, (usersByTenant.get(u.tenant_id) ?? 0) + 1);
|
|
922
|
-
}
|
|
923
|
-
const tenantName = new Map(safeTenants.map((t) => [t.id, t.name]));
|
|
924
|
-
return {
|
|
925
|
-
profiles: safeProfiles.map((p) => ({
|
|
926
|
-
id: p.id,
|
|
927
|
-
name: p.name,
|
|
928
|
-
tenantId: (p.tenant_id ?? null),
|
|
929
|
-
tenantName: p.tenant_id ? tenantName.get(p.tenant_id) ?? null : null,
|
|
930
|
-
isDefault: Boolean(p.is_default),
|
|
931
|
-
roleIds: safeProfileRoles.filter((r) => r.profile_id === p.id).map((r) => r.role_id),
|
|
932
|
-
moduleIds: safeProfileModules
|
|
933
|
-
.filter((m) => m.profile_id === p.id)
|
|
934
|
-
.map((m) => m.module_id),
|
|
935
|
-
userCount: p.tenant_id ? usersByTenant.get(p.tenant_id) ?? 0 : 0,
|
|
936
|
-
})),
|
|
937
|
-
tenants: safeTenants.map((t) => ({ id: t.id, name: t.name })),
|
|
938
|
-
profileLocked: effective.locked,
|
|
939
|
-
lockedProfileId: effective.locked ? effective.profileId ?? null : null,
|
|
940
|
-
};
|
|
941
|
-
});
|
|
942
|
-
export const createProfile = createServerFn({ method: "POST" })
|
|
943
|
-
.middleware([requireSupabaseAuth])
|
|
944
|
-
.inputValidator((data) => createProfileInput.parse(data))
|
|
945
|
-
.handler(async ({ context, data }) => {
|
|
946
|
-
await requireAdmin(context);
|
|
947
|
-
// `tenants` heeft geen insert-policy voor admins; na de adminguard mag dit
|
|
948
|
-
// veilig via de serverclient.
|
|
949
|
-
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
950
|
-
const tenantId = await resolveTenant(supabaseAdmin, data.tenantId, data.tenantName);
|
|
951
|
-
const { data: created, error } = await context.supabase
|
|
952
|
-
.from("profiles_org")
|
|
953
|
-
.insert({ name: data.name, tenant_id: tenantId })
|
|
954
|
-
.select("id")
|
|
955
|
-
.single();
|
|
956
|
-
if (error)
|
|
957
|
-
throw error;
|
|
958
|
-
await writeProfileLinks(context.supabase, created.id, data.roleIds, data.moduleIds);
|
|
959
|
-
return { ok: true, profileId: created.id };
|
|
960
|
-
});
|
|
961
|
-
export const updateProfile = createServerFn({ method: "POST" })
|
|
962
|
-
.middleware([requireSupabaseAuth])
|
|
963
|
-
.inputValidator((data) => updateProfileInput.parse(data))
|
|
964
|
-
.handler(async ({ context, data }) => {
|
|
965
|
-
await requireAdmin(context);
|
|
966
|
-
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
967
|
-
const tenantId = await resolveTenant(supabaseAdmin, data.tenantId, data.tenantName);
|
|
968
|
-
const { error } = await context.supabase
|
|
969
|
-
.from("profiles_org")
|
|
970
|
-
.update({ name: data.name, tenant_id: tenantId, updated_at: new Date().toISOString() })
|
|
971
|
-
.eq("id", data.profileId);
|
|
972
|
-
if (error)
|
|
973
|
-
throw error;
|
|
974
|
-
await writeProfileLinks(context.supabase, data.profileId, data.roleIds, data.moduleIds);
|
|
975
|
-
return { ok: true };
|
|
976
|
-
});
|
|
977
|
-
export const duplicateProfile = createServerFn({ method: "POST" })
|
|
978
|
-
.middleware([requireSupabaseAuth])
|
|
979
|
-
.inputValidator((data) => profileIdInput.parse(data))
|
|
980
|
-
.handler(async ({ context, data }) => {
|
|
981
|
-
await requireAdmin(context);
|
|
982
|
-
const { data: source, error: sourceError } = await context.supabase
|
|
983
|
-
.from("profiles_org")
|
|
984
|
-
.select("name, tenant_id")
|
|
985
|
-
.eq("id", data.profileId)
|
|
986
|
-
.maybeSingle();
|
|
987
|
-
if (sourceError)
|
|
988
|
-
throw sourceError;
|
|
989
|
-
if (!source)
|
|
990
|
-
throw new Error("Profiel bestaat niet.");
|
|
991
|
-
const [{ data: roleRows }, { data: moduleRows }] = await Promise.all([
|
|
992
|
-
context.supabase.from("profile_roles").select("role_id").eq("profile_id", data.profileId),
|
|
993
|
-
context.supabase.from("profile_modules").select("module_id").eq("profile_id", data.profileId),
|
|
994
|
-
]);
|
|
995
|
-
const { data: created, error } = await context.supabase
|
|
996
|
-
.from("profiles_org")
|
|
997
|
-
.insert({ name: `${source.name} (kopie)`, tenant_id: source.tenant_id })
|
|
998
|
-
.select("id")
|
|
999
|
-
.single();
|
|
1000
|
-
if (error)
|
|
1001
|
-
throw error;
|
|
1002
|
-
await writeProfileLinks(context.supabase, created.id, (Array.isArray(roleRows) ? roleRows : []).map((r) => r.role_id), (Array.isArray(moduleRows) ? moduleRows : []).map((m) => m.module_id));
|
|
1003
|
-
return { ok: true, profileId: created.id };
|
|
1004
|
-
});
|
|
1005
|
-
export const deleteProfile = createServerFn({ method: "POST" })
|
|
1006
|
-
.middleware([requireSupabaseAuth])
|
|
1007
|
-
.inputValidator((data) => profileIdInput.parse(data))
|
|
1008
|
-
.handler(async ({ context, data }) => {
|
|
1009
|
-
await requireAdmin(context);
|
|
1010
|
-
const { data: profile, error: profileError } = await context.supabase
|
|
1011
|
-
.from("profiles_org")
|
|
1012
|
-
.select("id, tenant_id, is_default")
|
|
1013
|
-
.eq("id", data.profileId)
|
|
1014
|
-
.maybeSingle();
|
|
1015
|
-
if (profileError)
|
|
1016
|
-
throw profileError;
|
|
1017
|
-
if (!profile)
|
|
1018
|
-
throw new Error("Profiel bestaat niet.");
|
|
1019
|
-
if (profile.is_default)
|
|
1020
|
-
throw new Error("Het actieve profiel kan niet verwijderd worden.");
|
|
1021
|
-
if (profile.tenant_id) {
|
|
1022
|
-
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
1023
|
-
const { count, error: countError } = await supabaseAdmin
|
|
1024
|
-
.from("profiles")
|
|
1025
|
-
.select("id", { count: "exact", head: true })
|
|
1026
|
-
.eq("tenant_id", profile.tenant_id);
|
|
1027
|
-
if (countError)
|
|
1028
|
-
throw countError;
|
|
1029
|
-
if ((count ?? 0) > 0) {
|
|
1030
|
-
throw new Error("Er zijn nog gebruikers aan dit bedrijf gekoppeld.");
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
const { error } = await context.supabase.from("profiles_org").delete().eq("id", data.profileId);
|
|
1034
|
-
if (error)
|
|
1035
|
-
throw error;
|
|
1036
|
-
return { ok: true };
|
|
1037
|
-
});
|
|
1038
|
-
/** Zet precies één profiel als actief (is_default). */
|
|
1039
|
-
export const setActiveProfile = createServerFn({ method: "POST" })
|
|
1040
|
-
.middleware([requireSupabaseAuth])
|
|
1041
|
-
.inputValidator((data) => profileIdInput.parse(data))
|
|
1042
|
-
.handler(async ({ context, data }) => {
|
|
1043
|
-
await requireAdmin(context);
|
|
1044
|
-
const { error: clearError } = await context.supabase
|
|
1045
|
-
.from("profiles_org")
|
|
1046
|
-
.update({ is_default: false })
|
|
1047
|
-
.eq("is_default", true)
|
|
1048
|
-
.neq("id", data.profileId);
|
|
1049
|
-
if (clearError)
|
|
1050
|
-
throw clearError;
|
|
1051
|
-
const { error } = await context.supabase
|
|
1052
|
-
.from("profiles_org")
|
|
1053
|
-
.update({ is_default: true, updated_at: new Date().toISOString() })
|
|
1054
|
-
.eq("id", data.profileId);
|
|
1055
|
-
if (error)
|
|
1056
|
-
throw error;
|
|
1057
|
-
return { ok: true };
|
|
1058
|
-
});
|