From c1259f0bf5a25215daae46301cb608f70c9b0b0a Mon Sep 17 00:00:00 2001 From: Nico Date: Wed, 18 Feb 2026 14:52:35 -0800 Subject: [PATCH] fix: recover when sessions table is missing --- backend/models/session.model.js | 129 ++++++++++++++++++++++++-------- 1 file changed, 98 insertions(+), 31 deletions(-) diff --git a/backend/models/session.model.js b/backend/models/session.model.js index 6eb5988..4216db6 100644 --- a/backend/models/session.model.js +++ b/backend/models/session.model.js @@ -2,27 +2,10 @@ const crypto = require("crypto"); const pool = require("../db/pool"); const { SESSION_TTL_DAYS } = require("../utils/session-cookie"); -function generateSessionId() { - if (typeof crypto.randomUUID === "function") { - return crypto.randomUUID().replace(/-/g, "") + crypto.randomBytes(8).toString("hex"); - } - return crypto.randomBytes(32).toString("hex"); -} - -exports.createSession = async (userId, userAgent = null) => { - const id = generateSessionId(); - const result = await pool.query( - `INSERT INTO sessions (id, user_id, expires_at, user_agent) +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`, - [id, userId, String(SESSION_TTL_DAYS), userAgent] - ); - return result.rows[0]; -}; - -exports.getActiveSessionWithUser = async (sessionId) => { - const result = await pool.query( - `SELECT + RETURNING id, user_id, created_at, expires_at`; +const SELECT_ACTIVE_SESSION_SQL = `SELECT s.id, s.user_id, s.expires_at, @@ -31,26 +14,110 @@ exports.getActiveSessionWithUser = async (sessionId) => { FROM sessions s JOIN users u ON u.id = s.user_id WHERE s.id = $1 - AND s.expires_at > NOW()`, - [sessionId] - ); + 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; - await pool.query( - `UPDATE sessions + try { + await pool.query( + `UPDATE sessions SET last_seen_at = NOW() WHERE id = $1`, - [sessionId] - ); + [sessionId] + ); + } catch (error) { + if (!isUndefinedTableError(error)) { + throw error; + } + } return session; }; exports.deleteSession = async (sessionId) => { - await pool.query( - `DELETE FROM sessions WHERE id = $1`, - [sessionId] - ); + try { + await pool.query( + `DELETE FROM sessions WHERE id = $1`, + [sessionId] + ); + } catch (error) { + if (!isUndefinedTableError(error)) { + throw error; + } + } };