feat: PG session store via connect-pg-simple (src/middleware/session.js)
This commit is contained in:
Generated
+12
@@ -8,6 +8,7 @@
|
|||||||
"name": "ipwhitelist",
|
"name": "ipwhitelist",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"connect-pg-simple": "^10.0.0",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"csrf-csrf": "^4.0.3",
|
"csrf-csrf": "^4.0.3",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
@@ -118,6 +119,17 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/content-disposition": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"dev": "node --watch server.js"
|
"dev": "node --watch server.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"connect-pg-simple": "^10.0.0",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"csrf-csrf": "^4.0.3",
|
"csrf-csrf": "^4.0.3",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const session = require('express-session');
|
|
||||||
const cookieParser = require('cookie-parser');
|
const cookieParser = require('cookie-parser');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const helmet = require('helmet');
|
const helmet = require('helmet');
|
||||||
@@ -30,6 +29,7 @@ const q = require('./src/queries');
|
|||||||
const { MOCK_USERS, backUrl } = require('./src/config');
|
const { MOCK_USERS, backUrl } = require('./src/config');
|
||||||
const { mutationLimiter, exportLimiter } = require('./src/middleware/rateLimit');
|
const { mutationLimiter, exportLimiter } = require('./src/middleware/rateLimit');
|
||||||
const { initCsrf } = require('./src/middleware/csrf');
|
const { initCsrf } = require('./src/middleware/csrf');
|
||||||
|
const { createSessionMiddleware } = require('./src/middleware/session');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
@@ -53,19 +53,11 @@ async function start() {
|
|||||||
const auth = await initAuth();
|
const auth = await initAuth();
|
||||||
|
|
||||||
// ── Сессии ────────────────────────────────────────────────────────────────
|
// ── Сессии ────────────────────────────────────────────────────────────────
|
||||||
// SESSION_SECRET из .env (обязателен в production).
|
// PG-стор: сессии переживают рестарт, работают на нескольких подах.
|
||||||
// MemoryStore — только для одного инстанса; для multi-pod нужен Redis.
|
// Таблица `session` создаётся автоматически при первом старте.
|
||||||
app.use(session({
|
// src/middleware/session.js содержит логику и комментарии.
|
||||||
secret: process.env.SESSION_SECRET || 'dev-session-secret-change-me',
|
const { pool } = require('./src/db');
|
||||||
resave: false,
|
app.use(createSessionMiddleware(pool));
|
||||||
saveUninitialized: false,
|
|
||||||
cookie: {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production', // только HTTPS в проде
|
|
||||||
sameSite: 'lax',
|
|
||||||
maxAge: 8 * 60 * 60 * 1000, // 8 часов
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// ── CSRF защита ────────────────────────────────────────────────────────────
|
// ── CSRF защита ────────────────────────────────────────────────────────────
|
||||||
// initCsrf() читает CSRF_SECRET и NODE_ENV из process.env.
|
// initCsrf() читает CSRF_SECRET и NODE_ENV из process.env.
|
||||||
|
|||||||
@@ -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 };
|
||||||
Reference in New Issue
Block a user