import { and, eq, gt } from 'drizzle-orm' import { createHash, randomBytes } from 'node:crypto' import type { Role } from '../../shared/enums.ts' import type { Db } from '../db/client.ts' import { sessions, users } from '../db/schema.ts' export const SESSION_COOKIE = 'safetylog_session' export const SESSION_TTL_MS = 14 * 24 * 60 * 60 * 1000 export interface SessionUser { id: string organizationId: string email: string name: string role: Role } const hashToken = (token: string) => createHash('sha256').update(token).digest('hex') export async function createSession(db: Db, userId: string): Promise<{ token: string; expiresAt: Date }> { const token = randomBytes(32).toString('base64url') const expiresAt = new Date(Date.now() + SESSION_TTL_MS) await db.insert(sessions).values({ id: hashToken(token), userId, expiresAt }) return { token, expiresAt } } /** Returns the session's user, extending the session once it is past half its lifetime. */ export async function validateSession(db: Db, token: string): Promise<{ user: SessionUser; renewedUntil?: Date } | null> { const id = hashToken(token) const [row] = await db .select({ expiresAt: sessions.expiresAt, user: { id: users.id, organizationId: users.organizationId, email: users.email, name: users.name, role: users.role }, }) .from(sessions) .innerJoin(users, eq(users.id, sessions.userId)) .where(and(eq(sessions.id, id), gt(sessions.expiresAt, new Date()), eq(users.active, true))) if (!row) return null if (row.expiresAt.getTime() - Date.now() < SESSION_TTL_MS / 2) { const renewedUntil = new Date(Date.now() + SESSION_TTL_MS) await db.update(sessions).set({ expiresAt: renewedUntil }).where(eq(sessions.id, id)) return { user: row.user, renewedUntil } } return { user: row.user } } export async function deleteSession(db: Db, token: string): Promise { await db.delete(sessions).where(eq(sessions.id, hashToken(token))) } export async function deleteUserSessions(db: Db, userId: string): Promise { await db.delete(sessions).where(eq(sessions.userId, userId)) }