@flowselections/admin-installatietechniek 1.0.1

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.
Files changed (60) hide show
  1. package/dist-lib/_core-safelist.d.ts +2 -0
  2. package/dist-lib/_core-safelist.d.ts.map +1 -0
  3. package/dist-lib/_core-safelist.js +15 -0
  4. package/dist-lib/components/AdminForInstallatieTechniek.d.ts +12 -0
  5. package/dist-lib/components/AdminForInstallatieTechniek.d.ts.map +1 -0
  6. package/dist-lib/components/AdminForInstallatieTechniek.js +359 -0
  7. package/dist-lib/components/TemplateModuleLogo.d.ts +6 -0
  8. package/dist-lib/components/TemplateModuleLogo.d.ts.map +1 -0
  9. package/dist-lib/components/TemplateModuleLogo.js +4 -0
  10. package/dist-lib/components/TemplateModulePage.d.ts +2 -0
  11. package/dist-lib/components/TemplateModulePage.d.ts.map +1 -0
  12. package/dist-lib/components/TemplateModulePage.js +44 -0
  13. package/dist-lib/components/TemplatePage.d.ts +2 -0
  14. package/dist-lib/components/TemplatePage.d.ts.map +1 -0
  15. package/dist-lib/components/TemplatePage.js +4 -0
  16. package/dist-lib/components/settings/MijnModuleSettingsCard.d.ts +2 -0
  17. package/dist-lib/components/settings/MijnModuleSettingsCard.d.ts.map +1 -0
  18. package/dist-lib/components/settings/MijnModuleSettingsCard.js +42 -0
  19. package/dist-lib/hooks/useMijnModuleData.d.ts +13 -0
  20. package/dist-lib/hooks/useMijnModuleData.d.ts.map +1 -0
  21. package/dist-lib/hooks/useMijnModuleData.js +49 -0
  22. package/dist-lib/hooks/useModulePermission.d.ts +17 -0
  23. package/dist-lib/hooks/useModulePermission.d.ts.map +1 -0
  24. package/dist-lib/hooks/useModulePermission.js +42 -0
  25. package/dist-lib/index.d.ts +10 -0
  26. package/dist-lib/index.d.ts.map +1 -0
  27. package/dist-lib/index.js +19 -0
  28. package/dist-lib/integrations/supabase/auth-attacher.d.ts +2 -0
  29. package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -0
  30. package/dist-lib/integrations/supabase/auth-attacher.js +12 -0
  31. package/dist-lib/integrations/supabase/auth-middleware.d.ts +20806 -0
  32. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -0
  33. package/dist-lib/integrations/supabase/auth-middleware.js +49 -0
  34. package/dist-lib/integrations/supabase/client.d.ts +20802 -0
  35. package/dist-lib/integrations/supabase/client.d.ts.map +1 -0
  36. package/dist-lib/integrations/supabase/client.js +13 -0
  37. package/dist-lib/integrations/supabase/client.server.d.ts +20802 -0
  38. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -0
  39. package/dist-lib/integrations/supabase/client.server.js +32 -0
  40. package/dist-lib/integrations/supabase/types.d.ts +21382 -0
  41. package/dist-lib/integrations/supabase/types.d.ts.map +1 -0
  42. package/dist-lib/integrations/supabase/types.js +89 -0
  43. package/dist-lib/lib/admin.functions.d.ts +228936 -0
  44. package/dist-lib/lib/admin.functions.d.ts.map +1 -0
  45. package/dist-lib/lib/admin.functions.js +410 -0
  46. package/dist-lib/lib/admin.server.d.ts +29 -0
  47. package/dist-lib/lib/admin.server.d.ts.map +1 -0
  48. package/dist-lib/lib/admin.server.js +63 -0
  49. package/dist-lib/lib/moduleDiscovery.d.ts +10 -0
  50. package/dist-lib/lib/moduleDiscovery.d.ts.map +1 -0
  51. package/dist-lib/lib/moduleDiscovery.js +54 -0
  52. package/dist-lib/lib/utils.d.ts +3 -0
  53. package/dist-lib/lib/utils.d.ts.map +1 -0
  54. package/dist-lib/lib/utils.js +5 -0
  55. package/dist-lib/lib/validationSchemas.d.ts +15 -0
  56. package/dist-lib/lib/validationSchemas.d.ts.map +1 -0
  57. package/dist-lib/lib/validationSchemas.js +25 -0
  58. package/dist-lib/styles.css +1 -0
  59. package/package.json +87 -0
  60. package/public/logo.png +0 -0
