|
17 | 17 | * the existing flow alive. |
18 | 18 | */ |
19 | 19 | import { headers } from "next/headers"; |
| 20 | +import { and, eq, ne, sql } from "drizzle-orm"; |
20 | 21 | import { db } from "@/db"; |
21 | 22 | import { user as localUser } from "@/db/schema"; |
22 | 23 |
|
@@ -63,20 +64,49 @@ export async function getServerSession(): Promise<ServerSession | null> { |
63 | 64 | return null; |
64 | 65 | } |
65 | 66 |
|
66 | | - // Best-effort shadow upsert. Failure here doesn't kill auth — it |
67 | | - // just means an old user predates this code or the local DB is |
68 | | - // unreachable, both of which the caller can survive. |
| 67 | + // Best-effort shadow upsert. Failure here doesn't kill auth — it just |
| 68 | + // means an old user predates this code or the local DB is unreachable, |
| 69 | + // both of which the caller can survive. |
| 70 | + // |
| 71 | + // The CP can re-issue user UUIDs (e.g. after a DB migration), leaving a |
| 72 | + // stale local row with the SAME email under an OLD id. Because email is |
| 73 | + // unique, a plain insert of the new id then throws on the email |
| 74 | + // constraint and no row for the current id ever lands — which breaks |
| 75 | + // api_keys.user_id (and other) foreign keys. So we reconcile in a |
| 76 | + // transaction: first free the email from any *other* shadow id (rename |
| 77 | + // it uniquely so old dependent rows keep pointing at a row that still |
| 78 | + // exists), then upsert the current id with the real email. |
69 | 79 | try { |
70 | | - await db |
71 | | - .insert(localUser) |
72 | | - .values({ |
73 | | - id: user.id, |
74 | | - email: user.email, |
75 | | - name: user.name || user.email, |
76 | | - emailVerified: !!user.email_verified, |
77 | | - image: user.avatar_url || null, |
78 | | - }) |
79 | | - .onConflictDoNothing({ target: localUser.id }); |
| 80 | + await db.transaction(async (tx) => { |
| 81 | + await tx |
| 82 | + .update(localUser) |
| 83 | + .set({ |
| 84 | + email: sql`${localUser.email} || '.stale.' || ${localUser.id}`, |
| 85 | + }) |
| 86 | + .where( |
| 87 | + and(eq(localUser.email, user.email), ne(localUser.id, user.id)), |
| 88 | + ); |
| 89 | + |
| 90 | + await tx |
| 91 | + .insert(localUser) |
| 92 | + .values({ |
| 93 | + id: user.id, |
| 94 | + email: user.email, |
| 95 | + name: user.name || user.email, |
| 96 | + emailVerified: !!user.email_verified, |
| 97 | + image: user.avatar_url || null, |
| 98 | + }) |
| 99 | + .onConflictDoUpdate({ |
| 100 | + target: localUser.id, |
| 101 | + set: { |
| 102 | + email: user.email, |
| 103 | + name: user.name || user.email, |
| 104 | + emailVerified: !!user.email_verified, |
| 105 | + image: user.avatar_url || null, |
| 106 | + updatedAt: new Date(), |
| 107 | + }, |
| 108 | + }); |
| 109 | + }); |
80 | 110 | } catch (err) { |
81 | 111 | console.warn("[server-auth] shadow user upsert failed", err); |
82 | 112 | } |
|
0 commit comments