@flowselections/admin-installatietechniek 1.0.9 → 1.0.11

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.
@@ -26,6 +26,7 @@ const setUserPermissionInput = z.object({
26
26
  });
27
27
  const userPermissionsInput = z.object({
28
28
  userId: z.string().uuid(),
29
+ modules: syncModulesInput.shape.modules.optional(),
29
30
  });
30
31
  const roleIdInput = z.object({ roleId: z.string().uuid() });
31
32
  const assignRoleInput = z.object({
@@ -249,7 +250,8 @@ export const removeRole = createServerFn({ method: "POST" })
249
250
  /** Alle rollen met hun rechten, plus de geregistreerde modules en onderdelen. */
250
251
  export const getRoles = createServerFn({ method: "POST" })
251
252
  .middleware([normalizeServerErrors, requireSupabaseAuth])
252
- .handler(async ({ context }) => {
253
+ .inputValidator((data) => shellModulesInput.parse(data))
254
+ .handler(async ({ context, data }) => {
253
255
  await requireUserAdmin(context);
254
256
  const [{ data: roles, error: rolesError }, { data: perms, error: permsError }, { data: registry, error: registryError }, { data: featureRows, error: featuresError },] = await Promise.all([
255
257
  context.supabase.from("roles").select("id, key, name, is_protected").order("name", { ascending: true }),
@@ -281,16 +283,7 @@ export const getRoles = createServerFn({ method: "POST" })
281
283
  isProtected: r.is_protected,
282
284
  unmapped: unmappedKeys.has(r.key),
283
285
  })),
284
- modules: safeRegistry
285
- .filter((m) => m.is_active)
286
- .map((m) => ({
287
- id: m.module_id,
288
- name: m.name,
289
- enforce: Boolean(m.enforce),
290
- features: safeFeatures
291
- .filter((f) => f.is_active && f.module_id === m.module_id)
292
- .map((f) => ({ id: f.feature_id, name: f.name })),
293
- })),
286
+ modules: mergeShellModules(data?.modules, safeRegistry, safeFeatures),
294
287
  permissions: safePerms.map((p) => ({
295
288
  roleId: p.role_id,
296
289
  moduleId: p.module_id,
@@ -416,7 +409,8 @@ export const deleteRole = createServerFn({ method: "POST" })
416
409
  });
417
410
  /**
418
411
  * Registreert de modules (en hun onderdelen) die de shell heeft geladen.
419
- * Wat niet meer voorkomt blijft bestaan maar wordt op inactief gezet.
412
+ * Alleen toevoegen of bijwerken (naam, shell_id = "laatst gezien door").
413
+ * Nooit deactiveren of verwijderen: een ontbrekende module blijft staan.
420
414
  */
421
415
  export const syncModuleRegistry = createServerFn({ method: "POST" })
422
416
  .middleware([normalizeServerErrors, requireSupabaseAuth])
@@ -437,31 +431,18 @@ export const syncModuleRegistry = createServerFn({ method: "POST" })
437
431
  const { error } = await context.supabase.from("module_registry").upsert(modules.map((m) => ({
438
432
  module_id: m.id,
439
433
  name: m.name,
440
- shell_id: data.shellId ?? null,
441
- is_active: true,
434
+ ...(data.shellId ? { shell_id: data.shellId } : {}),
442
435
  last_seen_at: now,
443
- // Nieuwe modules komen standaard met afdwingen aan binnen; bestaande
444
- // modules behouden hun eigen instelling.
445
- ...(knownIds.has(m.id) ? {} : { enforce: true }),
436
+ // Nieuwe rijen: actief en afdwingen aan. Bestaande rijen behouden
437
+ // hun eigen is_active en enforce.
438
+ ...(knownIds.has(m.id) ? {} : { is_active: true, enforce: true }),
446
439
  })), { onConflict: "module_id" });
447
440
  if (error)
448
441
  throw error;
449
- if (data.shellId) {
450
- const ids = modules.map((m) => m.id);
451
- const { error: deactivateError } = await context.supabase
452
- .from("module_registry")
453
- .update({ is_active: false })
454
- .eq("shell_id", data.shellId)
455
- .not("module_id", "in", `(${ids.map((i) => `"${i}"`).join(",")})`);
456
- if (deactivateError)
457
- throw deactivateError;
458
- }
459
- // Onderdelen (subpagina's) synchroniseren.
460
442
  const featureRows = modules.flatMap((m) => (m.features ?? []).map((f) => ({
461
443
  module_id: m.id,
462
444
  feature_id: f.id,
463
445
  name: f.name,
464
- is_active: true,
465
446
  last_seen_at: now,
466
447
  })));
467
448
  if (featureRows.length > 0) {
@@ -471,21 +452,54 @@ export const syncModuleRegistry = createServerFn({ method: "POST" })
471
452
  if (featError)
472
453
  throw featError;
473
454
  }
474
- for (const m of modules) {
475
- const ids = (m.features ?? []).map((f) => f.id);
476
- let query = context.supabase
477
- .from("module_features")
478
- .update({ is_active: false })
479
- .eq("module_id", m.id);
480
- if (ids.length > 0) {
481
- query = query.not("feature_id", "in", `(${ids.map((i) => `"${i}"`).join(",")})`);
482
- }
483
- const { error: deactivateFeatError } = await query;
484
- if (deactivateFeatError)
485
- throw deactivateFeatError;
486
- }
487
455
  return { ok: true, count: modules.length };
488
456
  });
457
+ const shellModulesInput = z
458
+ .object({
459
+ modules: z
460
+ .array(z.object({
461
+ id: z.string().trim().min(1).max(100),
462
+ name: z.string().trim().min(1).max(120),
463
+ features: z.array(featureSchema).max(200).optional(),
464
+ }))
465
+ .max(200)
466
+ .optional(),
467
+ })
468
+ .optional();
469
+ /**
470
+ * Combineert de modules van de huidige shell met wat in module_registry en
471
+ * module_features staat. Zonder shell-lijst: alle registry-rijen. Geen
472
+ * filter op is_active. Een module zonder registry-rij is standaard
473
+ * toegestaan (enforce=false).
474
+ */
475
+ function mergeShellModules(shellModules, registry, features) {
476
+ const regById = new Map(registry.map((r) => [r.module_id, r]));
477
+ const featsFor = (moduleId, extra = []) => {
478
+ const map = new Map();
479
+ for (const f of features)
480
+ if (f.module_id === moduleId)
481
+ map.set(f.feature_id, f.name);
482
+ for (const f of extra)
483
+ map.set(f.id, f.name);
484
+ return Array.from(map, ([id, name]) => ({ id, name })).sort((a, b) => a.name.localeCompare(b.name, "nl"));
485
+ };
486
+ const source = Array.isArray(shellModules) && shellModules.length > 0
487
+ ? shellModules
488
+ : registry.map((r) => ({ id: r.module_id, name: r.name }));
489
+ const seen = new Set();
490
+ return source
491
+ .filter((m) => m?.id && !seen.has(m.id) && seen.add(m.id))
492
+ .map((m) => {
493
+ const reg = regById.get(m.id);
494
+ return {
495
+ id: m.id,
496
+ name: m.name || reg?.name || m.id,
497
+ enforce: reg ? Boolean(reg.enforce) : false,
498
+ features: featsFor(m.id, m.features ?? []),
499
+ };
500
+ })
501
+ .sort((a, b) => a.name.localeCompare(b.name, "nl"));
502
+ }
489
503
  const setModuleEnforceInput = z.object({
490
504
  moduleId: z.string().trim().min(1).max(100),
491
505
  enforce: z.boolean(),
@@ -526,15 +540,7 @@ export const getUserModulePermissions = createServerFn({ method: "POST" })
526
540
  const safeRegistry = Array.isArray(registry) ? registry : [];
527
541
  const safeFeatures = Array.isArray(featureRows) ? featureRows : [];
528
542
  return {
529
- modules: safeRegistry
530
- .filter((row) => row.is_active)
531
- .map((row) => ({
532
- id: row.module_id,
533
- name: row.name,
534
- features: safeFeatures
535
- .filter((f) => f.is_active && f.module_id === row.module_id)
536
- .map((f) => ({ id: f.feature_id, name: f.name })),
537
- })),
543
+ modules: mergeShellModules(data.modules, safeRegistry, safeFeatures),
538
544
  overrides: safeOverrides.map((row) => ({
539
545
  moduleId: row.module_id,
540
546
  featureId: row.feature_id ?? "",
@@ -31,9 +31,9 @@ export declare function requireUserAdmin(context: {
31
31
  isAdmin: boolean;
32
32
  }>;
33
33
  /**
34
- * Basis-URL van deze omgeving, uitsluitend uit de omgeving gelezen.
35
- * Fail closed: zonder geldige waarde wordt er geen mail met een kapotte
36
- * link verstuurd.
34
+ * Basis-URL van deze omgeving. Volgorde: APP_BASE_URL, PUBLIC_BASE_URL,
35
+ * origin van het verzoek. Fail closed: zonder geldige waarde (of als het
36
+ * naar localhost wijst) wordt er geen mail met een kapotte link verstuurd.
37
37
  */
38
38
  export declare function resolveAppBaseUrl(): string;
39
39
  /**
@@ -1 +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,mEAAmE;AACnE,eAAO,MAAM,oBAAoB,kCAAmC,CAAC;AAErE,sEAAsE;AACtE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE1E;AAsBD;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE;IAAE,QAAQ,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAI5F;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,CAK/B;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAuB1C;AAID;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAkD9E"}
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,mEAAmE;AACnE,eAAO,MAAM,oBAAoB,kCAAmC,CAAC;AAErE,sEAAsE;AACtE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE1E;AAsBD;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE;IAAE,QAAQ,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAI5F;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,CAK/B;AAgBD;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CA2B1C;AAID;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAkD9E"}
@@ -88,15 +88,33 @@ export async function requireUserAdmin(context) {
88
88
  return { isAdmin: false };
89
89
  throw new Error("Adminrechten vereist");
90
90
  }
91
+ /** Origin van het binnenkomende verzoek (Origin-header, anders forwarded host). */
92
+ function requestOrigin() {
93
+ try {
94
+ const origin = getRequestHeader("origin");
95
+ if (origin && origin !== "null")
96
+ return origin;
97
+ const host = getRequestHeader("x-forwarded-host") ?? getRequestHeader("host");
98
+ if (!host)
99
+ return "";
100
+ const proto = getRequestHeader("x-forwarded-proto") ?? "https";
101
+ return `${proto.split(",")[0].trim()}://${host.split(",")[0].trim()}`;
102
+ }
103
+ catch {
104
+ return "";
105
+ }
106
+ }
91
107
  /**
92
- * Basis-URL van deze omgeving, uitsluitend uit de omgeving gelezen.
93
- * Fail closed: zonder geldige waarde wordt er geen mail met een kapotte
94
- * link verstuurd.
108
+ * Basis-URL van deze omgeving. Volgorde: APP_BASE_URL, PUBLIC_BASE_URL,
109
+ * origin van het verzoek. Fail closed: zonder geldige waarde (of als het
110
+ * naar localhost wijst) wordt er geen mail met een kapotte link verstuurd.
95
111
  */
96
112
  export function resolveAppBaseUrl() {
97
- const raw = (process.env["APP_BASE_URL"] ?? "").trim().replace(/\/+$/, "");
113
+ const raw = (process.env["APP_BASE_URL"]?.trim() ||
114
+ process.env["PUBLIC_BASE_URL"]?.trim() ||
115
+ requestOrigin()).replace(/\/+$/, "");
98
116
  if (!raw) {
99
- throw new Error("Het webadres van deze omgeving is niet ingesteld (APP_BASE_URL). Zonder dat adres kan er geen werkende uitnodiging worden verstuurd.");
117
+ throw new Error("Het webadres van deze omgeving is onbekend: APP_BASE_URL/PUBLIC_BASE_URL ontbreken en het verzoek bevat geen herkomstadres. Er is geen uitnodiging verstuurd.");
100
118
  }
101
119
  let url;
102
120
  try {
package/package.json CHANGED
@@ -1,87 +1,87 @@
1
- {
2
- "name": "@flowselections/admin-installatietechniek",
3
- "version": "1.0.9",
4
- "flowselections": {
5
- "moduleId": "admin-installatietechniek",
6
- "pages": [
7
- {
8
- "export": "AdminForInstallatieTechniek",
9
- "route": "admin-installatietechniek",
10
- "title": "Rollen & machtigingen"
11
- }
12
- ]
13
- },
14
- "private": false,
15
- "sideEffects": false,
16
- "type": "module",
17
- "main": "./dist-lib/index.js",
18
- "types": "./dist-lib/index.d.ts",
19
- "exports": {
20
- ".": {
21
- "types": "./dist-lib/index.d.ts",
22
- "default": "./dist-lib/index.js"
23
- },
24
- "./styles": "./dist-lib/styles.css"
25
- },
26
- "files": [
27
- "dist-lib",
28
- "public"
29
- ],
30
- "publishConfig": {
31
- "access": "public"
32
- },
33
- "scripts": {
34
- "dev": "vite dev",
35
- "build": "vite build",
36
- "build:lib": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && node -e \"require('fs').writeFileSync('dist-lib/styles.css', '@source \\\"./\\\";\\n')\"",
37
- "build:lib:watch": "tsc -p tsconfig.build.json --watch",
38
- "build:dev": "vite build --mode development",
39
- "preview": "vite preview",
40
- "lint": "eslint ."
41
- },
42
- "peerDependencies": {
43
- "@flowselections/core": "1.0.22",
44
- "@tanstack/react-query": "^5.0.0",
45
- "@tanstack/react-router": "^1.0.0",
46
- "@tanstack/react-start": "^1.0.0",
47
- "lucide-react": "^0.400.0",
48
- "react": "^18 || ^19",
49
- "react-dom": "^18 || ^19",
50
- "sonner": "^2.0.0"
51
- },
52
- "dependencies": {
53
- "@supabase/supabase-js": "^2.110.7"
54
- },
55
- "devDependencies": {
56
- "@cloudflare/vite-plugin": "^1.25.5",
57
- "@eslint/js": "^9.32.0",
58
- "@flowselections/core": "1.0.22",
59
- "@lovable.dev/vite-tanstack-config": "2.23.1",
60
- "@rsbuild/core": "^1.0.0",
61
- "@tailwindcss/vite": "^4.2.1",
62
- "@tanstack/react-query": "^5.83.0",
63
- "@tanstack/react-router": "^1.168.0",
64
- "@tanstack/react-start": "^1.167.14",
65
- "@tanstack/router-plugin": "^1.167.10",
66
- "@types/node": "^22.16.5",
67
- "@types/react": "^19.2.0",
68
- "@types/react-dom": "^19.2.0",
69
- "@vitejs/plugin-react": "^5.0.4",
70
- "eslint": "^9.32.0",
71
- "eslint-plugin-react-hooks": "^5.2.0",
72
- "eslint-plugin-react-refresh": "^0.4.20",
73
- "globals": "^15.15.0",
74
- "lucide-react": "^0.575.0",
75
- "react": "^19.2.0",
76
- "react-dom": "^19.2.0",
77
- "sonner": "^2.0.7",
78
- "tailwindcss": "^4.2.1",
79
- "tw-animate-css": "^1.3.4",
80
- "typescript": "^5.8.3",
81
- "tsc-alias": "^1.8.10",
82
- "typescript-eslint": "^8.56.1",
83
- "vite": "^7.3.1",
84
- "vite-tsconfig-paths": "^6.0.2",
85
- "nitro": "3.0.260603-beta"
86
- }
87
- }
1
+ {
2
+ "name": "@flowselections/admin-installatietechniek",
3
+ "version": "1.0.11",
4
+ "flowselections": {
5
+ "moduleId": "admin-installatietechniek",
6
+ "pages": [
7
+ {
8
+ "export": "AdminForInstallatieTechniek",
9
+ "route": "admin-installatietechniek",
10
+ "title": "Rollen & machtigingen"
11
+ }
12
+ ]
13
+ },
14
+ "private": false,
15
+ "sideEffects": false,
16
+ "type": "module",
17
+ "main": "./dist-lib/index.js",
18
+ "types": "./dist-lib/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist-lib/index.d.ts",
22
+ "default": "./dist-lib/index.js"
23
+ },
24
+ "./styles": "./dist-lib/styles.css"
25
+ },
26
+ "files": [
27
+ "dist-lib",
28
+ "public"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "scripts": {
34
+ "dev": "vite dev",
35
+ "build": "vite build",
36
+ "build:lib": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && node -e \"require('fs').writeFileSync('dist-lib/styles.css', '@source \\\"./\\\";\\n')\"",
37
+ "build:lib:watch": "tsc -p tsconfig.build.json --watch",
38
+ "build:dev": "vite build --mode development",
39
+ "preview": "vite preview",
40
+ "lint": "eslint ."
41
+ },
42
+ "peerDependencies": {
43
+ "@flowselections/core": ">=1.0.26",
44
+ "@tanstack/react-query": "^5.0.0",
45
+ "@tanstack/react-router": "^1.0.0",
46
+ "@tanstack/react-start": "^1.0.0",
47
+ "lucide-react": "^0.400.0",
48
+ "react": "^18 || ^19",
49
+ "react-dom": "^18 || ^19",
50
+ "sonner": "^2.0.0"
51
+ },
52
+ "dependencies": {
53
+ "@supabase/supabase-js": "^2.110.7"
54
+ },
55
+ "devDependencies": {
56
+ "@cloudflare/vite-plugin": "^1.25.5",
57
+ "@eslint/js": "^9.32.0",
58
+ "@flowselections/core": "1.0.26",
59
+ "@lovable.dev/vite-tanstack-config": "2.23.1",
60
+ "@rsbuild/core": "^1.0.0",
61
+ "@tailwindcss/vite": "^4.2.1",
62
+ "@tanstack/react-query": "^5.83.0",
63
+ "@tanstack/react-router": "^1.168.0",
64
+ "@tanstack/react-start": "^1.167.14",
65
+ "@tanstack/router-plugin": "^1.167.10",
66
+ "@types/node": "^22.16.5",
67
+ "@types/react": "^19.2.0",
68
+ "@types/react-dom": "^19.2.0",
69
+ "@vitejs/plugin-react": "^5.0.4",
70
+ "eslint": "^9.32.0",
71
+ "eslint-plugin-react-hooks": "^5.2.0",
72
+ "eslint-plugin-react-refresh": "^0.4.20",
73
+ "globals": "^15.15.0",
74
+ "lucide-react": "^0.575.0",
75
+ "react": "^19.2.0",
76
+ "react-dom": "^19.2.0",
77
+ "sonner": "^2.0.7",
78
+ "tailwindcss": "^4.2.1",
79
+ "tw-animate-css": "^1.3.4",
80
+ "typescript": "^5.8.3",
81
+ "tsc-alias": "^1.8.10",
82
+ "typescript-eslint": "^8.56.1",
83
+ "vite": "^7.3.1",
84
+ "vite-tsconfig-paths": "^6.0.2",
85
+ "nitro": "3.0.260603-beta"
86
+ }
87
+ }