@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.
@@ -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
- /** Tenant + rol-/module-scope van een bedrijfsprofiel (of het actieve profiel). */
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
- let query = supabase.from("profiles_org").select("id, name, tenant_id, is_default");
105
- query = profileId ? query.eq("id", profileId) : query.eq("is_default", true);
106
- const { data, error } = await query.maybeSingle();
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 scope = await loadProfileScope(supabaseAdmin, data?.profileId);
130
- let profilesQuery = supabaseAdmin
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: safeProfiles.map((profile) => ({
178
- id: profile.id,
179
- email: profile.email ?? "-",
180
- displayName: profile.display_name ?? "-",
181
- roles: rolesByUser.get(profile.id) ?? [],
182
- emailConfirmed: confirmedById.get(profile.id) ?? false,
183
- isTestAccount: isTestById.get(profile.id) ?? false,
184
- })),
185
- availableRoles: safeRoles
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 scope = await loadProfileScope(context.supabase, data?.profileId);
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 allRoles = Array.isArray(roles) ? roles : [];
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
- context.supabase.from("tenants").select("id, name").order("name", { ascending: true }),
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 safeProfiles = Array.isArray(profilesOrg) ? profilesOrg : [];
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: data.tenantId ?? null })
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: data.tenantId ?? null, updated_at: new Date().toISOString() })
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.6",
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",