import type { RequestEvent } from '@sveltejs/kit'; import { eq } from 'drizzle-orm'; import { db } from '$lib/server/db'; import * as table from '$lib/server/db/schema'; const DAY_IN_MS = 1000 * 60 * 60 * 24; export const sessionCookieName = 'auth-session'; export function generateSessionToken() { const bytes = crypto.getRandomValues(new Uint8Array(18)); return Buffer.from(bytes).toString('base64url'); } export async function createSession(token: string, userId: string) { const sessionId = new Bun.CryptoHasher('sha256').update(token).digest('hex'); const session: table.Session = { id: sessionId, userId, expiresAt: new Date(Date.now() + DAY_IN_MS * 30) }; await db.insert(table.session).values(session); return session; } export async function validateSessionToken(token: string) { const sessionId = new Bun.CryptoHasher('sha256').update(token).digest('hex'); const [result] = await db .select({ // Adjust user table here to tweak returned data user: { id: table.user.id, email: table.user.email, firstName: table.user.firstName, lastName: table.user.lastName, appleId: table.user.appleId, googleId: table.user.googleId }, session: table.session }) .from(table.session) .innerJoin(table.user, eq(table.session.userId, table.user.id)) .where(eq(table.session.id, sessionId)); if (!result) { return { session: null, user: null }; } const { session, user } = result; const sessionExpired = Date.now() >= session.expiresAt.getTime(); if (sessionExpired) { await db.delete(table.session).where(eq(table.session.id, session.id)); return { session: null, user: null }; } const renewSession = Date.now() >= session.expiresAt.getTime() - DAY_IN_MS * 15; if (renewSession) { session.expiresAt = new Date(Date.now() + DAY_IN_MS * 30); await db .update(table.session) .set({ expiresAt: session.expiresAt }) .where(eq(table.session.id, session.id)); } return { session, user }; } export type SessionValidationResult = Awaited>; export async function invalidateSession(sessionId: string) { await db.delete(table.session).where(eq(table.session.id, sessionId)); } export function setSessionTokenCookie(event: RequestEvent, token: string, expiresAt: Date) { event.cookies.set(sessionCookieName, token, { expires: expiresAt, path: '/' }); } export function deleteSessionTokenCookie(event: RequestEvent) { event.cookies.delete(sessionCookieName, { path: '/' }); } export async function hashPassword(password: string): Promise { return await Bun.password.hash(password, { algorithm: 'argon2id', memoryCost: 4, timeCost: 3 }); } export async function verifyPassword(password: string, hash: string): Promise { 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, appleId: null, googleId: null, 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; } export async function getUserByAppleId(appleId: string) { const [user] = await db.select().from(table.user).where(eq(table.user.appleId, appleId)); return user || null; } export async function getUserByGoogleId(googleId: string) { const [user] = await db.select().from(table.user).where(eq(table.user.googleId, googleId)); return user || null; } export async function migrateAnonymousStats(anonymousId: string | undefined, userId: string) { if (!anonymousId || anonymousId === userId) return; try { const { dailyCompletions } = await import('$lib/server/db/schema'); const anonCompletions = await db .select() .from(dailyCompletions) .where(eq(dailyCompletions.anonymousId, anonymousId)); const userCompletions = await db .select() .from(dailyCompletions) .where(eq(dailyCompletions.anonymousId, userId)); const userDates = new Set(userCompletions.map((c) => c.date)); let migrated = 0; let skipped = 0; for (const completion of anonCompletions) { if (!userDates.has(completion.date)) { await db .update(dailyCompletions) .set({ anonymousId: userId }) .where(eq(dailyCompletions.id, completion.id)); migrated++; } else { await db.delete(dailyCompletions).where(eq(dailyCompletions.id, completion.id)); skipped++; } } console.log(`Migration complete: ${migrated} moved, ${skipped} duplicates removed`); } catch (error) { console.error('Error migrating anonymous stats:', error); } }