Possible fix for sign in with apple migrations failing

This commit is contained in:
George Powell
2026-02-18 17:54:01 -05:00
parent c50cccd3d3
commit e8b2d2e35e
6 changed files with 153 additions and 17 deletions

View File

@@ -413,18 +413,31 @@
</div>
<div>Daily Verse Date: {dailyVerse.date}</div>
</div>
<DevButtons />
<DevButtons anonymousId={persistence.anonymousId} />
</div>
{/if}
{#if user && session}
<div class="mt-6 pt-4 border-t border-gray-200 text-center text-xs text-gray-400">
Signed in as {[user.firstName, user.lastName].filter(Boolean).join(" ")}{user.email ? ` (${user.email})` : ""}{user.appleId ? " using Apple" : ""}
<form method="POST" action="/auth/logout" use:enhance class="inline">
<div
class="mt-6 pt-4 border-t border-gray-200 text-center text-xs text-gray-400"
>
Signed in as {[user.firstName, user.lastName]
.filter(Boolean)
.join(" ")}{user.email
? ` (${user.email})`
: ""}{user.appleId ? " using Apple" : ""} |
<form
method="POST"
action="/auth/logout"
use:enhance
class="inline"
>
<button
type="submit"
class="ml-2 underline hover:text-gray-600 transition-colors cursor-pointer"
>Sign out</button>
>Sign out</button
>
</form>
</div>
{/if}

View File

@@ -0,0 +1,66 @@
import type { RequestHandler } from './$types';
import { db } from '$lib/server/db';
import { dailyCompletions } from '$lib/server/db/schema';
import { json } from '@sveltejs/kit';
import crypto from 'node:crypto';
const DEV_HOSTS = ['localhost:5173', 'test.bibdle.com'];
// A spread of book IDs to use as fake guesses
const SAMPLE_BOOK_IDS = [
'GEN', 'EXO', 'PSA', 'PRO', 'ISA', 'JER', 'MAT', 'MRK', 'LUK', 'JHN',
'ROM', 'GAL', 'EPH', 'PHP', 'REV', 'ACT', 'HEB', 'JAS', '1CO', '2CO',
];
export const POST: RequestHandler = async ({ request }) => {
const host = request.headers.get('host') ?? '';
if (!DEV_HOSTS.includes(host)) {
return json({ error: 'Not allowed in production' }, { status: 403 });
}
try {
const { anonymousId } = await request.json();
if (!anonymousId || typeof anonymousId !== 'string') {
return json({ error: 'anonymousId required' }, { status: 400 });
}
const today = new Date();
const inserted: string[] = [];
const skipped: string[] = [];
for (let i = 1; i <= 10; i++) {
const d = new Date(today);
d.setDate(d.getDate() - i);
const date = d.toLocaleDateString('en-CA'); // YYYY-MM-DD
const guessCount = Math.floor(Math.random() * 6) + 1; // 1-6 guesses
// Pick `guessCount` random books (last one is the "correct" answer)
const shuffled = [...SAMPLE_BOOK_IDS].sort(() => Math.random() - 0.5);
const guesses = shuffled.slice(0, guessCount);
try {
await db.insert(dailyCompletions).values({
id: crypto.randomUUID(),
anonymousId,
date,
guessCount,
guesses: JSON.stringify(guesses),
completedAt: new Date(d.getTime() + 12 * 60 * 60 * 1000), // noon on that day
});
inserted.push(date);
} catch (err: any) {
if (err?.code === 'SQLITE_CONSTRAINT_UNIQUE' || err?.message?.includes('UNIQUE')) {
skipped.push(date);
} else {
throw err;
}
}
}
return json({ success: true, inserted, skipped });
} catch (err) {
console.error('Error seeding history:', err);
return json({ error: 'Failed to seed history' }, { status: 500 });
}
};

View File

@@ -25,6 +25,12 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
}
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);
@@ -51,7 +57,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
if (existingAppleUser) {
userId = existingAppleUser.id;
await auth.migrateAnonymousStats(stored.anonymousId, userId);
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);
@@ -59,10 +66,12 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
// Link Apple account to existing user
await db.update(userTable).set({ appleId }).where(eq(userTable.id, existingEmailUser.id));
userId = existingEmailUser.id;
await auth.migrateAnonymousStats(stored.anonymousId, userId);
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 = stored.anonymousId || crypto.randomUUID();
userId = anonId;
console.log(`[Apple auth] New user (has email): userId=${userId}`);
try {
await db.insert(userTable).values({
id: userId,
@@ -79,6 +88,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
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');
}
@@ -89,7 +100,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
}
} else {
// No email from Apple — create account with appleId only
userId = stored.anonymousId || crypto.randomUUID();
userId = anonId;
console.log(`[Apple auth] New user (no email): userId=${userId}`);
try {
await db.insert(userTable).values({
id: userId,
@@ -105,6 +117,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
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');
}

View File

@@ -1,25 +1,36 @@
<script lang="ts">
import { page } from '$app/state';
import AuthModal from '$lib/components/AuthModal.svelte';
import { page } from "$app/state";
import { browser } from "$app/environment";
import AuthModal from "$lib/components/AuthModal.svelte";
let isOpen = $state(true);
const user = $derived(page.data.user);
const anonymousId = crypto.randomUUID();
let anonymousId = $state("");
$effect(() => {
if (browser) {
anonymousId = localStorage.getItem("bibdle-anonymous-id") ?? "";
}
});
</script>
<div class="min-h-screen bg-gray-900 flex items-center justify-center p-4">
{#if user}
<div class="text-white text-center space-y-4">
<p class="text-lg">Signed in as <strong>{user.email ?? 'no email'}</strong></p>
<p class="text-lg">
Signed in as <strong>{user.email ?? "no email"}</strong>
</p>
<form method="POST" action="/auth/logout">
<button class="px-4 py-2 bg-red-600 rounded-md hover:bg-red-700 transition-colors">
<button
class="px-4 py-2 bg-red-600 rounded-md hover:bg-red-700 transition-colors"
>
Sign Out
</button>
</form>
</div>
{:else}
<button
onclick={() => isOpen = true}
onclick={() => (isOpen = true)}
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
Open Auth Modal