Files
bibdle/src/routes/auth/apple/callback/+server.ts
2026-02-18 17:54:01 -05:00

138 lines
4.7 KiB
TypeScript

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, '/');
};