switched to bun:sqlite

This commit is contained in:
George Powell
2026-02-05 00:47:55 -05:00
parent dfe784b744
commit dfe1c40a8a
10 changed files with 223 additions and 17 deletions

View File

@@ -6,7 +6,6 @@
"name": "bibdle",
"dependencies": {
"@xenova/transformers": "^2.17.2",
"better-sqlite3": "^12.6.2",
"fast-xml-parser": "^5.3.3",
"xml2js": "^0.6.2",
},
@@ -18,11 +17,10 @@
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.18",
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^22.19.7",
"drizzle-kit": "^0.31.8",
"drizzle-orm": "^0.45.1",
"svelte": "^5.48.3",
"svelte": "^5.48.5",
"svelte-check": "^4.3.5",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",

View File

@@ -8,6 +8,13 @@
"when": 1765934144883,
"tag": "0000_clumsy_impossible_man",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1770266674489,
"tag": "0001_loose_kree",
"breakpoints": true
}
]
}

View File

@@ -4,7 +4,7 @@
"version": "3.0.0alpha",
"type": "module",
"scripts": {
"dev": "vite dev",
"dev": "bun --bun vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
@@ -23,7 +23,6 @@
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.18",
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^22.19.7",
"drizzle-kit": "^0.31.8",
"drizzle-orm": "^0.45.1",
@@ -35,7 +34,6 @@
},
"dependencies": {
"@xenova/transformers": "^2.17.2",
"better-sqlite3": "^12.6.2",
"fast-xml-parser": "^5.3.3",
"xml2js": "^0.6.2"
}

View File

@@ -31,7 +31,7 @@ export async function validateSessionToken(token: string) {
const [result] = await db
.select({
// Adjust user table here to tweak returned data
user: { id: table.user.id, username: table.user.username },
user: { id: table.user.id, email: table.user.email },
session: table.session
})
.from(table.session)
@@ -79,3 +79,37 @@ export function deleteSessionTokenCookie(event: RequestEvent) {
path: '/'
});
}
export async function hashPassword(password: string): Promise<string> {
return await Bun.password.hash(password, {
algorithm: 'argon2id',
memoryCost: 4,
timeCost: 3
});
}
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
try {
return await Bun.password.verify(password, hash);
} catch {
return false;
}
}
export async function createUser(anonymousId: string, email: string, passwordHash: string, firstName?: string, lastName?: string) {
const user: table.User = {
id: anonymousId, // Use anonymousId as the user ID to preserve stats
email,
passwordHash,
firstName: firstName || null,
lastName: lastName || null,
isPrivate: false
};
await db.insert(table.user).values(user);
return user;
}
export async function getUserByEmail(email: string) {
const [user] = await db.select().from(table.user).where(eq(table.user.email, email));
return user || null;
}

View File

@@ -1,5 +1,5 @@
import { drizzle } from 'drizzle-orm/better-sqlite3';
import Database from 'better-sqlite3';
import { drizzle } from 'drizzle-orm/bun-sqlite';
import { Database } from 'bun:sqlite';
import * as schema from './schema';
import { env } from '$env/dynamic/private';

View File

