From 6fd37e975eb77911784458513f446bb76af5a39a Mon Sep 17 00:00:00 2001 From: Ridanshi Date: Wed, 27 May 2026 00:49:52 +0530 Subject: [PATCH] fix(session): persist sessions in Mongo --- README.md | 10 +++ backend/config/session.js | 32 ++++++++++ backend/package.json | 1 + backend/server.js | 10 +-- package.json | 1 + spec/session.config.spec.cjs | 115 +++++++++++++++++++++++++++++++++++ 6 files changed, 164 insertions(+), 5 deletions(-) create mode 100644 backend/config/session.js create mode 100644 spec/session.config.spec.cjs diff --git a/README.md b/README.md index a747b53a..297f9ede 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,16 @@ $ npm i $ npm start ``` +### Backend Environment Variables + +Create `backend/.env` for local backend runs: + +| Variable | Required | Purpose | +| --- | --- | --- | +| `MONGO_URI` | Yes | MongoDB connection string used by Mongoose and the persistent session store | +| `SESSION_SECRET` | Yes | Secret used to sign Express session cookies | +| `NODE_ENV` | No | Set to `production` to send session cookies only over HTTPS | + ## 🧪 Backend Unit & Integration Testing with Jasmine This project uses the Jasmine framework for backend unit and integration tests. The tests cover: diff --git a/backend/config/session.js b/backend/config/session.js new file mode 100644 index 00000000..2f5d7800 --- /dev/null +++ b/backend/config/session.js @@ -0,0 +1,32 @@ +const MongoStore = require('connect-mongo'); + +const SESSION_TTL_SECONDS = 14 * 24 * 60 * 60; + +function createSessionConfig({ + mongoUrl = process.env.MONGO_URI, + sessionSecret = process.env.SESSION_SECRET, + nodeEnv = process.env.NODE_ENV, + storeFactory = MongoStore, +} = {}) { + if (!mongoUrl) { + throw new Error('MONGO_URI is required to configure the session store'); + } + + return { + secret: sessionSecret, + resave: false, + saveUninitialized: false, + store: storeFactory.create({ + mongoUrl, + ttl: SESSION_TTL_SECONDS, + }), + cookie: { + secure: nodeEnv === 'production', + }, + }; +} + +module.exports = { + SESSION_TTL_SECONDS, + createSessionConfig, +}; diff --git a/backend/package.json b/backend/package.json index 74ab9dd7..af00fdb4 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,6 +14,7 @@ "dependencies": { "bcryptjs": "^2.4.3", "body-parser": "^1.20.3", + "connect-mongo": "^5.1.0", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.1", diff --git a/backend/server.js b/backend/server.js index 48d6ccfb..f861a259 100644 --- a/backend/server.js +++ b/backend/server.js @@ -5,6 +5,7 @@ const passport = require('passport'); const bodyParser = require('body-parser'); require('dotenv').config(); const cors = require('cors'); +const { createSessionConfig } = require('./config/session'); // Passport configuration require('./config/passportConfig'); @@ -28,11 +29,10 @@ app.use(cors({ // Middleware app.use(bodyParser.json()); -app.use(session({ - secret: process.env.SESSION_SECRET, - resave: false, - saveUninitialized: false, -})); +if (process.env.NODE_ENV === 'production') { + app.set('trust proxy', 1); +} +app.use(session(createSessionConfig())); app.use(passport.initialize()); app.use(passport.session()); diff --git a/package.json b/package.json index 43ad31cc..c96bd9be 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@primer/octicons-react": "^19.25.0", "@vitejs/plugin-react": "^4.3.3", "axios": "^1.7.7", + "connect-mongo": "^5.1.0", "express": "^5.2.1", "framer-motion": "^12.23.12", "lucide-react": "^0.525.0", diff --git a/spec/session.config.spec.cjs b/spec/session.config.spec.cjs new file mode 100644 index 00000000..7722904a --- /dev/null +++ b/spec/session.config.spec.cjs @@ -0,0 +1,115 @@ +const express = require('express'); +const request = require('supertest'); +const session = require('express-session'); + +const { SESSION_TTL_SECONDS, createSessionConfig } = require('../backend/config/session'); + +class TestSessionStore extends session.Store { + constructor() { + super(); + this.sessions = new Map(); + } + + get(sid, callback) { + const sessionData = this.sessions.get(sid); + callback(null, sessionData ? JSON.parse(sessionData) : null); + } + + set(sid, sessionData, callback) { + this.sessions.set(sid, JSON.stringify(sessionData)); + callback(null); + } + + destroy(sid, callback) { + this.sessions.delete(sid); + callback(null); + } +} + +function createStoreFactory(store = new TestSessionStore()) { + const calls = []; + + return { + calls, + store, + create(options) { + calls.push(options); + return store; + }, + }; +} + +describe('Session configuration', () => { + it('initializes connect-mongo with the configured Mongo URL and TTL', () => { + const storeFactory = createStoreFactory(); + + const config = createSessionConfig({ + mongoUrl: 'mongodb://127.0.0.1:27017/github_tracker_test', + sessionSecret: 'test-secret', + storeFactory, + }); + + expect(storeFactory.calls).toEqual([{ + mongoUrl: 'mongodb://127.0.0.1:27017/github_tracker_test', + ttl: SESSION_TTL_SECONDS, + }]); + expect(config.store).toBe(storeFactory.store); + expect(SESSION_TTL_SECONDS).toBe(14 * 24 * 60 * 60); + }); + + it('does not fall back to express-session MemoryStore', () => { + const config = createSessionConfig({ + mongoUrl: 'mongodb://127.0.0.1:27017/github_tracker_test', + sessionSecret: 'test-secret', + storeFactory: createStoreFactory(), + }); + + expect(config.store).toBeDefined(); + expect(config.store instanceof session.MemoryStore).toBeFalse(); + }); + + it('uses secure cookies in production only', () => { + const productionConfig = createSessionConfig({ + mongoUrl: 'mongodb://127.0.0.1:27017/github_tracker_test', + sessionSecret: 'test-secret', + nodeEnv: 'production', + storeFactory: createStoreFactory(), + }); + const developmentConfig = createSessionConfig({ + mongoUrl: 'mongodb://127.0.0.1:27017/github_tracker_test', + sessionSecret: 'test-secret', + nodeEnv: 'development', + storeFactory: createStoreFactory(), + }); + + expect(productionConfig.cookie.secure).toBeTrue(); + expect(developmentConfig.cookie.secure).toBeFalse(); + }); + + it('requires MongoDB configuration for persistent sessions', () => { + expect(() => createSessionConfig({ + mongoUrl: '', + sessionSecret: 'test-secret', + storeFactory: createStoreFactory(), + })).toThrowError(/MONGO_URI/); + }); + + it('persists session data through the configured store', async () => { + const app = express(); + + app.use(session(createSessionConfig({ + mongoUrl: 'mongodb://127.0.0.1:27017/github_tracker_test', + sessionSecret: 'test-secret', + storeFactory: createStoreFactory(), + }))); + app.get('/count', (req, res) => { + req.session.views = (req.session.views || 0) + 1; + res.json({ views: req.session.views }); + }); + + const agent = request.agent(app); + + await agent.get('/count').expect(200, { views: 1 }); + await agent.get('/count').expect(200, { views: 2 }); + }); +});