From f4c9983612673afd5afa58af4a9ccbaa2da572d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sat, 30 May 2026 15:34:50 +0300 Subject: [PATCH] feat: PG session store via connect-pg-simple (src/middleware/session.js) --- package-lock.json | 12 ++++++++ package.json | 1 + server.js | 20 ++++--------- src/middleware/session.js | 60 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 14 deletions(-) create mode 100644 src/middleware/session.js diff --git a/package-lock.json b/package-lock.json index a6fec17..b03d800 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "ipwhitelist", "version": "0.1.0", "dependencies": { + "connect-pg-simple": "^10.0.0", "cookie-parser": "^1.4.7", "csrf-csrf": "^4.0.3", "dotenv": "^16.4.7", @@ -118,6 +119,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/connect-pg-simple": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-10.0.0.tgz", + "integrity": "sha512-pBGVazlqiMrackzCr0eKhn4LO5trJXsOX0nQoey9wCOayh80MYtThCbq8eoLsjpiWgiok/h+1/uti9/2/Una8A==", + "dependencies": { + "pg": "^8.12.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=22.0.0" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", diff --git a/package.json b/package.json index 95e670a..4b2227b 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "dev": "node --watch server.js" }, "dependencies": { + "connect-pg-simple": "^10.0.0", "cookie-parser": "^1.4.7", "csrf-csrf": "^4.0.3", "dotenv": "^16.4.7", diff --git a/server.js b/server.js index 4506caa..a08a5e6 100644 --- a/server.js +++ b/server.js @@ -17,7 +17,6 @@ 'use strict'; const express = require('express'); -const session = require('express-session'); const cookieParser = require('cookie-parser'); const path = require('path'); const helmet = require('helmet'); @@ -30,6 +29,7 @@ const q = require('./src/queries'); const { MOCK_USERS, backUrl } = require('./src/config'); const { mutationLimiter, exportLimiter } = require('./src/middleware/rateLimit'); const { initCsrf } = require('./src/middleware/csrf'); +const { createSessionMiddleware } = require('./src/middleware/session'); const app = express(); const PORT = process.env.PORT || 3000; @@ -53,19 +53,11 @@ async function start() { const auth = await initAuth(); // ── Сессии ──────────────────────────────────────────────────────────────── - // SESSION_SECRET из .env (обязателен в production). - // MemoryStore — только для одного инстанса; для multi-pod нужен Redis. - app.use(session({ - secret: process.env.SESSION_SECRET || 'dev-session-secret-change-me', - resave: false, - saveUninitialized: false, - cookie: { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', // только HTTPS в проде - sameSite: 'lax', - maxAge: 8 * 60 * 60 * 1000, // 8 часов - }, - })); + // PG-стор: сессии переживают рестарт, работают на нескольких подах. + // Таблица `session` создаётся автоматически при первом старте. + // src/middleware/session.js содержит логику и комментарии. + const { pool } = require('./src/db'); + app.use(createSessionMiddleware(pool)); // ── CSRF защита ──────────────────────────────────────────────────────────── // initCsrf() читает CSRF_SECRET и NODE_ENV из process.env. diff --git a/src/middleware/session.js b/src/middleware/session.js new file mode 100644 index 0000000..870c5a0 --- /dev/null +++ b/src/middleware/session.js @@ -0,0 +1,60 @@ +/** + * src/middleware/session.js — конфигурация express-session. + * + * Стор выбирается по наличию пула БД: + * - pool передан → connect-pg-simple (PG): сессии переживают рестарт, работают + * на нескольких подах. + * - pool не передан → MemoryStore: только для юнит-тестов (не требует БД). + * + * Таблица session создаётся автоматически connect-pg-simple при первом старте + * (createTableIfMissing: true). SQL-схема совместима с PostgreSQL 9.6+. + * + * Экспортирует одну функцию: createSessionMiddleware(pool) → express middleware. + */ + +'use strict'; + +const session = require('express-session'); +const ConnectPgSimple = require('connect-pg-simple'); + +const SESSION_MAX_AGE = 8 * 60 * 60 * 1000; // 8 часов, в мс + +/** + * Создаёт и возвращает сконфигурированный express-session middleware. + * @param {import('pg').Pool|null} pool — pg Pool; null → MemoryStore (тесты) + * @returns {import('express').RequestHandler} + */ +function createSessionMiddleware(pool) { + const store = pool ? buildPgStore(pool) : undefined; // undefined → MemoryStore + + return session({ + store, + secret: process.env.SESSION_SECRET || 'dev-session-secret-change-me', + resave: false, + saveUninitialized: false, + cookie: { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', // HTTPS only в проде + sameSite: 'lax', + maxAge: SESSION_MAX_AGE, + }, + }); +} + +/** + * Создаёт connect-pg-simple Store поверх переданного пула. + * Таблица `session` создаётся автоматически если отсутствует. + * @param {import('pg').Pool} pool + * @returns {session.Store} + */ +function buildPgStore(pool) { + const PgSession = ConnectPgSimple(session); + return new PgSession({ + pool, + createTableIfMissing: true, + // Автоочистка протухших сессий раз в час (прунинг не блокирует запросы). + pruneSessionInterval: 60 * 60, // секунды + }); +} + +module.exports = { createSessionMiddleware };