@flowselections/admin-installatietechniek 1.0.5 → 1.0.6

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.
@@ -53,6 +53,7 @@ const inviteUserInput = z.object({
53
53
  email: z.string().trim().email().max(255),
54
54
  displayName: z.string().trim().min(1).max(100),
55
55
  roleId: z.string().uuid(),
56
+ profileId: z.string().uuid().optional(),
56
57
  });
57
58
  const deleteUserInput = z.object({
58
59
  userId: z.string().uuid(),
@@ -65,6 +66,7 @@ const createTestUserInput = z.object({
65
66
  displayName: z.string().trim().min(1).max(100),
66
67
  roleId: z.string().uuid(),
67
68
  email: z.string().trim().email().max(255).optional(),
69
+ profileId: z.string().uuid().optional(),
68
70
  });
69
71
  function slugify(value) {
70
72
  return (value
@@ -94,13 +96,45 @@ async function assertMayUseRole(supabase, roleId, isAdmin) {
94
96
  }
95
97
  return role;
96
98
  }
97
- export const getUsersWithRoles = createServerFn({ method: "GET" })
99
+ const profileScopeInput = z
100
+ .object({ profileId: z.string().uuid().optional() })
101
+ .optional();
102
+ /** Tenant + rol-/module-scope van een bedrijfsprofiel (of het actieve profiel). */
103
+ 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();
107
+ if (error)
108
+ throw error;
109
+ if (!data)
110
+ return null;
111
+ const [{ data: roleRows }, { data: moduleRows }] = await Promise.all([
112
+ supabase.from("profile_roles").select("role_id").eq("profile_id", data.id),
113
+ supabase.from("profile_modules").select("module_id").eq("profile_id", data.id),
114
+ ]);
115
+ return {
116
+ id: data.id,
117
+ name: data.name,
118
+ tenantId: (data.tenant_id ?? null),
119
+ roleIds: (Array.isArray(roleRows) ? roleRows : []).map((r) => r.role_id),
120
+ moduleIds: (Array.isArray(moduleRows) ? moduleRows : []).map((m) => m.module_id),
121
+ };
122
+ }
123
+ export const getUsersWithRoles = createServerFn({ method: "POST" })
98
124
  .middleware([requireSupabaseAuth])
99
- .handler(async ({ context }) => {
125
+ .inputValidator((data) => profileScopeInput.parse(data))
126
+ .handler(async ({ context, data }) => {
100
127
  const { isAdmin } = await requireUserAdmin(context);
101
128
  const { supabaseAdmin } = await import("../integrations/supabase/client.server");
129
+ const scope = await loadProfileScope(supabaseAdmin, data?.profileId);
130
+ let profilesQuery = supabaseAdmin
131
+ .from("profiles")
132
+ .select("id, email, display_name, tenant_id")
133
+ .order("email", { ascending: true });
134
+ if (scope?.tenantId)
135
+ profilesQuery = profilesQuery.eq("tenant_id", scope.tenantId);
102
136
  const [{ data: profiles, error: profilesError }, { data: assignments, error: assignmentsError }, { data: roles, error: rolesError }, { data: authList, error: authError },] = await Promise.all([
103
- supabaseAdmin.from("profiles").select("id, email, display_name").order("email", { ascending: true }),
137
+ profilesQuery,
104
138
  supabaseAdmin.from("role_assignments").select("user_id, role_id"),
105
139
  supabaseAdmin.from("roles").select("id, key, name, is_protected").order("name", { ascending: true }),
106
140
  supabaseAdmin.auth.admin.listUsers({ page: 1, perPage: 1000 }),
@@ -115,9 +149,12 @@ export const getUsersWithRoles = createServerFn({ method: "GET" })
115
149
  throw authError;
116
150
  const safeProfiles = Array.isArray(profiles) ? profiles : [];
117
151
  const safeAssignments = Array.isArray(assignments) ? assignments : [];
118
- const safeRoles = Array.isArray(roles) ? roles : [];
152
+ const allRoles = Array.isArray(roles) ? roles : [];
153
+ const safeRoles = scope && scope.roleIds.length > 0
154
+ ? allRoles.filter((r) => scope.roleIds.includes(r.id))
155
+ : allRoles;
119
156
  const safeAuthUsers = Array.isArray(authList?.users) ? authList.users : [];
120
- const roleById = new Map(safeRoles.map((r) => [r.id, r]));
157
+ const roleById = new Map(allRoles.map((r) => [r.id, r]));
121
158
  const confirmedById = new Map();
122
159
  const isTestById = new Map();
123
160
  for (const u of safeAuthUsers) {
@@ -149,6 +186,7 @@ export const getUsersWithRoles = createServerFn({ method: "GET" })
149
186
  .filter((r) => isAdmin || r.key !== "admin")
150
187
  .map((r) => ({ id: r.id, key: r.key, name: r.name, isProtected: r.is_protected })),
151
188
  isAdmin,
189
+ activeProfile: scope ? { id: scope.id, name: scope.name, tenantId: scope.tenantId } : null,
152
190
  };
153
191
  });
154
192
  export const assignRole = createServerFn({ method: "POST" })
@@ -193,10 +231,12 @@ export const removeRole = createServerFn({ method: "POST" })
193
231
  return { ok: true };
194
232
  });
195
233
  /** Alle rollen met hun rechten, plus de geregistreerde modules en onderdelen. */
196
- export const getRoles = createServerFn({ method: "GET" })
234
+ export const getRoles = createServerFn({ method: "POST" })
197
235
  .middleware([requireSupabaseAuth])
198
- .handler(async ({ context }) => {
236
+ .inputValidator((data) => profileScopeInput.parse(data))
237
+ .handler(async ({ context, data }) => {
199
238
  await requireUserAdmin(context);
239
+ const scope = await loadProfileScope(context.supabase, data?.profileId);
200
240
  const [{ data: roles, error: rolesError }, { data: perms, error: permsError }, { data: registry, error: registryError }, { data: featureRows, error: featuresError },] = await Promise.all([
201
241
  context.supabase.from("roles").select("id, key, name, is_protected").order("name", { ascending: true }),
202
242
  context.supabase.from("role_permissions").select("role_id, module_id, feature_id, can_view, can_edit"),
@@ -211,9 +251,15 @@ export const getRoles = createServerFn({ method: "GET" })
211
251
  throw registryError;
212
252
  if (featuresError)
213
253
  throw featuresError;
214
- const safeRoles = Array.isArray(roles) ? roles : [];
254
+ const allRoles = Array.isArray(roles) ? roles : [];
255
+ const safeRoles = scope && scope.roleIds.length > 0
256
+ ? allRoles.filter((r) => scope.roleIds.includes(r.id))
257
+ : allRoles;
215
258
  const safePerms = Array.isArray(perms) ? perms : [];
216
- const safeRegistry = Array.isArray(registry) ? registry : [];
259
+ const allRegistry = Array.isArray(registry) ? registry : [];
260
+ const safeRegistry = scope && scope.moduleIds.length > 0
261
+ ? allRegistry.filter((m) => scope.moduleIds.includes(m.module_id))
262
+ : allRegistry;
217
263
  const safeFeatures = Array.isArray(featureRows) ? featureRows : [];
218
264
  return {
219
265
  roles: safeRoles.map((r) => ({
@@ -239,6 +285,7 @@ export const getRoles = createServerFn({ method: "GET" })
239
285
  canView: Boolean(p.can_view),
240
286
  canEdit: Boolean(p.can_edit),
241
287
  })),
288
+ activeProfile: scope ? { id: scope.id, name: scope.name, tenantId: scope.tenantId } : null,
242
289
  };
243
290
  });
244
291
  async function writeRolePermissions(supabase, roleId, permissions) {
@@ -525,9 +572,13 @@ export const inviteUser = createServerFn({ method: "POST" })
525
572
  throw new Error(/already|exists|registered/i.test(msg) ? "Deze gebruiker bestaat al." : msg);
526
573
  }
527
574
  const userId = invited.user.id;
528
- const { error: profileError } = await supabaseAdmin
529
- .from("profiles")
530
- .upsert({ id: userId, email: data.email, display_name: data.displayName }, { onConflict: "id" });
575
+ const scope = await loadProfileScope(supabaseAdmin, data.profileId);
576
+ const { error: profileError } = await supabaseAdmin.from("profiles").upsert({
577
+ id: userId,
578
+ email: data.email,
579
+ display_name: data.displayName,
580
+ ...(scope?.tenantId ? { tenant_id: scope.tenantId } : {}),
581
+ }, { onConflict: "id" });
531
582
  if (profileError)
532
583
  throw profileError;
533
584
  const { error: roleError } = await supabaseAdmin
@@ -623,9 +674,13 @@ export const createTestUser = createServerFn({ method: "POST" })
623
674
  throw new Error(/already|exists|registered/i.test(msg) ? "Deze gebruiker bestaat al." : msg);
624
675
  }
625
676
  const userId = created.user.id;
626
- const { error: profileError } = await supabaseAdmin
627
- .from("profiles")
628
- .upsert({ id: userId, email, display_name: data.displayName }, { onConflict: "id" });
677
+ const scope = await loadProfileScope(supabaseAdmin, data.profileId);
678
+ const { error: profileError } = await supabaseAdmin.from("profiles").upsert({
679
+ id: userId,
680
+ email,
681
+ display_name: data.displayName,
682
+ ...(scope?.tenantId ? { tenant_id: scope.tenantId } : {}),
683
+ }, { onConflict: "id" });
629
684
  if (profileError)
630
685
  throw profileError;
631
686
  const { error: roleError } = await supabaseAdmin
@@ -635,3 +690,206 @@ export const createTestUser = createServerFn({ method: "POST" })
635
690
  throw roleError;
636
691
  return { ok: true, userId, email, password };
637
692
  });
693
+ // ============================================================================
694
+ // Bedrijfsprofielen: bundelen rollen + modules per organisatie (tenant)
695
+ // ============================================================================
696
+ const profileIdInput = z.object({ profileId: z.string().uuid() });
697
+ const profileBodySchema = {
698
+ name: z.string().trim().min(1).max(120),
699
+ tenantId: z.string().uuid().nullable().optional(),
700
+ roleIds: z.array(z.string().uuid()).max(200).default([]),
701
+ moduleIds: z.array(z.string().trim().min(1).max(100)).max(200).default([]),
702
+ };
703
+ const createProfileInput = z.object(profileBodySchema);
704
+ const updateProfileInput = z.object({ profileId: z.string().uuid(), ...profileBodySchema });
705
+ async function writeProfileLinks(supabase, profileId, roleIds, moduleIds) {
706
+ const { error: delRoles } = await supabase.from("profile_roles").delete().eq("profile_id", profileId);
707
+ if (delRoles)
708
+ throw delRoles;
709
+ const { error: delModules } = await supabase.from("profile_modules").delete().eq("profile_id", profileId);
710
+ if (delModules)
711
+ throw delModules;
712
+ const uniqueRoles = Array.from(new Set(roleIds));
713
+ if (uniqueRoles.length > 0) {
714
+ const { error } = await supabase
715
+ .from("profile_roles")
716
+ .insert(uniqueRoles.map((roleId) => ({ profile_id: profileId, role_id: roleId })));
717
+ if (error)
718
+ throw error;
719
+ }
720
+ const uniqueModules = Array.from(new Set(moduleIds));
721
+ if (uniqueModules.length > 0) {
722
+ const { error } = await supabase
723
+ .from("profile_modules")
724
+ .insert(uniqueModules.map((moduleId) => ({ profile_id: profileId, module_id: moduleId })));
725
+ if (error)
726
+ throw error;
727
+ }
728
+ }
729
+ /** Alle bedrijfsprofielen met hun rollen, modules en tellingen. */
730
+ export const getProfiles = createServerFn({ method: "GET" })
731
+ .middleware([requireSupabaseAuth])
732
+ .handler(async ({ context }) => {
733
+ await requireUserAdmin(context);
734
+ const [{ data: profilesOrg, error: profilesError }, { data: profileRoles, error: profileRolesError }, { data: profileModules, error: profileModulesError }, { data: tenants, error: tenantsError },] = await Promise.all([
735
+ context.supabase
736
+ .from("profiles_org")
737
+ .select("id, name, tenant_id, is_default")
738
+ .order("name", { ascending: true }),
739
+ context.supabase.from("profile_roles").select("profile_id, role_id"),
740
+ context.supabase.from("profile_modules").select("profile_id, module_id"),
741
+ context.supabase.from("tenants").select("id, name").order("name", { ascending: true }),
742
+ ]);
743
+ if (profilesError)
744
+ throw profilesError;
745
+ if (profileRolesError)
746
+ throw profileRolesError;
747
+ if (profileModulesError)
748
+ throw profileModulesError;
749
+ if (tenantsError)
750
+ throw tenantsError;
751
+ const { supabaseAdmin } = await import("../integrations/supabase/client.server");
752
+ const { data: userRows, error: userError } = await supabaseAdmin
753
+ .from("profiles")
754
+ .select("id, tenant_id");
755
+ if (userError)
756
+ throw userError;
757
+ const safeProfiles = Array.isArray(profilesOrg) ? profilesOrg : [];
758
+ const safeProfileRoles = Array.isArray(profileRoles) ? profileRoles : [];
759
+ const safeProfileModules = Array.isArray(profileModules) ? profileModules : [];
760
+ const safeTenants = Array.isArray(tenants) ? tenants : [];
761
+ const safeUsers = Array.isArray(userRows) ? userRows : [];
762
+ const usersByTenant = new Map();
763
+ for (const u of safeUsers) {
764
+ if (!u.tenant_id)
765
+ continue;
766
+ usersByTenant.set(u.tenant_id, (usersByTenant.get(u.tenant_id) ?? 0) + 1);
767
+ }
768
+ const tenantName = new Map(safeTenants.map((t) => [t.id, t.name]));
769
+ return {
770
+ profiles: safeProfiles.map((p) => ({
771
+ id: p.id,
772
+ name: p.name,
773
+ tenantId: (p.tenant_id ?? null),
774
+ tenantName: p.tenant_id ? tenantName.get(p.tenant_id) ?? null : null,
775
+ isDefault: Boolean(p.is_default),
776
+ roleIds: safeProfileRoles.filter((r) => r.profile_id === p.id).map((r) => r.role_id),
777
+ moduleIds: safeProfileModules
778
+ .filter((m) => m.profile_id === p.id)
779
+ .map((m) => m.module_id),
780
+ userCount: p.tenant_id ? usersByTenant.get(p.tenant_id) ?? 0 : 0,
781
+ })),
782
+ tenants: safeTenants.map((t) => ({ id: t.id, name: t.name })),
783
+ };
784
+ });
785
+ export const createProfile = createServerFn({ method: "POST" })
786
+ .middleware([requireSupabaseAuth])
787
+ .inputValidator((data) => createProfileInput.parse(data))
788
+ .handler(async ({ context, data }) => {
789
+ await requireAdmin(context);
790
+ const { data: created, error } = await context.supabase
791
+ .from("profiles_org")
792
+ .insert({ name: data.name, tenant_id: data.tenantId ?? null })
793
+ .select("id")
794
+ .single();
795
+ if (error)
796
+ throw error;
797
+ await writeProfileLinks(context.supabase, created.id, data.roleIds, data.moduleIds);
798
+ return { ok: true, profileId: created.id };
799
+ });
800
+ export const updateProfile = createServerFn({ method: "POST" })
801
+ .middleware([requireSupabaseAuth])
802
+ .inputValidator((data) => updateProfileInput.parse(data))
803
+ .handler(async ({ context, data }) => {
804
+ await requireAdmin(context);
805
+ const { error } = await context.supabase
806
+ .from("profiles_org")
807
+ .update({ name: data.name, tenant_id: data.tenantId ?? null, updated_at: new Date().toISOString() })
808
+ .eq("id", data.profileId);
809
+ if (error)
810
+ throw error;
811
+ await writeProfileLinks(context.supabase, data.profileId, data.roleIds, data.moduleIds);
812
+ return { ok: true };
813
+ });
814
+ export const duplicateProfile = createServerFn({ method: "POST" })
815
+ .middleware([requireSupabaseAuth])
816
+ .inputValidator((data) => profileIdInput.parse(data))
817
+ .handler(async ({ context, data }) => {
818
+ await requireAdmin(context);
819
+ const { data: source, error: sourceError } = await context.supabase
820
+ .from("profiles_org")
821
+ .select("name, tenant_id")
822
+ .eq("id", data.profileId)
823
+ .maybeSingle();
824
+ if (sourceError)
825
+ throw sourceError;
826
+ if (!source)
827
+ throw new Error("Profiel bestaat niet.");
828
+ const [{ data: roleRows }, { data: moduleRows }] = await Promise.all([
829
+ context.supabase.from("profile_roles").select("role_id").eq("profile_id", data.profileId),
830
+ context.supabase.from("profile_modules").select("module_id").eq("profile_id", data.profileId),
831
+ ]);
832
+ const { data: created, error } = await context.supabase
833
+ .from("profiles_org")
834
+ .insert({ name: `${source.name} (kopie)`, tenant_id: source.tenant_id })
835
+ .select("id")
836
+ .single();
837
+ if (error)
838
+ throw error;
839
+ await writeProfileLinks(context.supabase, created.id, (Array.isArray(roleRows) ? roleRows : []).map((r) => r.role_id), (Array.isArray(moduleRows) ? moduleRows : []).map((m) => m.module_id));
840
+ return { ok: true, profileId: created.id };
841
+ });
842
+ export const deleteProfile = createServerFn({ method: "POST" })
843
+ .middleware([requireSupabaseAuth])
844
+ .inputValidator((data) => profileIdInput.parse(data))
845
+ .handler(async ({ context, data }) => {
846
+ await requireAdmin(context);
847
+ const { data: profile, error: profileError } = await context.supabase
848
+ .from("profiles_org")
849
+ .select("id, tenant_id, is_default")
850
+ .eq("id", data.profileId)
851
+ .maybeSingle();
852
+ if (profileError)
853
+ throw profileError;
854
+ if (!profile)
855
+ throw new Error("Profiel bestaat niet.");
856
+ if (profile.is_default)
857
+ throw new Error("Het actieve profiel kan niet verwijderd worden.");
858
+ if (profile.tenant_id) {
859
+ const { supabaseAdmin } = await import("../integrations/supabase/client.server");
860
+ const { count, error: countError } = await supabaseAdmin
861
+ .from("profiles")
862
+ .select("id", { count: "exact", head: true })
863
+ .eq("tenant_id", profile.tenant_id);
864
+ if (countError)
865
+ throw countError;
866
+ if ((count ?? 0) > 0) {
867
+ throw new Error("Er zijn nog gebruikers aan dit bedrijf gekoppeld.");
868
+ }
869
+ }
870
+ const { error } = await context.supabase.from("profiles_org").delete().eq("id", data.profileId);
871
+ if (error)
872
+ throw error;
873
+ return { ok: true };
874
+ });
875
+ /** Zet precies één profiel als actief (is_default). */
876
+ export const setActiveProfile = createServerFn({ method: "POST" })
877
+ .middleware([requireSupabaseAuth])
878
+ .inputValidator((data) => profileIdInput.parse(data))
879
+ .handler(async ({ context, data }) => {
880
+ await requireAdmin(context);
881
+ const { error: clearError } = await context.supabase
882
+ .from("profiles_org")
883
+ .update({ is_default: false })
884
+ .eq("is_default", true)
885
+ .neq("id", data.profileId);
886
+ if (clearError)
887
+ throw clearError;
888
+ const { error } = await context.supabase
889
+ .from("profiles_org")
890
+ .update({ is_default: true, updated_at: new Date().toISOString() })
891
+ .eq("id", data.profileId);
892
+ if (error)
893
+ throw error;
894
+ return { ok: true };
895
+ });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Losse types voor de check-laag (lezen/afdwingen van moduletoegang).
3
+ * Bewust zonder imports uit de rest van deze module, zodat de check-laag
4
+ * later 1-op-1 naar @flowselections/core kan verhuizen.
5
+ */
6
+ export type ModulePermission = "none" | "view" | "edit";
7
+ //# sourceMappingURL=permission-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-types.d.ts","sourceRoot":"","sources":["../../src/lib/permission-types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
- import type { ModulePermission } from "../hooks/useModulePermission";
1
+ import type { ModulePermission } from "../lib/permission-types";
2
2
  interface PermissionContext {
3
3
  supabase: any;
4
4
  userId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"permissions.server.d.ts","sourceRoot":"","sources":["../../src/lib/permissions.server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAEpE,UAAU,iBAAiB;IACzB,QAAQ,EAAE,GAAG,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,iBAAiB,EAC1B,QAAQ,EAAE,MAAM,EAChB,SAAS,SAAK,EACd,KAAK,GAAE,MAAM,GAAG,MAAe,6BA0BhC"}
1
+ {"version":3,"file":"permissions.server.d.ts","sourceRoot":"","sources":["../../src/lib/permissions.server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,UAAU,iBAAiB;IACzB,QAAQ,EAAE,GAAG,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,iBAAiB,EAC1B,QAAQ,EAAE,MAAM,EAChB,SAAS,SAAK,EACd,KAAK,GAAE,MAAM,GAAG,MAAe,6BA0BhC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowselections/admin-installatietechniek",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "flowselections": {
5
5
  "moduleId": "admin-installatietechniek",
6
6
  "pages": [
@@ -55,7 +55,7 @@
55
55
  "devDependencies": {
56
56
  "@cloudflare/vite-plugin": "^1.25.5",
57
57
  "@eslint/js": "^9.32.0",
58
- "@flowselections/core": "1.0.16",
58
+ "@flowselections/core": "1.0.18",
59
59
  "@lovable.dev/vite-tanstack-config": "2.13.1",
60
60
  "@rsbuild/core": "^1.0.0",
61
61
  "@tailwindcss/vite": "^4.2.1",