@flowselections/floriday-authenticatie-module 1.0.11 → 1.0.13
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/assets/floriday-logo.png.asset.json +11 -0
- package/dist-lib/components/floriday/FlorydaySyncJobsCard.d.ts +2 -0
- package/dist-lib/components/floriday/FlorydaySyncJobsCard.d.ts.map +1 -0
- package/dist-lib/components/floriday/FlorydaySyncJobsCard.js +99 -0
- package/dist-lib/components/floriday/TokenHealthCard.d.ts +2 -0
- package/dist-lib/components/floriday/TokenHealthCard.d.ts.map +1 -0
- package/dist-lib/components/floriday/TokenHealthCard.js +93 -0
- package/dist-lib/components/settings/FlorydaySettingsCard.d.ts.map +1 -1
- package/dist-lib/components/settings/FlorydaySettingsCard.js +116 -3
- package/dist-lib/index.d.ts +1 -0
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +2 -1
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +4685 -595
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +4685 -595
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +4685 -595
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.js +8 -2
- package/dist-lib/integrations/supabase/types.d.ts +4788 -586
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +11 -0
- package/dist-lib/lib/floriday-sync.functions.d.ts +49839 -0
- package/dist-lib/lib/floriday-sync.functions.d.ts.map +1 -0
- package/dist-lib/lib/floriday-sync.functions.js +306 -0
- package/dist-lib/lib/floriday.functions.d.ts +82115 -6526
- package/dist-lib/lib/floriday.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday.functions.js +475 -37
- package/package.json +5 -4
|
@@ -37,9 +37,9 @@ const FLORYDAY_ENDPOINTS = {
|
|
|
37
37
|
apiBaseUrl: "https://api.floriday.io",
|
|
38
38
|
},
|
|
39
39
|
};
|
|
40
|
-
const DEFAULT_SCOPE = "role:app catalog:read sales-order:write organization:read supply:read supply:write sales-order:read delivery-conditions:read fulfillment:write fulfillment:read";
|
|
41
|
-
const SUPPLIERS_API_VERSION = "suppliers-api-2026v1";
|
|
42
|
-
const TOKEN_REFRESH_LEEWAY_S =
|
|
40
|
+
const DEFAULT_SCOPE = "role:app catalog:read catalog:write sales-order:write organization:read supply:read supply:write sales-order:read delivery-conditions:read fulfillment:write fulfillment:read network:read network:write";
|
|
41
|
+
export const SUPPLIERS_API_VERSION = "suppliers-api-2026v1";
|
|
42
|
+
const TOKEN_REFRESH_LEEWAY_S = 300;
|
|
43
43
|
function mask(value, keep = 4) {
|
|
44
44
|
if (!value)
|
|
45
45
|
return "";
|
|
@@ -108,29 +108,62 @@ export async function getFlorydayAccessToken(supabase, env, options = {}) {
|
|
|
108
108
|
if (!options.forceRefresh && row.access_token && expiresAt - TOKEN_REFRESH_LEEWAY_S * 1000 > now) {
|
|
109
109
|
return row.access_token;
|
|
110
110
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
111
|
+
// Concurrency-guard: vlak voor de tokenrequest opnieuw lezen — een parallelle worker
|
|
112
|
+
// kan zojuist een fresh token hebben geschreven en dan hoeven we niet nóg eens te refreshen.
|
|
113
|
+
if (!options.forceRefresh) {
|
|
114
|
+
const { data: fresh } = await supabase
|
|
115
|
+
.from("floriday_connections")
|
|
116
|
+
.select("access_token, token_expires_at")
|
|
117
|
+
.eq("id", row.id)
|
|
118
|
+
.maybeSingle();
|
|
119
|
+
const freshExp = fresh?.token_expires_at ? Date.parse(fresh.token_expires_at) : 0;
|
|
120
|
+
if (fresh?.access_token && freshExp - TOKEN_REFRESH_LEEWAY_S * 1000 > now) {
|
|
121
|
+
return fresh.access_token;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
let json;
|
|
125
|
+
let newExpiresAt;
|
|
126
|
+
try {
|
|
127
|
+
const tokenRes = await fetch(FLORYDAY_ENDPOINTS[env].tokenUrl, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: {
|
|
130
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
131
|
+
Accept: "application/json",
|
|
132
|
+
},
|
|
133
|
+
body: new URLSearchParams({
|
|
134
|
+
grant_type: "client_credentials",
|
|
135
|
+
client_id: deepSanitizeCredential(row.client_id),
|
|
136
|
+
client_secret: deepSanitizeCredential(row.client_secret),
|
|
137
|
+
scope: DEFAULT_SCOPE,
|
|
138
|
+
}).toString(),
|
|
139
|
+
});
|
|
140
|
+
const text = await tokenRes.text();
|
|
141
|
+
if (!tokenRes.ok) {
|
|
142
|
+
throw new Error(`Floriday token-aanvraag mislukt [${tokenRes.status}]: ${text.slice(0, 300)}`);
|
|
143
|
+
}
|
|
144
|
+
json = JSON.parse(text);
|
|
145
|
+
newExpiresAt = new Date(now + json.expires_in * 1000).toISOString();
|
|
146
|
+
await supabase
|
|
147
|
+
.from("floriday_connections")
|
|
148
|
+
.update({ access_token: json.access_token, token_expires_at: newExpiresAt })
|
|
149
|
+
.eq("id", row.id);
|
|
150
|
+
// Best-effort logging (faalt nooit hard).
|
|
151
|
+
await supabase.from("floriday_token_refresh_log").insert({
|
|
152
|
+
connection_id: row.id,
|
|
153
|
+
environment: env,
|
|
154
|
+
ok: true,
|
|
155
|
+
expires_at: newExpiresAt,
|
|
156
|
+
}).then(() => undefined, () => undefined);
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
await supabase.from("floriday_token_refresh_log").insert({
|
|
160
|
+
connection_id: row.id,
|
|
161
|
+
environment: env,
|
|
162
|
+
ok: false,
|
|
163
|
+
error: String(e?.message ?? e).slice(0, 500),
|
|
164
|
+
}).then(() => undefined, () => undefined);
|
|
165
|
+
throw e;
|
|
127
166
|
}
|
|
128
|
-
const json = JSON.parse(text);
|
|
129
|
-
const newExpiresAt = new Date(now + json.expires_in * 1000).toISOString();
|
|
130
|
-
await supabase
|
|
131
|
-
.from("floriday_connections")
|
|
132
|
-
.update({ access_token: json.access_token, token_expires_at: newExpiresAt })
|
|
133
|
-
.eq("id", row.id);
|
|
134
167
|
return json.access_token;
|
|
135
168
|
}
|
|
136
169
|
async function fetchAndStoreFlorydayOrganizationName(supabase, env, connectionId) {
|
|
@@ -205,16 +238,32 @@ export async function florydayFetch(supabase, pathOrEnv, pathOrInit, maybeInit =
|
|
|
205
238
|
const row = await getActiveFlorydayCredentials(supabase, env);
|
|
206
239
|
if (!row)
|
|
207
240
|
throw new Error(`Geen actieve Floriday-koppeling voor ${env}`);
|
|
208
|
-
const token = await getFlorydayAccessToken(supabase, env);
|
|
209
241
|
const url = path.startsWith("http")
|
|
210
242
|
? path
|
|
211
243
|
: `${FLORYDAY_ENDPOINTS[env].apiBaseUrl}${path.startsWith("/") ? "" : "/"}${path}`;
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
headers.
|
|
217
|
-
|
|
244
|
+
const doFetch = async (token) => {
|
|
245
|
+
const headers = new Headers(init.headers);
|
|
246
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
247
|
+
headers.set("X-Api-Key", deepSanitizeCredential(row.api_key));
|
|
248
|
+
if (!headers.has("Accept"))
|
|
249
|
+
headers.set("Accept", "application/json");
|
|
250
|
+
return fetch(url, { ...init, headers });
|
|
251
|
+
};
|
|
252
|
+
let token = await getFlorydayAccessToken(supabase, env);
|
|
253
|
+
let res = await doFetch(token);
|
|
254
|
+
// Auto-retry op 401/403 met een force-refreshed token. Dit is het vangnet
|
|
255
|
+
// voor het geval de cron faalt of een race-condition ons toch een verlopen
|
|
256
|
+
// token gaf. Body wordt niet gelezen vóór de retry — als de caller .json()
|
|
257
|
+
// doet werkt dat normaal op het nieuwe Response object.
|
|
258
|
+
if (res.status === 401 || res.status === 403) {
|
|
259
|
+
token = await getFlorydayAccessToken(supabase, env, { forceRefresh: true });
|
|
260
|
+
res = await doFetch(token);
|
|
261
|
+
if (res.status === 401 || res.status === 403) {
|
|
262
|
+
const body = await res.clone().text().catch(() => "");
|
|
263
|
+
throw new Error(`Floriday API ${res.status} na force-refresh — controleer client_id/client_secret op /floriday-connection. Body: ${body.slice(0, 200)}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return res;
|
|
218
267
|
}
|
|
219
268
|
// ─── Server functions exposed to the UI ─────────────────────────────────────
|
|
220
269
|
export const listFlorydayConnections = createServerFn({ method: "GET" })
|
|
@@ -228,6 +277,41 @@ export const listFlorydayConnections = createServerFn({ method: "GET" })
|
|
|
228
277
|
throw new Error(error.message);
|
|
229
278
|
return (data ?? []).map(toPublic);
|
|
230
279
|
});
|
|
280
|
+
export const getFlorydayTokenHealth = createServerFn({ method: "GET" })
|
|
281
|
+
.middleware([requireSupabaseAuth])
|
|
282
|
+
.handler(async ({ context }) => {
|
|
283
|
+
const { data: conns } = await context.supabase
|
|
284
|
+
.from("floriday_connections")
|
|
285
|
+
.select("id, environment, token_expires_at, is_active")
|
|
286
|
+
.eq("is_active", true);
|
|
287
|
+
const { data: log } = await context.supabase
|
|
288
|
+
.from("floriday_token_refresh_log")
|
|
289
|
+
.select("connection_id, attempted_at, ok, error, expires_at")
|
|
290
|
+
.order("attempted_at", { ascending: false })
|
|
291
|
+
.limit(20);
|
|
292
|
+
return {
|
|
293
|
+
connections: conns ?? [],
|
|
294
|
+
recent: log ?? [],
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
export const forceRefreshFlorydayToken = createServerFn({ method: "POST" })
|
|
298
|
+
.middleware([requireSupabaseAuth])
|
|
299
|
+
.inputValidator((input) => {
|
|
300
|
+
if (!["staging", "live"].includes(input.environment)) {
|
|
301
|
+
throw new Error("Ongeldige environment");
|
|
302
|
+
}
|
|
303
|
+
return input;
|
|
304
|
+
})
|
|
305
|
+
.handler(async ({ data, context }) => {
|
|
306
|
+
await getFlorydayAccessToken(context.supabase, data.environment, { forceRefresh: true });
|
|
307
|
+
const { data: row } = await context.supabase
|
|
308
|
+
.from("floriday_connections")
|
|
309
|
+
.select("token_expires_at")
|
|
310
|
+
.eq("environment", data.environment)
|
|
311
|
+
.eq("is_active", true)
|
|
312
|
+
.maybeSingle();
|
|
313
|
+
return { ok: true, expires_at: row?.token_expires_at ?? null };
|
|
314
|
+
});
|
|
231
315
|
export const upsertFlorydayConnection = createServerFn({ method: "POST" })
|
|
232
316
|
.middleware([requireSupabaseAuth])
|
|
233
317
|
.inputValidator((input) => {
|
|
@@ -268,7 +352,36 @@ export const upsertFlorydayConnection = createServerFn({ method: "POST" })
|
|
|
268
352
|
await scheduleFlorydayTokenRefreshInternal(context.supabase);
|
|
269
353
|
}
|
|
270
354
|
catch (e) {
|
|
271
|
-
console.error("[floriday] auto-schedule cron failed:", e);
|
|
355
|
+
console.error("[floriday] auto-schedule token cron failed:", e);
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
await scheduleFlorydayConnectionsSyncInternal(context.supabase);
|
|
359
|
+
}
|
|
360
|
+
catch (e) {
|
|
361
|
+
console.error("[floriday] auto-schedule connections-sync cron failed:", e);
|
|
362
|
+
}
|
|
363
|
+
// Plan ook alle resource-syncs automatisch in (trade-items, sales-orders,
|
|
364
|
+
// customer-organizations, carriers). Best-effort: een mislukte cron breekt
|
|
365
|
+
// het opslaan van credentials niet.
|
|
366
|
+
try {
|
|
367
|
+
const { scheduleFlorydayResourceSyncInternal } = await import("./floriday-sync.functions");
|
|
368
|
+
const resources = [
|
|
369
|
+
"trade-items",
|
|
370
|
+
"sales-orders",
|
|
371
|
+
"customer-organizations",
|
|
372
|
+
"carriers",
|
|
373
|
+
];
|
|
374
|
+
for (const r of resources) {
|
|
375
|
+
try {
|
|
376
|
+
await scheduleFlorydayResourceSyncInternal(context.supabase, r);
|
|
377
|
+
}
|
|
378
|
+
catch (e) {
|
|
379
|
+
console.error(`[floriday] auto-schedule ${r} cron failed:`, e);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch (e) {
|
|
384
|
+
console.error("[floriday] auto-schedule resource crons failed:", e);
|
|
272
385
|
}
|
|
273
386
|
// Organisatienaam ophalen gebeurt non-blocking via een aparte call
|
|
274
387
|
// (testFlorydayConnection / handmatige refresh). Houdt opslag snel.
|
|
@@ -322,14 +435,43 @@ export const testFlorydayConnection = createServerFn({ method: "POST" })
|
|
|
322
435
|
return { ok: false, error: e?.message ?? "Onbekende fout" };
|
|
323
436
|
}
|
|
324
437
|
});
|
|
438
|
+
/**
|
|
439
|
+
* Bepaalt de base-URL waar pg_cron de Lovable-app moet aanroepen.
|
|
440
|
+
*
|
|
441
|
+
* Volgorde (eerste hit wint):
|
|
442
|
+
* 1. `FLORIDAY_CRON_BASE_URL` — custom domain of override
|
|
443
|
+
* 2. `LOVABLE_PROJECT_ID` → `https://project--{id}.lovable.app`
|
|
444
|
+
* 3. Hardcoded fallback voor dít project
|
|
445
|
+
*
|
|
446
|
+
* Bewust NIET terugvallen op `VITE_SUPABASE_PROJECT_ID` /
|
|
447
|
+
* `SUPABASE_PROJECT_ID`: die bevatten de Supabase project-ref, niet de
|
|
448
|
+
* Lovable project-ID — gebruik daarvan was de oorzaak van een stille
|
|
449
|
+
* cron-mismatch waardoor tokens nooit werden ververst.
|
|
450
|
+
*/
|
|
451
|
+
const HARDCODED_LOVABLE_PROJECT_ID = "27b6733b-2e2f-4518-b204-681958559055";
|
|
452
|
+
const MANAGED_FLORYDAY_CRON_JOB_NAMES = new Set([
|
|
453
|
+
"refresh-floriday-tokens-hourly",
|
|
454
|
+
"floriday-connections-sync-twice-daily",
|
|
455
|
+
"floriday-trade-items-sync-5min",
|
|
456
|
+
"floriday-salesorders-sync-5min",
|
|
457
|
+
"floriday-customer-organizations-sync-twice-daily",
|
|
458
|
+
"floriday-carriers-sync-twice-daily",
|
|
459
|
+
]);
|
|
460
|
+
export function resolveFlorydayCronBaseUrl() {
|
|
461
|
+
const custom = process.env.FLORIDAY_CRON_BASE_URL?.trim();
|
|
462
|
+
if (custom)
|
|
463
|
+
return custom.replace(/\/+$/, "");
|
|
464
|
+
const lovableId = process.env.LOVABLE_PROJECT_ID?.trim() || HARDCODED_LOVABLE_PROJECT_ID;
|
|
465
|
+
if (!lovableId) {
|
|
466
|
+
throw new Error("Kan cron-URL niet bepalen: zet FLORIDAY_CRON_BASE_URL of LOVABLE_PROJECT_ID");
|
|
467
|
+
}
|
|
468
|
+
return `https://project--${lovableId}.lovable.app`;
|
|
469
|
+
}
|
|
325
470
|
async function scheduleFlorydayTokenRefreshInternal(supabase) {
|
|
326
|
-
const projectId = process.env.VITE_SUPABASE_PROJECT_ID ?? process.env.SUPABASE_PROJECT_ID;
|
|
327
471
|
const apikey = process.env.SUPABASE_PUBLISHABLE_KEY ?? process.env.VITE_SUPABASE_PUBLISHABLE_KEY;
|
|
328
|
-
if (!projectId)
|
|
329
|
-
throw new Error("Project-id ontbreekt in server-env");
|
|
330
472
|
if (!apikey)
|
|
331
473
|
throw new Error("Supabase anon key ontbreekt in server-env");
|
|
332
|
-
const url =
|
|
474
|
+
const url = `${resolveFlorydayCronBaseUrl()}/api/public/cron/refresh-floriday-tokens`;
|
|
333
475
|
const { data, error } = await supabase.rpc("schedule_floriday_token_refresh", { p_url: url, p_apikey: apikey });
|
|
334
476
|
if (error)
|
|
335
477
|
throw new Error(error.message);
|
|
@@ -449,3 +591,299 @@ export const updateFlorydayPublishingSettings = createServerFn({ method: "POST"
|
|
|
449
591
|
throw new Error(error.message);
|
|
450
592
|
return { ok: true };
|
|
451
593
|
});
|
|
594
|
+
// ─── Generieke sequence-sync laag ───────────────────────────────────────────
|
|
595
|
+
// Floriday vereist sequence-endpoints i.p.v. GetById: eerst
|
|
596
|
+
// /<entity>/max-sequence, daarna /<entity>/sync/{seq}. Deze helper houdt
|
|
597
|
+
// de cursor in `floriday_sync_state` per (connection, entity).
|
|
598
|
+
const SEQUENCE_BATCH_CAP = 1000;
|
|
599
|
+
async function fetchMaxSequence(supabase, env, maxSequenceEndpoint) {
|
|
600
|
+
const res = await florydayFetch(supabase, env, maxSequenceEndpoint, { cache: "no-store" });
|
|
601
|
+
if (!res.ok) {
|
|
602
|
+
const body = await res.text().catch(() => "");
|
|
603
|
+
throw new Error(`max-sequence [${res.status}]: ${body.slice(0, 200)}`);
|
|
604
|
+
}
|
|
605
|
+
const json = await res.json().catch(() => null);
|
|
606
|
+
if (typeof json === "number")
|
|
607
|
+
return json;
|
|
608
|
+
const n = json?.maxSequenceNumber ?? json?.maxSequence ?? json?.value;
|
|
609
|
+
if (typeof n !== "number") {
|
|
610
|
+
throw new Error(`Onbekende max-sequence response: ${JSON.stringify(json).slice(0, 200)}`);
|
|
611
|
+
}
|
|
612
|
+
return n;
|
|
613
|
+
}
|
|
614
|
+
async function fetchBySequence(supabase, env, bySequenceEndpointBase, fromSequence) {
|
|
615
|
+
const url = `${bySequenceEndpointBase}/${fromSequence}`;
|
|
616
|
+
const res = await florydayFetch(supabase, env, url, { cache: "no-store" });
|
|
617
|
+
if (!res.ok) {
|
|
618
|
+
const body = await res.text().catch(() => "");
|
|
619
|
+
throw new Error(`by-sequence [${res.status}]: ${body.slice(0, 200)}`);
|
|
620
|
+
}
|
|
621
|
+
const json = await res.json().catch(() => null);
|
|
622
|
+
return Array.isArray(json) ? json : Array.isArray(json?.results) ? json.results : [];
|
|
623
|
+
}
|
|
624
|
+
export async function syncFloridayEntityBySequence(supabase, env, connectionId, opts) {
|
|
625
|
+
const t0 = Date.now();
|
|
626
|
+
const { data: stateRow } = await supabase
|
|
627
|
+
.from("floriday_sync_state")
|
|
628
|
+
.select("last_sequence_number")
|
|
629
|
+
.eq("connection_id", connectionId)
|
|
630
|
+
.eq("entity_type", opts.entityType)
|
|
631
|
+
.maybeSingle();
|
|
632
|
+
const startCursor = stateRow?.last_sequence_number ?? 0;
|
|
633
|
+
try {
|
|
634
|
+
// Debug-logging: helpt 401's onderscheid maken tussen scope-issue,
|
|
635
|
+
// expired token en wrong env. Token zelf wordt NIET gelogd.
|
|
636
|
+
try {
|
|
637
|
+
const { data: connRow } = await supabase
|
|
638
|
+
.from("floriday_connections")
|
|
639
|
+
.select("token_expires_at")
|
|
640
|
+
.eq("id", connectionId)
|
|
641
|
+
.maybeSingle();
|
|
642
|
+
console.log("[floriday-sync] resource=%s connection_id=%s scopes=%s expires=%s", opts.entityType, connectionId, DEFAULT_SCOPE, connRow?.token_expires_at ?? "n/a");
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
/* logging mag nooit de sync breken */
|
|
646
|
+
}
|
|
647
|
+
const maxSeq = await fetchMaxSequence(supabase, env, opts.maxSequenceEndpoint);
|
|
648
|
+
let cursor = startCursor;
|
|
649
|
+
let totalSynced = 0;
|
|
650
|
+
let safety = 0;
|
|
651
|
+
while (cursor < maxSeq && safety < 1000) {
|
|
652
|
+
safety++;
|
|
653
|
+
const batch = await fetchBySequence(supabase, env, opts.bySequenceEndpointBase, cursor);
|
|
654
|
+
if (batch.length === 0)
|
|
655
|
+
break;
|
|
656
|
+
const highest = batch.reduce((m, item) => {
|
|
657
|
+
const s = Number(item?.sequenceNumber ?? item?.sequence_number ?? 0);
|
|
658
|
+
return s > m ? s : m;
|
|
659
|
+
}, cursor);
|
|
660
|
+
await opts.onBatch(batch, highest);
|
|
661
|
+
totalSynced += batch.length;
|
|
662
|
+
if (highest <= cursor)
|
|
663
|
+
break;
|
|
664
|
+
cursor = highest;
|
|
665
|
+
if (batch.length < SEQUENCE_BATCH_CAP)
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
await supabase
|
|
669
|
+
.from("floriday_sync_state")
|
|
670
|
+
.upsert({
|
|
671
|
+
connection_id: connectionId,
|
|
672
|
+
entity_type: opts.entityType,
|
|
673
|
+
last_sequence_number: cursor,
|
|
674
|
+
last_synced_at: new Date().toISOString(),
|
|
675
|
+
last_error: null,
|
|
676
|
+
});
|
|
677
|
+
return {
|
|
678
|
+
synced: totalSynced,
|
|
679
|
+
fromSequence: startCursor,
|
|
680
|
+
toSequence: cursor,
|
|
681
|
+
durationMs: Date.now() - t0,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
catch (e) {
|
|
685
|
+
const msg = e?.message ?? String(e);
|
|
686
|
+
await supabase
|
|
687
|
+
.from("floriday_sync_state")
|
|
688
|
+
.upsert({
|
|
689
|
+
connection_id: connectionId,
|
|
690
|
+
entity_type: opts.entityType,
|
|
691
|
+
last_sequence_number: startCursor,
|
|
692
|
+
last_error: msg.slice(0, 500),
|
|
693
|
+
});
|
|
694
|
+
throw e;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
// ─── Connections sync (productie) ───────────────────────────────────────────
|
|
698
|
+
// In-memory cache voor organisatienamen — TTL 24u, per Worker-instance.
|
|
699
|
+
// Voorkomt dat we voor elke connection-sync opnieuw /organizations/{id} aanroepen.
|
|
700
|
+
const ORG_NAME_TTL_MS = 24 * 60 * 60 * 1000;
|
|
701
|
+
const orgNameCache = new Map();
|
|
702
|
+
async function fetchBuyerOrganizationName(supabase, env, orgId) {
|
|
703
|
+
const key = `${env}:${orgId}`;
|
|
704
|
+
const hit = orgNameCache.get(key);
|
|
705
|
+
if (hit && Date.now() - hit.at < ORG_NAME_TTL_MS)
|
|
706
|
+
return hit.name;
|
|
707
|
+
try {
|
|
708
|
+
const res = await florydayFetch(supabase, env, `/${SUPPLIERS_API_VERSION}/organizations/${orgId}`, { cache: "no-store" });
|
|
709
|
+
if (!res.ok) {
|
|
710
|
+
const body = await res.text().catch(() => "");
|
|
711
|
+
console.error(`[floriday] organizations/${orgId} failed [${res.status}]: ${body.slice(0, 200)}`);
|
|
712
|
+
orgNameCache.set(key, { name: null, at: Date.now() });
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
const json = await res.json().catch(() => null);
|
|
716
|
+
const name = json?.name ?? json?.commercialName ?? json?.organizationName ?? null;
|
|
717
|
+
if (!name) {
|
|
718
|
+
console.error(`[floriday] organizations/${orgId} OK but no name field; keys=${Object.keys(json ?? {}).join(",")}`);
|
|
719
|
+
}
|
|
720
|
+
orgNameCache.set(key, { name, at: Date.now() });
|
|
721
|
+
return name;
|
|
722
|
+
}
|
|
723
|
+
catch (e) {
|
|
724
|
+
console.error(`[floriday] organizations/${orgId} threw: ${e?.message ?? e}`);
|
|
725
|
+
orgNameCache.set(key, { name: null, at: Date.now() });
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
export async function runFloridayConnectionsSync(supabase, env) {
|
|
730
|
+
const row = await getActiveFlorydayCredentials(supabase, env);
|
|
731
|
+
if (!row)
|
|
732
|
+
throw new Error(`Geen actieve Floriday-koppeling voor ${env}`);
|
|
733
|
+
return syncFloridayEntityBySequence(supabase, env, row.id, {
|
|
734
|
+
entityType: "connections",
|
|
735
|
+
maxSequenceEndpoint: `/${SUPPLIERS_API_VERSION}/connections/current-max-sequence`,
|
|
736
|
+
bySequenceEndpointBase: `/${SUPPLIERS_API_VERSION}/connections/sync`,
|
|
737
|
+
onBatch: async (items, _highest) => {
|
|
738
|
+
const baseRows = items
|
|
739
|
+
.map((item) => {
|
|
740
|
+
const fid = item?.customerOrganizationId ?? null;
|
|
741
|
+
if (!fid)
|
|
742
|
+
return null;
|
|
743
|
+
return {
|
|
744
|
+
connection_id: row.id,
|
|
745
|
+
floriday_connection_id: String(fid),
|
|
746
|
+
sequence_number: Number(item?.sequenceNumber ?? 0),
|
|
747
|
+
buyer_organization_id: String(fid),
|
|
748
|
+
status: null,
|
|
749
|
+
is_deleted: Boolean(item?.isDeleted),
|
|
750
|
+
data: item,
|
|
751
|
+
synced_at: new Date().toISOString(),
|
|
752
|
+
};
|
|
753
|
+
})
|
|
754
|
+
.filter(Boolean);
|
|
755
|
+
if (baseRows.length === 0)
|
|
756
|
+
return;
|
|
757
|
+
// Verzamel unieke buyer_organization_ids en haal namen op (parallel, max 5 tegelijk).
|
|
758
|
+
const uniqueIds = Array.from(new Set(baseRows.map((r) => r.buyer_organization_id)));
|
|
759
|
+
const nameMap = new Map();
|
|
760
|
+
const CONCURRENCY = 5;
|
|
761
|
+
for (let i = 0; i < uniqueIds.length; i += CONCURRENCY) {
|
|
762
|
+
const slice = uniqueIds.slice(i, i + CONCURRENCY);
|
|
763
|
+
const results = await Promise.all(slice.map((id) => fetchBuyerOrganizationName(supabase, env, id)));
|
|
764
|
+
slice.forEach((id, idx) => nameMap.set(id, results[idx] ?? null));
|
|
765
|
+
}
|
|
766
|
+
const rows = baseRows.map((r) => ({
|
|
767
|
+
...r,
|
|
768
|
+
buyer_organization_name: nameMap.get(r.buyer_organization_id) ?? null,
|
|
769
|
+
}));
|
|
770
|
+
const { error } = await supabase
|
|
771
|
+
.from("floriday_connections_cache")
|
|
772
|
+
.upsert(rows, { onConflict: "connection_id,floriday_connection_id" });
|
|
773
|
+
if (error)
|
|
774
|
+
throw new Error(`cache upsert: ${error.message}`);
|
|
775
|
+
},
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
export const syncFlorydayConnections = createServerFn({ method: "POST" })
|
|
779
|
+
.middleware([requireSupabaseAuth])
|
|
780
|
+
.inputValidator((input) => {
|
|
781
|
+
if (!["staging", "live"].includes(input.environment)) {
|
|
782
|
+
throw new Error("Ongeldige environment");
|
|
783
|
+
}
|
|
784
|
+
return input;
|
|
785
|
+
})
|
|
786
|
+
.handler(async ({ data, context }) => {
|
|
787
|
+
try {
|
|
788
|
+
const result = await runFloridayConnectionsSync(context.supabase, data.environment);
|
|
789
|
+
return { ok: true, ...result };
|
|
790
|
+
}
|
|
791
|
+
catch (e) {
|
|
792
|
+
return { ok: false, error: e?.message ?? "Onbekende fout" };
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
export const getFlorydaySyncStatus = createServerFn({ method: "POST" })
|
|
796
|
+
.middleware([requireSupabaseAuth])
|
|
797
|
+
.inputValidator((input) => {
|
|
798
|
+
if (!["staging", "live"].includes(input.environment)) {
|
|
799
|
+
throw new Error("Ongeldige environment");
|
|
800
|
+
}
|
|
801
|
+
return input;
|
|
802
|
+
})
|
|
803
|
+
.handler(async ({ data, context }) => {
|
|
804
|
+
const row = await getActiveFlorydayCredentials(context.supabase, data.environment);
|
|
805
|
+
if (!row)
|
|
806
|
+
return { ok: false, error: `Geen actieve koppeling voor ${data.environment}` };
|
|
807
|
+
const { data: states } = await context.supabase
|
|
808
|
+
.from("floriday_sync_state")
|
|
809
|
+
.select("entity_type, last_sequence_number, last_synced_at, last_error")
|
|
810
|
+
.eq("connection_id", row.id);
|
|
811
|
+
const CACHE_TABLE = {
|
|
812
|
+
"connections": "floriday_connections_cache",
|
|
813
|
+
"trade-items": "floriday_trade_items_cache",
|
|
814
|
+
"sales-orders": "floriday_salesorders_cache",
|
|
815
|
+
"customer-organizations": "floriday_customers_cache",
|
|
816
|
+
"carriers": "floriday_carriers_cache",
|
|
817
|
+
};
|
|
818
|
+
const statuses = [];
|
|
819
|
+
for (const s of (states ?? [])) {
|
|
820
|
+
let cached_count = 0;
|
|
821
|
+
const table = CACHE_TABLE[s.entity_type];
|
|
822
|
+
if (table) {
|
|
823
|
+
const { count } = await context.supabase
|
|
824
|
+
.from(table)
|
|
825
|
+
.select("connection_id", { count: "exact", head: true })
|
|
826
|
+
.eq("connection_id", row.id);
|
|
827
|
+
cached_count = count ?? 0;
|
|
828
|
+
}
|
|
829
|
+
statuses.push({
|
|
830
|
+
entity_type: s.entity_type,
|
|
831
|
+
last_sequence_number: s.last_sequence_number,
|
|
832
|
+
last_synced_at: s.last_synced_at,
|
|
833
|
+
last_error: s.last_error,
|
|
834
|
+
cached_count,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
return { ok: true, statuses };
|
|
838
|
+
});
|
|
839
|
+
async function scheduleFlorydayConnectionsSyncInternal(supabase) {
|
|
840
|
+
const apikey = process.env.SUPABASE_PUBLISHABLE_KEY ?? process.env.VITE_SUPABASE_PUBLISHABLE_KEY;
|
|
841
|
+
if (!apikey)
|
|
842
|
+
throw new Error("Supabase anon key ontbreekt in server-env");
|
|
843
|
+
const url = `${resolveFlorydayCronBaseUrl()}/api/public/cron/sync-floriday-connections`;
|
|
844
|
+
const { data, error } = await supabase.rpc("schedule_floriday_connections_sync", { p_url: url, p_apikey: apikey });
|
|
845
|
+
if (error)
|
|
846
|
+
throw new Error(error.message);
|
|
847
|
+
return { jobid: data, url };
|
|
848
|
+
}
|
|
849
|
+
export const setupFlorydayConnectionsSyncCron = createServerFn({ method: "POST" })
|
|
850
|
+
.middleware([requireSupabaseAuth])
|
|
851
|
+
.handler(async ({ context }) => {
|
|
852
|
+
const { jobid, url } = await scheduleFlorydayConnectionsSyncInternal(context.supabase);
|
|
853
|
+
return { ok: true, jobid, url };
|
|
854
|
+
});
|
|
855
|
+
/**
|
|
856
|
+
* Geeft een snapshot van de geplande Floriday-cron jobs zodat de UI kan
|
|
857
|
+
* tonen welke URL is ingepland (zo zien we direct als er een verkeerde
|
|
858
|
+
* project-ID in de cron staat zoals in de oude bug).
|
|
859
|
+
*/
|
|
860
|
+
export const getFlorydayCronStatus = createServerFn({ method: "GET" })
|
|
861
|
+
.middleware([requireSupabaseAuth])
|
|
862
|
+
.handler(async ({ context }) => {
|
|
863
|
+
const expectedBase = (() => {
|
|
864
|
+
try {
|
|
865
|
+
return resolveFlorydayCronBaseUrl();
|
|
866
|
+
}
|
|
867
|
+
catch {
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
})();
|
|
871
|
+
const { data, error } = await context.supabase
|
|
872
|
+
.rpc("get_floriday_cron_jobs");
|
|
873
|
+
if (error) {
|
|
874
|
+
return { ok: false, error: error.message, expectedBase };
|
|
875
|
+
}
|
|
876
|
+
const jobs = (data ?? []).filter((job) => MANAGED_FLORYDAY_CRON_JOB_NAMES.has(job.jobname));
|
|
877
|
+
return { ok: true, jobs, expectedBase };
|
|
878
|
+
});
|
|
879
|
+
/**
|
|
880
|
+
* Herinstalleert beide Floriday-crons in één call met de correct
|
|
881
|
+
* gedetecteerde base-URL. Idempotent: bestaande jobs worden overschreven.
|
|
882
|
+
*/
|
|
883
|
+
export const reinstallFlorydayCrons = createServerFn({ method: "POST" })
|
|
884
|
+
.middleware([requireSupabaseAuth])
|
|
885
|
+
.handler(async ({ context }) => {
|
|
886
|
+
const token = await scheduleFlorydayTokenRefreshInternal(context.supabase);
|
|
887
|
+
const conn = await scheduleFlorydayConnectionsSyncInternal(context.supabase);
|
|
888
|
+
return { ok: true, token, connections: conn };
|
|
889
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowselections/floriday-authenticatie-module",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist-lib/index.js",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@flowselections/core": "^1.0.11",
|
|
39
39
|
"@eslint/js": "^9.32.0",
|
|
40
|
-
"@lovable.dev/vite-tanstack-config": "
|
|
40
|
+
"@lovable.dev/vite-tanstack-config": "2.5.3",
|
|
41
41
|
"@types/node": "^22.16.5",
|
|
42
42
|
"@types/react": "^19.2.0",
|
|
43
43
|
"@types/react-dom": "^19.2.0",
|
|
@@ -58,7 +58,8 @@
|
|
|
58
58
|
"tw-animate-css": "^1.3.4",
|
|
59
59
|
"vite-tsconfig-paths": "^6.0.2",
|
|
60
60
|
"react": "^19.2.0",
|
|
61
|
-
"react-dom": "^19.2.0"
|
|
61
|
+
"react-dom": "^19.2.0",
|
|
62
|
+
"nitro": "3.0.260603-beta"
|
|
62
63
|
},
|
|
63
64
|
"private": false,
|
|
64
65
|
"flowselections": {
|
|
@@ -66,7 +67,7 @@
|
|
|
66
67
|
"pages": [
|
|
67
68
|
{
|
|
68
69
|
"export": "FlorydayConnectionPage",
|
|
69
|
-
"route": "floriday-
|
|
70
|
+
"route": "floriday-connection",
|
|
70
71
|
"title": "Connectie"
|
|
71
72
|
}
|
|
72
73
|
]
|