mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-04-05 17:33:31 -04:00
Possible fix for sign in with apple migrations failing
This commit is contained in:
@@ -2,6 +2,30 @@
|
|||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
import Button from "$lib/components/Button.svelte";
|
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() {
|
function clearLocalStorage() {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
// Clear all bibdle-related localStorage items
|
// Clear all bibdle-related localStorage items
|
||||||
@@ -86,4 +110,13 @@
|
|||||||
>
|
>
|
||||||
Clear LocalStorage
|
Clear LocalStorage
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
|
|||||||
@@ -71,8 +71,7 @@
|
|||||||
</h2>
|
</h2>
|
||||||
<p class="text-gray-700 leading-relaxed italic">
|
<p class="text-gray-700 leading-relaxed italic">
|
||||||
Guess what book of the bible you think the verse is from. You will
|
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
|
get clues to help you after each guess.
|
||||||
category is correct; red means wrong.
|
|
||||||
</p>
|
</p>
|
||||||
</Container>
|
</Container>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -413,18 +413,31 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>Daily Verse Date: {dailyVerse.date}</div>
|
<div>Daily Verse Date: {dailyVerse.date}</div>
|
||||||
</div>
|
</div>
|
||||||
<DevButtons />
|
<DevButtons anonymousId={persistence.anonymousId} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if user && session}
|
{#if user && session}
|
||||||
<div class="mt-6 pt-4 border-t border-gray-200 text-center text-xs text-gray-400">
|
<div
|
||||||
Signed in as {[user.firstName, user.lastName].filter(Boolean).join(" ")}{user.email ? ` (${user.email})` : ""}{user.appleId ? " using Apple" : ""}
|
class="mt-6 pt-4 border-t border-gray-200 text-center text-xs text-gray-400"
|
||||||
<form method="POST" action="/auth/logout" use:enhance class="inline">
|
>
|
||||||
|
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
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="ml-2 underline hover:text-gray-600 transition-colors cursor-pointer"
|
class="ml-2 underline hover:text-gray-600 transition-colors cursor-pointer"
|
||||||
>Sign out</button>
|
>Sign out</button
|
||||||
|
>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
66
src/routes/api/dev/seed-history/+server.ts
Normal file
66
src/routes/api/dev/seed-history/+server.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -25,6 +25,12 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
|||||||
}
|
}
|
||||||
cookies.delete('apple_oauth_state', { path: '/' });
|
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
|
// Exchange authorization code for tokens
|
||||||
const tokens = await exchangeAppleCode(code, `${publicEnv.PUBLIC_SITE_URL}/auth/apple/callback`);
|
const tokens = await exchangeAppleCode(code, `${publicEnv.PUBLIC_SITE_URL}/auth/apple/callback`);
|
||||||
const claims = decodeAppleIdToken(tokens.id_token);
|
const claims = decodeAppleIdToken(tokens.id_token);
|
||||||
@@ -51,7 +57,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
|||||||
|
|
||||||
if (existingAppleUser) {
|
if (existingAppleUser) {
|
||||||
userId = existingAppleUser.id;
|
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) {
|
} else if (claims.email) {
|
||||||
// 2. Check if email matches an existing email/password user
|
// 2. Check if email matches an existing email/password user
|
||||||
const existingEmailUser = await auth.getUserByEmail(claims.email);
|
const existingEmailUser = await auth.getUserByEmail(claims.email);
|
||||||
@@ -59,10 +66,12 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
|||||||
// Link Apple account to existing user
|
// Link Apple account to existing user
|
||||||
await db.update(userTable).set({ appleId }).where(eq(userTable.id, existingEmailUser.id));
|
await db.update(userTable).set({ appleId }).where(eq(userTable.id, existingEmailUser.id));
|
||||||
userId = 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 {
|
} else {
|
||||||
// 3. Brand new user — use anonymousId as user ID to preserve local stats
|
// 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 {
|
try {
|
||||||
await db.insert(userTable).values({
|
await db.insert(userTable).values({
|
||||||
id: userId,
|
id: userId,
|
||||||
@@ -79,6 +88,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
|||||||
const retryUser = await auth.getUserByAppleId(appleId);
|
const retryUser = await auth.getUserByAppleId(appleId);
|
||||||
if (retryUser) {
|
if (retryUser) {
|
||||||
userId = retryUser.id;
|
userId = retryUser.id;
|
||||||
|
console.log(`[Apple auth] Race condition (has email): resolved to userId=${userId}, anonId=${anonId}`);
|
||||||
|
await auth.migrateAnonymousStats(anonId, userId);
|
||||||
} else {
|
} else {
|
||||||
throw error(500, 'Failed to create user');
|
throw error(500, 'Failed to create user');
|
||||||
}
|
}
|
||||||
@@ -89,7 +100,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No email from Apple — create account with appleId only
|
// 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 {
|
try {
|
||||||
await db.insert(userTable).values({
|
await db.insert(userTable).values({
|
||||||
id: userId,
|
id: userId,
|
||||||
@@ -105,6 +117,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
|||||||
const retryUser = await auth.getUserByAppleId(appleId);
|
const retryUser = await auth.getUserByAppleId(appleId);
|
||||||
if (retryUser) {
|
if (retryUser) {
|
||||||
userId = retryUser.id;
|
userId = retryUser.id;
|
||||||
|
console.log(`[Apple auth] Race condition (no email): resolved to userId=${userId}, anonId=${anonId}`);
|
||||||
|
await auth.migrateAnonymousStats(anonId, userId);
|
||||||
} else {
|
} else {
|
||||||
throw error(500, 'Failed to create user');
|
throw error(500, 'Failed to create user');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,36 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/state';
|
import { page } from "$app/state";
|
||||||
import AuthModal from '$lib/components/AuthModal.svelte';
|
import { browser } from "$app/environment";
|
||||||
|
import AuthModal from "$lib/components/AuthModal.svelte";
|
||||||
|
|
||||||
let isOpen = $state(true);
|
let isOpen = $state(true);
|
||||||
const user = $derived(page.data.user);
|
const user = $derived(page.data.user);
|
||||||
const anonymousId = crypto.randomUUID();
|
let anonymousId = $state("");
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (browser) {
|
||||||
|
anonymousId = localStorage.getItem("bibdle-anonymous-id") ?? "";
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="min-h-screen bg-gray-900 flex items-center justify-center p-4">
|
<div class="min-h-screen bg-gray-900 flex items-center justify-center p-4">
|
||||||
{#if user}
|
{#if user}
|
||||||
<div class="text-white text-center space-y-4">
|
<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">
|
<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
|
Sign Out
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<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"
|
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
||||||
>
|
>
|
||||||
Open Auth Modal
|
Open Auth Modal
|
||||||
|
|||||||
Reference in New Issue
Block a user