@@ -2,7 +2,14 @@ import { integer, sqliteTable, text, index, unique } from 'drizzle-orm/sqlite-co
import { sql } from 'drizzle-orm';
export const user = sqliteTable('user', { id: text('id').primaryKey(), age: integer('age') });
export const user = sqliteTable('user', {
id: text('id').primaryKey(),
firstName: text('first_name'),
lastName: text('last_name'),
email: text('email').unique(),
passwordHash: text('password_hash'),
isPrivate: integer('is_private', { mode: 'boolean' }).default(false)
});
export const session = sqliteTable('session', {
id: text('id').primaryKey(),

View File

@@ -11,7 +11,9 @@
import Credits from "$lib/components/Credits.svelte";
import TitleAnimation from "$lib/components/TitleAnimation.svelte";
import DevButtons from "$lib/components/DevButtons.svelte";
import AuthModal from "$lib/components/AuthModal.svelte";
import { getGrade } from "$lib/utils/game";
import { enhance } from '$app/forms';
interface Guess {
book: BibleBook;
@@ -25,6 +27,8 @@
let dailyVerse = $derived(data.dailyVerse);
let correctBookId = $derived(data.correctBookId);
let user = $derived(data.user);
let session = $derived(data.session);
let guesses = $state<Guess[]>([]);
@@ -37,6 +41,7 @@
let anonymousId = $state("");
let statsSubmitted = $state(false);
let authModalOpen = $state(false);
let statsData = $state<{
solveRank: number;
guessRank: number;
@@ -447,13 +452,42 @@
<span class="big-text"
>{isDev ? "Dev Edition | " : ""}{currentDate}</span
>
<div class="mt-4">
<a
href="/stats?anonymousId={anonymousId}"
class="inline-flex items-center px-4 py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-colors text-sm font-medium shadow-md"
>
📊 View Stats
</a>
<div class="mt-4 flex flex-col items-center gap-3">
<div class="flex gap-3">
<a
href="/stats?anonymousId={anonymousId}"
class="inline-flex items-center px-4 py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-colors text-sm font-medium shadow-md"
>
📊 View Stats
</a>
{#if user}
<form method="POST" action="/auth/logout" use:enhance>
<button
type="submit"
class="inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors text-sm font-medium shadow-md"
>
🚪 Sign Out
</button>
</form>
{:else}
<button
onclick={() => authModalOpen = true}
class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium shadow-md"
>
🔐 Sign In
</button>
{/if}
</div>
{#if isDev}
<div class="text-xs text-gray-600 bg-gray-100 px-3 py-2 rounded border">
<div><strong>Debug Info:</strong></div>
<div>User: {user ? `${user.email} (ID: ${user.id})` : 'Not signed in'}</div>
<div>Session: {session ? `Expires ${session.expiresAt.toLocaleDateString()}` : 'No session'}</div>
<div>Anonymous ID: {anonymousId || 'Not set'}</div>
</div>
{/if}
</div>
</div>
@@ -502,3 +536,5 @@
{/if}
</div>
</div>
<AuthModal bind:isOpen={authModalOpen} {anonymousId} />

View File

@@ -0,0 +1,13 @@
import { redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
import * as auth from '$lib/server/auth';
export const actions: Actions = {
default: async ({ locals, cookies }) => {
if (locals.session) {
await auth.invalidateSession(locals.session.id);
}
auth.deleteSessionTokenCookie({ cookies });
redirect(302, '/');
}
};

View File

@@ -0,0 +1,49 @@
import { redirect, fail } from '@sveltejs/kit';
import type { Actions } from './$types';
import * as auth from '$lib/server/auth';
export const actions: Actions = {
default: async ({ request, cookies }) => {
const data = await request.formData();
const email = data.get('email')?.toString();
const password = data.get('password')?.toString();
if (!email || !password) {
return fail(400, { error: 'Email and password are required' });
}
// Basic email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return fail(400, { error: 'Please enter a valid email address' });
}
if (password.length < 6) {
return fail(400, { error: 'Password must be at least 6 characters' });
}
try {
// Get user by email
const user = await auth.getUserByEmail(email);
if (!user || !user.passwordHash) {
return fail(400, { error: 'Invalid email or password' });
}
// Verify password
const isValidPassword = await auth.verifyPassword(password, user.passwordHash);
if (!isValidPassword) {
return fail(400, { error: 'Invalid email or password' });
}
// Create session
const sessionToken = auth.generateSessionToken();
const session = await auth.createSession(sessionToken, user.id);
auth.setSessionTokenCookie({ cookies }, sessionToken, session.expiresAt);
return { success: true };
} catch (error) {
console.error('Sign in error:', error);
return fail(500, { error: 'An error occurred during sign in' });
}
}
};

View File

@@ -0,0 +1,64 @@
import { redirect, fail } from '@sveltejs/kit';
import type { Actions } from './$types';
import * as auth from '$lib/server/auth';
export const actions: Actions = {
default: async ({ request, cookies }) => {
const data = await request.formData();
const email = data.get('email')?.toString();
const password = data.get('password')?.toString();
const firstName = data.get('firstName')?.toString();
const lastName = data.get('lastName')?.toString();
const anonymousId = data.get('anonymousId')?.toString();
if (!email || !password || !anonymousId) {
return fail(400, { error: 'Email, password, and anonymous ID are required' });
}
// Basic email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return fail(400, { error: 'Please enter a valid email address' });
}
if (password.length < 6) {
return fail(400, { error: 'Password must be at least 6 characters' });
}
try {
// Check if user already exists
const existingUser = await auth.getUserByEmail(email);
if (existingUser) {
return fail(400, { error: 'An account with this email already exists' });
}
// Hash password
const passwordHash = await auth.hashPassword(password);
// Create user with anonymousId as the user ID
const user = await auth.createUser(
anonymousId,
email,
passwordHash,
firstName || undefined,
lastName || undefined
);
// Create session
const sessionToken = auth.generateSessionToken();
const session = await auth.createSession(sessionToken, user.id);
auth.setSessionTokenCookie({ cookies }, sessionToken, session.expiresAt);
return { success: true };
} catch (error) {
console.error('Sign up error:', error);
// Check if it's a unique constraint error (user with this ID already exists)
if (error instanceof Error && error.message.includes('UNIQUE constraint')) {
return fail(400, { error: 'This account is already registered. Please sign in instead.' });
}
return fail(500, { error: 'An error occurred during account creation' });
}
}
};