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

@@ -2,6 +2,30 @@
import { browser } from "$app/environment";
import Button from "$lib/components/Button.svelte";
let { anonymousId }: { anonymousId: string | null } = $props();
let seeding = $state(false);
async function seedHistory() {
if (!browser || !anonymousId || seeding) return;
seeding = true;
try {
const response = await fetch("/api/dev/seed-history", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ anonymousId })
});
const result = await response.json();
alert(
`Seeded! Inserted: ${result.inserted?.join(", ")}. Skipped (already exist): ${result.skipped?.join(", ") || "none"}`
);
} catch {
alert("Failed to seed history");
} finally {
seeding = false;
}
}
function clearLocalStorage() {
if (!browser) return;
// Clear all bibdle-related localStorage items
@@ -86,4 +110,13 @@
>
Clear LocalStorage
</Button>
<Button
variant="secondary"
onclick={seedHistory}
disabled={seeding}
class="w-full py-4 md:py-2"
>
{seeding ? "Seeding..." : "Seed 10 Days of History"}
</Button>
</div>

View File

@@ -71,8 +71,7 @@
</h2>
<p class="text-gray-700 leading-relaxed italic">
Guess what book of the bible you think the verse is from. You will
get clues to tell you if your guess is close or not. Green means the
category is correct; red means wrong.
get clues to help you after each guess.
</p>
</Container>
{:else}

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