@flowselections/admin-installatietechniek 1.0.8 → 1.0.9

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.
@@ -6,18 +6,23 @@ type AppRole = Database["public"]["Enums"]["app_role"];
6
6
  * geen refresh hebben gehad na het activeren van de custom_access_token_hook).
7
7
  */
8
8
  export declare function readJwtUserRoles(): AppRole[];
9
+ /** Rolsleutels die volledige adminrechten geven in deze module. */
10
+ export declare const FULL_ADMIN_ROLE_KEYS: readonly ["admin", "superadmin"];
11
+ /** True als de rolsleutel een beschermde (volledige admin) rol is. */
12
+ export declare function isFullAdminRoleKey(key: string | null | undefined): boolean;
9
13
  /**
10
14
  * Adminguard die eerst de JWT-claim probeert (0 round-trips) en dan pas
11
15
  * terugvalt op de `has_role` RPC voor sessies zonder claim.
16
+ * Superadmin mag alles wat admin mag.
12
17
  */
13
18
  export declare function requireAdmin(context: {
14
19
  supabase: any;
15
20
  userId: string;
16
21
  }): Promise<void>;
17
22
  /**
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.
23
+ * Guard voor gebruikersbeheer: admin, superadmin OF administratie mag hier bij.
24
+ * Geeft terug of de aanvrager volledig admin is (admin of superadmin), zodat
25
+ * aanroepers de beschermde rollen kunnen afschermen voor administratie.
21
26
  */
22
27
  export declare function requireUserAdmin(context: {
23
28
  supabase: any;
@@ -25,5 +30,17 @@ export declare function requireUserAdmin(context: {
25
30
  }): Promise<{
26
31
  isAdmin: boolean;
27
32
  }>;
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.
37
+ */
38
+ export declare function resolveAppBaseUrl(): string;
39
+ /**
40
+ * Controleert een adres bij Google Maps (Geocoding). Fail closed: zonder
41
+ * gekoppelde Google Maps-verbinding wordt er niet stilzwijgend opgeslagen.
42
+ * Retourneert het door Google genormaliseerde adres.
43
+ */
44
+ export declare function verifyAddressWithGoogle(address: string): Promise<string>;
28
45
  export {};
29
46
  //# sourceMappingURL=admin.server.d.ts.map
@@ -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;;;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"}
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"}
@@ -40,24 +40,122 @@ async function hasRole(context, role) {
40
40
  });
41
41
  return !error && Boolean(data);
42
42
  }
43
+ /** Rolsleutels die volledige adminrechten geven in deze module. */
44
+ export const FULL_ADMIN_ROLE_KEYS = ["admin", "superadmin"];
45
+ /** True als de rolsleutel een beschermde (volledige admin) rol is. */
46
+ export function isFullAdminRoleKey(key) {
47
+ return Boolean(key) && FULL_ADMIN_ROLE_KEYS.includes(key);
48
+ }
49
+ /** Superadmin via het nieuwe rolmodel: role_assignments → roles.key. */
50
+ async function hasRoleAssignmentKey(context, key) {
51
+ const { data, error } = await context.supabase
52
+ .from("role_assignments")
53
+ .select("roles!inner(key)")
54
+ .eq("user_id", context.userId)
55
+ .eq("roles.key", key)
56
+ .limit(1);
57
+ return !error && Array.isArray(data) && data.length > 0;
58
+ }
59
+ /** Superadmin via enum-model (JWT/has_role) of via role_assignments. */
60
+ async function isSuperadmin(context) {
61
+ if (await hasRole(context, "superadmin"))
62
+ return true;
63
+ return hasRoleAssignmentKey(context, "superadmin");
64
+ }
43
65
  /**
44
66
  * Adminguard die eerst de JWT-claim probeert (0 round-trips) en dan pas
45
67
  * terugvalt op de `has_role` RPC voor sessies zonder claim.
68
+ * Superadmin mag alles wat admin mag.
46
69
  */
47
70
  export async function requireAdmin(context) {
48
71
  if (await hasRole(context, "admin"))
49
72
  return;
73
+ if (await isSuperadmin(context))
74
+ return;
50
75
  throw new Error("Adminrechten vereist");
51
76
  }
52
77
  /**
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.
78
+ * Guard voor gebruikersbeheer: admin, superadmin OF administratie mag hier bij.
79
+ * Geeft terug of de aanvrager volledig admin is (admin of superadmin), zodat
80
+ * aanroepers de beschermde rollen kunnen afschermen voor administratie.
56
81
  */
