import { createHash, randomBytes } from "node:crypto"; import { eq, lt } from "drizzle-orm"; import { db } from "@/db"; import { sessions, type UserRole, users } from "@/db/schema"; // Deliberately free of next/* imports so it can be exercised directly by // integration tests; cookie handling lives in cookies.ts. export const SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days export type SessionUser = { id: number; username: string; role: UserRole }; function hashToken(token: string): string { return createHash("sha256").update(token).digest("hex"); } /** Creates a DB session and returns the raw bearer token for the cookie. */ export async function createSession( userId: number, ): 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 }; } /** Resolves a raw token to its user, treating expired sessions as absent. */ export async function validateSessionToken(token: string): Promise { if (!token) return null; const id = hashToken(token); const [row] = await db .select({ userId: users.id, username: users.username, role: users.role, expiresAt: sessions.expiresAt, }) .from(sessions) .innerJoin(users, eq(users.id, sessions.userId)) .where(eq(sessions.id, id)) .limit(1); if (!row) return null; if (row.expiresAt.getTime() <= Date.now()) { await db.delete(sessions).where(eq(sessions.id, id)); return null; } return { id: row.userId, username: row.username, role: row.role }; } export async function deleteSession(token: string): Promise { await db.delete(sessions).where(eq(sessions.id, hashToken(token))); } /** Opportunistic cleanup, called on login so the table cannot grow unbounded. */ export async function deleteExpiredSessions(): Promise { await db.delete(sessions).where(lt(sessions.expiresAt, new Date())); }