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
@@ -89,8 +89,24 @@ export default function GoogleSignInButton({
|
||||
// to avoid open redirects.
|
||||
redirectAfterAuth(safeInternalPath(redirectTo, '/dashboard'));
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Google login failed';
|
||||
const displayError = locale === 'es' ? 'Error al iniciar sesion con Google' : errorMessage;
|
||||
// better-auth returns OAUTH_LINK_ERROR (message: "account not linked")
|
||||
// when it refuses to attach the Google identity to the existing user
|
||||
// row for that address. The backend now links unverified local rows
|
||||
// (see lib/betterAuth.ts accountLinking), so this should be
|
||||
// unreachable — but a bare "account not linked" toast is a dead end,
|
||||
// so keep an actionable fallback rather than a generic one.
|
||||
const isLinkError = (error as { code?: string } | null)?.code === 'OAUTH_LINK_ERROR';
|
||||
const errorMessage = isLinkError
|
||||
? 'This email is already registered. Sign in with your password, or use the "Email Link" option on the login page.'
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: 'Google login failed';
|
||||
const displayError =
|
||||
locale === 'es'
|
||||
? isLinkError
|
||||
? 'Este correo ya esta registrado. Inicia sesion con tu contrasena o usa la opcion "Enlace por correo".'
|
||||
: 'Error al iniciar sesion con Google'
|
||||
: errorMessage;
|
||||
onError?.(displayError);
|
||||
toast.error(displayError);
|
||||
} finally {
|
||||
|
||||
@@ -114,7 +114,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
idToken: { token: credential },
|
||||
});
|
||||
if (error) {
|
||||
throw new Error(messageFrom(error, 'Google login failed'));
|
||||
// Carry the code through: GoogleSignInButton turns OAUTH_LINK_ERROR into
|
||||
// something actionable instead of showing better-auth's bare
|
||||
// "account not linked".
|
||||
const err = new Error(messageFrom(error, 'Google login failed'));
|
||||
(err as Error & { code?: string }).code = error.code;
|
||||
throw err;
|
||||
}
|
||||
await refreshUser();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user