import { redirect, error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { exchangeAppleCode, decodeAppleIdToken } from '$lib/server/apple-auth'; import { env as publicEnv } from '$env/dynamic/public'; 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 POST: RequestHandler = async ({ request, cookies }) => { const formData = await request.formData(); const code = formData.get('code')?.toString(); const state = formData.get('state')?.toString(); // Apple sends user info as JSON string on FIRST authorization only const userInfoStr = formData.get('user')?.toString(); // Validate CSRF state const storedRaw = cookies.get('apple_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('apple_oauth_state', { path: '/' }); const anonId = stored.anonymousId; if (!anonId) { console.error('[Apple 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 exchangeAppleCode(code, `${publicEnv.PUBLIC_SITE_URL}/auth/apple/callback`); const claims = decodeAppleIdToken(tokens.id_token); const appleId = claims.sub; // Parse user info (only present on first authorization) let appleFirstName: string | undefined; let appleLastName: string | undefined; if (userInfoStr) { try { const userInfo = JSON.parse(userInfoStr); appleFirstName = userInfo.name?.firstName; appleLastName = userInfo.name?.lastName; } catch { /* ignore parse errors */ } } // --- User resolution --- let userId: string; // 1. Check if a user with this appleId already exists (returning user) const existingAppleUser = await auth.getUserByAppleId(appleId); if (existingAppleUser) { userId = existingAppleUser.id; console.log(`[Apple auth] Returning Apple user: userId=${userId}, anonId=${anonId}`); await auth.migrateAnonymousStats(anonId, userId); } else if (claims.email) { // 2. Check if email matches an existing email/password user const existingEmailUser = await auth.getUserByEmail(claims.email); if (existingEmailUser) { // Link Apple account to existing user await db.update(userTable).set({ appleId }).where(eq(userTable.id, existingEmailUser.id)); userId = existingEmailUser.id; console.log(`[Apple auth] Linked Apple 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(`[Apple auth] New user (has email): userId=${userId}`); try { await db.insert(userTable).values({ id: userId, email: claims.email, passwordHash: null, appleId, firstName: appleFirstName || null, lastName: appleLastName || null, isPrivate: false }); } catch (e: any) { // Handle race condition: if appleId was inserted between our check and insert if (e?.message?.includes('UNIQUE constraint')) { const retryUser = await auth.getUserByAppleId(appleId); if (retryUser) { userId = retryUser.id; console.log(`[Apple 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 Apple — create account with appleId only userId = anonId; console.log(`[Apple auth] New user (no email): userId=${userId}`); try { await db.insert(userTable).values({ id: userId, email: null, passwordHash: null, appleId, firstName: appleFirstName || null, lastName: appleLastName || null, isPrivate: false }); } catch (e: any) { if (e?.message?.includes('UNIQUE constraint')) { const retryUser = await auth.getUserByAppleId(appleId); if (retryUser) { userId = retryUser.id; console.log(`[Apple 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, '/'); };