Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 43 additions & 13 deletions apps/web/lib/server-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -63,20 +64,49 @@ export async function getServerSession(): Promise<ServerSession | null> {
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,
Comment on lines +101 to +105

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Avoid clearing optional CP fields during refresh.

Line 104 and Line 105 turn omitted email_verified / avatar_url values into false / null, so an existing local row can lose stored verification or avatar data on any session reconciliation. Only update those fields when the CP response includes them.

Proposed fix
           set: {
             email: user.email,
             name: user.name || user.email,
-            emailVerified: !!user.email_verified,
-            image: user.avatar_url || null,
+            ...(typeof user.email_verified === "boolean"
+              ? { emailVerified: user.email_verified }
+              : {}),
+            ...(user.avatar_url !== undefined
+              ? { image: user.avatar_url || null }
+              : {}),
             updatedAt: new Date(),
           },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
set: {
email: user.email,
name: user.name || user.email,
emailVerified: !!user.email_verified,
image: user.avatar_url || null,
set: {
email: user.email,
name: user.name || user.email,
...(typeof user.email_verified === "boolean"
? { emailVerified: user.email_verified }
: {}),
...(user.avatar_url !== undefined
? { image: user.avatar_url || null }
: {}),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/server-auth.ts` around lines 101 - 105, The session
reconciliation in server-auth.ts is clearing optional CP fields by defaulting
missing email_verified and avatar_url values to false/null, which can overwrite
existing local data. Update the set block in the auth refresh flow so the logic
around user.email_verified and user.avatar_url only assigns those fields when
the CP response actually includes them, and otherwise leaves the stored values
unchanged. Use the existing mapping in the user update path as the place to
apply this conditional behavior.

updatedAt: new Date(),
},
});
});
} catch (err) {
console.warn("[server-auth] shadow user upsert failed", err);
}
Expand Down
Loading