@flowselections/admin-installatietechniek 1.0.5 → 1.0.7
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 +265 -21
- package/dist-lib/hooks/useModulePermission.d.ts +2 -1
- package/dist-lib/hooks/useModulePermission.d.ts.map +1 -1
- 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 +841 -155
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +841 -155
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +841 -155
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +870 -157
- 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 +146927 -1852
- package/dist-lib/lib/admin.functions.d.ts.map +1 -1
- package/dist-lib/lib/admin.functions.js +446 -25
- 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 +1 -1
- package/dist-lib/lib/permissions.server.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -31,6 +31,7 @@ const roleIdInput = z.object({ roleId: z.string().uuid() });
|
|
|
31
31
|
const assignRoleInput = z.object({
|
|
32
32
|
userId: z.string().uuid(),
|
|
33
33
|
roleId: z.string().uuid(),
|
|
34
|
+
profileId: z.string().uuid().optional(),
|
|
34
35
|
});
|
|
35
36
|
const rolePermissionsSchema = z
|
|
36
37
|
.array(z.object({
|
|
@@ -53,6 +54,7 @@ const inviteUserInput = z.object({
|
|
|
53
54
|
email: z.string().trim().email().max(255),
|
|
54
55
|
displayName: z.string().trim().min(1).max(100),
|
|
55
56
|
roleId: z.string().uuid(),
|
|
57
|
+
profileId: z.string().uuid().optional(),
|
|
56
58
|
});
|
|
57
59
|
const deleteUserInput = z.object({
|
|
58
60
|
userId: z.string().uuid(),
|
|
@@ -65,6 +67,7 @@ const createTestUserInput = z.object({
|
|
|
65
67
|
displayName: z.string().trim().min(1).max(100),
|
|
66
68
|
roleId: z.string().uuid(),
|
|
67
69
|
email: z.string().trim().email().max(255).optional(),
|
|
70
|
+
profileId: z.string().uuid().optional(),
|
|
68
71
|
});
|
|
69
72
|
function slugify(value) {
|
|
70
73
|
return (value
|
|
@@ -94,13 +97,93 @@ async function assertMayUseRole(supabase, roleId, isAdmin) {
|
|
|
94
97
|
}
|
|
95
98
|
return role;
|
|
96
99
|
}
|
|
97
|
-
|
|
100
|
+
const profileScopeInput = z
|
|
101
|
+
.object({ profileId: z.string().uuid().optional() })
|
|
102
|
+
.optional();
|
|
103
|
+
/**
|
|
104
|
+
* Tenant + rol-/module-scope van een bedrijfsprofiel.
|
|
105
|
+
* Zonder `profileId` is er geen scope (= overzicht: alles zichtbaar).
|
|
106
|
+
*/
|
|
107
|
+
async function loadProfileScope(supabase, profileId) {
|
|
108
|
+
if (!profileId)
|
|
109
|
+
return null;
|
|
110
|
+
const { data, error } = await supabase
|
|
111
|
+
.from("profiles_org")
|
|
112
|
+
.select("id, name, tenant_id, is_default")
|
|
113
|
+
.eq("id", profileId)
|
|
114
|
+
.maybeSingle();
|
|
115
|
+
if (error)
|
|
116
|
+
throw error;
|
|
117
|
+
if (!data)
|
|
118
|
+
return null;
|
|
119
|
+
const [{ data: roleRows }, { data: moduleRows }] = await Promise.all([
|
|
120
|
+
supabase.from("profile_roles").select("role_id").eq("profile_id", data.id),
|
|
121
|
+
supabase.from("profile_modules").select("module_id").eq("profile_id", data.id),
|
|
122
|
+
]);
|
|
123
|
+
return {
|
|
124
|
+
id: data.id,
|
|
125
|
+
name: data.name,
|
|
126
|
+
tenantId: (data.tenant_id ?? null),
|
|
127
|
+
roleIds: (Array.isArray(roleRows) ? roleRows : []).map((r) => r.role_id),
|
|
128
|
+
moduleIds: (Array.isArray(moduleRows) ? moduleRows : []).map((m) => m.module_id),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Bepaalt op welk bedrijfsprofiel deze aanvraag draait.
|
|
133
|
+
* - Admin: vrije keuze (leeg = overzicht met alles).
|
|
134
|
+
* - Niet-admin (administratie): altijd vastgezet op het eigen profiel,
|
|
135
|
+
* afgeleid uit de tenant van de gebruiker of anders uit de profielen
|
|
136
|
+
* waaraan een van zijn rollen gekoppeld is.
|
|
137
|
+
*/
|
|
138
|
+
async function resolveEffectiveProfileId(admin, userId, requestedProfileId, isAdmin) {
|
|
139
|
+
if (isAdmin)
|
|
140
|
+
return { profileId: requestedProfileId, locked: false };
|
|
141
|
+
const { data: me } = await admin
|
|
142
|
+
.from("profiles")
|
|
143
|
+
.select("tenant_id")
|
|
144
|
+
.eq("id", userId)
|
|
145
|
+
.maybeSingle();
|
|
146
|
+
if (me?.tenant_id) {
|
|
147
|
+
const { data: byTenant } = await admin
|
|
148
|
+
.from("profiles_org")
|
|
149
|
+
.select("id")
|
|
150
|
+
.eq("tenant_id", me.tenant_id)
|
|
151
|
+
.limit(1);
|
|
152
|
+
const first = Array.isArray(byTenant) ? byTenant[0] : null;
|
|
153
|
+
if (first?.id)
|
|
154
|
+
return { profileId: first.id, locked: true };
|
|
155
|
+
}
|
|
156
|
+
const { data: myRoles } = await admin
|
|
157
|
+
.from("role_assignments")
|
|
158
|
+
.select("role_id")
|
|
159
|
+
.eq("user_id", userId);
|
|
160
|
+
const roleIds = (Array.isArray(myRoles) ? myRoles : []).map((r) => r.role_id);
|
|
161
|
+
if (roleIds.length > 0) {
|
|
162
|
+
const { data: links } = await admin
|
|
163
|
+
.from("profile_roles")
|
|
164
|
+
.select("profile_id")
|
|
165
|
+
.in("role_id", roleIds)
|
|
166
|
+
.limit(1);
|
|
167
|
+
const link = Array.isArray(links) ? links[0] : null;
|
|
168
|
+
if (link?.profile_id)
|
|
169
|
+
return { profileId: link.profile_id, locked: true };
|
|
170
|
+
}
|
|
171
|
+
return { profileId: undefined, locked: true };
|
|
172
|
+
}
|
|
173
|
+
export const getUsersWithRoles = createServerFn({ method: "POST" })
|
|
98
174
|
.middleware([requireSupabaseAuth])
|
|
99
|
-
.
|
|
175
|
+
.inputValidator((data) => profileScopeInput.parse(data))
|
|
176
|
+
.handler(async ({ context, data }) => {
|
|
100
177
|
const { isAdmin } = await requireUserAdmin(context);
|
|
101
178
|
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
179
|
+
const effective = await resolveEffectiveProfileId(supabaseAdmin, context.userId, data?.profileId, isAdmin);
|
|
180
|
+
const scope = await loadProfileScope(supabaseAdmin, effective.profileId);
|
|
181
|
+
const profilesQuery = supabaseAdmin
|
|
182
|
+
.from("profiles")
|
|
183
|
+
.select("id, email, display_name, tenant_id")
|
|
184
|
+
.order("email", { ascending: true });
|
|
102
185
|
const [{ data: profiles, error: profilesError }, { data: assignments, error: assignmentsError }, { data: roles, error: rolesError }, { data: authList, error: authError },] = await Promise.all([
|
|
103
|
-
|
|
186
|
+
profilesQuery,
|
|
104
187
|
supabaseAdmin.from("role_assignments").select("user_id, role_id"),
|
|
105
188
|
supabaseAdmin.from("roles").select("id, key, name, is_protected").order("name", { ascending: true }),
|
|
106
189
|
supabaseAdmin.auth.admin.listUsers({ page: 1, perPage: 1000 }),
|
|
@@ -115,9 +198,12 @@ export const getUsersWithRoles = createServerFn({ method: "GET" })
|
|
|
115
198
|
throw authError;
|
|
116
199
|
const safeProfiles = Array.isArray(profiles) ? profiles : [];
|
|
117
200
|
const safeAssignments = Array.isArray(assignments) ? assignments : [];
|
|
118
|
-
const
|
|
201
|
+
const allRoles = Array.isArray(roles) ? roles : [];
|
|
202
|
+
const safeRoles = scope && scope.roleIds.length > 0
|
|
203
|
+
? allRoles.filter((r) => scope.roleIds.includes(r.id))
|
|
204
|
+
: allRoles;
|
|
119
205
|
const safeAuthUsers = Array.isArray(authList?.users) ? authList.users : [];
|
|
120
|
-
const roleById = new Map(
|
|
206
|
+
const roleById = new Map(allRoles.map((r) => [r.id, r]));
|
|
121
207
|
const confirmedById = new Map();
|
|
122
208
|
const isTestById = new Map();
|
|
123
209
|
for (const u of safeAuthUsers) {
|
|
@@ -136,19 +222,52 @@ export const getUsersWithRoles = createServerFn({ method: "GET" })
|
|
|
136
222
|
}
|
|
137
223
|
rolesByUser.set(a.user_id, list);
|
|
138
224
|
}
|
|
225
|
+
// Binnen een profiel horen gebruikers erbij via de tenant OF via een rol
|
|
226
|
+
// die aan het profiel gekoppeld is. Zonder profiel: iedereen.
|
|
227
|
+
const scopedRoleIds = new Set(scope?.roleIds ?? []);
|
|
228
|
+
const usersViaRole = new Set();
|
|
229
|
+
if (scope && scopedRoleIds.size > 0) {
|
|
230
|
+
for (const a of safeAssignments) {
|
|
231
|
+
if (scopedRoleIds.has(a.role_id))
|
|
232
|
+
usersViaRole.add(a.user_id);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Vastgezette gebruiker zonder gekoppeld profiel: niets tonen.
|
|
236
|
+
const noProfileLock = effective.locked && !scope;
|
|
237
|
+
const scopedProfiles = noProfileLock
|
|
238
|
+
? []
|
|
239
|
+
: !scope
|
|
240
|
+
? safeProfiles
|
|
241
|
+
: safeProfiles.filter((p) => {
|
|
242
|
+
const viaTenant = Boolean(scope.tenantId) && p.tenant_id === scope.tenantId;
|
|
243
|
+
return viaTenant || usersViaRole.has(p.id);
|
|
244
|
+
});
|
|
139
245
|
return {
|
|
140
|
-
users:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
246
|
+
users: scopedProfiles.map((profile) => {
|
|
247
|
+
const viaTenant = Boolean(scope?.tenantId) && profile.tenant_id === scope?.tenantId;
|
|
248
|
+
const viaRole = usersViaRole.has(profile.id);
|
|
249
|
+
return {
|
|
250
|
+
id: profile.id,
|
|
251
|
+
email: profile.email ?? "-",
|
|
252
|
+
displayName: profile.display_name ?? "-",
|
|
253
|
+
roles: rolesByUser.get(profile.id) ?? [],
|
|
254
|
+
emailConfirmed: confirmedById.get(profile.id) ?? false,
|
|
255
|
+
isTestAccount: isTestById.get(profile.id) ?? false,
|
|
256
|
+
matchedVia: !scope
|
|
257
|
+
? null
|
|
258
|
+
: viaTenant && viaRole
|
|
259
|
+
? "both"
|
|
260
|
+
: viaTenant
|
|
261
|
+
? "tenant"
|
|
262
|
+
: "role",
|
|
263
|
+
};
|
|
264
|
+
}),
|
|
265
|
+
availableRoles: (noProfileLock ? [] : safeRoles)
|
|
149
266
|
.filter((r) => isAdmin || r.key !== "admin")
|
|
150
267
|
.map((r) => ({ id: r.id, key: r.key, name: r.name, isProtected: r.is_protected })),
|
|
151
268
|
isAdmin,
|
|
269
|
+
profileLocked: effective.locked,
|
|
270
|
+
activeProfile: scope ? { id: scope.id, name: scope.name, tenantId: scope.tenantId } : null,
|
|
152
271
|
};
|
|
153
272
|
});
|
|
154
273
|
export const assignRole = createServerFn({ method: "POST" })
|
|
@@ -172,6 +291,19 @@ export const assignRole = createServerFn({ method: "POST" })
|
|
|
172
291
|
.insert({ user_id: data.userId, role_id: data.roleId });
|
|
173
292
|
if (error)
|
|
174
293
|
throw error;
|
|
294
|
+
// Binnen een actief bedrijfsprofiel: koppel de gebruiker aan de tenant van
|
|
295
|
+
// dat profiel als die nog leeg is, zodat de koppeling vanzelf dichtgroeit.
|
|
296
|
+
if (data.profileId) {
|
|
297
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
298
|
+
const scope = await loadProfileScope(supabaseAdmin, data.profileId);
|
|
299
|
+
if (scope?.tenantId) {
|
|
300
|
+
await supabaseAdmin
|
|
301
|
+
.from("profiles")
|
|
302
|
+
.update({ tenant_id: scope.tenantId })
|
|
303
|
+
.eq("id", data.userId)
|
|
304
|
+
.is("tenant_id", null);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
175
307
|
return { ok: true };
|
|
176
308
|
});
|
|
177
309
|
export const removeRole = createServerFn({ method: "POST" })
|
|
@@ -193,10 +325,14 @@ export const removeRole = createServerFn({ method: "POST" })
|
|
|
193
325
|
return { ok: true };
|
|
194
326
|
});
|
|
195
327
|
/** Alle rollen met hun rechten, plus de geregistreerde modules en onderdelen. */
|
|
196
|
-
export const getRoles = createServerFn({ method: "
|
|
328
|
+
export const getRoles = createServerFn({ method: "POST" })
|
|
197
329
|
.middleware([requireSupabaseAuth])
|
|
198
|
-
.
|
|
199
|
-
|
|
330
|
+
.inputValidator((data) => profileScopeInput.parse(data))
|
|
331
|
+
.handler(async ({ context, data }) => {
|
|
332
|
+
const { isAdmin } = await requireUserAdmin(context);
|
|
333
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
334
|
+
const effective = await resolveEffectiveProfileId(supabaseAdmin, context.userId, data?.profileId, isAdmin);
|
|
335
|
+
const scope = await loadProfileScope(supabaseAdmin, effective.profileId);
|
|
200
336
|
const [{ data: roles, error: rolesError }, { data: perms, error: permsError }, { data: registry, error: registryError }, { data: featureRows, error: featuresError },] = await Promise.all([
|
|
201
337
|
context.supabase.from("roles").select("id, key, name, is_protected").order("name", { ascending: true }),
|
|
202
338
|
context.supabase.from("role_permissions").select("role_id, module_id, feature_id, can_view, can_edit"),
|
|
@@ -211,9 +347,16 @@ export const getRoles = createServerFn({ method: "GET" })
|
|
|
211
347
|
throw registryError;
|
|
212
348
|
if (featuresError)
|
|
213
349
|
throw featuresError;
|
|
214
|
-
const
|
|
350
|
+
const noProfileLock = effective.locked && !scope;
|
|
351
|
+
const allRoles = noProfileLock ? [] : Array.isArray(roles) ? roles : [];
|
|
352
|
+
const safeRoles = scope && scope.roleIds.length > 0
|
|
353
|
+
? allRoles.filter((r) => scope.roleIds.includes(r.id))
|
|
354
|
+
: allRoles;
|
|
215
355
|
const safePerms = Array.isArray(perms) ? perms : [];
|
|
216
|
-
const
|
|
356
|
+
const allRegistry = noProfileLock ? [] : Array.isArray(registry) ? registry : [];
|
|
357
|
+
const safeRegistry = scope && scope.moduleIds.length > 0
|
|
358
|
+
? allRegistry.filter((m) => scope.moduleIds.includes(m.module_id))
|
|
359
|
+
: allRegistry;
|
|
217
360
|
const safeFeatures = Array.isArray(featureRows) ? featureRows : [];
|
|
218
361
|
return {
|
|
219
362
|
roles: safeRoles.map((r) => ({
|
|
@@ -239,6 +382,8 @@ export const getRoles = createServerFn({ method: "GET" })
|
|
|
239
382
|
canView: Boolean(p.can_view),
|
|
240
383
|
canEdit: Boolean(p.can_edit),
|
|
241
384
|
})),
|
|
385
|
+
profileLocked: effective.locked,
|
|
386
|
+
activeProfile: scope ? { id: scope.id, name: scope.name, tenantId: scope.tenantId } : null,
|
|
242
387
|
};
|
|
243
388
|
});
|
|
244
389
|
async function writeRolePermissions(supabase, roleId, permissions) {
|
|
@@ -368,12 +513,22 @@ export const syncModuleRegistry = createServerFn({ method: "POST" })
|
|
|
368
513
|
if (modules.length === 0)
|
|
369
514
|
return { ok: true, count: 0 };
|
|
370
515
|
const now = new Date().toISOString();
|
|
516
|
+
const { data: existingRows, error: existingError } = await context.supabase
|
|
517
|
+
.from("module_registry")
|
|
518
|
+
.select("module_id")
|
|
519
|
+
.in("module_id", modules.map((m) => m.id));
|
|
520
|
+
if (existingError)
|
|
521
|
+
throw existingError;
|
|
522
|
+
const knownIds = new Set((Array.isArray(existingRows) ? existingRows : []).map((r) => r.module_id));
|
|
371
523
|
const { error } = await context.supabase.from("module_registry").upsert(modules.map((m) => ({
|
|
372
524
|
module_id: m.id,
|
|
373
525
|
name: m.name,
|
|
374
526
|
shell_id: data.shellId ?? null,
|
|
375
527
|
is_active: true,
|
|
376
528
|
last_seen_at: now,
|
|
529
|
+
// Nieuwe modules komen standaard met afdwingen aan binnen; bestaande
|
|
530
|
+
// modules behouden hun eigen instelling.
|
|
531
|
+
...(knownIds.has(m.id) ? {} : { enforce: true }),
|
|
377
532
|
})), { onConflict: "module_id" });
|
|
378
533
|
if (error)
|
|
379
534
|
throw error;
|
|
@@ -525,9 +680,13 @@ export const inviteUser = createServerFn({ method: "POST" })
|
|
|
525
680
|
throw new Error(/already|exists|registered/i.test(msg) ? "Deze gebruiker bestaat al." : msg);
|
|
526
681
|
}
|
|
527
682
|
const userId = invited.user.id;
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
683
|
+
const scope = await loadProfileScope(supabaseAdmin, data.profileId);
|
|
684
|
+
const { error: profileError } = await supabaseAdmin.from("profiles").upsert({
|
|
685
|
+
id: userId,
|
|
686
|
+
email: data.email,
|
|
687
|
+
display_name: data.displayName,
|
|
688
|
+
...(scope?.tenantId ? { tenant_id: scope.tenantId } : {}),
|
|
689
|
+
}, { onConflict: "id" });
|
|
531
690
|
if (profileError)
|
|
532
691
|
throw profileError;
|
|
533
692
|
const { error: roleError } = await supabaseAdmin
|
|
@@ -623,9 +782,13 @@ export const createTestUser = createServerFn({ method: "POST" })
|
|
|
623
782
|
throw new Error(/already|exists|registered/i.test(msg) ? "Deze gebruiker bestaat al." : msg);
|
|
624
783
|
}
|
|
625
784
|
const userId = created.user.id;
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
785
|
+
const scope = await loadProfileScope(supabaseAdmin, data.profileId);
|
|
786
|
+
const { error: profileError } = await supabaseAdmin.from("profiles").upsert({
|
|
787
|
+
id: userId,
|
|
788
|
+
email,
|
|
789
|
+
display_name: data.displayName,
|
|
790
|
+
...(scope?.tenantId ? { tenant_id: scope.tenantId } : {}),
|
|
791
|
+
}, { onConflict: "id" });
|
|
629
792
|
if (profileError)
|
|
630
793
|
throw profileError;
|
|
631
794
|
const { error: roleError } = await supabaseAdmin
|
|
@@ -635,3 +798,261 @@ export const createTestUser = createServerFn({ method: "POST" })
|
|
|
635
798
|
throw roleError;
|
|
636
799
|
return { ok: true, userId, email, password };
|
|
637
800
|
});
|
|
801
|
+
// ============================================================================
|
|
802
|
+
// Bedrijfsprofielen: bundelen rollen + modules per organisatie (tenant)
|
|
803
|
+
// ============================================================================
|
|
804
|
+
const profileIdInput = z.object({ profileId: z.string().uuid() });
|
|
805
|
+
const profileBodySchema = {
|
|
806
|
+
name: z.string().trim().min(1).max(120),
|
|
807
|
+
tenantId: z.string().uuid().nullable().optional(),
|
|
808
|
+
/** Naam van het bedrijf; maakt een nieuw bedrijf aan of hernoemt het gekozen bedrijf. */
|
|
809
|
+
tenantName: z.string().trim().max(120).optional(),
|
|
810
|
+
roleIds: z.array(z.string().uuid()).max(200).default([]),
|
|
811
|
+
moduleIds: z.array(z.string().trim().min(1).max(100)).max(200).default([]),
|
|
812
|
+
};
|
|
813
|
+
const createProfileInput = z.object(profileBodySchema);
|
|
814
|
+
const updateProfileInput = z.object({ profileId: z.string().uuid(), ...profileBodySchema });
|
|
815
|
+
/**
|
|
816
|
+
* Bepaalt het tenant-id voor een profiel:
|
|
817
|
+
* - bestaand bedrijf + gewijzigde naam -> hernoemen
|
|
818
|
+
* - geen bedrijf + naam -> nieuw bedrijf aanmaken
|
|
819
|
+
*/
|
|
820
|
+
async function resolveTenant(supabase, tenantId, tenantName) {
|
|
821
|
+
const name = (tenantName ?? "").trim();
|
|
822
|
+
if (tenantId) {
|
|
823
|
+
if (name) {
|
|
824
|
+
const { data: current, error: readError } = await supabase
|
|
825
|
+
.from("tenants")
|
|
826
|
+
.select("name")
|
|
827
|
+
.eq("id", tenantId)
|
|
828
|
+
.maybeSingle();
|
|
829
|
+
if (readError)
|
|
830
|
+
throw readError;
|
|
831
|
+
if (current && current.name !== name) {
|
|
832
|
+
const { error } = await supabase
|
|
833
|
+
.from("tenants")
|
|
834
|
+
.update({ name, updated_at: new Date().toISOString() })
|
|
835
|
+
.eq("id", tenantId);
|
|
836
|
+
if (error)
|
|
837
|
+
throw error;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return tenantId;
|
|
841
|
+
}
|
|
842
|
+
if (!name)
|
|
843
|
+
return null;
|
|
844
|
+
const { data: created, error } = await supabase
|
|
845
|
+
.from("tenants")
|
|
846
|
+
.insert({ name })
|
|
847
|
+
.select("id")
|
|
848
|
+
.single();
|
|
849
|
+
if (error)
|
|
850
|
+
throw error;
|
|
851
|
+
return created.id;
|
|
852
|
+
}
|
|
853
|
+
async function writeProfileLinks(supabase, profileId, roleIds, moduleIds) {
|
|
854
|
+
const { error: delRoles } = await supabase.from("profile_roles").delete().eq("profile_id", profileId);
|
|
855
|
+
if (delRoles)
|
|
856
|
+
throw delRoles;
|
|
857
|
+
const { error: delModules } = await supabase.from("profile_modules").delete().eq("profile_id", profileId);
|
|
858
|
+
if (delModules)
|
|
859
|
+
throw delModules;
|
|
860
|
+
const uniqueRoles = Array.from(new Set(roleIds));
|
|
861
|
+
if (uniqueRoles.length > 0) {
|
|
862
|
+
const { error } = await supabase
|
|
863
|
+
.from("profile_roles")
|
|
864
|
+
.insert(uniqueRoles.map((roleId) => ({ profile_id: profileId, role_id: roleId })));
|
|
865
|
+
if (error)
|
|
866
|
+
throw error;
|
|
867
|
+
}
|
|
868
|
+
const uniqueModules = Array.from(new Set(moduleIds));
|
|
869
|
+
if (uniqueModules.length > 0) {
|
|
870
|
+
const { error } = await supabase
|
|
871
|
+
.from("profile_modules")
|
|
872
|
+
.insert(uniqueModules.map((moduleId) => ({ profile_id: profileId, module_id: moduleId })));
|
|
873
|
+
if (error)
|
|
874
|
+
throw error;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
/** Alle bedrijfsprofielen met hun rollen, modules en tellingen. */
|
|
878
|
+
export const getProfiles = createServerFn({ method: "GET" })
|
|
879
|
+
.middleware([requireSupabaseAuth])
|
|
880
|
+
.handler(async ({ context }) => {
|
|
881
|
+
const { isAdmin } = await requireUserAdmin(context);
|
|
882
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
883
|
+
const effective = await resolveEffectiveProfileId(supabaseAdmin, context.userId, undefined, isAdmin);
|
|
884
|
+
const [{ data: profilesOrg, error: profilesError }, { data: profileRoles, error: profileRolesError }, { data: profileModules, error: profileModulesError }, { data: tenants, error: tenantsError },] = await Promise.all([
|
|
885
|
+
context.supabase
|
|
886
|
+
.from("profiles_org")
|
|
887
|
+
.select("id, name, tenant_id, is_default")
|
|
888
|
+
.order("name", { ascending: true }),
|
|
889
|
+
context.supabase.from("profile_roles").select("profile_id, role_id"),
|
|
890
|
+
context.supabase.from("profile_modules").select("profile_id, module_id"),
|
|
891
|
+
// `tenants` is via RLS beperkt tot de eigen tenant; namen tonen mag hier
|
|
892
|
+
// omdat de aanvrager al is geverifieerd als admin/administratie.
|
|
893
|
+
supabaseAdmin.from("tenants").select("id, name").order("name", { ascending: true }),
|
|
894
|
+
]);
|
|
895
|
+
if (profilesError)
|
|
896
|
+
throw profilesError;
|
|
897
|
+
if (profileRolesError)
|
|
898
|
+
throw profileRolesError;
|
|
899
|
+
if (profileModulesError)
|
|
900
|
+
throw profileModulesError;
|
|
901
|
+
if (tenantsError)
|
|
902
|
+
throw tenantsError;
|
|
903
|
+
const { data: userRows, error: userError } = await supabaseAdmin
|
|
904
|
+
.from("profiles")
|
|
905
|
+
.select("id, tenant_id");
|
|
906
|
+
if (userError)
|
|
907
|
+
throw userError;
|
|
908
|
+
const allProfilesOrg = Array.isArray(profilesOrg) ? profilesOrg : [];
|
|
909
|
+
// Niet-admin: alleen het eigen (vastgezette) profiel.
|
|
910
|
+
const safeProfiles = effective.locked
|
|
911
|
+
? allProfilesOrg.filter((p) => p.id === effective.profileId)
|
|
912
|
+
: allProfilesOrg;
|
|
913
|
+
const safeProfileRoles = Array.isArray(profileRoles) ? profileRoles : [];
|
|
914
|
+
const safeProfileModules = Array.isArray(profileModules) ? profileModules : [];
|
|
915
|
+
const safeTenants = Array.isArray(tenants) ? tenants : [];
|
|
916
|
+
const safeUsers = Array.isArray(userRows) ? userRows : [];
|
|
917
|
+
const usersByTenant = new Map();
|
|
918
|
+
for (const u of safeUsers) {
|
|
919
|
+
if (!u.tenant_id)
|
|
920
|
+
continue;
|
|
921
|
+
usersByTenant.set(u.tenant_id, (usersByTenant.get(u.tenant_id) ?? 0) + 1);
|
|
922
|
+
}
|
|
923
|
+
const tenantName = new Map(safeTenants.map((t) => [t.id, t.name]));
|
|
924
|
+
return {
|
|
925
|
+
profiles: safeProfiles.map((p) => ({
|
|
926
|
+
id: p.id,
|
|
927
|
+
name: p.name,
|
|
928
|
+
tenantId: (p.tenant_id ?? null),
|
|
929
|
+
tenantName: p.tenant_id ? tenantName.get(p.tenant_id) ?? null : null,
|
|
930
|
+
isDefault: Boolean(p.is_default),
|
|
931
|
+
roleIds: safeProfileRoles.filter((r) => r.profile_id === p.id).map((r) => r.role_id),
|
|
932
|
+
moduleIds: safeProfileModules
|
|
933
|
+
.filter((m) => m.profile_id === p.id)
|
|
934
|
+
.map((m) => m.module_id),
|
|
935
|
+
userCount: p.tenant_id ? usersByTenant.get(p.tenant_id) ?? 0 : 0,
|
|
936
|
+
})),
|
|
937
|
+
tenants: safeTenants.map((t) => ({ id: t.id, name: t.name })),
|
|
938
|
+
profileLocked: effective.locked,
|
|
939
|
+
lockedProfileId: effective.locked ? effective.profileId ?? null : null,
|
|
940
|
+
};
|
|
941
|
+
});
|
|
942
|
+
export const createProfile = createServerFn({ method: "POST" })
|
|
943
|
+
.middleware([requireSupabaseAuth])
|
|
944
|
+
.inputValidator((data) => createProfileInput.parse(data))
|
|
945
|
+
.handler(async ({ context, data }) => {
|
|
946
|
+
await requireAdmin(context);
|
|
947
|
+
// `tenants` heeft geen insert-policy voor admins; na de adminguard mag dit
|
|
948
|
+
// veilig via de serverclient.
|
|
949
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
950
|
+
const tenantId = await resolveTenant(supabaseAdmin, data.tenantId, data.tenantName);
|
|
951
|
+
const { data: created, error } = await context.supabase
|
|
952
|
+
.from("profiles_org")
|
|
953
|
+
.insert({ name: data.name, tenant_id: tenantId })
|
|
954
|
+
.select("id")
|
|
955
|
+
.single();
|
|
956
|
+
if (error)
|
|
957
|
+
throw error;
|
|
958
|
+
await writeProfileLinks(context.supabase, created.id, data.roleIds, data.moduleIds);
|
|
959
|
+
return { ok: true, profileId: created.id };
|
|
960
|
+
});
|
|
961
|
+
export const updateProfile = createServerFn({ method: "POST" })
|
|
962
|
+
.middleware([requireSupabaseAuth])
|
|
963
|
+
.inputValidator((data) => updateProfileInput.parse(data))
|
|
964
|
+
.handler(async ({ context, data }) => {
|
|
965
|
+
await requireAdmin(context);
|
|
966
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
967
|
+
const tenantId = await resolveTenant(supabaseAdmin, data.tenantId, data.tenantName);
|
|
968
|
+
const { error } = await context.supabase
|
|
969
|
+
.from("profiles_org")
|
|
970
|
+
.update({ name: data.name, tenant_id: tenantId, updated_at: new Date().toISOString() })
|
|
971
|
+
.eq("id", data.profileId);
|
|
972
|
+
if (error)
|
|
973
|
+
throw error;
|
|
974
|
+
await writeProfileLinks(context.supabase, data.profileId, data.roleIds, data.moduleIds);
|
|
975
|
+
return { ok: true };
|
|
976
|
+
});
|
|
977
|
+
export const duplicateProfile = createServerFn({ method: "POST" })
|
|
978
|
+
.middleware([requireSupabaseAuth])
|
|
979
|
+
.inputValidator((data) => profileIdInput.parse(data))
|
|
980
|
+
.handler(async ({ context, data }) => {
|
|
981
|
+
await requireAdmin(context);
|
|
982
|
+
const { data: source, error: sourceError } = await context.supabase
|
|
983
|
+
.from("profiles_org")
|
|
984
|
+
.select("name, tenant_id")
|
|
985
|
+
.eq("id", data.profileId)
|
|
986
|
+
.maybeSingle();
|
|
987
|
+
if (sourceError)
|
|
988
|
+
throw sourceError;
|
|
989
|
+
if (!source)
|
|
990
|
+
throw new Error("Profiel bestaat niet.");
|
|
991
|
+
const [{ data: roleRows }, { data: moduleRows }] = await Promise.all([
|
|
992
|
+
context.supabase.from("profile_roles").select("role_id").eq("profile_id", data.profileId),
|
|
993
|
+
context.supabase.from("profile_modules").select("module_id").eq("profile_id", data.profileId),
|
|
994
|
+
]);
|
|
995
|
+
const { data: created, error } = await context.supabase
|
|
996
|
+
.from("profiles_org")
|
|
997
|
+
.insert({ name: `${source.name} (kopie)`, tenant_id: source.tenant_id })
|
|
998
|
+
.select("id")
|
|
999
|
+
.single();
|
|
1000
|
+
if (error)
|
|
1001
|
+
throw error;
|
|
1002
|
+
await writeProfileLinks(context.supabase, created.id, (Array.isArray(roleRows) ? roleRows : []).map((r) => r.role_id), (Array.isArray(moduleRows) ? moduleRows : []).map((m) => m.module_id));
|
|
1003
|
+
return { ok: true, profileId: created.id };
|
|
1004
|
+
});
|
|
1005
|
+
export const deleteProfile = createServerFn({ method: "POST" })
|
|
1006
|
+
.middleware([requireSupabaseAuth])
|
|
1007
|
+
.inputValidator((data) => profileIdInput.parse(data))
|
|
1008
|
+
.handler(async ({ context, data }) => {
|
|
1009
|
+
await requireAdmin(context);
|
|
1010
|
+
const { data: profile, error: profileError } = await context.supabase
|
|
1011
|
+
.from("profiles_org")
|
|
1012
|
+
.select("id, tenant_id, is_default")
|
|
1013
|
+
.eq("id", data.profileId)
|
|
1014
|
+
.maybeSingle();
|
|
1015
|
+
if (profileError)
|
|
1016
|
+
throw profileError;
|
|
1017
|
+
if (!profile)
|
|
1018
|
+
throw new Error("Profiel bestaat niet.");
|
|
1019
|
+
if (profile.is_default)
|
|
1020
|
+
throw new Error("Het actieve profiel kan niet verwijderd worden.");
|
|
1021
|
+
if (profile.tenant_id) {
|
|
1022
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
1023
|
+
const { count, error: countError } = await supabaseAdmin
|
|
1024
|
+
.from("profiles")
|
|
1025
|
+
.select("id", { count: "exact", head: true })
|
|
1026
|
+
.eq("tenant_id", profile.tenant_id);
|
|
1027
|
+
if (countError)
|
|
1028
|
+
throw countError;
|
|
1029
|
+
if ((count ?? 0) > 0) {
|
|
1030
|
+
throw new Error("Er zijn nog gebruikers aan dit bedrijf gekoppeld.");
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
const { error } = await context.supabase.from("profiles_org").delete().eq("id", data.profileId);
|
|
1034
|
+
if (error)
|
|
1035
|
+
throw error;
|
|
1036
|
+
return { ok: true };
|
|
1037
|
+
});
|
|
1038
|
+
/** Zet precies één profiel als actief (is_default). */
|
|
1039
|
+
export const setActiveProfile = createServerFn({ method: "POST" })
|
|
1040
|
+
.middleware([requireSupabaseAuth])
|
|
1041
|
+
.inputValidator((data) => profileIdInput.parse(data))
|
|
1042
|
+
.handler(async ({ context, data }) => {
|
|
1043
|
+
await requireAdmin(context);
|
|
1044
|
+
const { error: clearError } = await context.supabase
|
|
1045
|
+
.from("profiles_org")
|
|
1046
|
+
.update({ is_default: false })
|
|
1047
|
+
.eq("is_default", true)
|
|
1048
|
+
.neq("id", data.profileId);
|
|
1049
|
+
if (clearError)
|
|
1050
|
+
throw clearError;
|
|
1051
|
+
const { error } = await context.supabase
|
|
1052
|
+
.from("profiles_org")
|
|
1053
|
+
.update({ is_default: true, updated_at: new Date().toISOString() })
|
|
1054
|
+
.eq("id", data.profileId);
|
|
1055
|
+
if (error)
|
|
1056
|
+
throw error;
|
|
1057
|
+
return { ok: true };
|
|
1058
|
+
});
|
|
@@ -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 +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,
|
|
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.
|
|
3
|
+
"version": "1.0.7",
|
|
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",
|
|
@@ -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.
|
|
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",
|