@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.
- package/dist-lib/_core-safelist.d.ts +2 -0
- package/dist-lib/_core-safelist.d.ts.map +1 -0
- package/dist-lib/_core-safelist.js +15 -0
- package/dist-lib/components/AdminForInstallatieTechniek.d.ts +12 -0
- package/dist-lib/components/AdminForInstallatieTechniek.d.ts.map +1 -0
- package/dist-lib/components/AdminForInstallatieTechniek.js +359 -0
- package/dist-lib/components/TemplateModuleLogo.d.ts +6 -0
- package/dist-lib/components/TemplateModuleLogo.d.ts.map +1 -0
- package/dist-lib/components/TemplateModuleLogo.js +4 -0
- package/dist-lib/components/TemplateModulePage.d.ts +2 -0
- package/dist-lib/components/TemplateModulePage.d.ts.map +1 -0
- package/dist-lib/components/TemplateModulePage.js +44 -0
- package/dist-lib/components/TemplatePage.d.ts +2 -0
- package/dist-lib/components/TemplatePage.d.ts.map +1 -0
- package/dist-lib/components/TemplatePage.js +4 -0
- package/dist-lib/components/settings/MijnModuleSettingsCard.d.ts +2 -0
- package/dist-lib/components/settings/MijnModuleSettingsCard.d.ts.map +1 -0
- package/dist-lib/components/settings/MijnModuleSettingsCard.js +42 -0
- package/dist-lib/hooks/useMijnModuleData.d.ts +13 -0
- package/dist-lib/hooks/useMijnModuleData.d.ts.map +1 -0
- package/dist-lib/hooks/useMijnModuleData.js +49 -0
- package/dist-lib/hooks/useModulePermission.d.ts +17 -0
- package/dist-lib/hooks/useModulePermission.d.ts.map +1 -0
- package/dist-lib/hooks/useModulePermission.js +42 -0
- package/dist-lib/index.d.ts +10 -0
- package/dist-lib/index.d.ts.map +1 -0
- package/dist-lib/index.js +19 -0
- package/dist-lib/integrations/supabase/auth-attacher.d.ts +2 -0
- package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/auth-attacher.js +12 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +20806 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/auth-middleware.js +49 -0
- package/dist-lib/integrations/supabase/client.d.ts +20802 -0
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/client.js +13 -0
- package/dist-lib/integrations/supabase/client.server.d.ts +20802 -0
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/client.server.js +32 -0
- package/dist-lib/integrations/supabase/types.d.ts +21382 -0
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/types.js +89 -0
- package/dist-lib/lib/admin.functions.d.ts +228936 -0
- package/dist-lib/lib/admin.functions.d.ts.map +1 -0
- package/dist-lib/lib/admin.functions.js +410 -0
- package/dist-lib/lib/admin.server.d.ts +29 -0
- package/dist-lib/lib/admin.server.d.ts.map +1 -0
- package/dist-lib/lib/admin.server.js +63 -0
- package/dist-lib/lib/moduleDiscovery.d.ts +10 -0
- package/dist-lib/lib/moduleDiscovery.d.ts.map +1 -0
- package/dist-lib/lib/moduleDiscovery.js +54 -0
- package/dist-lib/lib/utils.d.ts +3 -0
- package/dist-lib/lib/utils.d.ts.map +1 -0
- package/dist-lib/lib/utils.js +5 -0
- package/dist-lib/lib/validationSchemas.d.ts +15 -0
- package/dist-lib/lib/validationSchemas.d.ts.map +1 -0
- package/dist-lib/lib/validationSchemas.js +25 -0
- package/dist-lib/styles.css +1 -0
- package/package.json +87 -0
- 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 @@
|
|
|
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,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 "./";
|