import { sha256 } from '@oslojs/crypto/sha2'; import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding'; import { db } from '$lib/server/db/index.js'; import { sessions, users } from '$lib/server/db/schema.js'; import { eq } from 'drizzle-orm'; import type { RequestEvent } from '@sveltejs/kit'; const SESSION_DURATION_MS = 30 * 24 * 60 * 60 * 1000; // 30 days const SESSION_REFRESH_MS = 15 * 24 * 60 * 60 * 1000; // 15 days export function generateSessionToken(): string { const bytes = new Uint8Array(20); crypto.getRandomValues(bytes); return encodeBase32LowerCaseNoPadding(bytes); } export function generateUserId(): string { const bytes = new Uint8Array(15); crypto.getRandomValues(bytes); return encodeBase32LowerCaseNoPadding(bytes); } function hashToken(token: string): string { const encoded = new TextEncoder().encode(token); return encodeHexLowerCase(sha256(encoded)); } export async function createSession(token: string, userId: string) { const id = hashToken(token); const expiresAt = new Date(Date.now() + SESSION_DURATION_MS); await db.insert(sessions).values({ id, userId, expiresAt }); return { id, userId, expiresAt, fresh: false }; } export async function validateSession(token: string) { const id = hashToken(token); const [result] = await db .select({ sessionId: sessions.id, userId: sessions.userId, expiresAt: sessions.expiresAt, userEmail: users.email, userDisplayName: users.displayName }) .from(sessions) .innerJoin(users, eq(sessions.userId, users.id)) .where(eq(sessions.id, id)); if (!result) return { session: null, user: null }; if (result.expiresAt < new Date()) { await db.delete(sessions).where(eq(sessions.id, id)); return { session: null, user: null }; } const session = { id: result.sessionId, userId: result.userId, expiresAt: result.expiresAt, fresh: false }; // Refresh if older than 15 days if (Date.now() - (result.expiresAt.getTime() - SESSION_DURATION_MS) > SESSION_REFRESH_MS) { session.expiresAt = new Date(Date.now() + SESSION_DURATION_MS); session.fresh = true; await db .update(sessions) .set({ expiresAt: session.expiresAt }) .where(eq(sessions.id, id)); } const user = { id: result.userId, email: result.userEmail, displayName: result.userDisplayName }; return { session, user }; } export async function invalidateSession(token: string) { const id = hashToken(token); await db.delete(sessions).where(eq(sessions.id, id)); } export function setSessionCookie(event: RequestEvent, token: string, expiresAt: Date) { // Use the actual request origin, not the forwarded protocol // This allows Secure cookies over HTTPS but plain cookies over HTTP (NetBird, LAN) const actualOrigin = event.request.headers.get('origin') ?? event.url.origin; const isSecure = actualOrigin.startsWith('https:'); event.cookies.set('session', token, { httpOnly: true, sameSite: 'lax', secure: isSecure, path: '/', expires: expiresAt }); } export function deleteSessionCookie(event: RequestEvent) { event.cookies.set('session', '', { httpOnly: true, sameSite: 'lax', secure: event.url.protocol === 'https:', path: '/', maxAge: 0 }); }