Fix Google sign-in for existing email and ticket-buyer accounts.
Google sign-in only worked for people who already had a linked google row in auth_accounts. Anyone who first appeared another way -- a guest ticket purchase, or an email/password signup made after the Better Auth migration -- got a 401 "account not linked". trustedProviders: ['google'] defeats only one of better-auth's two linking gates. The second, requireLocalEmailVerified, defaults to true and refuses the link whenever the LOCAL users.email_verified is false, independently of whether the provider is trusted. That flag is false for every guest-booking row and for every post-migration signup, since requireEmailVerification is off and no verification mail is sent. Turn that gate off: the Google id_token is signature-verified against Google's JWKS with issuer/audience/max-age checks and carries its own email_verified, so the local column proves nothing extra here. Linking alone was not enough. getAuthUser() rejects any session whose user is not 'active', so a ticket buyer would link Google, receive a cookie, and still look logged out. A databaseHooks.account.create.after hook now promotes unclaimed rows to claimed/active when a google account is attached, scoped in the WHERE clause so a suspended account is never reactivated this way. Also normalize users.email. The unique index is case-sensitive while better-auth lowercases every lookup, so someone who booked as John@Gmail.com was invisible to sign-in and Google minted a SECOND user row, stranding their tickets on the first. normalizeEmail() covers the find-or-create sites in tickets.ts and door.ts plus the claim-eligibility lookup, and an idempotent migration lowercases existing rows -- skipping any that would collide and reporting those for manual merge, since merging two people's tickets and payments is not a migration's call. tickets.attendeeEmail still stores the address exactly as typed. Tests drive the real signInSocial id-token path with Google stubbed by signing tokens with a throwaway RS256 key and serving our own JWKS, so the actual verification runs without network or credentials. That also makes the deprecation risk loud: requireLocalEmailVerified is marked for removal upstream, and an upgrade that drops it now fails CI instead of silently locking ticket buyers out again. Frontend carries error.code through so OAUTH_LINK_ERROR renders an actionable message in both locales rather than a bare "account not linked". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3f7b2d51db
commit
87bf9a6151
@@ -5,7 +5,7 @@ import { eq } from 'drizzle-orm';
|
||||
import { auth } from '../lib/betterAuth.js';
|
||||
import { validatePassword } from '../lib/passwordPolicy.js';
|
||||
import { db, dbGet, users } from '../db/index.js';
|
||||
import { getNow, toDbBool } from '../lib/utils.js';
|
||||
import { getNow, toDbBool, normalizeEmail } from '../lib/utils.js';
|
||||
import { rateLimitMiddleware } from '../lib/rateLimit.js';
|
||||
|
||||
// Custom auth flows that Better Auth doesn't provide out of the box. Mounted
|
||||
@@ -83,8 +83,9 @@ authExt.get('/claim-eligibility', authExtRateLimit, async (c) => {
|
||||
return c.json({ canClaim: false });
|
||||
}
|
||||
|
||||
// Normalized to match how the row is stored (see lib/utils.ts normalizeEmail)
|
||||
const user = await dbGet<any>(
|
||||
(db as any).select().from(users).where(eq((users as any).email, email))
|
||||
(db as any).select().from(users).where(eq((users as any).email, normalizeEmail(email)))
|
||||
);
|
||||
|
||||
const canClaim = !!user && !user.banned && user.accountStatus !== 'suspended'
|
||||
|
||||
Reference in New Issue
Block a user