import type { RequestEvent } from '@sveltejs/kit'; import { eq } from 'drizzle-orm'; import { sha256 } from '@oslojs/crypto/sha2'; import { encodeBase64url, encodeHexLowerCase } from '@oslojs/encoding'; import { db } from '$lib/server/db'; import * as table from '$lib/server/db/schema'; import { getLocalR2Url } from '$lib/server/r2'; const DAY_IN_MS = 1000 * 60 * 60 * 24; export const sessionCookieName = 'auth-session'; export function generateSessionToken() { const bytes = crypto.getRandomValues(new Uint8Array(18)); const token = encodeBase64url(bytes); return token; } export async function createSession(token: string, userId: string) { const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); 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 = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); const [result] = await db .select({ // Adjust user table here to tweak returned data user: { id: table.user.id, username: table.user.username, profilePictureUrl: table.user.profilePictureUrl }, 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)); } // Generate local proxy URL for profile picture if it exists let profilePictureUrl = user.profilePictureUrl; if (profilePictureUrl && !profilePictureUrl.startsWith('http')) { // It's a path, generate local proxy URL profilePictureUrl = getLocalR2Url(profilePictureUrl); } return { session, user: { ...user, profilePictureUrl } }; } 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: '/', httpOnly: true, secure: false, // Allow HTTP in development sameSite: 'lax' }); } export function deleteSessionTokenCookie(event: RequestEvent) { event.cookies.delete(sessionCookieName, { path: '/' }); } export async function getUserFromGoogleId(googleId: string) { const [user] = await db.select().from(table.user).where(eq(table.user.googleId, googleId)); return user || null; } export async function createUser(googleId: string, name: string) { const userId = generateUserId(); const user: table.User = { id: userId, username: name, googleId, passwordHash: null, age: null, profilePictureUrl: null }; await db.insert(table.user).values(user); return user; } function generateUserId(): string { const bytes = crypto.getRandomValues(new Uint8Array(15)); return encodeBase64url(bytes); }