import { redirect, error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { exchangeGoogleCode, decodeGoogleIdToken } from '$lib/server/google-auth'; import * as auth from '$lib/server/auth'; import { db } from '$lib/server/db'; import { user as userTable } from '$lib/server/db/schema'; import { eq } from 'drizzle-orm'; export const GET: RequestHandler = async ({ url, cookies }) => { const code = url.searchParams.get('code'); const state = url.searchParams.get('state'); const errorParam = url.searchParams.get('error'); // User denied access if (errorParam) { redirect(302, '/'); } const storedRaw = cookies.get('google_oauth_state'); if (!storedRaw || !state || !code) { throw error(400, 'Invalid OAuth callback'); } const stored = JSON.parse(storedRaw) as { state: string; anonymousId: string }; if (stored.state !== state) { throw error(400, 'State mismatch'); } cookies.delete('google_oauth_state', { path: '/' }); const anonId = stored.anonymousId; if (!anonId) { console.error('[Google auth] Missing anonymousId in state cookie'); throw error(400, 'Missing anonymous ID — please return to the game and try again'); } // Exchange authorization code for tokens const tokens = await exchangeGoogleCode( code, `${Bun.env.PUBLIC_SITE_URL}/auth/google/callback` ); const claims = decodeGoogleIdToken(tokens.id_token); const googleId = claims.sub; // --- User resolution --- let userId: string; // 1. Check if a user with this googleId already exists (returning user) const existingGoogleUser = await auth.getUserByGoogleId(googleId); if (existingGoogleUser) { userId = existingGoogleUser.id; console.log(`[Google auth] Returning Google user: userId=${userId}, anonId=${anonId}`); await auth.migrateAnonymousStats(anonId, userId); } else if (claims.email) { // 2. Check if email matches an existing email/password or Apple user const existingEmailUser = await auth.getUserByEmail(claims.email); if (existingEmailUser) { // Link Google account to existing user await db.update(userTable).set({ googleId }).where(eq(userTable.id, existingEmailUser.id)); userId = existingEmailUser.id; console.log(`[Google auth] Linked Google to existing email user: userId=${userId}, anonId=${anonId}`); await auth.migrateAnonymousStats(anonId, userId); } else { // 3. Brand new user — use anonymousId as user ID to preserve local stats userId = anonId; console.log(`[Google auth] New user (has email): userId=${userId}`); try { await db.insert(userTable).values({ id: userId, email: claims.email, passwordHash: null, appleId: null, googleId, firstName: claims.given_name || null, lastName: claims.family_name || null, isPrivate: false }); } catch (e: any) { // Handle race condition: if googleId was inserted between our check and insert if (e?.message?.includes('UNIQUE constraint')) { const retryUser = await auth.getUserByGoogleId(googleId); if (retryUser) { userId = retryUser.id; console.log(`[Google auth] Race condition (has email): resolved to userId=${userId}, anonId=${anonId}`); await auth.migrateAnonymousStats(anonId, userId); } else { throw error(500, 'Failed to create user'); } } else { throw e; } } } } else { // No email from Google (edge case — Google almost always returns email) userId = anonId; console.log(`[Google auth] New user (no email): userId=${userId}`); try { await db.insert(userTable).values({ id: userId, email: null, passwordHash: null, appleId: null, googleId, firstName: claims.given_name || null, lastName: claims.family_name || null, isPrivate: false }); } catch (e: any) { if (e?.message?.includes('UNIQUE constraint')) { const retryUser = await auth.getUserByGoogleId(googleId); if (retryUser) { userId = retryUser.id; console.log(`[Google auth] Race condition (no email): resolved to userId=${userId}, anonId=${anonId}`); await auth.migrateAnonymousStats(anonId, userId); } else { throw error(500, 'Failed to create user'); } } else { throw e; } } } // Create session const sessionToken = auth.generateSessionToken(); const session = await auth.createSession(sessionToken, userId); auth.setSessionTokenCookie({ cookies } as any, sessionToken, session.expiresAt); redirect(302, '/'); };