57
82
  export async function requireUserAdmin(context) {
58
83
  if (await hasRole(context, "admin"))
59
84
  return { isAdmin: true };
85
+ if (await isSuperadmin(context))
86
+ return { isAdmin: true };
60
87
  if (await hasRole(context, "administratie"))
61
88
  return { isAdmin: false };
62
89
  throw new Error("Adminrechten vereist");
63
90
  }
91
+ /**
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.
95
+ */
96
+ export function resolveAppBaseUrl() {
97
+ const raw = (process.env["APP_BASE_URL"] ?? "").trim().replace(/\/+$/, "");
98
+ 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.");
100
+ }
101
+ let url;
102
+ try {
103
+ url = new URL(raw);
104
+ }
105
+ catch {
106
+ throw new Error("Het ingestelde webadres (APP_BASE_URL) is geen geldige URL.");
107
+ }
108
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
109
+ throw new Error("Het ingestelde webadres (APP_BASE_URL) moet met http of https beginnen.");
110
+ }
111
+ const host = url.hostname.toLowerCase();
112
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".local")) {
113
+ throw new Error("Het ingestelde webadres (APP_BASE_URL) verwijst naar deze computer. Stel het publieke adres van deze omgeving in.");
114
+ }
115
+ return `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
116
+ }
117
+ const GEOCODE_GATEWAY_URL = "https://connector-gateway.lovable.dev/google_maps";
118
+ /**
119
+ * Controleert een adres bij Google Maps (Geocoding). Fail closed: zonder
120
+ * gekoppelde Google Maps-verbinding wordt er niet stilzwijgend opgeslagen.
121
+ * Retourneert het door Google genormaliseerde adres.
122
+ */
123
+ export async function verifyAddressWithGoogle(address) {
124
+ const trimmed = address.trim();
125
+ if (!trimmed)
126
+ return "";
127
+ const lovableApiKey = process.env["LOVABLE_API_KEY"];
128
+ const mapsKey = process.env["GOOGLE_MAPS_API_KEY"];
129
+ if (!lovableApiKey || !mapsKey) {
130
+ throw new Error("Adrescontrole is niet beschikbaar: er is geen Google Maps-verbinding gekoppeld aan deze omgeving.");
131
+ }
132
+ const response = await fetch(`${GEOCODE_GATEWAY_URL}/maps/api/geocode/json?address=${encodeURIComponent(trimmed)}`, {
133
+ headers: {
134
+ Authorization: `Bearer ${lovableApiKey}`,
135
+ "X-Connection-Api-Key": mapsKey,
136
+ },
137
+ });
138
+ if (!response.ok) {
139
+ const body = await response.text();
140
+ throw new Error(`Adrescontrole mislukte [${response.status}]: ${body}`);
141
+ }
142
+ const payload = await response.json();
143
+ const status = payload?.status;
144
+ const results = Array.isArray(payload?.results) ? payload.results : [];
145
+ if (status === "ZERO_RESULTS" || results.length === 0) {
146
+ throw new Error("Dit adres is niet gevonden op Google Maps. Controleer straat, huisnummer en plaats.");
147
+ }
148
+ if (status !== "OK") {
149
+ throw new Error(`Adrescontrole mislukte: ${payload?.error_message ?? status}`);
150
+ }
151
+ const best = results[0];
152
+ const types = Array.isArray(best?.types) ? best.types : [];
153
+ const isPrecise = best?.geometry?.location_type === "ROOFTOP" ||
154
+ types.includes("street_address") ||
155
+ types.includes("premise") ||
156
+ types.includes("subpremise");
157
+ if (!isPrecise) {
158
+ throw new Error("Dit adres is te onnauwkeurig. Vul straat, huisnummer, postcode en plaats in.");
159
+ }
160
+ return typeof best?.formatted_address === "string" ? best.formatted_address : trimmed;
161
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowselections/admin-installatietechniek",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
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": "1.0.18",
43
+ "@flowselections/core": "1.0.22",
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,8 +55,8 @@
55
55
  "devDependencies": {
56
56
  "@cloudflare/vite-plugin": "^1.25.5",
57
57
  "@eslint/js": "^9.32.0",
58
- "@flowselections/core": "1.0.18",
59
- "@lovable.dev/vite-tanstack-config": "2.13.1",
58
+ "@flowselections/core": "1.0.22",
59
+ "@lovable.dev/vite-tanstack-config": "2.23.1",
60
60
  "@rsbuild/core": "^1.0.0",
61
61
  "@tailwindcss/vite": "^4.2.1",
62
62
  "@tanstack/react-query": "^5.83.0",