import { createHash, randomBytes } from "node:crypto"; import { eq, and, gt } from "drizzle-orm"; import type { Context } from "hono"; import { getSignedCookie, setSignedCookie, deleteCookie } from "hono/cookie"; import { db } from "../db/client.js"; import { sessions, users, type User } from "../db/schema.js"; const SESSION_COOKIE = "anydrop_session"; const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days function getSessionSecret(): string { const secret = process.env.SESSION_SECRET; if (!secret || secret.length < 32) { throw new Error("SESSION_SECRET env var must be set and at least 32 chars"); } return secret; } function isSecureEnv(): boolean { const url = process.env.APP_URL ?? ""; return url.startsWith("https://"); } export function generateToken(): string { return randomBytes(32).toString("base64url"); } export function hashToken(token: string): string { return createHash("sha256").update(token).digest("hex"); } export async function createSession( userId: string, userAgent: string | undefined, ipHash: string | undefined, ): Promise { const token = generateToken(); const tokenHash = hashToken(token); const expiresAt = new Date(Date.now() + SESSION_TTL_MS); await db.insert(sessions).values({ userId, tokenHash, userAgent: userAgent ?? null, ipHash: ipHash ?? null, expiresAt, }); return token; } export async function setSessionCookie(c: Context, token: string): Promise { await setSignedCookie(c, SESSION_COOKIE, token, getSessionSecret(), { httpOnly: true, secure: isSecureEnv(), sameSite: "Lax", path: "/", maxAge: SESSION_TTL_MS / 1000, }); } export function clearSessionCookie(c: Context): void { deleteCookie(c, SESSION_COOKIE, { path: "/", secure: isSecureEnv(), }); } export async function resolveSession(c: Context): Promise { let token: string | false | undefined; try { token = await getSignedCookie(c, getSessionSecret(), SESSION_COOKIE); } catch { return null; } if (!token) return null; const tokenHash = hashToken(token); const rows = await db .select({ user: users, sessionId: sessions.id, }) .from(sessions) .innerJoin(users, eq(users.id, sessions.userId)) .where(and(eq(sessions.tokenHash, tokenHash), gt(sessions.expiresAt, new Date()))) .limit(1); if (rows.length === 0) return null; await db .update(sessions) .set({ lastUsedAt: new Date() }) .where(eq(sessions.id, rows[0].sessionId)); return rows[0].user; } export async function revokeSession(c: Context): Promise { let token: string | false | undefined; try { token = await getSignedCookie(c, getSessionSecret(), SESSION_COOKIE); } catch { token = undefined; } if (token) { await db.delete(sessions).where(eq(sessions.tokenHash, hashToken(token))); } clearSessionCookie(c); }