@flowselections/admin-installatietechniek 1.0.6 → 1.0.8
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 +126 -28
- package/dist-lib/index.d.ts +5 -4
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +3 -3
- package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/auth-attacher.js +11 -1
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +511 -145
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +511 -145
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +511 -145
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +536 -161
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/admin.functions.d.ts +11273 -3212
- package/dist-lib/lib/admin.functions.d.ts.map +1 -1
- package/dist-lib/lib/admin.functions.js +213 -49
- 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 +2 -2
|
@@ -2,6 +2,7 @@ import { createServerFn } from "@tanstack/react-start";
|
|
|
2
2
|
import { getRequestHost } from "@tanstack/react-start/server";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { requireSupabaseAuth } from "../integrations/supabase/auth-middleware";
|
|
5
|
+
import { normalizeServerErrors } from "../lib/serverErrors";
|
|
5
6
|
import { requireAdmin, requireUserAdmin } from "../lib/admin.server";
|
|
6
7
|
const featureSchema = z.object({
|
|
7
8
|
id: z.string().trim().min(1).max(200),
|
|
@@ -31,6 +32,7 @@ const roleIdInput = z.object({ roleId: z.string().uuid() });
|
|
|
31
32
|
const assignRoleInput = z.object({
|
|
32
33
|
userId: z.string().uuid(),
|
|
33
34
|
roleId: z.string().uuid(),
|
|
35
|
+
profileId: z.string().uuid().optional(),
|
|
34
36
|
});
|
|
35
37
|
const rolePermissionsSchema = z
|
|
36
38
|
.array(z.object({
|
|
@@ -99,11 +101,18 @@ async function assertMayUseRole(supabase, roleId, isAdmin) {
|
|
|
99
101
|
const profileScopeInput = z
|
|
100
102
|
.object({ profileId: z.string().uuid().optional() })
|
|
101
103
|
.optional();
|
|
102
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* Tenant + rol-/module-scope van een bedrijfsprofiel.
|
|
106
|
+
* Zonder `profileId` is er geen scope (= overzicht: alles zichtbaar).
|
|
107
|
+
*/
|
|
103
108
|
async function loadProfileScope(supabase, profileId) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const { data, error } = await
|
|
109
|
+
if (!profileId)
|
|
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();
|
|
107
116
|
if (error)
|
|
108
117
|
throw error;
|
|
109
118
|
if (!data)
|
|
@@ -120,19 +129,60 @@ async function loadProfileScope(supabase, profileId) {
|
|
|
120
129
|
moduleIds: (Array.isArray(moduleRows) ? moduleRows : []).map((m) => m.module_id),
|
|
121
130
|
};
|
|
122
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 };
|
|
173
|
+
}
|
|
123
174
|
export const getUsersWithRoles = createServerFn({ method: "POST" })
|
|
124
|
-
.middleware([requireSupabaseAuth])
|
|
175
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
125
176
|
.inputValidator((data) => profileScopeInput.parse(data))
|
|
126
177
|
.handler(async ({ context, data }) => {
|
|
127
178
|
const { isAdmin } = await requireUserAdmin(context);
|
|
128
179
|
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
129
|
-
const
|
|
130
|
-
|
|
180
|
+
const effective = await resolveEffectiveProfileId(supabaseAdmin, context.userId, data?.profileId, isAdmin);
|
|
181
|
+
const scope = await loadProfileScope(supabaseAdmin, effective.profileId);
|
|
182
|
+
const profilesQuery = supabaseAdmin
|
|
131
183
|
.from("profiles")
|
|
132
184
|
.select("id, email, display_name, tenant_id")
|
|
133
185
|
.order("email", { ascending: true });
|
|
134
|
-
if (scope?.tenantId)
|
|
135
|
-
profilesQuery = profilesQuery.eq("tenant_id", scope.tenantId);
|
|
136
186
|
const [{ data: profiles, error: profilesError }, { data: assignments, error: assignmentsError }, { data: roles, error: rolesError }, { data: authList, error: authError },] = await Promise.all([
|
|
137
187
|
profilesQuery,
|
|
138
188
|
supabaseAdmin.from("role_assignments").select("user_id, role_id"),
|
|
@@ -173,24 +223,56 @@ export const getUsersWithRoles = createServerFn({ method: "POST" })
|
|
|
173
223
|
}
|
|
174
224
|
rolesByUser.set(a.user_id, list);
|
|
175
225
|
}
|
|
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
|
+
});
|
|
176
246
|
return {
|
|
177
|
-
users:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
247
|
+
users: scopedProfiles.map((profile) => {
|
|
248
|
+
const viaTenant = Boolean(scope?.tenantId) && profile.tenant_id === scope?.tenantId;
|
|
249
|
+
const viaRole = usersViaRole.has(profile.id);
|
|
250
|
+
return {
|
|
251
|
+
id: profile.id,
|
|
252
|
+
email: profile.email ?? "-",
|
|
253
|
+
displayName: profile.display_name ?? "-",
|
|
254
|
+
roles: rolesByUser.get(profile.id) ?? [],
|
|
255
|
+
emailConfirmed: confirmedById.get(profile.id) ?? false,
|
|
256
|
+
isTestAccount: isTestById.get(profile.id) ?? false,
|
|
257
|
+
matchedVia: !scope
|
|
258
|
+
? null
|
|
259
|
+
: viaTenant && viaRole
|
|
260
|
+
? "both"
|
|
261
|
+
: viaTenant
|
|
262
|
+
? "tenant"
|
|
263
|
+
: "role",
|
|
264
|
+
};
|
|
265
|
+
}),
|
|
266
|
+
availableRoles: (noProfileLock ? [] : safeRoles)
|
|
186
267
|
.filter((r) => isAdmin || r.key !== "admin")
|
|
187
268
|
.map((r) => ({ id: r.id, key: r.key, name: r.name, isProtected: r.is_protected })),
|
|
188
269
|
isAdmin,
|
|
270
|
+
profileLocked: effective.locked,
|
|
189
271
|
activeProfile: scope ? { id: scope.id, name: scope.name, tenantId: scope.tenantId } : null,
|
|
190
272
|
};
|
|
191
273
|
});
|
|
192
274
|
export const assignRole = createServerFn({ method: "POST" })
|
|
193
|
-
.middleware([requireSupabaseAuth])
|
|
275
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
194
276
|
.inputValidator((data) => assignRoleInput.parse(data))
|
|
195
277
|
.handler(async ({ context, data }) => {
|
|
196
278
|
const { isAdmin } = await requireUserAdmin(context);
|
|
@@ -210,10 +292,23 @@ export const assignRole = createServerFn({ method: "POST" })
|
|
|
210
292
|
.insert({ user_id: data.userId, role_id: data.roleId });
|
|
211
293
|
if (error)
|
|
212
294
|
throw error;
|
|
295
|
+
// Binnen een actief bedrijfsprofiel: koppel de gebruiker aan de tenant van
|
|
296
|
+
// dat profiel als die nog leeg is, zodat de koppeling vanzelf dichtgroeit.
|
|
297
|
+
if (data.profileId) {
|
|
298
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
299
|
+
const scope = await loadProfileScope(supabaseAdmin, data.profileId);
|
|
300
|
+
if (scope?.tenantId) {
|
|
301
|
+
await supabaseAdmin
|
|
302
|
+
.from("profiles")
|
|
303
|
+
.update({ tenant_id: scope.tenantId })
|
|
304
|
+
.eq("id", data.userId)
|
|
305
|
+
.is("tenant_id", null);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
213
308
|
return { ok: true };
|
|
214
309
|
});
|
|
215
310
|
export const removeRole = createServerFn({ method: "POST" })
|
|
216
|
-
.middleware([requireSupabaseAuth])
|
|
311
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
217
312
|
.inputValidator((data) => assignRoleInput.parse(data))
|
|
218
313
|
.handler(async ({ context, data }) => {
|
|
219
314
|
const { isAdmin } = await requireUserAdmin(context);
|
|
@@ -232,11 +327,13 @@ export const removeRole = createServerFn({ method: "POST" })
|
|
|
232
327
|
});
|
|
233
328
|
/** Alle rollen met hun rechten, plus de geregistreerde modules en onderdelen. */
|
|
234
329
|
export const getRoles = createServerFn({ method: "POST" })
|
|
235
|
-
.middleware([requireSupabaseAuth])
|
|
330
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
236
331
|
.inputValidator((data) => profileScopeInput.parse(data))
|
|
237
332
|
.handler(async ({ context, data }) => {
|
|
238
|
-
await requireUserAdmin(context);
|
|
239
|
-
const
|
|
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);
|
|
240
337
|
const [{ data: roles, error: rolesError }, { data: perms, error: permsError }, { data: registry, error: registryError }, { data: featureRows, error: featuresError },] = await Promise.all([
|
|
241
338
|
context.supabase.from("roles").select("id, key, name, is_protected").order("name", { ascending: true }),
|
|
242
339
|
context.supabase.from("role_permissions").select("role_id, module_id, feature_id, can_view, can_edit"),
|
|
@@ -251,12 +348,13 @@ export const getRoles = createServerFn({ method: "POST" })
|
|
|
251
348
|
throw registryError;
|
|
252
349
|
if (featuresError)
|
|
253
350
|
throw featuresError;
|
|
254
|
-
const
|
|
351
|
+
const noProfileLock = effective.locked && !scope;
|
|
352
|
+
const allRoles = noProfileLock ? [] : Array.isArray(roles) ? roles : [];
|
|
255
353
|
const safeRoles = scope && scope.roleIds.length > 0
|
|
256
354
|
? allRoles.filter((r) => scope.roleIds.includes(r.id))
|
|
257
355
|
: allRoles;
|
|
258
356
|
const safePerms = Array.isArray(perms) ? perms : [];
|
|
259
|
-
const allRegistry = Array.isArray(registry) ? registry : [];
|
|
357
|
+
const allRegistry = noProfileLock ? [] : Array.isArray(registry) ? registry : [];
|
|
260
358
|
const safeRegistry = scope && scope.moduleIds.length > 0
|
|
261
359
|
? allRegistry.filter((m) => scope.moduleIds.includes(m.module_id))
|
|
262
360
|
: allRegistry;
|
|
@@ -285,6 +383,7 @@ export const getRoles = createServerFn({ method: "POST" })
|
|
|
285
383
|
canView: Boolean(p.can_view),
|
|
286
384
|
canEdit: Boolean(p.can_edit),
|
|
287
385
|
})),
|
|
386
|
+
profileLocked: effective.locked,
|
|
288
387
|
activeProfile: scope ? { id: scope.id, name: scope.name, tenantId: scope.tenantId } : null,
|
|
289
388
|
};
|
|
290
389
|
});
|
|
@@ -309,7 +408,7 @@ async function writeRolePermissions(supabase, roleId, permissions) {
|
|
|
309
408
|
throw error;
|
|
310
409
|
}
|
|
311
410
|
export const createRole = createServerFn({ method: "POST" })
|
|
312
|
-
.middleware([requireSupabaseAuth])
|
|
411
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
313
412
|
.inputValidator((data) => createRoleInput.parse(data))
|
|
314
413
|
.handler(async ({ context, data }) => {
|
|
315
414
|
await requireAdmin(context);
|
|
@@ -338,7 +437,7 @@ export const createRole = createServerFn({ method: "POST" })
|
|
|
338
437
|
return { ok: true, roleId: created.id };
|
|
339
438
|
});
|
|
340
439
|
export const updateRole = createServerFn({ method: "POST" })
|
|
341
|
-
.middleware([requireSupabaseAuth])
|
|
440
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
342
441
|
.inputValidator((data) => updateRoleInput.parse(data))
|
|
343
442
|
.handler(async ({ context, data }) => {
|
|
344
443
|
await requireAdmin(context);
|
|
@@ -352,7 +451,7 @@ export const updateRole = createServerFn({ method: "POST" })
|
|
|
352
451
|
return { ok: true };
|
|
353
452
|
});
|
|
354
453
|
export const duplicateRole = createServerFn({ method: "POST" })
|
|
355
|
-
.middleware([requireSupabaseAuth])
|
|
454
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
356
455
|
.inputValidator((data) => roleIdInput.parse(data))
|
|
357
456
|
.handler(async ({ context, data }) => {
|
|
358
457
|
await requireAdmin(context);
|
|
@@ -389,7 +488,7 @@ export const duplicateRole = createServerFn({ method: "POST" })
|
|
|
389
488
|
return { ok: true, roleId: created.id };
|
|
390
489
|
});
|
|
391
490
|
export const deleteRole = createServerFn({ method: "POST" })
|
|
392
|
-
.middleware([requireSupabaseAuth])
|
|
491
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
393
492
|
.inputValidator((data) => roleIdInput.parse(data))
|
|
394
493
|
.handler(async ({ context, data }) => {
|
|
395
494
|
await requireAdmin(context);
|
|
@@ -407,7 +506,7 @@ export const deleteRole = createServerFn({ method: "POST" })
|
|
|
407
506
|
* Wat niet meer voorkomt blijft bestaan maar wordt op inactief gezet.
|
|
408
507
|
*/
|
|
409
508
|
export const syncModuleRegistry = createServerFn({ method: "POST" })
|
|
410
|
-
.middleware([requireSupabaseAuth])
|
|
509
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
411
510
|
.inputValidator((data) => syncModulesInput.parse(data))
|
|
412
511
|
.handler(async ({ context, data }) => {
|
|
413
512
|
await requireAdmin(context);
|
|
@@ -415,12 +514,22 @@ export const syncModuleRegistry = createServerFn({ method: "POST" })
|
|
|
415
514
|
if (modules.length === 0)
|
|
416
515
|
return { ok: true, count: 0 };
|
|
417
516
|
const now = new Date().toISOString();
|
|
517
|
+
const { data: existingRows, error: existingError } = await context.supabase
|
|
518
|
+
.from("module_registry")
|
|
519
|
+
.select("module_id")
|
|
520
|
+
.in("module_id", modules.map((m) => m.id));
|
|
521
|
+
if (existingError)
|
|
522
|
+
throw existingError;
|
|
523
|
+
const knownIds = new Set((Array.isArray(existingRows) ? existingRows : []).map((r) => r.module_id));
|
|
418
524
|
const { error } = await context.supabase.from("module_registry").upsert(modules.map((m) => ({
|
|
419
525
|
module_id: m.id,
|
|
420
526
|
name: m.name,
|
|
421
527
|
shell_id: data.shellId ?? null,
|
|
422
528
|
is_active: true,
|
|
423
529
|
last_seen_at: now,
|
|
530
|
+
// Nieuwe modules komen standaard met afdwingen aan binnen; bestaande
|
|
531
|
+
// modules behouden hun eigen instelling.
|
|
532
|
+
...(knownIds.has(m.id) ? {} : { enforce: true }),
|
|
424
533
|
})), { onConflict: "module_id" });
|
|
425
534
|
if (error)
|
|
426
535
|
throw error;
|
|
@@ -469,7 +578,7 @@ const setModuleEnforceInput = z.object({
|
|
|
469
578
|
enforce: z.boolean(),
|
|
470
579
|
});
|
|
471
580
|
export const setModuleEnforce = createServerFn({ method: "POST" })
|
|
472
|
-
.middleware([requireSupabaseAuth])
|
|
581
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
473
582
|
.inputValidator((data) => setModuleEnforceInput.parse(data))
|
|
474
583
|
.handler(async ({ context, data }) => {
|
|
475
584
|
await requireAdmin(context);
|
|
@@ -482,7 +591,7 @@ export const setModuleEnforce = createServerFn({ method: "POST" })
|
|
|
482
591
|
return { ok: true };
|
|
483
592
|
});
|
|
484
593
|
export const getUserModulePermissions = createServerFn({ method: "POST" })
|
|
485
|
-
.middleware([requireSupabaseAuth])
|
|
594
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
486
595
|
.inputValidator((data) => userPermissionsInput.parse(data))
|
|
487
596
|
.handler(async ({ context, data }) => {
|
|
488
597
|
await requireUserAdmin(context);
|
|
@@ -521,7 +630,7 @@ export const getUserModulePermissions = createServerFn({ method: "POST" })
|
|
|
521
630
|
};
|
|
522
631
|
});
|
|
523
632
|
export const setUserModulePermission = createServerFn({ method: "POST" })
|
|
524
|
-
.middleware([requireSupabaseAuth])
|
|
633
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
525
634
|
.inputValidator((data) => setUserPermissionInput.parse(data))
|
|
526
635
|
.handler(async ({ context, data }) => {
|
|
527
636
|
await requireUserAdmin(context);
|
|
@@ -549,7 +658,7 @@ export const setUserModulePermission = createServerFn({ method: "POST" })
|
|
|
549
658
|
return { ok: true };
|
|
550
659
|
});
|
|
551
660
|
export const inviteUser = createServerFn({ method: "POST" })
|
|
552
|
-
.middleware([requireSupabaseAuth])
|
|
661
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
553
662
|
.inputValidator((data) => inviteUserInput.parse(data))
|
|
554
663
|
.handler(async ({ context, data }) => {
|
|
555
664
|
const { isAdmin } = await requireUserAdmin(context);
|
|
@@ -589,7 +698,7 @@ export const inviteUser = createServerFn({ method: "POST" })
|
|
|
589
698
|
return { ok: true, userId };
|
|
590
699
|
});
|
|
591
700
|
export const deleteUser = createServerFn({ method: "POST" })
|
|
592
|
-
.middleware([requireSupabaseAuth])
|
|
701
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
593
702
|
.inputValidator((data) => deleteUserInput.parse(data))
|
|
594
703
|
.handler(async ({ context, data }) => {
|
|
595
704
|
const { isAdmin } = await requireUserAdmin(context);
|
|
@@ -615,7 +724,7 @@ export const deleteUser = createServerFn({ method: "POST" })
|
|
|
615
724
|
return { ok: true };
|
|
616
725
|
});
|
|
617
726
|
export const updateUserName = createServerFn({ method: "POST" })
|
|
618
|
-
.middleware([requireSupabaseAuth])
|
|
727
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
619
728
|
.inputValidator((data) => updateUserNameInput.parse(data))
|
|
620
729
|
.handler(async ({ context, data }) => {
|
|
621
730
|
const { isAdmin } = await requireUserAdmin(context);
|
|
@@ -655,7 +764,7 @@ function generatePassword(length = 16) {
|
|
|
655
764
|
return out;
|
|
656
765
|
}
|
|
657
766
|
export const createTestUser = createServerFn({ method: "POST" })
|
|
658
|
-
.middleware([requireSupabaseAuth])
|
|
767
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
659
768
|
.inputValidator((data) => createTestUserInput.parse(data))
|
|
660
769
|
.handler(async ({ context, data }) => {
|
|
661
770
|
const { isAdmin } = await requireUserAdmin(context);
|
|
@@ -697,11 +806,51 @@ const profileIdInput = z.object({ profileId: z.string().uuid() });
|
|
|
697
806
|
const profileBodySchema = {
|
|
698
807
|
name: z.string().trim().min(1).max(120),
|
|
699
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(),
|
|
700
811
|
roleIds: z.array(z.string().uuid()).max(200).default([]),
|
|
701
812
|
moduleIds: z.array(z.string().trim().min(1).max(100)).max(200).default([]),
|
|
702
813
|
};
|
|
703
814
|
const createProfileInput = z.object(profileBodySchema);
|
|
704
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
|
+
}
|
|
705
854
|
async function writeProfileLinks(supabase, profileId, roleIds, moduleIds) {
|
|
706
855
|
const { error: delRoles } = await supabase.from("profile_roles").delete().eq("profile_id", profileId);
|
|
707
856
|
if (delRoles)
|
|
@@ -728,9 +877,11 @@ async function writeProfileLinks(supabase, profileId, roleIds, moduleIds) {
|
|
|
728
877
|
}
|
|
729
878
|
/** Alle bedrijfsprofielen met hun rollen, modules en tellingen. */
|
|
730
879
|
export const getProfiles = createServerFn({ method: "GET" })
|
|
731
|
-
.middleware([requireSupabaseAuth])
|
|
880
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
732
881
|
.handler(async ({ context }) => {
|
|
733
|
-
await requireUserAdmin(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);
|
|
734
885
|
const [{ data: profilesOrg, error: profilesError }, { data: profileRoles, error: profileRolesError }, { data: profileModules, error: profileModulesError }, { data: tenants, error: tenantsError },] = await Promise.all([
|
|
735
886
|
context.supabase
|
|
736
887
|
.from("profiles_org")
|
|
@@ -738,7 +889,9 @@ export const getProfiles = createServerFn({ method: "GET" })
|
|
|
738
889
|
.order("name", { ascending: true }),
|
|
739
890
|
context.supabase.from("profile_roles").select("profile_id, role_id"),
|
|
740
891
|
context.supabase.from("profile_modules").select("profile_id, module_id"),
|
|
741
|
-
|
|
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 }),
|
|
742
895
|
]);
|
|
743
896
|
if (profilesError)
|
|
744
897
|
throw profilesError;
|
|
@@ -748,13 +901,16 @@ export const getProfiles = createServerFn({ method: "GET" })
|
|
|
748
901
|
throw profileModulesError;
|
|
749
902
|
if (tenantsError)
|
|
750
903
|
throw tenantsError;
|
|
751
|
-
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
752
904
|
const { data: userRows, error: userError } = await supabaseAdmin
|
|
753
905
|
.from("profiles")
|
|
754
906
|
.select("id, tenant_id");
|
|
755
907
|
if (userError)
|
|
756
908
|
throw userError;
|
|
757
|
-
const
|
|
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;
|
|
758
914
|
const safeProfileRoles = Array.isArray(profileRoles) ? profileRoles : [];
|
|
759
915
|
const safeProfileModules = Array.isArray(profileModules) ? profileModules : [];
|
|
760
916
|
const safeTenants = Array.isArray(tenants) ? tenants : [];
|
|
@@ -780,16 +936,22 @@ export const getProfiles = createServerFn({ method: "GET" })
|
|
|
780
936
|
userCount: p.tenant_id ? usersByTenant.get(p.tenant_id) ?? 0 : 0,
|
|
781
937
|
})),
|
|
782
938
|
tenants: safeTenants.map((t) => ({ id: t.id, name: t.name })),
|
|
939
|
+
profileLocked: effective.locked,
|
|
940
|
+
lockedProfileId: effective.locked ? effective.profileId ?? null : null,
|
|
783
941
|
};
|
|
784
942
|
});
|
|
785
943
|
export const createProfile = createServerFn({ method: "POST" })
|
|
786
|
-
.middleware([requireSupabaseAuth])
|
|
944
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
787
945
|
.inputValidator((data) => createProfileInput.parse(data))
|
|
788
946
|
.handler(async ({ context, data }) => {
|
|
789
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);
|
|
790
952
|
const { data: created, error } = await context.supabase
|
|
791
953
|
.from("profiles_org")
|
|
792
|
-
.insert({ name: data.name, tenant_id:
|
|
954
|
+
.insert({ name: data.name, tenant_id: tenantId })
|
|
793
955
|
.select("id")
|
|
794
956
|
.single();
|
|
795
957
|
if (error)
|
|
@@ -798,13 +960,15 @@ export const createProfile = createServerFn({ method: "POST" })
|
|
|
798
960
|
return { ok: true, profileId: created.id };
|
|
799
961
|
});
|
|
800
962
|
export const updateProfile = createServerFn({ method: "POST" })
|
|
801
|
-
.middleware([requireSupabaseAuth])
|
|
963
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
802
964
|
.inputValidator((data) => updateProfileInput.parse(data))
|
|
803
965
|
.handler(async ({ context, data }) => {
|
|
804
966
|
await requireAdmin(context);
|
|
967
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
968
|
+
const tenantId = await resolveTenant(supabaseAdmin, data.tenantId, data.tenantName);
|
|
805
969
|
const { error } = await context.supabase
|
|
806
970
|
.from("profiles_org")
|
|
807
|
-
.update({ name: data.name, tenant_id:
|
|
971
|
+
.update({ name: data.name, tenant_id: tenantId, updated_at: new Date().toISOString() })
|
|
808
972
|
.eq("id", data.profileId);
|
|
809
973
|
if (error)
|
|
810
974
|
throw error;
|
|
@@ -812,7 +976,7 @@ export const updateProfile = createServerFn({ method: "POST" })
|
|
|
812
976
|
return { ok: true };
|
|
813
977
|
});
|
|
814
978
|
export const duplicateProfile = createServerFn({ method: "POST" })
|
|
815
|
-
.middleware([requireSupabaseAuth])
|
|
979
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
816
980
|
.inputValidator((data) => profileIdInput.parse(data))
|
|
817
981
|
.handler(async ({ context, data }) => {
|
|
818
982
|
await requireAdmin(context);
|
|
@@ -840,7 +1004,7 @@ export const duplicateProfile = createServerFn({ method: "POST" })
|
|
|
840
1004
|
return { ok: true, profileId: created.id };
|
|
841
1005
|
});
|
|
842
1006
|
export const deleteProfile = createServerFn({ method: "POST" })
|
|
843
|
-
.middleware([requireSupabaseAuth])
|
|
1007
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
844
1008
|
.inputValidator((data) => profileIdInput.parse(data))
|
|
845
1009
|
.handler(async ({ context, data }) => {
|
|
846
1010
|
await requireAdmin(context);
|
|
@@ -874,7 +1038,7 @@ export const deleteProfile = createServerFn({ method: "POST" })
|
|
|
874
1038
|
});
|
|
875
1039
|
/** Zet precies één profiel als actief (is_default). */
|
|
876
1040
|
export const setActiveProfile = createServerFn({ method: "POST" })
|
|
877
|
-
.middleware([requireSupabaseAuth])
|
|
1041
|
+
.middleware([normalizeServerErrors, requireSupabaseAuth])
|
|
878
1042
|
.inputValidator((data) => profileIdInput.parse(data))
|
|
879
1043
|
.handler(async ({ context, data }) => {
|
|
880
1044
|
await requireAdmin(context);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zet ruwe `Response`-throws (o.a. uit de auth-middleware) om naar gewone
|
|
3
|
+
* `Error`s. Anders komt er client-side alleen "Error: [object Response]" aan
|
|
4
|
+
* en klapt het scherm om zonder leesbare melding.
|
|
5
|
+
*/
|
|
6
|
+
export declare const normalizeServerErrors: import("@tanstack/start-client-core").FunctionMiddlewareAfterServer<{}, unknown, undefined, undefined, undefined, undefined, undefined>;
|
|
7
|
+
//# sourceMappingURL=serverErrors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serverErrors.d.ts","sourceRoot":"","sources":["../../src/lib/serverErrors.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,yIAsBjC,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createMiddleware } from "@tanstack/react-start";
|
|
2
|
+
/**
|
|
3
|
+
* Zet ruwe `Response`-throws (o.a. uit de auth-middleware) om naar gewone
|
|
4
|
+
* `Error`s. Anders komt er client-side alleen "Error: [object Response]" aan
|
|
5
|
+
* en klapt het scherm om zonder leesbare melding.
|
|
6
|
+
*/
|
|
7
|
+
export const normalizeServerErrors = createMiddleware({ type: "function" }).server(async ({ next }) => {
|
|
8
|
+
try {
|
|
9
|
+
return await next();
|
|
10
|
+
}
|
|
11
|
+
catch (err) {
|
|
12
|
+
if (err instanceof Response) {
|
|
13
|
+
let text = "";
|
|
14
|
+
try {
|
|
15
|
+
text = await err.clone().text();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
/* body niet leesbaar */
|
|
19
|
+
}
|
|
20
|
+
const message = err.status === 401
|
|
21
|
+
? "Je sessie is verlopen. Log opnieuw in en probeer het nog eens."
|
|
22
|
+
: text || `Serverfout (${err.status})`;
|
|
23
|
+
throw new Error(message);
|
|
24
|
+
}
|
|
25
|
+
if (err instanceof Error)
|
|
26
|
+
throw err;
|
|
27
|
+
throw new Error(typeof err === "string" ? err : "Onbekende serverfout");
|
|
28
|
+
}
|
|
29
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowselections/admin-installatietechniek",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"flowselections": {
|
|
5
5
|
"moduleId": "admin-installatietechniek",
|
|
6
6
|
"pages": [
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"lint": "eslint ."
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@flowselections/core": "
|
|
43
|
+
"@flowselections/core": "1.0.18",
|
|
44
44
|
"@tanstack/react-query": "^5.0.0",
|
|
45
45
|
"@tanstack/react-router": "^1.0.0",
|
|
46
46
|
"@tanstack/react-start": "^1.0.0",
|