@flowselections/admin-installatietechniek 1.0.4 → 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.
- package/dist-lib/components/AdminForInstallatieTechniek.d.ts.map +1 -1
- package/dist-lib/components/AdminForInstallatieTechniek.js +166 -9
- package/dist-lib/components/ModuleAccessGuard.d.ts +15 -0
- package/dist-lib/components/ModuleAccessGuard.d.ts.map +1 -0
- package/dist-lib/components/ModuleAccessGuard.js +40 -0
- package/dist-lib/hooks/useModulePermission.d.ts +10 -3
- package/dist-lib/hooks/useModulePermission.d.ts.map +1 -1
- package/dist-lib/hooks/useModulePermission.js +26 -23
- package/dist-lib/index.d.ts +2 -0
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +2 -0
- package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/auth-attacher.js +5 -1
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +1126 -155
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/auth-middleware.js +21 -7
- package/dist-lib/integrations/supabase/client.d.ts +1126 -155
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +1126 -155
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +1148 -154
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +3 -0
- package/dist-lib/lib/admin.functions.d.ts +171971 -2043
- package/dist-lib/lib/admin.functions.d.ts.map +1 -1
- package/dist-lib/lib/admin.functions.js +292 -16
- package/dist-lib/lib/permission-types.d.ts +7 -0
- package/dist-lib/lib/permission-types.d.ts.map +1 -0
- package/dist-lib/lib/permission-types.js +1 -0
- package/dist-lib/lib/permissions.server.d.ts +14 -0
- package/dist-lib/lib/permissions.server.d.ts.map +1 -0
- package/dist-lib/lib/permissions.server.js +26 -0
- package/package.json +87 -87
|
@@ -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
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
|
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(
|
|
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,14 +231,16 @@ 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: "
|
|
234
|
+
export const getRoles = createServerFn({ method: "POST" })
|
|
197
235
|
.middleware([requireSupabaseAuth])
|
|
198
|
-
.
|
|
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"),
|
|
203
|
-
context.supabase.from("module_registry").select("module_id, name, is_active").order("name", { ascending: true }),
|
|
243
|
+
context.supabase.from("module_registry").select("module_id, name, is_active, enforce").order("name", { ascending: true }),
|
|
204
244
|
context.supabase.from("module_features").select("module_id, feature_id, name, is_active").order("name", { ascending: true }),
|
|
205
245
|
]);
|
|
206
246
|
if (rolesError)
|
|
@@ -211,9 +251,15 @@ export const getRoles = createServerFn({ method: "GET" })
|
|
|
211
251
|
throw registryError;
|
|
212
252
|
if (featuresError)
|
|
213
253
|
throw featuresError;
|
|
214
|
-
const
|
|
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
|
|
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) => ({
|
|
@@ -227,6 +273,7 @@ export const getRoles = createServerFn({ method: "GET" })
|
|
|
227
273
|
.map((m) => ({
|
|
228
274
|
id: m.module_id,
|
|
229
275
|
name: m.name,
|
|
276
|
+
enforce: Boolean(m.enforce),
|
|
230
277
|
features: safeFeatures
|
|
231
278
|
.filter((f) => f.is_active && f.module_id === m.module_id)
|
|
232
279
|
.map((f) => ({ id: f.feature_id, name: f.name })),
|
|
@@ -238,6 +285,7 @@ export const getRoles = createServerFn({ method: "GET" })
|
|
|
238
285
|
canView: Boolean(p.can_view),
|
|
239
286
|
canEdit: Boolean(p.can_edit),
|
|
240
287
|
})),
|
|
288
|
+
activeProfile: scope ? { id: scope.id, name: scope.name, tenantId: scope.tenantId } : null,
|
|
241
289
|
};
|
|
242
290
|
});
|
|
243
291
|
async function writeRolePermissions(supabase, roleId, permissions) {
|
|
@@ -416,6 +464,23 @@ export const syncModuleRegistry = createServerFn({ method: "POST" })
|
|
|
416
464
|
}
|
|
417
465
|
return { ok: true, count: modules.length };
|
|
418
466
|
});
|
|
467
|
+
const setModuleEnforceInput = z.object({
|
|
468
|
+
moduleId: z.string().trim().min(1).max(100),
|
|
469
|
+
enforce: z.boolean(),
|
|
470
|
+
});
|
|
471
|
+
export const setModuleEnforce = createServerFn({ method: "POST" })
|
|
472
|
+
.middleware([requireSupabaseAuth])
|
|
473
|
+
.inputValidator((data) => setModuleEnforceInput.parse(data))
|
|
474
|
+
.handler(async ({ context, data }) => {
|
|
475
|
+
await requireAdmin(context);
|
|
476
|
+
const { error } = await context.supabase
|
|
477
|
+
.from("module_registry")
|
|
478
|
+
.update({ enforce: data.enforce })
|
|
479
|
+
.eq("module_id", data.moduleId);
|
|
480
|
+
if (error)
|
|
481
|
+
throw error;
|
|
482
|
+
return { ok: true };
|
|
483
|
+
});
|
|
419
484
|
export const getUserModulePermissions = createServerFn({ method: "POST" })
|
|
420
485
|
.middleware([requireSupabaseAuth])
|
|
421
486
|
.inputValidator((data) => userPermissionsInput.parse(data))
|
|
@@ -507,9 +572,13 @@ export const inviteUser = createServerFn({ method: "POST" })
|
|
|
507
572
|
throw new Error(/already|exists|registered/i.test(msg) ? "Deze gebruiker bestaat al." : msg);
|
|
508
573
|
}
|
|
509
574
|
const userId = invited.user.id;
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
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" });
|
|
513
582
|
if (profileError)
|
|
514
583
|
throw profileError;
|
|
515
584
|
const { error: roleError } = await supabaseAdmin
|
|
@@ -605,9 +674,13 @@ export const createTestUser = createServerFn({ method: "POST" })
|
|
|
605
674
|
throw new Error(/already|exists|registered/i.test(msg) ? "Deze gebruiker bestaat al." : msg);
|
|
606
675
|
}
|
|
607
676
|
const userId = created.user.id;
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
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" });
|
|
611
684
|
if (profileError)
|
|
612
685
|
throw profileError;
|
|
613
686
|
const { error: roleError } = await supabaseAdmin
|
|
@@ -617,3 +690,206 @@ export const createTestUser = createServerFn({ method: "POST" })
|
|
|
617
690
|
throw roleError;
|
|
618
691
|
return { ok: true, userId, email, password };
|
|
619
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 {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ModulePermission } from "../lib/permission-types";
|
|
2
|
+
interface PermissionContext {
|
|
3
|
+
supabase: any;
|
|
4
|
+
userId: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Server-side controle voor moduletoegang. Moet in elke server function van
|
|
8
|
+
* een beveiligde module aangeroepen worden die gevoelige data leest of wijzigt.
|
|
9
|
+
* Deze functie draait dezelfde SQL-check als de frontendguard, dus de database
|
|
10
|
+
* is de enige bron van waarheid.
|
|
11
|
+
*/
|
|
12
|
+
export declare function requireModulePermission(context: PermissionContext, moduleId: string, featureId?: string, level?: "view" | "edit"): Promise<ModulePermission>;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=permissions.server.d.ts.map
|
|
@@ -0,0 +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,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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side controle voor moduletoegang. Moet in elke server function van
|
|
3
|
+
* een beveiligde module aangeroepen worden die gevoelige data leest of wijzigt.
|
|
4
|
+
* Deze functie draait dezelfde SQL-check als de frontendguard, dus de database
|
|
5
|
+
* is de enige bron van waarheid.
|
|
6
|
+
*/
|
|
7
|
+
export async function requireModulePermission(context, moduleId, featureId = "", level = "view") {
|
|
8
|
+
const { supabase, userId } = context;
|
|
9
|
+
const { data, error } = await supabase.rpc("effective_module_permission", {
|
|
10
|
+
_user_id: userId,
|
|
11
|
+
_module_id: moduleId,
|
|
12
|
+
_feature_id: featureId,
|
|
13
|
+
});
|
|
14
|
+
if (error) {
|
|
15
|
+
console.error("[requireModulePermission] RPC error", error);
|
|
16
|
+
throw new Error("Kan moduletoegang niet controleren");
|
|
17
|
+
}
|
|
18
|
+
const permission = (data ?? "edit");
|
|
19
|
+
if (level === "edit" && permission !== "edit") {
|
|
20
|
+
throw new Error("Geen schrijfrechten voor deze module");
|
|
21
|
+
}
|
|
22
|
+
if (level === "view" && permission !== "view" && permission !== "edit") {
|
|
23
|
+
throw new Error("Geen leesrechten voor deze module");
|
|
24
|
+
}
|
|
25
|
+
return permission;
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,87 +1,87 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@flowselections/admin-installatietechniek",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"flowselections": {
|
|
5
|
-
"moduleId": "admin-installatietechniek",
|
|
6
|
-
"pages": [
|
|
7
|
-
{
|
|
8
|
-
"export": "AdminForInstallatieTechniek",
|
|
9
|
-
"route": "admin-installatietechniek",
|
|
10
|
-
"title": "Rollen & machtigingen"
|
|
11
|
-
}
|
|
12
|
-
]
|
|
13
|
-
},
|
|
14
|
-
"private": false,
|
|
15
|
-
"sideEffects": false,
|
|
16
|
-
"type": "module",
|
|
17
|
-
"main": "./dist-lib/index.js",
|
|
18
|
-
"types": "./dist-lib/index.d.ts",
|
|
19
|
-
"exports": {
|
|
20
|
-
".": {
|
|
21
|
-
"types": "./dist-lib/index.d.ts",
|
|
22
|
-
"default": "./dist-lib/index.js"
|
|
23
|
-
},
|
|
24
|
-
"./styles": "./dist-lib/styles.css"
|
|
25
|
-
},
|
|
26
|
-
"files": [
|
|
27
|
-
"dist-lib",
|
|
28
|
-
"public"
|
|
29
|
-
],
|
|
30
|
-
"publishConfig": {
|
|
31
|
-
"access": "public"
|
|
32
|
-
},
|
|
33
|
-
"scripts": {
|
|
34
|
-
"dev": "vite dev",
|
|
35
|
-
"build": "vite build",
|
|
36
|
-
"build:lib": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && node -e \"require('fs').writeFileSync('dist-lib/styles.css', '@source \\\"./\\\";\\n')\"",
|
|
37
|
-
"build:lib:watch": "tsc -p tsconfig.build.json --watch",
|
|
38
|
-
"build:dev": "vite build --mode development",
|
|
39
|
-
"preview": "vite preview",
|
|
40
|
-
"lint": "eslint ."
|
|
41
|
-
},
|
|
42
|
-
"peerDependencies": {
|
|
43
|
-
"@flowselections/core": "*",
|
|
44
|
-
"@tanstack/react-query": "^5.0.0",
|
|
45
|
-
"@tanstack/react-router": "^1.0.0",
|
|
46
|
-
"@tanstack/react-start": "^1.0.0",
|
|
47
|
-
"lucide-react": "^0.400.0",
|
|
48
|
-
"react": "^18 || ^19",
|
|
49
|
-
"react-dom": "^18 || ^19",
|
|
50
|
-
"sonner": "^2.0.0"
|
|
51
|
-
},
|
|
52
|
-
"dependencies": {
|
|
53
|
-
"@supabase/supabase-js": "^2.110.7"
|
|
54
|
-
},
|
|
55
|
-
"devDependencies": {
|
|
56
|
-
"@cloudflare/vite-plugin": "^1.25.5",
|
|
57
|
-
"@eslint/js": "^9.32.0",
|
|
58
|
-
"@flowselections/core": "1.0.
|
|
59
|
-
"@lovable.dev/vite-tanstack-config": "2.13.1",
|
|
60
|
-
"@rsbuild/core": "^1.0.0",
|
|
61
|
-
"@tailwindcss/vite": "^4.2.1",
|
|
62
|
-
"@tanstack/react-query": "^5.83.0",
|
|
63
|
-
"@tanstack/react-router": "^1.168.0",
|
|
64
|
-
"@tanstack/react-start": "^1.167.14",
|
|
65
|
-
"@tanstack/router-plugin": "^1.167.10",
|
|
66
|
-
"@types/node": "^22.16.5",
|
|
67
|
-
"@types/react": "^19.2.0",
|
|
68
|
-
"@types/react-dom": "^19.2.0",
|
|
69
|
-
"@vitejs/plugin-react": "^5.0.4",
|
|
70
|
-
"eslint": "^9.32.0",
|
|
71
|
-
"eslint-plugin-react-hooks": "^5.2.0",
|
|
72
|
-
"eslint-plugin-react-refresh": "^0.4.20",
|
|
73
|
-
"globals": "^15.15.0",
|
|
74
|
-
"lucide-react": "^0.575.0",
|
|
75
|
-
"react": "^19.2.0",
|
|
76
|
-
"react-dom": "^19.2.0",
|
|
77
|
-
"sonner": "^2.0.7",
|
|
78
|
-
"tailwindcss": "^4.2.1",
|
|
79
|
-
"tw-animate-css": "^1.3.4",
|
|
80
|
-
"typescript": "^5.8.3",
|
|
81
|
-
"tsc-alias": "^1.8.10",
|
|
82
|
-
"typescript-eslint": "^8.56.1",
|
|
83
|
-
"vite": "^7.3.1",
|
|
84
|
-
"vite-tsconfig-paths": "^6.0.2",
|
|
85
|
-
"nitro": "3.0.260603-beta"
|
|
86
|
-
}
|
|
87
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@flowselections/admin-installatietechniek",
|
|
3
|
+
"version": "1.0.6",
|
|
4
|
+
"flowselections": {
|
|
5
|
+
"moduleId": "admin-installatietechniek",
|
|
6
|
+
"pages": [
|
|
7
|
+
{
|
|
8
|
+
"export": "AdminForInstallatieTechniek",
|
|
9
|
+
"route": "admin-installatietechniek",
|
|
10
|
+
"title": "Rollen & machtigingen"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"private": false,
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./dist-lib/index.js",
|
|
18
|
+
"types": "./dist-lib/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist-lib/index.d.ts",
|
|
22
|
+
"default": "./dist-lib/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./styles": "./dist-lib/styles.css"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist-lib",
|
|
28
|
+
"public"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"dev": "vite dev",
|
|
35
|
+
"build": "vite build",
|
|
36
|
+
"build:lib": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && node -e \"require('fs').writeFileSync('dist-lib/styles.css', '@source \\\"./\\\";\\n')\"",
|
|
37
|
+
"build:lib:watch": "tsc -p tsconfig.build.json --watch",
|
|
38
|
+
"build:dev": "vite build --mode development",
|
|
39
|
+
"preview": "vite preview",
|
|
40
|
+
"lint": "eslint ."
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@flowselections/core": "*",
|
|
44
|
+
"@tanstack/react-query": "^5.0.0",
|
|
45
|
+
"@tanstack/react-router": "^1.0.0",
|
|
46
|
+
"@tanstack/react-start": "^1.0.0",
|
|
47
|
+
"lucide-react": "^0.400.0",
|
|
48
|
+
"react": "^18 || ^19",
|
|
49
|
+
"react-dom": "^18 || ^19",
|
|
50
|
+
"sonner": "^2.0.0"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@supabase/supabase-js": "^2.110.7"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@cloudflare/vite-plugin": "^1.25.5",
|
|
57
|
+
"@eslint/js": "^9.32.0",
|
|
58
|
+
"@flowselections/core": "1.0.18",
|
|
59
|
+
"@lovable.dev/vite-tanstack-config": "2.13.1",
|
|
60
|
+
"@rsbuild/core": "^1.0.0",
|
|
61
|
+
"@tailwindcss/vite": "^4.2.1",
|
|
62
|
+
"@tanstack/react-query": "^5.83.0",
|
|
63
|
+
"@tanstack/react-router": "^1.168.0",
|
|
64
|
+
"@tanstack/react-start": "^1.167.14",
|
|
65
|
+
"@tanstack/router-plugin": "^1.167.10",
|
|
66
|
+
"@types/node": "^22.16.5",
|
|
67
|
+
"@types/react": "^19.2.0",
|
|
68
|
+
"@types/react-dom": "^19.2.0",
|
|
69
|
+
"@vitejs/plugin-react": "^5.0.4",
|
|
70
|
+
"eslint": "^9.32.0",
|
|
71
|
+
"eslint-plugin-react-hooks": "^5.2.0",
|
|
72
|
+
"eslint-plugin-react-refresh": "^0.4.20",
|
|
73
|
+
"globals": "^15.15.0",
|
|
74
|
+
"lucide-react": "^0.575.0",
|
|
75
|
+
"react": "^19.2.0",
|
|
76
|
+
"react-dom": "^19.2.0",
|
|
77
|
+
"sonner": "^2.0.7",
|
|
78
|
+
"tailwindcss": "^4.2.1",
|
|
79
|
+
"tw-animate-css": "^1.3.4",
|
|
80
|
+
"typescript": "^5.8.3",
|
|
81
|
+
"tsc-alias": "^1.8.10",
|
|
82
|
+
"typescript-eslint": "^8.56.1",
|
|
83
|
+
"vite": "^7.3.1",
|
|
84
|
+
"vite-tsconfig-paths": "^6.0.2",
|
|
85
|
+
"nitro": "3.0.260603-beta"
|
|
86
|
+
}
|
|
87
|
+
}
|