@@ -0,0 +1,410 @@
1
+ import { createServerFn } from "@tanstack/react-start";
2
+ import { getRequestHost } from "@tanstack/react-start/server";
3
+ import { z } from "zod";
4
+ import { requireSupabaseAuth } from "../integrations/supabase/auth-middleware";
5
+ import { requireAdmin, requireUserAdmin } from "../lib/admin.server";
6
+ const APP_ROLES = ["admin", "planner", "monteur", "administratie"];
7
+ const roleSchema = z.enum(["admin", "planner", "monteur", "administratie"]);
8
+ const permissionSchema = z.enum(["none", "view", "edit"]);
9
+ const syncModulesInput = z.object({
10
+ shellId: z.string().trim().min(1).max(100).optional(),
11
+ modules: z
12
+ .array(z.object({
13
+ id: z.string().trim().min(1).max(100),
14
+ name: z.string().trim().min(1).max(120),
15
+ }))
16
+ .max(200),
17
+ });
18
+ const setUserPermissionInput = z.object({
19
+ userId: z.string().uuid(),
20
+ moduleId: z.string().min(1),
21
+ permission: z.union([permissionSchema, z.literal("inherit")]),
22
+ });
23
+ const userPermissionsInput = z.object({
24
+ userId: z.string().uuid(),
25
+ });
26
+ const assignRoleInput = z.object({
27
+ userId: z.string().uuid(),
28
+ role: roleSchema,
29
+ });
30
+ const removeRoleInput = z.object({
31
+ userId: z.string().uuid(),
32
+ role: roleSchema,
33
+ });
34
+ const setPermissionInput = z.object({
35
+ role: roleSchema,
36
+ moduleId: z.string().min(1),
37
+ permission: permissionSchema,
38
+ });
39
+ const inviteUserInput = z.object({
40
+ email: z.string().trim().email().max(255),
41
+ displayName: z.string().trim().min(1).max(100),
42
+ role: roleSchema,
43
+ });
44
+ const deleteUserInput = z.object({
45
+ userId: z.string().uuid(),
46
+ });
47
+ const createTestUserInput = z.object({
48
+ displayName: z.string().trim().min(1).max(100),
49
+ role: roleSchema,
50
+ email: z.string().trim().email().max(255).optional(),
51
+ });
52
+ function uniqueRoles(roles) {
53
+ return Array.from(new Set(roles));
54
+ }
55
+ export const getUsersWithRoles = createServerFn({ method: "GET" })
56
+ .middleware([requireSupabaseAuth])
57
+ .handler(async ({ context }) => {
58
+ const { isAdmin } = await requireUserAdmin(context);
59
+ const { supabaseAdmin } = await import("../integrations/supabase/client.server");
60
+ const [{ data: profiles, error: profilesError }, { data: roles, error: rolesError }, { data: authList, error: authError }] = await Promise.all([
61
+ supabaseAdmin
62
+ .from("profiles")
63
+ .select("id, email, display_name")
64
+ .order("email", { ascending: true }),
65
+ supabaseAdmin
66
+ .from("user_roles")
67
+ .select("id, user_id, role")
68
+ .order("created_at", { ascending: false }),
69
+ supabaseAdmin.auth.admin.listUsers({ page: 1, perPage: 1000 }),
70
+ ]);
71
+ if (profilesError)
72
+ throw profilesError;
73
+ if (rolesError)
74
+ throw rolesError;
75
+ if (authError)
76
+ throw authError;
77
+ const safeProfiles = Array.isArray(profiles) ? profiles : [];
78
+ const safeRoles = Array.isArray(roles) ? roles : [];
79
+ const safeAuthUsers = Array.isArray(authList?.users) ? authList.users : [];
80
+ const confirmedById = new Map();
81
+ const isTestById = new Map();
82
+ for (const u of safeAuthUsers) {
83
+ confirmedById.set(u.id, Boolean(u.email_confirmed_at));
84
+ const meta = (u.user_metadata ?? {});
85
+ isTestById.set(u.id, Boolean(meta.is_test_account));
86
+ }
87
+ const rolesByUser = new Map();
88
+ for (const role of safeRoles) {
89
+ const list = rolesByUser.get(role.user_id) ?? [];
90
+ list.push(role);
91
+ rolesByUser.set(role.user_id, list);
92
+ }
93
+ return {
94
+ users: safeProfiles.map((profile) => ({
95
+ id: profile.id,
96
+ email: profile.email ?? "-",
97
+ displayName: profile.display_name ?? "-",
98
+ roles: uniqueRoles((rolesByUser.get(profile.id)?.map((r) => r.role) ?? [])),
99
+ emailConfirmed: confirmedById.get(profile.id) ?? false,
100
+ isTestAccount: isTestById.get(profile.id) ?? false,
101
+ })),
102
+ availableRoles: isAdmin ? APP_ROLES : APP_ROLES.filter((r) => r !== "admin"),
103
+ isAdmin,
104
+ };
105
+ });
106
+ export const assignRole = createServerFn({ method: "POST" })
107
+ .middleware([requireSupabaseAuth])
108
+ .inputValidator((data) => assignRoleInput.parse(data))
109
+ .handler(async ({ context, data }) => {
110
+ const { isAdmin } = await requireUserAdmin(context);
111
+ if (!isAdmin && data.role === "admin") {
112
+ throw new Error("Alleen een admin kan de adminrol toekennen.");
113
+ }
114
+ // Schrijft via de user-scoped client: RLS op user_roles is nu de echte gate.
115
+ const { data: existing, error: checkError } = await context.supabase
116
+ .from("user_roles")
117
+ .select("id")
118
+ .eq("user_id", data.userId)
119
+ .eq("role", data.role)
120
+ .maybeSingle();
121
+ if (checkError)
122
+ throw checkError;
123
+ if (existing) {
124
+ throw new Error(`Gebruiker heeft de rol '${data.role}' al.`);
125
+ }
126
+ const { error } = await context.supabase
127
+ .from("user_roles")
128
+ .insert({ user_id: data.userId, role: data.role });
129
+ if (error)
130
+ throw error;
131
+ return { ok: true };
132
+ });
133
+ export const removeRole = createServerFn({ method: "POST" })
134
+ .middleware([requireSupabaseAuth])
135
+ .inputValidator((data) => removeRoleInput.parse(data))
136
+ .handler(async ({ context, data }) => {
137
+ const { isAdmin } = await requireUserAdmin(context);
138
+ if (!isAdmin && data.role === "admin") {
139
+ throw new Error("Alleen een admin kan de adminrol verwijderen.");
140
+ }
141
+ if (data.role === "admin" && data.userId === context.userId) {
142
+ throw new Error("Je kunt je eigen adminrol niet verwijderen.");
143
+ }
144
+ // Delete via user-scoped client (RLS gate). De DB-trigger
145
+ // `prevent_last_admin_delete` is het definitieve vangnet voor de laatste admin.
146
+ const { error } = await context.supabase
147
+ .from("user_roles")
148
+ .delete()
149
+ .eq("user_id", data.userId)
150
+ .eq("role", data.role);
151
+ if (error)
152
+ throw error;
153
+ return { ok: true };
154
+ });
155
+ /**
156
+ * Registreert de modules die de shell heeft geladen. Modules die niet in de
157
+ * lijst voorkomen blijven bestaan maar worden op inactief gezet.
158
+ */
159
+ export const syncModuleRegistry = createServerFn({ method: "POST" })
160
+ .middleware([requireSupabaseAuth])
161
+ .inputValidator((data) => syncModulesInput.parse(data))
162
+ .handler(async ({ context, data }) => {
163
+ await requireAdmin(context);
164
+ const modules = Array.isArray(data.modules) ? data.modules : [];
165
+ if (modules.length === 0)
166
+ return { ok: true, count: 0 };
167
+ const now = new Date().toISOString();
168
+ const { error } = await context.supabase.from("module_registry").upsert(modules.map((m) => ({
169
+ module_id: m.id,
170
+ name: m.name,
171
+ shell_id: data.shellId ?? null,
172
+ is_active: true,
173
+ last_seen_at: now,
174
+ })), { onConflict: "module_id" });
175
+ if (error)
176
+ throw error;
177
+ // Modules van deze shell die niet meer geleverd worden → inactief.
178
+ if (data.shellId) {
179
+ const ids = modules.map((m) => m.id);
180
+ const { error: deactivateError } = await context.supabase
181
+ .from("module_registry")
182
+ .update({ is_active: false })
183
+ .eq("shell_id", data.shellId)
184
+ .not("module_id", "in", `(${ids.map((i) => `"${i}"`).join(",")})`);
185
+ if (deactivateError)
186
+ throw deactivateError;
187
+ }
188
+ return { ok: true, count: modules.length };
189
+ });
190
+ export const getModulePermissions = createServerFn({ method: "GET" })
191
+ .middleware([requireSupabaseAuth])
192
+ .handler(async ({ context }) => {
193
+ await requireAdmin(context);
194
+ const [{ data, error }, { data: registry, error: registryError }] = await Promise.all([
195
+ context.supabase.from("module_permissions").select("role, module_id, permission"),
196
+ context.supabase
197
+ .from("module_registry")
198
+ .select("module_id, name, is_active")
199
+ .order("name", { ascending: true }),
200
+ ]);
201
+ if (error)
202
+ throw error;
203
+ if (registryError)
204
+ throw registryError;
205
+ const safe = Array.isArray(data) ? data : [];
206
+ const safeRegistry = Array.isArray(registry) ? registry : [];
207
+ return {
208
+ roles: APP_ROLES,
209
+ modules: safeRegistry
210
+ .filter((row) => row.is_active)
211
+ .map((row) => ({
212
+ id: row.module_id,
213
+ name: row.name,
214
+ })),
215
+ permissions: safe.map((row) => ({
216
+ role: row.role,
217
+ moduleId: row.module_id,
218
+ permission: row.permission,
219
+ })),
220
+ };
221
+ });
222
+ export const getUserModulePermissions = createServerFn({ method: "POST" })
223
+ .middleware([requireSupabaseAuth])
224
+ .inputValidator((data) => userPermissionsInput.parse(data))
225
+ .handler(async ({ context, data }) => {
226
+ await requireUserAdmin(context);
227
+ const [{ data: overrides, error }, { data: registry, error: registryError }] = await Promise.all([
228
+ context.supabase
229
+ .from("user_module_permissions")
230
+ .select("module_id, permission")
231
+ .eq("user_id", data.userId),
232
+ context.supabase
233
+ .from("module_registry")
234
+ .select("module_id, name, is_active")
235
+ .order("name", { ascending: true }),
236
+ ]);
237
+ if (error)
238
+ throw error;
239
+ if (registryError)
240
+ throw registryError;
241
+ const safeOverrides = Array.isArray(overrides) ? overrides : [];
242
+ const safeRegistry = Array.isArray(registry) ? registry : [];
243
+ return {
244
+ modules: safeRegistry
245
+ .filter((row) => row.is_active)
246
+ .map((row) => ({
247
+ id: row.module_id,
248
+ name: row.name,
249
+ })),
250
+ overrides: safeOverrides.map((row) => ({
251
+ moduleId: row.module_id,
252
+ permission: row.permission,
253
+ })),
254
+ };
255
+ });
256
+ export const setUserModulePermission = createServerFn({ method: "POST" })
257
+ .middleware([requireSupabaseAuth])
258
+ .inputValidator((data) => setUserPermissionInput.parse(data))
259
+ .handler(async ({ context, data }) => {
260
+ await requireUserAdmin(context);
261
+ if (data.permission === "inherit") {
262
+ const { error } = await context.supabase
263
+ .from("user_module_permissions")
264
+ .delete()
265
+ .eq("user_id", data.userId)
266
+ .eq("module_id", data.moduleId);
267
+ if (error)
268
+ throw error;
269
+ return { ok: true };
270
+ }
271
+ const { error } = await context.supabase.from("user_module_permissions").upsert({
272
+ user_id: data.userId,
273
+ module_id: data.moduleId,
274
+ permission: data.permission,
275
+ updated_at: new Date().toISOString(),
276
+ }, { onConflict: "user_id,module_id" });
277
+ if (error)
278
+ throw error;
279
+ return { ok: true };
280
+ });
281
+ export const setModulePermission = createServerFn({ method: "POST" })
282
+ .middleware([requireSupabaseAuth])
283
+ .inputValidator((data) => setPermissionInput.parse(data))
284
+ .handler(async ({ context, data }) => {
285
+ await requireAdmin(context);
286
+ // Upsert via user-scoped client — RLS write-policy op module_permissions
287
+ // vereist admin (via JWT-claim of has_role-fallback).
288
+ const { error } = await context.supabase
289
+ .from("module_permissions")
290
+ .upsert({
291
+ role: data.role,
292
+ module_id: data.moduleId,
293
+ permission: data.permission,
294
+ }, { onConflict: "role,module_id" });
295
+ if (error)
296
+ throw error;
297
+ return { ok: true };
298
+ });
299
+ export const inviteUser = createServerFn({ method: "POST" })
300
+ .middleware([requireSupabaseAuth])
301
+ .inputValidator((data) => inviteUserInput.parse(data))
302
+ .handler(async ({ context, data }) => {
303
+ const { isAdmin } = await requireUserAdmin(context);
304
+ if (!isAdmin && data.role === "admin") {
305
+ throw new Error("Alleen een admin kan een account met de adminrol aanmaken.");
306
+ }
307
+ const { supabaseAdmin } = await import("../integrations/supabase/client.server");
308
+ let redirectTo;
309
+ try {
310
+ const host = getRequestHost();
311
+ if (host) {
312
+ const proto = host.startsWith("localhost") ? "http" : "https";
313
+ redirectTo = `${proto}://${host}/reset-wachtwoord`;
314
+ }
315
+ }
316
+ catch {
317
+ redirectTo = undefined;
318
+ }
319
+ const { data: invited, error: inviteError } = await supabaseAdmin.auth.admin.inviteUserByEmail(data.email, {
320
+ data: { display_name: data.displayName },
321
+ redirectTo,
322
+ });
323
+ if (inviteError || !invited?.user) {
324
+ const msg = inviteError?.message ?? "Uitnodigen mislukt";
325
+ throw new Error(/already|exists|registered/i.test(msg) ? "Deze gebruiker bestaat al." : msg);
326
+ }
327
+ const userId = invited.user.id;
328
+ const { error: profileError } = await supabaseAdmin
329
+ .from("profiles")
330
+ .upsert({ id: userId, email: data.email, display_name: data.displayName }, { onConflict: "id" });
331
+ if (profileError)
332
+ throw profileError;
333
+ const { error: roleError } = await supabaseAdmin
334
+ .from("user_roles")
335
+ .upsert({ user_id: userId, role: data.role }, { onConflict: "user_id,role", ignoreDuplicates: true });
336
+ if (roleError)
337
+ throw roleError;
338
+ return { ok: true, userId };
339
+ });
340
+ export const deleteUser = createServerFn({ method: "POST" })
341
+ .middleware([requireSupabaseAuth])
342
+ .inputValidator((data) => deleteUserInput.parse(data))
343
+ .handler(async ({ context, data }) => {
344
+ const { isAdmin } = await requireUserAdmin(context);
345
+ if (data.userId === context.userId) {
346
+ throw new Error("Je kunt je eigen account niet verwijderen.");
347
+ }
348
+ if (!isAdmin) {
349
+ const { data: targetRoles, error: targetError } = await context.supabase
350
+ .from("user_roles")
351
+ .select("role")
352
+ .eq("user_id", data.userId);
353
+ if (targetError)
354
+ throw targetError;
355
+ const safeTargetRoles = Array.isArray(targetRoles) ? targetRoles : [];
356
+ if (safeTargetRoles.some((r) => r.role === "admin")) {
357
+ throw new Error("Alleen een admin kan een adminaccount verwijderen.");
358
+ }
359
+ }
360
+ const { supabaseAdmin } = await import("../integrations/supabase/client.server");
361
+ const { error } = await supabaseAdmin.auth.admin.deleteUser(data.userId);
362
+ if (error)
363
+ throw error;
364
+ return { ok: true };
365
+ });
366
+ function generatePassword(length = 16) {
367
+ const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%*";
368
+ const bytes = new Uint8Array(length);
369
+ crypto.getRandomValues(bytes);
370
+ let out = "";
371
+ for (let i = 0; i < length; i++)
372
+ out += alphabet[bytes[i] % alphabet.length];
373
+ return out;
374
+ }
375
+ export const createTestUser = createServerFn({ method: "POST" })
376
+ .middleware([requireSupabaseAuth])
377
+ .inputValidator((data) => createTestUserInput.parse(data))
378
+ .handler(async ({ context, data }) => {
379
+ const { isAdmin } = await requireUserAdmin(context);
380
+ if (!isAdmin && data.role === "admin") {
381
+ throw new Error("Alleen een admin kan een account met de adminrol aanmaken.");
382
+ }
383
+ const { supabaseAdmin } = await import("../integrations/supabase/client.server");
384
+ const email = data.email?.trim() ||
385
+ `test-${Date.now()}-${Math.floor(Math.random() * 1000)}@test.local`;
386
+ const password = generatePassword(16);
387
+ const { data: created, error: createError } = await supabaseAdmin.auth.admin.createUser({
388
+ email,
389
+ password,
390
+ email_confirm: true,
391
+ user_metadata: { display_name: data.displayName, is_test_account: true },
392
+ });
393
+ if (createError || !created?.user) {
394
+ const msg = createError?.message ?? "Aanmaken mislukt";
395
+ throw new Error(/already|exists|registered/i.test(msg) ? "Deze gebruiker bestaat al." : msg);
396
+ }
397
+ const userId = created.user.id;
398
+ // Trigger vult profiles al; toch upsert voor zekerheid (idempotent).
399
+ const { error: profileError } = await supabaseAdmin
400
+ .from("profiles")
401
+ .upsert({ id: userId, email, display_name: data.displayName }, { onConflict: "id" });
402
+ if (profileError)
403
+ throw profileError;
404
+ const { error: roleError } = await supabaseAdmin
405
+ .from("user_roles")
406
+ .upsert({ user_id: userId, role: data.role }, { onConflict: "user_id,role", ignoreDuplicates: true });
407
+ if (roleError)
408
+ throw roleError;
409
+ return { ok: true, userId, email, password };
410
+ });
@@ -0,0 +1,29 @@
1
+ import type { Database } from "../integrations/supabase/types";
2
+ type AppRole = Database["public"]["Enums"]["app_role"];
3
+ /**
4
+ * Leest de `user_roles` claim uit het access-token in de Authorization header.
5
+ * Retourneert een lege array als de claim ontbreekt (bijv. oude sessies die nog
6
+ * geen refresh hebben gehad na het activeren van de custom_access_token_hook).
7
+ */
8
+ export declare function readJwtUserRoles(): AppRole[];
9
+ /**
10
+ * Adminguard die eerst de JWT-claim probeert (0 round-trips) en dan pas
11
+ * terugvalt op de `has_role` RPC voor sessies zonder claim.
12
+ */
13
+ export declare function requireAdmin(context: {
14
+ supabase: any;
15
+ userId: string;
16
+ }): Promise<void>;
17
+ /**
18
+ * Guard voor gebruikersbeheer: admin OF administratie mag hier bij.
19
+ * Geeft terug of de aanvrager volledig admin is, zodat aanroepers de
20
+ * adminrol kunnen afschermen voor administratie.
21
+ */
22
+ export declare function requireUserAdmin(context: {
23
+ supabase: any;
24
+ userId: string;
25
+ }): Promise<{
26
+ isAdmin: boolean;
27
+ }>;
28
+ export {};
29
+ //# sourceMappingURL=admin.server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin.server.d.ts","sourceRoot":"","sources":["../../src/lib/admin.server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAE9D,KAAK,OAAO,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC,CAAC;AAEvD;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,EAAE,CAoB5C;AAgBD;;;GAGG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE;IAAE,QAAQ,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5F;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE;IAAE,QAAQ,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACzC,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAI/B"}
@@ -0,0 +1,63 @@
1
+ // Server-only helpers voor de admin-module. Nooit importeren vanuit client-code.
2
+ import { getRequestHeader } from "@tanstack/react-start/server";
3
+ /**
4
+ * Leest de `user_roles` claim uit het access-token in de Authorization header.
5
+ * Retourneert een lege array als de claim ontbreekt (bijv. oude sessies die nog
6
+ * geen refresh hebben gehad na het activeren van de custom_access_token_hook).
7
+ */
8
+ export function readJwtUserRoles() {
9
+ try {
10
+ const authHeader = getRequestHeader("authorization");
11
+ if (!authHeader?.startsWith("Bearer "))
12
+ return [];
13
+ const token = authHeader.slice("Bearer ".length);
14
+ const parts = token.split(".");
15
+ if (parts.length !== 3)
16
+ return [];
17
+ // base64url decode
18
+ const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
19
+ const padded = payload + "===".slice((payload.length + 3) % 4);
20
+ const json = typeof atob === "function"
21
+ ? atob(padded)
22
+ : Buffer.from(padded, "base64").toString("utf-8");
23
+ const claims = JSON.parse(json);
24
+ const roles = claims?.user_roles;
25
+ if (!Array.isArray(roles))
26
+ return [];
27
+ return roles.filter((r) => typeof r === "string");
28
+ }
29
+ catch {
30
+ return [];
31
+ }
32
+ }
33
+ async function hasRole(context, role) {
34
+ const roles = readJwtUserRoles();
35
+ if (roles.includes(role))
36
+ return true;
37
+ const { data, error } = await context.supabase.rpc("has_role", {
38
+ _user_id: context.userId,
39
+ _role: role,
40
+ });
41
+ return !error && Boolean(data);
42
+ }
43
+ /**
44
+ * Adminguard die eerst de JWT-claim probeert (0 round-trips) en dan pas
45
+ * terugvalt op de `has_role` RPC voor sessies zonder claim.
46
+ */
47
+ export async function requireAdmin(context) {
48
+ if (await hasRole(context, "admin"))
49
+ return;
50
+ throw new Error("Adminrechten vereist");
51
+ }
52
+ /**
53
+ * Guard voor gebruikersbeheer: admin OF administratie mag hier bij.
54
+ * Geeft terug of de aanvrager volledig admin is, zodat aanroepers de
55
+ * adminrol kunnen afschermen voor administratie.
56
+ */
57
+ export async function requireUserAdmin(context) {
58
+ if (await hasRole(context, "admin"))
59
+ return { isAdmin: true };
60
+ if (await hasRole(context, "administratie"))
61
+ return { isAdmin: false };
62
+ throw new Error("Adminrechten vereist");
63
+ }
@@ -0,0 +1,10 @@
1
+ export interface DiscoveredModule {
2
+ id: string;
3
+ name: string;
4
+ }
5
+ /**
6
+ * Leidt de modulelijst af uit de routeboom van de draaiende shell.
7
+ * Geen hardcoded modules: elke shell levert automatisch zijn eigen set.
8
+ */
9
+ export declare function useDiscoveredModules(): DiscoveredModule[];
10
+ //# sourceMappingURL=moduleDiscovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"moduleDiscovery.d.ts","sourceRoot":"","sources":["../../src/lib/moduleDiscovery.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAiBD;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,gBAAgB,EAAE,CAqCzD"}
@@ -0,0 +1,54 @@
1
+ import { useMemo } from "react";
2
+ import { useRouter } from "@tanstack/react-router";
3
+ const EXCLUDED_SEGMENTS = new Set([
4
+ "api",
5
+ "login",
6
+ "logout",
7
+ "auth",
8
+ "reset-wachtwoord",
9
+ "instellingen",
10
+ ]);
11
+ function prettify(segment) {
12
+ const cleaned = segment.replace(/[-_]+/g, " ").trim();
13
+ if (!cleaned)
14
+ return segment;
15
+ return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
16
+ }
17
+ /**
18
+ * Leidt de modulelijst af uit de routeboom van de draaiende shell.
19
+ * Geen hardcoded modules: elke shell levert automatisch zijn eigen set.
20
+ */
21
+ export function useDiscoveredModules() {
22
+ const router = useRouter();
23
+ const routesById = (router?.routesById ?? {});
24
+ const ids = Object.keys(routesById).sort().join("|");
25
+ return useMemo(() => {
26
+ const found = new Map();
27
+ for (const routeId of Object.keys(routesById)) {
28
+ // Alleen routes binnen de beveiligde shell-omgeving.
29
+ if (!routeId.startsWith("/_authenticated/"))
30
+ continue;
31
+ const path = routeId.slice("/_authenticated/".length);
32
+ const segments = path.split("/").filter(Boolean);
33
+ const first = segments[0];
34
+ if (!first)
35
+ continue;
36
+ if (first.startsWith("$") || first.startsWith("_"))
37
+ continue;
38
+ if (EXCLUDED_SEGMENTS.has(first))
39
+ continue;
40
+ const route = routesById[routeId];
41
+ const title = route?.options?.staticData?.title;
42
+ const isTopLevel = segments.length === 1 || (segments.length === 2 && segments[1] === "index");
43
+ const existing = found.get(first);
44
+ // Titel van de bovenliggende route wint van die van detailroutes.
45
+ if (!existing || (isTopLevel && typeof title === "string" && title.trim())) {
46
+ found.set(first, (isTopLevel && title?.trim()) || existing || prettify(first));
47
+ }
48
+ }
49
+ return Array.from(found.entries())
50
+ .map(([id, name]) => ({ id, name }))
51
+ .sort((a, b) => a.name.localeCompare(b.name, "nl"));
52
+ // eslint-disable-next-line react-hooks/exhaustive-deps
53
+ }, [ids]);
54
+ }
@@ -0,0 +1,3 @@
1
+ import { type ClassValue } from "clsx";
2
+ export declare function cn(...inputs: ClassValue[]): string;
3
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/lib/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,MAAM,CAAA;AAG5C,wBAAgB,EAAE,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,UAEzC"}
@@ -0,0 +1,5 @@
1
+ import { clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+ export function cn(...inputs) {
4
+ return twMerge(clsx(inputs));
5
+ }
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ export declare const loginSchema: z.ZodObject<{
3
+ email: z.ZodString;
4
+ password: z.ZodString;
5
+ }, z.core.$strip>;
6
+ export declare const bugReportSchema: z.ZodObject<{
7
+ title: z.ZodString;
8
+ description: z.ZodString;
9
+ }, z.core.$strip>;
10
+ export declare const ALLOWED_FILE_TYPES: string[];
11
+ export declare function validateFileUpload(file: File): {
12
+ valid: boolean;
13
+ error?: string;
14
+ };
15
+ //# sourceMappingURL=validationSchemas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validationSchemas.d.ts","sourceRoot":"","sources":["../../src/lib/validationSchemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,WAAW;;;iBAGtB,CAAC;AAEH,eAAO,MAAM,eAAe;;;iBAG1B,CAAC;AAEH,eAAO,MAAM,kBAAkB,UAK9B,CAAC;AAIF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,IAAI,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAQjF"}
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ export const loginSchema = z.object({
3
+ email: z.string().email("Ongeldig e-mailadres"),
4
+ password: z.string().min(6, "Wachtwoord moet minimaal 6 tekens bevatten"),
5
+ });
6
+ export const bugReportSchema = z.object({
7
+ title: z.string().min(1, "Titel is verplicht").max(200),
8
+ description: z.string().min(1, "Beschrijving is verplicht").max(5000),
9
+ });
10
+ export const ALLOWED_FILE_TYPES = [
11
+ "image/png",
12
+ "image/jpeg",
13
+ "image/gif",
14
+ "application/pdf",
15
+ ];
16
+ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
17
+ export function validateFileUpload(file) {
18
+ if (!ALLOWED_FILE_TYPES.includes(file.type)) {
19
+ return { valid: false, error: "Bestandstype niet toegestaan" };
20
+ }
21
+ if (file.size > MAX_FILE_SIZE) {
22
+ return { valid: false, error: "Bestand is te groot (max 10MB)" };
23
+ }
24
+ return { valid: true };
25
+ }
@@ -0,0 +1 @@
1
+ @source "./";