@flowselections/admin-installatietechniek 1.0.8 → 1.0.10
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/index.d.ts +0 -1
- 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 +15657 -6640
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +15657 -6640
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +15657 -6640
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +15971 -6840
- 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 +165430 -124956
- package/dist-lib/lib/admin.functions.d.ts.map +1 -1
- package/dist-lib/lib/admin.functions.js +115 -429
- 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/package.json +87 -87
|
@@ -1,9 +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
4
|
import { normalizeServerErrors } from "../lib/serverErrors";
|
|
6
|
-
import { requireAdmin, requireUserAdmin } from "../lib/admin.server";
|
|
5
|
+
import { isFullAdminRoleKey, requireAdmin, requireUserAdmin, resolveAppBaseUrl, verifyAddressWithGoogle } from "../lib/admin.server";
|
|
7
6
|
const featureSchema = z.object({
|
|
8
7
|
id: z.string().trim().min(1).max(200),
|
|
9
8
|
name: z.string().trim().min(1).max(200),
|
|
@@ -32,7 +31,6 @@ const roleIdInput = z.object({ roleId: z.string().uuid() });
|
|
|
32
31
|
const assignRoleInput = z.object({
|
|
33
32
|
userId: z.string().uuid(),
|
|
34
33
|
roleId: z.string().uuid(),
|
|
35
|
-
profileId: z.string().uuid().optional(),
|
|
36
34
|
});
|
|
37
35
|
const rolePermissionsSchema = z
|
|
38
36
|
.array(z.object({
|
|
@@ -51,11 +49,33 @@ const updateRoleInput = z.object({
|
|
|
51
49
|
name: z.string().trim().min(1).max(80),
|
|
52
50
|
permissions: rolePermissionsSchema.default([]),
|
|
53
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
|
+
}
|
|
54
68
|
const inviteUserInput = z.object({
|
|
55
69
|
email: z.string().trim().email().max(255),
|
|
56
70
|
displayName: z.string().trim().min(1).max(100),
|
|
57
71
|
roleId: z.string().uuid(),
|
|
58
|
-
|
|
72
|
+
address: optionalAddress,
|
|
73
|
+
phone: optionalPhone,
|
|
74
|
+
});
|
|
75
|
+
const updateUserContactInput = z.object({
|
|
76
|
+
userId: z.string().uuid(),
|
|
77
|
+
address: optionalAddress,
|
|
78
|
+
phone: optionalPhone,
|
|
59
79
|
});
|
|
60
80
|
const deleteUserInput = z.object({
|
|
61
81
|
userId: z.string().uuid(),
|
|
@@ -68,7 +88,6 @@ const createTestUserInput = z.object({
|
|
|
68
88
|
displayName: z.string().trim().min(1).max(100),
|
|
69
89
|
roleId: z.string().uuid(),
|
|
70
90
|
email: z.string().trim().email().max(255).optional(),
|
|
71
|
-
profileId: z.string().uuid().optional(),
|
|
72
91
|
});
|
|
73
92
|
function slugify(value) {
|
|
74
93
|
return (value
|
|
@@ -93,95 +112,30 @@ async function fetchRole(supabase, roleId) {
|
|
|
93
112
|
}
|
|
94
113
|
async function assertMayUseRole(supabase, roleId, isAdmin) {
|
|
95
114
|
const role = await fetchRole(supabase, roleId);
|
|
96
|
-
if (!isAdmin && role.key
|
|
115
|
+
if (!isAdmin && isFullAdminRoleKey(role.key)) {
|
|
97
116
|
throw new Error("Alleen een admin kan de adminrol gebruiken.");
|
|
98
117
|
}
|
|
99
118
|
return role;
|
|
100
119
|
}
|
|
101
|
-
const profileScopeInput = z
|
|
102
|
-
.object({ profileId: z.string().uuid().optional() })
|
|
103
|
-
.optional();
|
|
104
120
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
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.
|
|
107
123
|
*/
|
|
108
|
-
async function
|
|
109
|
-
|
|
110
|
-
return null;
|
|
111
|
-
const { data, error } = await supabase
|
|
112
|
-
.from("profiles_org")
|
|
113
|
-
.select("id, name, tenant_id, is_default")
|
|
114
|
-
.eq("id", profileId)
|
|
115
|
-
.maybeSingle();
|
|
124
|
+
async function resolveSingleTenantId(admin) {
|
|
125
|
+
const { data, error } = await admin.from("tenants").select("id").limit(2);
|
|
116
126
|
if (error)
|
|
117
127
|
throw error;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const [{ data: roleRows }, { data: moduleRows }] = await Promise.all([
|
|
121
|
-
supabase.from("profile_roles").select("role_id").eq("profile_id", data.id),
|
|
122
|
-
supabase.from("profile_modules").select("module_id").eq("profile_id", data.id),
|
|
123
|
-
]);
|
|
124
|
-
return {
|
|
125
|
-
id: data.id,
|
|
126
|
-
name: data.name,
|
|
127
|
-
tenantId: (data.tenant_id ?? null),
|
|
128
|
-
roleIds: (Array.isArray(roleRows) ? roleRows : []).map((r) => r.role_id),
|
|
129
|
-
moduleIds: (Array.isArray(moduleRows) ? moduleRows : []).map((m) => m.module_id),
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Bepaalt op welk bedrijfsprofiel deze aanvraag draait.
|
|
134
|
-
* - Admin: vrije keuze (leeg = overzicht met alles).
|
|
135
|
-
* - Niet-admin (administratie): altijd vastgezet op het eigen profiel,
|
|
136
|
-
* afgeleid uit de tenant van de gebruiker of anders uit de profielen
|
|
137
|
-
* waaraan een van zijn rollen gekoppeld is.
|
|
138
|
-
*/
|
|
139
|
-
async function resolveEffectiveProfileId(admin, userId, requestedProfileId, isAdmin) {
|
|
140
|
-
if (isAdmin)
|
|
141
|
-
return { profileId: requestedProfileId, locked: false };
|
|
142
|
-
const { data: me } = await admin
|
|
143
|
-
.from("profiles")
|
|
144
|
-
.select("tenant_id")
|
|
145
|
-
.eq("id", userId)
|
|
146
|
-
.maybeSingle();
|
|
147
|
-
if (me?.tenant_id) {
|
|
148
|
-
const { data: byTenant } = await admin
|
|
149
|
-
.from("profiles_org")
|
|
150
|
-
.select("id")
|
|
151
|
-
.eq("tenant_id", me.tenant_id)
|
|
152
|
-
.limit(1);
|
|
153
|
-
const first = Array.isArray(byTenant) ? byTenant[0] : null;
|
|
154
|
-
if (first?.id)
|
|
155
|
-
return { profileId: first.id, locked: true };
|
|
156
|
-
}
|
|
157
|
-
const { data: myRoles } = await admin
|
|
158
|
-
.from("role_assignments")
|
|
159
|
-
.select("role_id")
|
|
160
|
-
.eq("user_id", userId);
|
|
161
|
-
const roleIds = (Array.isArray(myRoles) ? myRoles : []).map((r) => r.role_id);
|
|
162
|
-
if (roleIds.length > 0) {
|
|
163
|
-
const { data: links } = await admin
|
|
164
|
-
.from("profile_roles")
|
|
165
|
-
.select("profile_id")
|
|
166
|
-
.in("role_id", roleIds)
|
|
167
|
-
.limit(1);
|
|
168
|
-
const link = Array.isArray(links) ? links[0] : null;
|
|
169
|
-
if (link?.profile_id)
|
|
170
|
-
return { profileId: link.profile_id, locked: true };
|
|
171
|
-
}
|
|
172
|
-
return { profileId: undefined, locked: true };
|
|
128
|
+
const rows = Array.isArray(data) ? data : [];
|
|
129
|
+
return rows.length === 1 ? rows[0].id : null;
|
|
173
130
|
}
|
|
174
131
|
export const getUsersWithRoles = createServerFn({ method: "POST" })
|
|
175
132
|
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
176
|
-
.
|
|
177
|
-
.handler(async ({ context, data }) => {
|
|
133
|
+
.handler(async ({ context }) => {
|
|
178
134
|
const { isAdmin } = await requireUserAdmin(context);
|
|
179
135
|
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
180
|
-
const effective = await resolveEffectiveProfileId(supabaseAdmin, context.userId, data?.profileId, isAdmin);
|
|
181
|
-
const scope = await loadProfileScope(supabaseAdmin, effective.profileId);
|
|
182
136
|
const profilesQuery = supabaseAdmin
|
|
183
137
|
.from("profiles")
|
|
184
|
-
.select("id, email, display_name, tenant_id")
|
|
138
|
+
.select("id, email, display_name, tenant_id, address, phone")
|
|
185
139
|
.order("email", { ascending: true });
|
|
186
140
|
const [{ data: profiles, error: profilesError }, { data: assignments, error: assignmentsError }, { data: roles, error: rolesError }, { data: authList, error: authError },] = await Promise.all([
|
|
187
141
|
profilesQuery,
|
|
@@ -200,15 +154,15 @@ export const getUsersWithRoles = createServerFn({ method: "POST" })
|
|
|
200
154
|
const safeProfiles = Array.isArray(profiles) ? profiles : [];
|
|
201
155
|
const safeAssignments = Array.isArray(assignments) ? assignments : [];
|
|
202
156
|
const allRoles = Array.isArray(roles) ? roles : [];
|
|
203
|
-
const safeRoles = scope && scope.roleIds.length > 0
|
|
204
|
-
? allRoles.filter((r) => scope.roleIds.includes(r.id))
|
|
205
|
-
: allRoles;
|
|
206
157
|
const safeAuthUsers = Array.isArray(authList?.users) ? authList.users : [];
|
|
207
158
|
const roleById = new Map(allRoles.map((r) => [r.id, r]));
|
|
208
159
|
const confirmedById = new Map();
|
|
209
160
|
const isTestById = new Map();
|
|
161
|
+
const authEmailById = new Map();
|
|
210
162
|
for (const u of safeAuthUsers) {
|
|
211
163
|
confirmedById.set(u.id, Boolean(u.email_confirmed_at));
|
|
164
|
+
if (u.email)
|
|
165
|
+
authEmailById.set(u.id, u.email);
|
|
212
166
|
const meta = (u.user_metadata ?? {});
|
|
213
167
|
isTestById.set(u.id, Boolean(meta.is_test_account));
|
|
214
168
|
}
|
|
@@ -223,52 +177,21 @@ export const getUsersWithRoles = createServerFn({ method: "POST" })
|
|
|
223
177
|
}
|
|
224
178
|
rolesByUser.set(a.user_id, list);
|
|
225
179
|
}
|
|
226
|
-
// Binnen een profiel horen gebruikers erbij via de tenant OF via een rol
|
|
227
|
-
// die aan het profiel gekoppeld is. Zonder profiel: iedereen.
|
|
228
|
-
const scopedRoleIds = new Set(scope?.roleIds ?? []);
|
|
229
|
-
const usersViaRole = new Set();
|
|
230
|
-
if (scope && scopedRoleIds.size > 0) {
|
|
231
|
-
for (const a of safeAssignments) {
|
|
232
|
-
if (scopedRoleIds.has(a.role_id))
|
|
233
|
-
usersViaRole.add(a.user_id);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
// Vastgezette gebruiker zonder gekoppeld profiel: niets tonen.
|
|
237
|
-
const noProfileLock = effective.locked && !scope;
|
|
238
|
-
const scopedProfiles = noProfileLock
|
|
239
|
-
? []
|
|
240
|
-
: !scope
|
|
241
|
-
? safeProfiles
|
|
242
|
-
: safeProfiles.filter((p) => {
|
|
243
|
-
const viaTenant = Boolean(scope.tenantId) && p.tenant_id === scope.tenantId;
|
|
244
|
-
return viaTenant || usersViaRole.has(p.id);
|
|
245
|
-
});
|
|
246
180
|
return {
|
|
247
|
-
users:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
: viaTenant && viaRole
|
|
260
|
-
? "both"
|
|
261
|
-
: viaTenant
|
|
262
|
-
? "tenant"
|
|
263
|
-
: "role",
|
|
264
|
-
};
|
|
265
|
-
}),
|
|
266
|
-
availableRoles: (noProfileLock ? [] : safeRoles)
|
|
267
|
-
.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))
|
|
268
193
|
.map((r) => ({ id: r.id, key: r.key, name: r.name, isProtected: r.is_protected })),
|
|
269
194
|
isAdmin,
|
|
270
|
-
profileLocked: effective.locked,
|
|
271
|
-
activeProfile: scope ? { id: scope.id, name: scope.name, tenantId: scope.tenantId } : null,
|
|
272
195
|
};
|
|
273
196
|
});
|
|
274
197
|
export const assignRole = createServerFn({ method: "POST" })
|
|
@@ -292,18 +215,16 @@ export const assignRole = createServerFn({ method: "POST" })
|
|
|
292
215
|
.insert({ user_id: data.userId, role_id: data.roleId });
|
|
293
216
|
if (error)
|
|
294
217
|
throw error;
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
.is("tenant_id", null);
|
|
306
|
-
}
|
|
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);
|
|
307
228
|
}
|
|
308
229
|
return { ok: true };
|
|
309
230
|
});
|
|
@@ -328,12 +249,8 @@ export const removeRole = createServerFn({ method: "POST" })
|
|
|
328
249
|
/** Alle rollen met hun rechten, plus de geregistreerde modules en onderdelen. */
|
|
329
250
|
export const getRoles = createServerFn({ method: "POST" })
|
|
330
251
|
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
331
|
-
.
|
|
332
|
-
|
|
333
|
-
const { isAdmin } = await requireUserAdmin(context);
|
|
334
|
-
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
335
|
-
const effective = await resolveEffectiveProfileId(supabaseAdmin, context.userId, data?.profileId, isAdmin);
|
|
336
|
-
const scope = await loadProfileScope(supabaseAdmin, effective.profileId);
|
|
252
|
+
.handler(async ({ context }) => {
|
|
253
|
+
await requireUserAdmin(context);
|
|
337
254
|
const [{ data: roles, error: rolesError }, { data: perms, error: permsError }, { data: registry, error: registryError }, { data: featureRows, error: featuresError },] = await Promise.all([
|
|
338
255
|
context.supabase.from("roles").select("id, key, name, is_protected").order("name", { ascending: true }),
|
|
339
256
|
context.supabase.from("role_permissions").select("role_id, module_id, feature_id, can_view, can_edit"),
|
|
@@ -348,23 +265,21 @@ export const getRoles = createServerFn({ method: "POST" })
|
|
|
348
265
|
throw registryError;
|
|
349
266
|
if (featuresError)
|
|
350
267
|
throw featuresError;
|
|
351
|
-
const
|
|
352
|
-
const allRoles = noProfileLock ? [] : Array.isArray(roles) ? roles : [];
|
|
353
|
-
const safeRoles = scope && scope.roleIds.length > 0
|
|
354
|
-
? allRoles.filter((r) => scope.roleIds.includes(r.id))
|
|
355
|
-
: allRoles;
|
|
268
|
+
const safeRoles = Array.isArray(roles) ? roles : [];
|
|
356
269
|
const safePerms = Array.isArray(perms) ? perms : [];
|
|
357
|
-
const
|
|
358
|
-
const safeRegistry = scope && scope.moduleIds.length > 0
|
|
359
|
-
? allRegistry.filter((m) => scope.moduleIds.includes(m.module_id))
|
|
360
|
-
: allRegistry;
|
|
270
|
+
const safeRegistry = Array.isArray(registry) ? registry : [];
|
|
361
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));
|
|
362
276
|
return {
|
|
363
277
|
roles: safeRoles.map((r) => ({
|
|
364
278
|
id: r.id,
|
|
365
279
|
key: r.key,
|
|
366
280
|
name: r.name,
|
|
367
281
|
isProtected: r.is_protected,
|
|
282
|
+
unmapped: unmappedKeys.has(r.key),
|
|
368
283
|
})),
|
|
369
284
|
modules: safeRegistry
|
|
370
285
|
.filter((m) => m.is_active)
|
|
@@ -383,8 +298,6 @@ export const getRoles = createServerFn({ method: "POST" })
|
|
|
383
298
|
canView: Boolean(p.can_view),
|
|
384
299
|
canEdit: Boolean(p.can_edit),
|
|
385
300
|
})),
|
|
386
|
-
profileLocked: effective.locked,
|
|
387
|
-
activeProfile: scope ? { id: scope.id, name: scope.name, tenantId: scope.tenantId } : null,
|
|
388
301
|
};
|
|
389
302
|
});
|
|
390
303
|
async function writeRolePermissions(supabase, roleId, permissions) {
|
|
@@ -663,30 +576,31 @@ export const inviteUser = createServerFn({ method: "POST" })
|
|
|
663
576
|
.handler(async ({ context, data }) => {
|
|
664
577
|
const { isAdmin } = await requireUserAdmin(context);
|
|
665
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 ?? "");
|
|
666
588
|
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
667
|
-
|
|
668
|
-
try {
|
|
669
|
-
const host = getRequestHost();
|
|
670
|
-
if (host) {
|
|
671
|
-
const proto = host.startsWith("localhost") ? "http" : "https";
|
|
672
|
-
redirectTo = `${proto}://${host}/reset-wachtwoord`;
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
catch {
|
|
676
|
-
redirectTo = undefined;
|
|
677
|
-
}
|
|
589
|
+
const redirectTo = `${resolveAppBaseUrl()}/reset-wachtwoord`;
|
|
678
590
|
const { data: invited, error: inviteError } = await supabaseAdmin.auth.admin.inviteUserByEmail(data.email, { data: { display_name: data.displayName }, redirectTo });
|
|
679
591
|
if (inviteError || !invited?.user) {
|
|
680
592
|
const msg = inviteError?.message ?? "Uitnodigen mislukt";
|
|
681
593
|
throw new Error(/already|exists|registered/i.test(msg) ? "Deze gebruiker bestaat al." : msg);
|
|
682
594
|
}
|
|
683
595
|
const userId = invited.user.id;
|
|
684
|
-
const
|
|
596
|
+
const tenantId = await resolveSingleTenantId(supabaseAdmin);
|
|
685
597
|
const { error: profileError } = await supabaseAdmin.from("profiles").upsert({
|
|
686
598
|
id: userId,
|
|
687
599
|
email: data.email,
|
|
688
600
|
display_name: data.displayName,
|
|
689
|
-
|
|
601
|
+
address: verifiedAddress ? verifiedAddress : null,
|
|
602
|
+
phone: data.phone ? data.phone.trim() : null,
|
|
603
|
+
...(tenantId ? { tenant_id: tenantId } : {}),
|
|
690
604
|
}, { onConflict: "id" });
|
|
691
605
|
if (profileError)
|
|
692
606
|
throw profileError;
|
|
@@ -713,7 +627,7 @@ export const deleteUser = createServerFn({ method: "POST" })
|
|
|
713
627
|
if (targetError)
|
|
714
628
|
throw targetError;
|
|
715
629
|
const safeTargetRoles = Array.isArray(targetRoles) ? targetRoles : [];
|
|
716
|
-
if (safeTargetRoles.some((r) => r?.roles?.key
|
|
630
|
+
if (safeTargetRoles.some((r) => isFullAdminRoleKey(r?.roles?.key))) {
|
|
717
631
|
throw new Error("Alleen een admin kan een adminaccount verwijderen.");
|
|
718
632
|
}
|
|
719
633
|
}
|
|
@@ -736,7 +650,7 @@ export const updateUserName = createServerFn({ method: "POST" })
|
|
|
736
650
|
if (targetError)
|
|
737
651
|
throw targetError;
|
|
738
652
|
const safeTargetRoles = Array.isArray(targetRoles) ? targetRoles : [];
|
|
739
|
-
if (safeTargetRoles.some((r) => r?.roles?.key
|
|
653
|
+
if (safeTargetRoles.some((r) => isFullAdminRoleKey(r?.roles?.key))) {
|
|
740
654
|
throw new Error("Alleen een admin kan een adminaccount aanpassen.");
|
|
741
655
|
}
|
|
742
656
|
}
|
|
@@ -754,6 +668,36 @@ export const updateUserName = createServerFn({ method: "POST" })
|
|
|
754
668
|
throw error;
|
|
755
669
|
return { ok: true };
|
|
756
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
|
+
});
|
|
757
701
|
function generatePassword(length = 16) {
|
|
758
702
|
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%*";
|
|
759
703
|
const bytes = new Uint8Array(length);
|
|
@@ -783,12 +727,12 @@ export const createTestUser = createServerFn({ method: "POST" })
|
|
|
783
727
|
throw new Error(/already|exists|registered/i.test(msg) ? "Deze gebruiker bestaat al." : msg);
|
|
784
728
|
}
|
|
785
729
|
const userId = created.user.id;
|
|
786
|
-
const
|
|
730
|
+
const tenantId = await resolveSingleTenantId(supabaseAdmin);
|
|
787
731
|
const { error: profileError } = await supabaseAdmin.from("profiles").upsert({
|
|
788
732
|
id: userId,
|
|
789
733
|
email,
|
|
790
734
|
display_name: data.displayName,
|
|
791
|
-
...(
|
|
735
|
+
...(tenantId ? { tenant_id: tenantId } : {}),
|
|
792
736
|
}, { onConflict: "id" });
|
|
793
737
|
if (profileError)
|
|
794
738
|
throw profileError;
|
|
@@ -799,261 +743,3 @@ export const createTestUser = createServerFn({ method: "POST" })
|
|
|
799
743
|
throw roleError;
|
|
800
744
|
return { ok: true, userId, email, password };
|
|
801
745
|
});
|
|
802
|
-
// ============================================================================
|
|
803
|
-
// Bedrijfsprofielen: bundelen rollen + modules per organisatie (tenant)
|
|
804
|
-
// ============================================================================
|
|
805
|
-
const profileIdInput = z.object({ profileId: z.string().uuid() });
|
|
806
|
-
const profileBodySchema = {
|
|
807
|
-
name: z.string().trim().min(1).max(120),
|
|
808
|
-
tenantId: z.string().uuid().nullable().optional(),
|
|
809
|
-
/** Naam van het bedrijf; maakt een nieuw bedrijf aan of hernoemt het gekozen bedrijf. */
|
|
810
|
-
tenantName: z.string().trim().max(120).optional(),
|
|
811
|
-
roleIds: z.array(z.string().uuid()).max(200).default([]),
|
|
812
|
-
moduleIds: z.array(z.string().trim().min(1).max(100)).max(200).default([]),
|
|
813
|
-
};
|
|
814
|
-
const createProfileInput = z.object(profileBodySchema);
|
|
815
|
-
const updateProfileInput = z.object({ profileId: z.string().uuid(), ...profileBodySchema });
|
|
816
|
-
/**
|
|
817
|
-
* Bepaalt het tenant-id voor een profiel:
|
|
818
|
-
* - bestaand bedrijf + gewijzigde naam -> hernoemen
|
|
819
|
-
* - geen bedrijf + naam -> nieuw bedrijf aanmaken
|
|
820
|
-
*/
|
|
821
|
-
async function resolveTenant(supabase, tenantId, tenantName) {
|
|
822
|
-
const name = (tenantName ?? "").trim();
|
|
823
|
-
if (tenantId) {
|
|
824
|
-
if (name) {
|
|
825
|
-
const { data: current, error: readError } = await supabase
|
|
826
|
-
.from("tenants")
|
|
827
|
-
.select("name")
|
|
828
|
-
.eq("id", tenantId)
|
|
829
|
-
.maybeSingle();
|
|
830
|
-
if (readError)
|
|
831
|
-
throw readError;
|
|
832
|
-
if (current && current.name !== name) {
|
|
833
|
-
const { error } = await supabase
|
|
834
|
-
.from("tenants")
|
|
835
|
-
.update({ name, updated_at: new Date().toISOString() })
|
|
836
|
-
.eq("id", tenantId);
|
|
837
|
-
if (error)
|
|
838
|
-
throw error;
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
return tenantId;
|
|
842
|
-
}
|
|
843
|
-
if (!name)
|
|
844
|
-
return null;
|
|
845
|
-
const { data: created, error } = await supabase
|
|
846
|
-
.from("tenants")
|
|
847
|
-
.insert({ name })
|
|
848
|
-
.select("id")
|
|
849
|
-
.single();
|
|
850
|
-
if (error)
|
|
851
|
-
throw error;
|
|
852
|
-
return created.id;
|
|
853
|
-
}
|
|
854
|
-
async function writeProfileLinks(supabase, profileId, roleIds, moduleIds) {
|
|
855
|
-
const { error: delRoles } = await supabase.from("profile_roles").delete().eq("profile_id", profileId);
|
|
856
|
-
if (delRoles)
|
|
857
|
-
throw delRoles;
|
|
858
|
-
const { error: delModules } = await supabase.from("profile_modules").delete().eq("profile_id", profileId);
|
|
859
|
-
if (delModules)
|
|
860
|
-
throw delModules;
|
|
861
|
-
const uniqueRoles = Array.from(new Set(roleIds));
|
|
862
|
-
if (uniqueRoles.length > 0) {
|
|
863
|
-
const { error } = await supabase
|
|
864
|
-
.from("profile_roles")
|
|
865
|
-
.insert(uniqueRoles.map((roleId) => ({ profile_id: profileId, role_id: roleId })));
|
|
866
|
-
if (error)
|
|
867
|
-
throw error;
|
|
868
|
-
}
|
|
869
|
-
const uniqueModules = Array.from(new Set(moduleIds));
|
|
870
|
-
if (uniqueModules.length > 0) {
|
|
871
|
-
const { error } = await supabase
|
|
872
|
-
.from("profile_modules")
|
|
873
|
-
.insert(uniqueModules.map((moduleId) => ({ profile_id: profileId, module_id: moduleId })));
|
|
874
|
-
if (error)
|
|
875
|
-
throw error;
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
/** Alle bedrijfsprofielen met hun rollen, modules en tellingen. */
|
|
879
|
-
export const getProfiles = createServerFn({ method: "GET" })
|
|
880
|
-
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
881
|
-
.handler(async ({ context }) => {
|
|
882
|
-
const { isAdmin } = await requireUserAdmin(context);
|
|
883
|
-
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
884
|
-
const effective = await resolveEffectiveProfileId(supabaseAdmin, context.userId, undefined, isAdmin);
|
|
885
|
-
const [{ data: profilesOrg, error: profilesError }, { data: profileRoles, error: profileRolesError }, { data: profileModules, error: profileModulesError }, { data: tenants, error: tenantsError },] = await Promise.all([
|
|
886
|
-
context.supabase
|
|
887
|
-
.from("profiles_org")
|
|
888
|
-
.select("id, name, tenant_id, is_default")
|
|
889
|
-
.order("name", { ascending: true }),
|
|
890
|
-
context.supabase.from("profile_roles").select("profile_id, role_id"),
|
|
891
|
-
context.supabase.from("profile_modules").select("profile_id, module_id"),
|
|
892
|
-
// `tenants` is via RLS beperkt tot de eigen tenant; namen tonen mag hier
|
|
893
|
-
// omdat de aanvrager al is geverifieerd als admin/administratie.
|
|
894
|
-
supabaseAdmin.from("tenants").select("id, name").order("name", { ascending: true }),
|
|
895
|
-
]);
|
|
896
|
-
if (profilesError)
|
|
897
|
-
throw profilesError;
|
|
898
|
-
if (profileRolesError)
|
|
899
|
-
throw profileRolesError;
|
|
900
|
-
if (profileModulesError)
|
|
901
|
-
throw profileModulesError;
|
|
902
|
-
if (tenantsError)
|
|
903
|
-
throw tenantsError;
|
|
904
|
-
const { data: userRows, error: userError } = await supabaseAdmin
|
|
905
|
-
.from("profiles")
|
|
906
|
-
.select("id, tenant_id");
|
|
907
|
-
if (userError)
|
|
908
|
-
throw userError;
|
|
909
|
-
const allProfilesOrg = Array.isArray(profilesOrg) ? profilesOrg : [];
|
|
910
|
-
// Niet-admin: alleen het eigen (vastgezette) profiel.
|
|
911
|
-
const safeProfiles = effective.locked
|
|
912
|
-
? allProfilesOrg.filter((p) => p.id === effective.profileId)
|
|
913
|
-
: allProfilesOrg;
|
|
914
|
-
const safeProfileRoles = Array.isArray(profileRoles) ? profileRoles : [];
|
|
915
|
-
const safeProfileModules = Array.isArray(profileModules) ? profileModules : [];
|
|
916
|
-
const safeTenants = Array.isArray(tenants) ? tenants : [];
|
|
917
|
-
const safeUsers = Array.isArray(userRows) ? userRows : [];
|
|
918
|
-
const usersByTenant = new Map();
|
|
919
|
-
for (const u of safeUsers) {
|
|
920
|
-
if (!u.tenant_id)
|
|
921
|
-
continue;
|
|
922
|
-
usersByTenant.set(u.tenant_id, (usersByTenant.get(u.tenant_id) ?? 0) + 1);
|
|
923
|
-
}
|
|
924
|
-
const tenantName = new Map(safeTenants.map((t) => [t.id, t.name]));
|
|
925
|
-
return {
|
|
926
|
-
profiles: safeProfiles.map((p) => ({
|
|
927
|
-
id: p.id,
|
|
928
|
-
name: p.name,
|
|
929
|
-
tenantId: (p.tenant_id ?? null),
|
|
930
|
-
tenantName: p.tenant_id ? tenantName.get(p.tenant_id) ?? null : null,
|
|
931
|
-
isDefault: Boolean(p.is_default),
|
|
932
|
-
roleIds: safeProfileRoles.filter((r) => r.profile_id === p.id).map((r) => r.role_id),
|
|
933
|
-
moduleIds: safeProfileModules
|
|
934
|
-
.filter((m) => m.profile_id === p.id)
|
|
935
|
-
.map((m) => m.module_id),
|
|
936
|
-
userCount: p.tenant_id ? usersByTenant.get(p.tenant_id) ?? 0 : 0,
|
|
937
|
-
})),
|
|
938
|
-
tenants: safeTenants.map((t) => ({ id: t.id, name: t.name })),
|
|
939
|
-
profileLocked: effective.locked,
|
|
940
|
-
lockedProfileId: effective.locked ? effective.profileId ?? null : null,
|
|
941
|
-
};
|
|
942
|
-
});
|
|
943
|
-
export const createProfile = createServerFn({ method: "POST" })
|
|
944
|
-
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
945
|
-
.inputValidator((data) => createProfileInput.parse(data))
|
|
946
|
-
.handler(async ({ context, data }) => {
|
|
947
|
-
await requireAdmin(context);
|
|
948
|
-
// `tenants` heeft geen insert-policy voor admins; na de adminguard mag dit
|
|
949
|
-
// veilig via de serverclient.
|
|
950
|
-
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
951
|
-
const tenantId = await resolveTenant(supabaseAdmin, data.tenantId, data.tenantName);
|
|
952
|
-
const { data: created, error } = await context.supabase
|
|
953
|
-
.from("profiles_org")
|
|
954
|
-
.insert({ name: data.name, tenant_id: tenantId })
|
|
955
|
-
.select("id")
|
|
956
|
-
.single();
|
|
957
|
-
if (error)
|
|
958
|
-
throw error;
|
|
959
|
-
await writeProfileLinks(context.supabase, created.id, data.roleIds, data.moduleIds);
|
|
960
|
-
return { ok: true, profileId: created.id };
|
|
961
|
-
});
|
|
962
|
-
export const updateProfile = createServerFn({ method: "POST" })
|
|
963
|
-
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
964
|
-
.inputValidator((data) => updateProfileInput.parse(data))
|
|
965
|
-
.handler(async ({ context, data }) => {
|
|
966
|
-
await requireAdmin(context);
|
|
967
|
-
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
968
|
-
const tenantId = await resolveTenant(supabaseAdmin, data.tenantId, data.tenantName);
|
|
969
|
-
const { error } = await context.supabase
|
|
970
|
-
.from("profiles_org")
|
|
971
|
-
.update({ name: data.name, tenant_id: tenantId, updated_at: new Date().toISOString() })
|
|
972
|
-
.eq("id", data.profileId);
|
|
973
|
-
if (error)
|
|
974
|
-
throw error;
|
|
975
|
-
await writeProfileLinks(context.supabase, data.profileId, data.roleIds, data.moduleIds);
|
|
976
|
-
return { ok: true };
|
|
977
|
-
});
|
|
978
|
-
export const duplicateProfile = createServerFn({ method: "POST" })
|
|
979
|
-
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
980
|
-
.inputValidator((data) => profileIdInput.parse(data))
|
|
981
|
-
.handler(async ({ context, data }) => {
|
|
982
|
-
await requireAdmin(context);
|
|
983
|
-
const { data: source, error: sourceError } = await context.supabase
|
|
984
|
-
.from("profiles_org")
|
|
985
|
-
.select("name, tenant_id")
|
|
986
|
-
.eq("id", data.profileId)
|
|
987
|
-
.maybeSingle();
|
|
988
|
-
if (sourceError)
|
|
989
|
-
throw sourceError;
|
|
990
|
-
if (!source)
|
|
991
|
-
throw new Error("Profiel bestaat niet.");
|
|
992
|
-
const [{ data: roleRows }, { data: moduleRows }] = await Promise.all([
|
|
993
|
-
context.supabase.from("profile_roles").select("role_id").eq("profile_id", data.profileId),
|
|
994
|
-
context.supabase.from("profile_modules").select("module_id").eq("profile_id", data.profileId),
|
|
995
|
-
]);
|
|
996
|
-
const { data: created, error } = await context.supabase
|
|
997
|
-
.from("profiles_org")
|
|
998
|
-
.insert({ name: `${source.name} (kopie)`, tenant_id: source.tenant_id })
|
|
999
|
-
.select("id")
|
|
1000
|
-
.single();
|
|
1001
|
-
if (error)
|
|
1002
|
-
throw error;
|
|
1003
|
-
await writeProfileLinks(context.supabase, created.id, (Array.isArray(roleRows) ? roleRows : []).map((r) => r.role_id), (Array.isArray(moduleRows) ? moduleRows : []).map((m) => m.module_id));
|
|
1004
|
-
return { ok: true, profileId: created.id };
|
|
1005
|
-
});
|
|
1006
|
-
export const deleteProfile = createServerFn({ method: "POST" })
|
|
1007
|
-
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
1008
|
-
.inputValidator((data) => profileIdInput.parse(data))
|
|
1009
|
-
.handler(async ({ context, data }) => {
|
|
1010
|
-
await requireAdmin(context);
|
|
1011
|
-
const { data: profile, error: profileError } = await context.supabase
|
|
1012
|
-
.from("profiles_org")
|
|
1013
|
-
.select("id, tenant_id, is_default")
|
|
1014
|
-
.eq("id", data.profileId)
|
|
1015
|
-
.maybeSingle();
|
|
1016
|
-
if (profileError)
|
|
1017
|
-
throw profileError;
|
|
1018
|
-
if (!profile)
|
|
1019
|
-
throw new Error("Profiel bestaat niet.");
|
|
1020
|
-
if (profile.is_default)
|
|
1021
|
-
throw new Error("Het actieve profiel kan niet verwijderd worden.");
|
|
1022
|
-
if (profile.tenant_id) {
|
|
1023
|
-
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
1024
|
-
const { count, error: countError } = await supabaseAdmin
|
|
1025
|
-
.from("profiles")
|
|
1026
|
-
.select("id", { count: "exact", head: true })
|
|
1027
|
-
.eq("tenant_id", profile.tenant_id);
|
|
1028
|
-
if (countError)
|
|
1029
|
-
throw countError;
|
|
1030
|
-
if ((count ?? 0) > 0) {
|
|
1031
|
-
throw new Error("Er zijn nog gebruikers aan dit bedrijf gekoppeld.");
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
const { error } = await context.supabase.from("profiles_org").delete().eq("id", data.profileId);
|
|
1035
|
-
if (error)
|
|
1036
|
-
throw error;
|
|
1037
|
-
return { ok: true };
|
|
1038
|
-
});
|
|
1039
|
-
/** Zet precies één profiel als actief (is_default). */
|
|
1040
|
-
export const setActiveProfile = createServerFn({ method: "POST" })
|
|
1041
|
-
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
1042
|
-
.inputValidator((data) => profileIdInput.parse(data))
|
|
1043
|
-
.handler(async ({ context, data }) => {
|
|
1044
|
-
await requireAdmin(context);
|
|
1045
|
-
const { error: clearError } = await context.supabase
|
|
1046
|
-
.from("profiles_org")
|
|
1047
|
-
.update({ is_default: false })
|
|
1048
|
-
.eq("is_default", true)
|
|
1049
|
-
.neq("id", data.profileId);
|
|
1050
|
-
if (clearError)
|
|
1051
|
-
throw clearError;
|
|
1052
|
-
const { error } = await context.supabase
|
|
1053
|
-
.from("profiles_org")
|
|
1054
|
-
.update({ is_default: true, updated_at: new Date().toISOString() })
|
|
1055
|
-
.eq("id", data.profileId);
|
|
1056
|
-
if (error)
|
|
1057
|
-
throw error;
|
|
1058
|
-
return { ok: true };
|
|
1059
|
-
});
|