const crypto = require("crypto"); const pool = require("../db/pool"); const { SESSION_TTL_DAYS } = require("../utils/session-cookie"); const INSERT_SESSION_SQL = `INSERT INTO sessions (id, user_id, expires_at, user_agent) VALUES ($1, $2, NOW() + ($3 || ' days')::interval, $4) RETURNING id, user_id, created_at, expires_at`; const SELECT_ACTIVE_SESSION_SQL = `SELECT s.id, s.user_id, s.expires_at, u.username, u.role FROM sessions s JOIN users u ON u.id = s.user_id WHERE s.id = $1 AND s.expires_at > NOW()`; let ensureSessionsTablePromise = null; function generateSessionId() { if (typeof crypto.randomUUID === "function") { return crypto.randomUUID().replace(/-/g, "") + crypto.randomBytes(8).toString("hex"); } return crypto.randomBytes(32).toString("hex"); } function isUndefinedTableError(error) { return error && error.code === "42P01"; } async function ensureSessionsTable() { if (!ensureSessionsTablePromise) { ensureSessionsTablePromise = (async () => { await pool.query(`CREATE TABLE IF NOT EXISTS sessions ( id VARCHAR(128) PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ NOT NULL, last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), user_agent TEXT );`); await pool.query( "CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);" ); await pool.query( "CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);" ); })().catch((error) => { ensureSessionsTablePromise = null; throw error; }); } await ensureSessionsTablePromise; } async function insertSession(id, userId, userAgent) { const result = await pool.query(INSERT_SESSION_SQL, [ id, userId, String(SESSION_TTL_DAYS), userAgent, ]); return result.rows[0]; } exports.createSession = async (userId, userAgent = null) => { const id = generateSessionId(); try { return await insertSession(id, userId, userAgent); } catch (error) { if (!isUndefinedTableError(error)) { throw error; } await ensureSessionsTable(); return insertSession(id, userId, userAgent); } }; exports.getActiveSessionWithUser = async (sessionId) => { let result; try { result = await pool.query(SELECT_ACTIVE_SESSION_SQL, [sessionId]); } catch (error) { if (isUndefinedTableError(error)) { return null; } throw error; } const session = result.rows[0] || null; if (!session) return null; try { await pool.query( `UPDATE sessions SET last_seen_at = NOW() WHERE id = $1`, [sessionId] ); } catch (error) { if (!isUndefinedTableError(error)) { throw error; } } return session; }; exports.deleteSession = async (sessionId) => { try { await pool.query( `DELETE FROM sessions WHERE id = $1`, [sessionId] ); } catch (error) { if (!isUndefinedTableError(error)) { throw error; } } };