diff --git a/apps/web/lib/server-auth.ts b/apps/web/lib/server-auth.ts index 7f12ac8..d469255 100644 --- a/apps/web/lib/server-auth.ts +++ b/apps/web/lib/server-auth.ts @@ -17,6 +17,7 @@ * the existing flow alive. */ import { headers } from "next/headers"; +import { and, eq, ne, sql } from "drizzle-orm"; import { db } from "@/db"; import { user as localUser } from "@/db/schema"; @@ -63,20 +64,49 @@ export async function getServerSession(): Promise { return null; } - // Best-effort shadow upsert. Failure here doesn't kill auth — it - // just means an old user predates this code or the local DB is - // unreachable, both of which the caller can survive. + // Best-effort shadow upsert. Failure here doesn't kill auth — it just + // means an old user predates this code or the local DB is unreachable, + // both of which the caller can survive. + // + // The CP can re-issue user UUIDs (e.g. after a DB migration), leaving a + // stale local row with the SAME email under an OLD id. Because email is + // unique, a plain insert of the new id then throws on the email + // constraint and no row for the current id ever lands — which breaks + // api_keys.user_id (and other) foreign keys. So we reconcile in a + // transaction: first free the email from any *other* shadow id (rename + // it uniquely so old dependent rows keep pointing at a row that still + // exists), then upsert the current id with the real email. try { - await db - .insert(localUser) - .values({ - id: user.id, - email: user.email, - name: user.name || user.email, - emailVerified: !!user.email_verified, - image: user.avatar_url || null, - }) - .onConflictDoNothing({ target: localUser.id }); + await db.transaction(async (tx) => { + await tx + .update(localUser) + .set({ + email: sql`${localUser.email} || '.stale.' || ${localUser.id}`, + }) + .where( + and(eq(localUser.email, user.email), ne(localUser.id, user.id)), + ); + + await tx + .insert(localUser) + .values({ + id: user.id, + email: user.email, + name: user.name || user.email, + emailVerified: !!user.email_verified, + image: user.avatar_url || null, + }) + .onConflictDoUpdate({ + target: localUser.id, + set: { + email: user.email, + name: user.name || user.email, + emailVerified: !!user.email_verified, + image: user.avatar_url || null, + updatedAt: new Date(), + }, + }); + }); } catch (err) { console.warn("[server-auth] shadow user upsert failed", err); }