diff --git a/actions/emailAuth.tsx b/actions/emailAuth.tsx index da48b984..806e0887 100644 --- a/actions/emailAuth.tsx +++ b/actions/emailAuth.tsx @@ -11,6 +11,7 @@ import { pool } from "supabase/pool"; import { supabaseServerClient } from "supabase/serverClient"; import { LeafletConfirmEmail } from "emails/leafletConfirmEmail"; import { sendConfirmationEmail } from "src/utils/confirmationEmail"; +import { linkOrphanedEmailSubscribers } from "src/utils/linkOrphanedEmailSubscribers"; async function sendAuthCode(email: string, code: string) { await sendConfirmationEmail({ @@ -113,6 +114,8 @@ export async function confirmEmailAuthToken(tokenId: string, code: string) { identityID = identity.id; } + await linkOrphanedEmailSubscribers(identityID, token.email); + const [confirmedToken] = await db .update(email_auth_tokens) .set({ diff --git a/actions/login.ts b/actions/login.ts index 52c493c5..ff909cf8 100644 --- a/actions/login.ts +++ b/actions/login.ts @@ -11,6 +11,7 @@ import { cookies } from "next/headers"; import { redirect } from "next/navigation"; import { pool } from "supabase/pool"; import { supabaseServerClient } from "supabase/serverClient"; +import { linkOrphanedEmailSubscribers } from "src/utils/linkOrphanedEmailSubscribers"; export async function loginWithEmailToken( localLeaflets: { token: { id: string }; added_at: string }[], @@ -51,6 +52,7 @@ export async function loginWithEmailToken( .where(eq(identities.email, token.email)); let identity = existingIdentity; + let newlyOwnedEmail = false; if (!existingIdentity) { let identityCookie = (await cookies()).get("identity"); if (identityCookie) { @@ -69,6 +71,7 @@ export async function loginWithEmailToken( .set({ email: token.email }) .where(eq(identities.id, existingIdentityFromCookie.id)); identity = existingIdentityFromCookie; + newlyOwnedEmail = true; } } else { const { data: newIdentity } = await supabaseServerClient @@ -77,9 +80,14 @@ export async function loginWithEmailToken( .select() .single(); identity = newIdentity!; + newlyOwnedEmail = true; } } + if (newlyOwnedEmail && identity) { + await linkOrphanedEmailSubscribers(identity.id, token.email); + } + await tx .update(email_auth_tokens) .set({ identity: identity.id }) diff --git a/actions/publications/subscribeEmail.tsx b/actions/publications/subscribeEmail.tsx index 3b63096f..b5b5037f 100644 --- a/actions/publications/subscribeEmail.tsx +++ b/actions/publications/subscribeEmail.tsx @@ -24,6 +24,7 @@ import { } from "app/lish/subscribeToPublication"; import type { OAuthSessionError } from "src/atproto-oauth"; import { normalizePublicationRecord } from "src/utils/normalizeRecords"; +import { linkOrphanedEmailSubscribers } from "src/utils/linkOrphanedEmailSubscribers"; type RequestError = | "invalid_email" @@ -367,6 +368,7 @@ async function linkEmailToCurrentIdentity( console.error("[subscribeEmail] attach email failed:", error); return Err("database_error"); } + await linkOrphanedEmailSubscribers(current.id, email); await backfillAtprotoSubscriptionsForIdentity(current.id, current.atp_did); return Ok(current.id); } @@ -398,6 +400,10 @@ async function ensureAuthTokenForEmail(email: string): Promise { return null; } + // Cover any sibling subscriber rows (e.g. CSV-imported entries on other + // publications) so this confirmation also adopts them under the new identity. + await linkOrphanedEmailSubscribers(identity.id, email); + const { data: token, error: tokenError } = await supabaseServerClient .from("email_auth_tokens") .insert({ diff --git a/src/utils/linkOrphanedEmailSubscribers.ts b/src/utils/linkOrphanedEmailSubscribers.ts new file mode 100644 index 00000000..704ce902 --- /dev/null +++ b/src/utils/linkOrphanedEmailSubscribers.ts @@ -0,0 +1,25 @@ +import { supabaseServerClient } from "supabase/serverClient"; + +// Link any pre-existing email-subscriber rows for `email` to `identityId` if +// they're not already linked. Call this whenever an identity newly comes to +// own an email so that imported / pre-confirmation-flow subscribers become +// visible to their owner. Idempotent — safe to call on every signup/login. +export async function linkOrphanedEmailSubscribers( + identityId: string, + email: string, +) { + const normalized = email.trim().toLowerCase(); + if (!normalized) return; + const { error } = await supabaseServerClient + .from("publication_email_subscribers") + .update({ identity_id: identityId }) + .eq("email", normalized) + .is("identity_id", null); + if (error) { + console.error( + "[linkOrphanedEmailSubscribers] failed:", + error.message, + { identityId, email: normalized }, + ); + } +}