mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-04-05 17:33:31 -04:00
switched to bun:sqlite
This commit is contained in:
4
bun.lock
4
bun.lock
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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} />
|
||||
|
||||
13
src/routes/auth/logout/+page.server.ts
Normal file
13
src/routes/auth/logout/+page.server.ts
Normal 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, '/');
|
||||
}
|
||||
};
|
||||
49
src/routes/auth/signin/+page.server.ts
Normal file
49
src/routes/auth/signin/+page.server.ts
Normal 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' });
|
||||
}
|
||||
}
|
||||
};
|
||||
64
src/routes/auth/signup/+page.server.ts
Normal file
64
src/routes/auth/signup/+page.server.ts
Normal 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' });
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user