@brightweblabs/module-orgs 0.7.1 → 0.7.3
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/brightweb.module.json +3 -1
- package/package.json +5 -5
- package/src/handlers.ts +2 -1
- package/src/index.ts +1 -0
- package/src/invitations.ts +92 -166
package/brightweb.module.json
CHANGED
|
@@ -43,7 +43,9 @@
|
|
|
43
43
|
"touch_organization_invitations_updated_at",
|
|
44
44
|
"trg_touch_organization_invitations_updated_at",
|
|
45
45
|
"normalize_organization_invitation_email",
|
|
46
|
-
"trg_normalize_organization_invitation_email"
|
|
46
|
+
"trg_normalize_organization_invitation_email",
|
|
47
|
+
"accept_organization_invitation",
|
|
48
|
+
"assign_organization_member_atomic"
|
|
47
49
|
]
|
|
48
50
|
}
|
|
49
51
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brightweblabs/module-orgs",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.3",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
7
7
|
"files": [
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
"@supabase/supabase-js": "^2.110.8",
|
|
26
26
|
"lucide-react": "^1.26.0",
|
|
27
27
|
"server-only": "^0.0.1",
|
|
28
|
-
"@brightweblabs/app-shell": "0.16.
|
|
29
|
-
"@brightweblabs/core-auth": "0.12.
|
|
30
|
-
"@brightweblabs/
|
|
31
|
-
"@brightweblabs/
|
|
28
|
+
"@brightweblabs/app-shell": "0.16.3",
|
|
29
|
+
"@brightweblabs/core-auth": "0.12.2",
|
|
30
|
+
"@brightweblabs/ui": "1.5.6",
|
|
31
|
+
"@brightweblabs/infra": "0.7.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"react": "^19.0.0",
|
package/src/handlers.ts
CHANGED
|
@@ -25,10 +25,11 @@ import {
|
|
|
25
25
|
createOrganizationsPostHandler,
|
|
26
26
|
} from "./http";
|
|
27
27
|
|
|
28
|
-
export function createOrganizationRequestHandlers(options?: { ensureCrmContactForProfile?: EnsureCrmContact }) {
|
|
28
|
+
export function createOrganizationRequestHandlers(options?: { ensureCrmContactForProfile?: EnsureCrmContact; contactIntegration?: "database" }) {
|
|
29
29
|
const inviteMembers: typeof inviteOrganizationMembers = (supabase, organizationId, invites, actorProfileId) =>
|
|
30
30
|
inviteOrganizationMembers(supabase, organizationId, invites, actorProfileId, {
|
|
31
31
|
ensureCrmContactForProfile: options?.ensureCrmContactForProfile,
|
|
32
|
+
contactIntegration: options?.contactIntegration,
|
|
32
33
|
});
|
|
33
34
|
const writeDependencies = {
|
|
34
35
|
getCreateAccess: requireOrganizationsStaffAccess,
|
package/src/index.ts
CHANGED
package/src/invitations.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
-
import {
|
|
2
|
+
import type { OrganizationMemberRole } from "./data";
|
|
3
3
|
import { sendOrganizationInviteEmail } from "./invite-email";
|
|
4
4
|
|
|
5
5
|
const INVITE_EXPIRY_DAYS = 14;
|
|
@@ -34,6 +34,7 @@ export type OrganizationInvitationDetails = {
|
|
|
34
34
|
role: OrganizationMemberRole;
|
|
35
35
|
status: OrganizationInvitation["status"];
|
|
36
36
|
expiresAt: string;
|
|
37
|
+
acceptedByProfileId?: string | null;
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
export type OrganizationInviteSummary = {
|
|
@@ -180,7 +181,7 @@ export async function getOrganizationInvitationDetails(
|
|
|
180
181
|
): Promise<OrganizationInvitationDetails | null> {
|
|
181
182
|
const { data, error } = await supabase
|
|
182
183
|
.from("organization_invitations")
|
|
183
|
-
.select("id, organization_id, invited_email, role, status, expires_at, organizations(name)")
|
|
184
|
+
.select("id, organization_id, invited_email, role, status, expires_at, accepted_by_profile_id, organizations(name)")
|
|
184
185
|
.eq("id", invitationId)
|
|
185
186
|
.maybeSingle();
|
|
186
187
|
if (error) throw new Error(error.message);
|
|
@@ -199,6 +200,7 @@ export async function getOrganizationInvitationDetails(
|
|
|
199
200
|
role: raw.role === "admin" ? "admin" : "member",
|
|
200
201
|
status: normalizeStatus(raw.status),
|
|
201
202
|
expiresAt: String(raw.expires_at),
|
|
203
|
+
acceptedByProfileId: typeof raw.accepted_by_profile_id === "string" ? raw.accepted_by_profile_id : null,
|
|
202
204
|
};
|
|
203
205
|
}
|
|
204
206
|
|
|
@@ -264,8 +266,10 @@ export async function inviteOrganizationMembers(
|
|
|
264
266
|
options?: {
|
|
265
267
|
ensureCrmContactForProfile?: EnsureCrmContact;
|
|
266
268
|
sendInviteEmail?: SendOrganizationInvite;
|
|
269
|
+
contactIntegration?: "database";
|
|
267
270
|
},
|
|
268
271
|
): Promise<{ invitations: OrganizationInvitation[]; outcomes: OrganizationInviteOutcome[]; summary: OrganizationInviteSummary }> {
|
|
272
|
+
assertInvitationContactIntegration(options ?? {});
|
|
269
273
|
const normalized = dedupeInviteDrafts(invites);
|
|
270
274
|
const emptySummary = {
|
|
271
275
|
pendingInvitations: 0,
|
|
@@ -280,12 +284,10 @@ export async function inviteOrganizationMembers(
|
|
|
280
284
|
if (normalized.length === 0) return { invitations: [], outcomes: [], summary: emptySummary };
|
|
281
285
|
|
|
282
286
|
const emails = normalized.map((invite) => invite.email);
|
|
283
|
-
const [{ data:
|
|
284
|
-
supabase.from("organization_members").select("profile_id, role, profile:profiles!organization_members_profile_id_fkey(email)").eq("organization_id", organizationId),
|
|
287
|
+
const [{ data: profiles, error: profileError }, { data: existingInvitations, error: invitationError }] = await Promise.all([
|
|
285
288
|
supabase.from("profiles").select("id, email").in("email", emails),
|
|
286
289
|
supabase.from("organization_invitations").select("id, invited_email, role, status, expires_at").eq("organization_id", organizationId).in("invited_email", emails),
|
|
287
290
|
]);
|
|
288
|
-
if (memberError) throw new Error(memberError.message);
|
|
289
291
|
if (profileError) throw new Error(profileError.message);
|
|
290
292
|
if (invitationError) throw new Error(invitationError.message);
|
|
291
293
|
const { data: organization, error: organizationError } = await supabase
|
|
@@ -296,14 +298,6 @@ export async function inviteOrganizationMembers(
|
|
|
296
298
|
if (organizationError) throw new Error(organizationError.message);
|
|
297
299
|
const organizationName = organization?.name?.trim() || "Organização";
|
|
298
300
|
|
|
299
|
-
const memberByEmail = new Map<string, { profileId: string; role: OrganizationMemberRole }>();
|
|
300
|
-
for (const row of existingMembers ?? []) {
|
|
301
|
-
const rawProfile = Array.isArray(row.profile) ? row.profile[0] ?? null : row.profile;
|
|
302
|
-
const email = rawProfile && typeof rawProfile.email === "string" ? normalizeEmail(rawProfile.email) : "";
|
|
303
|
-
if (email && typeof row.profile_id === "string") {
|
|
304
|
-
memberByEmail.set(email, { profileId: row.profile_id, role: row.role === "admin" ? "admin" : "member" });
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
301
|
const profileByEmail = new Map<string, string>();
|
|
308
302
|
for (const profile of profiles ?? []) {
|
|
309
303
|
if (typeof profile.id === "string" && typeof profile.email === "string") {
|
|
@@ -325,76 +319,38 @@ export async function inviteOrganizationMembers(
|
|
|
325
319
|
}
|
|
326
320
|
|
|
327
321
|
const pendingRows: Array<Record<string, unknown>> = [];
|
|
328
|
-
const resolvedProfilesByEmail = new Map<string, string>();
|
|
329
322
|
const outcomes: OrganizationInviteOutcome[] = [];
|
|
330
323
|
let directAssignments = 0;
|
|
331
324
|
let updatedExistingMembers = 0;
|
|
332
325
|
let unchangedExistingMembers = 0;
|
|
333
326
|
for (const invite of normalized) {
|
|
334
|
-
const member = memberByEmail.get(invite.email);
|
|
335
|
-
if (member) {
|
|
336
|
-
if (member.role === invite.role) {
|
|
337
|
-
if (options?.ensureCrmContactForProfile) {
|
|
338
|
-
const linked = await options.ensureCrmContactForProfile(member.profileId, {
|
|
339
|
-
source: "organization_member_direct_access", organizationId, serviceClient: supabase,
|
|
340
|
-
});
|
|
341
|
-
if (!linked.success) {
|
|
342
|
-
outcomes.push({ email: invite.email, role: invite.role, status: "api_failed", profileId: member.profileId, message: "Não foi possível ligar o contacto CRM.", failureKind: "crm_link" });
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
unchangedExistingMembers += 1;
|
|
347
|
-
outcomes.push({ email: invite.email, role: invite.role, status: "already_member", profileId: member.profileId });
|
|
348
|
-
}
|
|
349
|
-
else {
|
|
350
|
-
const { error } = await supabase.from("organization_members").update({ role: invite.role })
|
|
351
|
-
.eq("organization_id", organizationId).eq("profile_id", member.profileId);
|
|
352
|
-
if (error) {
|
|
353
|
-
outcomes.push({ email: invite.email, role: invite.role, status: "api_failed", profileId: member.profileId, message: "Não foi possível atualizar a função do membro.", failureKind: "membership" });
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
if (options?.ensureCrmContactForProfile) {
|
|
357
|
-
const linked = await options.ensureCrmContactForProfile(member.profileId, {
|
|
358
|
-
source: "organization_member_direct_access", organizationId, serviceClient: supabase,
|
|
359
|
-
});
|
|
360
|
-
if (!linked.success) {
|
|
361
|
-
const { error: rollbackError } = await supabase.from("organization_members").update({ role: member.role })
|
|
362
|
-
.eq("organization_id", organizationId).eq("profile_id", member.profileId);
|
|
363
|
-
outcomes.push({ email: invite.email, role: invite.role, status: "api_failed", profileId: member.profileId, message: rollbackError ? "Não foi possível ligar o contacto CRM nem reverter a alteração de função; é necessária reconciliação." : "Não foi possível ligar o contacto CRM; a alteração de função foi revertida.", failureKind: "crm_link" });
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
updatedExistingMembers += 1;
|
|
368
|
-
outcomes.push({ email: invite.email, role: invite.role, status: "membership_updated", profileId: member.profileId });
|
|
369
|
-
}
|
|
370
|
-
resolvedProfilesByEmail.set(invite.email, member.profileId);
|
|
371
|
-
continue;
|
|
372
|
-
}
|
|
373
327
|
const profileId = profileByEmail.get(invite.email);
|
|
374
328
|
if (profileId) {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
329
|
+
// The database resolves current membership under lock and rolls back only
|
|
330
|
+
// this transaction's writes. Never compensate from an earlier snapshot.
|
|
331
|
+
const { data, error } = await supabase.rpc("assign_organization_member_atomic", {
|
|
332
|
+
p_organization_id: organizationId,
|
|
333
|
+
p_profile_id: profileId,
|
|
334
|
+
p_email: invite.email,
|
|
335
|
+
p_role: invite.role,
|
|
336
|
+
p_actor_profile_id: actorProfileId,
|
|
337
|
+
});
|
|
338
|
+
const status = data?.status;
|
|
339
|
+
if (error || !["immediate_access", "membership_updated", "already_member"].includes(status)) {
|
|
340
|
+
const contactFailure = error?.code === "BW001";
|
|
341
|
+
outcomes.push({
|
|
342
|
+
email: invite.email, role: invite.role, status: "api_failed", profileId,
|
|
343
|
+
message: contactFailure
|
|
344
|
+
? "Não foi possível ligar o contacto CRM. Nenhuma alteração de acesso foi guardada."
|
|
345
|
+
: "Não foi possível confirmar a alteração de acesso à organização. Tente novamente.",
|
|
346
|
+
failureKind: contactFailure ? "crm_link" : "membership",
|
|
387
347
|
});
|
|
388
|
-
|
|
389
|
-
const { error: rollbackError } = await supabase.from("organization_members").delete()
|
|
390
|
-
.eq("organization_id", organizationId).eq("profile_id", profileId);
|
|
391
|
-
outcomes.push({ email: invite.email, role: invite.role, status: "api_failed", profileId, message: rollbackError ? "Não foi possível ligar o contacto CRM nem reverter o acesso; é necessária reconciliação." : "Não foi possível ligar o contacto CRM; o acesso não foi mantido.", failureKind: "crm_link" });
|
|
392
|
-
continue;
|
|
393
|
-
}
|
|
348
|
+
continue;
|
|
394
349
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
350
|
+
if (status === "immediate_access") directAssignments += 1;
|
|
351
|
+
else if (status === "membership_updated") updatedExistingMembers += 1;
|
|
352
|
+
else unchangedExistingMembers += 1;
|
|
353
|
+
outcomes.push({ email: invite.email, role: invite.role, status, profileId });
|
|
398
354
|
continue;
|
|
399
355
|
}
|
|
400
356
|
const pendingInvitation = pendingInvitationByEmail.get(invite.email);
|
|
@@ -416,21 +372,6 @@ export async function inviteOrganizationMembers(
|
|
|
416
372
|
});
|
|
417
373
|
}
|
|
418
374
|
|
|
419
|
-
if (directAssignments > 0 || updatedExistingMembers > 0) {
|
|
420
|
-
try { await syncOrganizationPrimaryContactFromAdmins(supabase, organizationId); } catch { /* Retry on the next idempotent member operation. */ }
|
|
421
|
-
}
|
|
422
|
-
if (resolvedProfilesByEmail.size > 0) {
|
|
423
|
-
const acceptedAt = new Date().toISOString();
|
|
424
|
-
await Promise.all(Array.from(resolvedProfilesByEmail, async ([email, profileId]) => {
|
|
425
|
-
const { error } = await supabase.from("organization_invitations").update({
|
|
426
|
-
status: "accepted",
|
|
427
|
-
accepted_at: acceptedAt,
|
|
428
|
-
accepted_by_profile_id: profileId,
|
|
429
|
-
}).eq("organization_id", organizationId).eq("invited_email", email).eq("status", "pending");
|
|
430
|
-
// Access is already effective. Leave a pending row for the next idempotent call to reconcile.
|
|
431
|
-
if (error) return;
|
|
432
|
-
}));
|
|
433
|
-
}
|
|
434
375
|
if (pendingRows.length > 0) {
|
|
435
376
|
const { error } = await supabase.from("organization_invitations").upsert(pendingRows, {
|
|
436
377
|
onConflict: "organization_id,invited_email",
|
|
@@ -467,22 +408,24 @@ export async function inviteOrganizationMembers(
|
|
|
467
408
|
}),
|
|
468
409
|
})));
|
|
469
410
|
const failedIds = deliveries.filter((result) => !result.delivered).map((result) => result.id);
|
|
411
|
+
let cleanupFailed = false;
|
|
470
412
|
if (failedIds.length > 0) {
|
|
471
413
|
const { error: deleteError } = await supabase
|
|
472
414
|
.from("organization_invitations")
|
|
473
415
|
.delete()
|
|
474
416
|
.in("id", failedIds)
|
|
475
417
|
.eq("status", "pending");
|
|
476
|
-
|
|
477
|
-
void deleteError;
|
|
418
|
+
cleanupFailed = Boolean(deleteError);
|
|
478
419
|
}
|
|
479
420
|
for (const delivery of deliveries) {
|
|
480
421
|
outcomes.push({
|
|
481
422
|
email: delivery.email,
|
|
482
423
|
role: delivery.role,
|
|
483
424
|
status: delivery.delivered ? "pending_invitation" : "email_failed",
|
|
484
|
-
invitationId: delivery.delivered ? delivery.id : undefined,
|
|
485
|
-
message: delivery.delivered ? undefined :
|
|
425
|
+
invitationId: delivery.delivered || cleanupFailed ? delivery.id : undefined,
|
|
426
|
+
message: delivery.delivered ? undefined : cleanupFailed
|
|
427
|
+
? "Não foi possível enviar o email. O convite pendente foi mantido; reenvie-o ou revogue-o."
|
|
428
|
+
: ORGANIZATION_INVITE_EMAIL_DELIVERY_ERROR,
|
|
486
429
|
});
|
|
487
430
|
}
|
|
488
431
|
}
|
|
@@ -572,61 +515,22 @@ export async function acceptOrganizationInvitation(
|
|
|
572
515
|
invitationId: string;
|
|
573
516
|
profileId: string;
|
|
574
517
|
userEmail: string;
|
|
575
|
-
ensureCrmContactForProfile
|
|
518
|
+
ensureCrmContactForProfile?: EnsureCrmContact;
|
|
519
|
+
contactIntegration?: "database";
|
|
576
520
|
},
|
|
577
521
|
): Promise<{ organizationId: string }> {
|
|
578
|
-
|
|
579
|
-
const { data
|
|
580
|
-
.
|
|
581
|
-
.
|
|
582
|
-
|
|
583
|
-
.maybeSingle<{ id: string; organization_id: string; invited_email: string; role: string; status: string; expires_at: string }>();
|
|
584
|
-
if (error) throw new Error(error.message);
|
|
585
|
-
if (!invitation?.id) throw new Error("Convite não encontrado.");
|
|
586
|
-
if (normalizeEmail(invitation.invited_email) !== email) throw new Error("Este convite pertence a outro email.");
|
|
587
|
-
if (invitation.status !== "pending") throw new Error("Este convite já não está disponível.");
|
|
588
|
-
if (isInvitationExpired(invitation.expires_at)) {
|
|
589
|
-
const { error: expireError } = await supabase.from("organization_invitations")
|
|
590
|
-
.update({ status: "expired" }).eq("id", invitation.id);
|
|
591
|
-
if (expireError) throw new Error(expireError.message);
|
|
592
|
-
throw new Error("Este convite expirou.");
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
const acceptedContact = await params.ensureCrmContactForProfile(params.profileId, {
|
|
596
|
-
source: "organization_invitation_accept",
|
|
597
|
-
organizationId: invitation.organization_id,
|
|
598
|
-
serviceClient: supabase,
|
|
522
|
+
assertInvitationContactIntegration(params);
|
|
523
|
+
const { data, error } = await supabase.rpc("accept_organization_invitation", {
|
|
524
|
+
p_invitation_id: params.invitationId,
|
|
525
|
+
p_profile_id: params.profileId,
|
|
526
|
+
p_user_email: normalizeEmail(params.userEmail),
|
|
599
527
|
});
|
|
600
|
-
if (
|
|
601
|
-
|
|
528
|
+
if (error) throw new Error(error.message);
|
|
529
|
+
if (data?.status === "expired") throw new Error("Este convite expirou.");
|
|
530
|
+
if (data?.status !== "accepted" || typeof data.organizationId !== "string") {
|
|
531
|
+
throw new Error("Não foi possível confirmar a aceitação do convite.");
|
|
602
532
|
}
|
|
603
|
-
|
|
604
|
-
const { error: memberError } = await supabase.from("organization_members").upsert({
|
|
605
|
-
organization_id: invitation.organization_id,
|
|
606
|
-
profile_id: params.profileId,
|
|
607
|
-
role,
|
|
608
|
-
}, { onConflict: "organization_id,profile_id" });
|
|
609
|
-
if (memberError) throw new Error(memberError.message);
|
|
610
|
-
if (role === "admin") await syncOrganizationPrimaryContactFromAdmins(supabase, invitation.organization_id);
|
|
611
|
-
const { error: updateError } = await supabase.from("organization_invitations").update({
|
|
612
|
-
status: "accepted",
|
|
613
|
-
accepted_at: new Date().toISOString(),
|
|
614
|
-
accepted_by_profile_id: params.profileId,
|
|
615
|
-
accepted_contact_id: acceptedContact.contactId ?? null,
|
|
616
|
-
}).eq("id", invitation.id);
|
|
617
|
-
if (updateError) throw new Error(updateError.message);
|
|
618
|
-
await logOrganizationActivity(supabase, {
|
|
619
|
-
actorProfileId: params.profileId,
|
|
620
|
-
organizationId: invitation.organization_id,
|
|
621
|
-
eventType: "crm_organization_invitation_accepted",
|
|
622
|
-
summary: "Convite de organização aceite.",
|
|
623
|
-
payload: {
|
|
624
|
-
invitation_id: invitation.id,
|
|
625
|
-
organization_id: invitation.organization_id,
|
|
626
|
-
email,
|
|
627
|
-
},
|
|
628
|
-
});
|
|
629
|
-
return { organizationId: invitation.organization_id };
|
|
533
|
+
return { organizationId: data.organizationId };
|
|
630
534
|
}
|
|
631
535
|
|
|
632
536
|
export async function registerUserFromOrganizationInvitation(
|
|
@@ -636,13 +540,26 @@ export async function registerUserFromOrganizationInvitation(
|
|
|
636
540
|
firstName: string;
|
|
637
541
|
lastName: string;
|
|
638
542
|
password: string;
|
|
639
|
-
ensureCrmContactForProfile
|
|
543
|
+
ensureCrmContactForProfile?: EnsureCrmContact;
|
|
544
|
+
contactIntegration?: "database";
|
|
640
545
|
},
|
|
641
546
|
): Promise<{ email: string; organizationId: string }> {
|
|
547
|
+
assertInvitationContactIntegration(params);
|
|
642
548
|
const invitation = await getOrganizationInvitationDetails(supabase, params.invitationId);
|
|
643
549
|
if (!invitation) throw new Error("INVITATION_NOT_FOUND");
|
|
644
|
-
if (invitation.status !== "pending") throw new Error("INVITATION_NOT_AVAILABLE");
|
|
645
|
-
if (
|
|
550
|
+
if (invitation.status !== "pending" && invitation.status !== "accepted") throw new Error("INVITATION_NOT_AVAILABLE");
|
|
551
|
+
if (invitation.status === "accepted") {
|
|
552
|
+
if (!invitation.acceptedByProfileId) throw new Error("INVITATION_NOT_AVAILABLE");
|
|
553
|
+
const accepted = await acceptOrganizationInvitation(supabase, {
|
|
554
|
+
invitationId: invitation.id,
|
|
555
|
+
profileId: invitation.acceptedByProfileId,
|
|
556
|
+
userEmail: invitation.invitedEmail,
|
|
557
|
+
ensureCrmContactForProfile: params.ensureCrmContactForProfile,
|
|
558
|
+
contactIntegration: params.contactIntegration,
|
|
559
|
+
});
|
|
560
|
+
return { email: invitation.invitedEmail, ...accepted };
|
|
561
|
+
}
|
|
562
|
+
if (invitation.status === "pending" && isInvitationExpired(invitation.expiresAt)) {
|
|
646
563
|
const { error } = await supabase.from("organization_invitations")
|
|
647
564
|
.update({ status: "expired" }).eq("id", invitation.id).eq("status", "pending");
|
|
648
565
|
if (error) throw new Error(error.message);
|
|
@@ -658,31 +575,40 @@ export async function registerUserFromOrganizationInvitation(
|
|
|
658
575
|
user_metadata: { first_name: firstName || null, last_name: lastName || null },
|
|
659
576
|
});
|
|
660
577
|
let userId = data.user?.id ?? null;
|
|
661
|
-
let createdUserId = userId;
|
|
662
578
|
if (error) {
|
|
663
579
|
if (!isExistingAccountError(error)) throw new Error(error.message);
|
|
664
580
|
userId = await findAuthUserIdByEmail(supabase, invitation.invitedEmail);
|
|
665
581
|
if (!userId) throw new Error("ACCOUNT_ALREADY_EXISTS");
|
|
666
|
-
createdUserId = null;
|
|
667
582
|
}
|
|
668
583
|
if (!userId) throw new Error("Não foi possível criar utilizador.");
|
|
669
584
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
585
|
+
// A failed/unknown RPC response must not delete an identity whose acceptance
|
|
586
|
+
// may have committed. The next registration attempt resolves the same account.
|
|
587
|
+
const profile = await ensureInvitationProfile(supabase, {
|
|
588
|
+
userId,
|
|
589
|
+
email: invitation.invitedEmail,
|
|
590
|
+
firstName,
|
|
591
|
+
lastName,
|
|
592
|
+
});
|
|
593
|
+
const accepted = await acceptOrganizationInvitation(supabase, {
|
|
594
|
+
invitationId: invitation.id,
|
|
595
|
+
profileId: profile.id,
|
|
596
|
+
userEmail: invitation.invitedEmail,
|
|
597
|
+
ensureCrmContactForProfile: params.ensureCrmContactForProfile,
|
|
598
|
+
contactIntegration: params.contactIntegration,
|
|
599
|
+
});
|
|
600
|
+
return { email: invitation.invitedEmail, ...accepted };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** Marks a stock callback whose invitation behavior is implemented by the shipped SQL hook. */
|
|
604
|
+
export const DATABASE_INVITATION_CONTACT_INTEGRATION = Symbol.for("brightweb.orgs.database-invitation-contact-integration.v1");
|
|
605
|
+
|
|
606
|
+
function assertInvitationContactIntegration(params: {
|
|
607
|
+
ensureCrmContactForProfile?: EnsureCrmContact;
|
|
608
|
+
contactIntegration?: "database";
|
|
609
|
+
}): void {
|
|
610
|
+
if (params.ensureCrmContactForProfile && params.contactIntegration !== "database" &&
|
|
611
|
+
Reflect.get(params.ensureCrmContactForProfile, DATABASE_INVITATION_CONTACT_INTEGRATION) !== true) {
|
|
612
|
+
throw new Error("INVITATION_CONTACT_INTEGRATION_MIGRATION_REQUIRED");
|
|
686
613
|
}
|
|
687
|
-
return { email: invitation.invitedEmail, organizationId: invitation.organizationId };
|
|
688
614
|
}
|