feat: add Sign In with Google

Adds Google OAuth alongside existing Apple and email/password auth. Follows the same patterns as Apple Sign-In: state cookie for CSRF, anonymousId migration, and user linking by email. Key differences: Google callback is a GET redirect (sameSite: lax) and uses a static client secret instead of a signed JWT.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
George Powell
2026-03-25 01:39:24 -04:00
parent 321fac9aa8
commit db04da6a2c
12 changed files with 1654 additions and 1 deletions

View File

@@ -28,7 +28,7 @@ export async function validateSessionToken(token: string) {
const [result] = await db
.select({
// Adjust user table here to tweak returned data
user: { id: table.user.id, email: table.user.email, firstName: table.user.firstName, lastName: table.user.lastName, appleId: table.user.appleId },
user: { id: table.user.id, email: table.user.email, firstName: table.user.firstName, lastName: table.user.lastName, appleId: table.user.appleId, googleId: table.user.googleId },
session: table.session
})
.from(table.session)
@@ -99,6 +99,7 @@ export async function createUser(anonymousId: string, email: string, passwordHas
email,
passwordHash,
appleId: null,
googleId: null,
firstName: firstName || null,
lastName: lastName || null,
isPrivate: false
@@ -117,6 +118,11 @@ export async function getUserByAppleId(appleId: string) {
return user || null;
}
export async function getUserByGoogleId(googleId: string) {
const [user] = await db.select().from(table.user).where(eq(table.user.googleId, googleId));
return user || null;
}
export async function migrateAnonymousStats(anonymousId: string | undefined, userId: string) {
if (!anonymousId || anonymousId === userId) return;