Skip to content

Commit 6fd37e9

Browse files
committed
fix(session): persist sessions in Mongo
1 parent 4ae0ef6 commit 6fd37e9

6 files changed

Lines changed: 164 additions & 5 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,16 @@ $ npm i
6363
$ npm start
6464
```
6565

66+
### Backend Environment Variables
67+
68+
Create `backend/.env` for local backend runs:
69+
70+
| Variable | Required | Purpose |
71+
| --- | --- | --- |
72+
| `MONGO_URI` | Yes | MongoDB connection string used by Mongoose and the persistent session store |
73+
| `SESSION_SECRET` | Yes | Secret used to sign Express session cookies |
74+
| `NODE_ENV` | No | Set to `production` to send session cookies only over HTTPS |
75+
6676
## 🧪 Backend Unit & Integration Testing with Jasmine
6777

6878
This project uses the Jasmine framework for backend unit and integration tests. The tests cover:

backend/config/session.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const MongoStore = require('connect-mongo');
2+
3+
const SESSION_TTL_SECONDS = 14 * 24 * 60 * 60;
4+
5+
function createSessionConfig({
6+
mongoUrl = process.env.MONGO_URI,
7+
sessionSecret = process.env.SESSION_SECRET,
8+
nodeEnv = process.env.NODE_ENV,
9+
storeFactory = MongoStore,
10+
} = {}) {
11+
if (!mongoUrl) {
12+
throw new Error('MONGO_URI is required to configure the session store');
13+
}
14+
15+
return {
16+
secret: sessionSecret,
17+
resave: false,
18+
saveUninitialized: false,
19+
store: storeFactory.create({
20+
mongoUrl,
21+
ttl: SESSION_TTL_SECONDS,
22+
}),
23+
cookie: {
24+
secure: nodeEnv === 'production',
25+
},
26+
};
27+
}
28+
29+
module.exports = {
30+
SESSION_TTL_SECONDS,
31+
createSessionConfig,
32+
};

backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"dependencies": {
1515
"bcryptjs": "^2.4.3",
1616
"body-parser": "^1.20.3",
17+
"connect-mongo": "^5.1.0",
1718
"cors": "^2.8.5",
1819
"dotenv": "^16.4.5",
1920
"express": "^4.21.1",

backend/server.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const passport = require('passport');
55
const bodyParser = require('body-parser');
66
require('dotenv').config();
77
const cors = require('cors');
8+
const { createSessionConfig } = require('./config/session');
89

910
// Passport configuration
1011
require('./config/passportConfig');
@@ -28,11 +29,10 @@ app.use(cors({
2829

2930
// Middleware
3031
app.use(bodyParser.json());
31-
app.use(session({
32-
secret: process.env.SESSION_SECRET,
33-
resave: false,
34-
saveUninitialized: false,
35-
}));
32+
if (process.env.NODE_ENV === 'production') {
33+
app.set('trust proxy', 1);
34+
}
35+
app.use(session(createSessionConfig()));
3636
app.use(passport.initialize());
3737
app.use(passport.session());
3838

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@primer/octicons-react": "^19.25.0",
2424
"@vitejs/plugin-react": "^4.3.3",
2525
"axios": "^1.7.7",
26+
"connect-mongo": "^5.1.0",
2627
"express": "^5.2.1",
2728
"framer-motion": "^12.23.12",
2829
"lucide-react": "^0.525.0",

spec/session.config.spec.cjs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
const express = require('express');
2+
const request = require('supertest');
3+
const session = require('express-session');
4+
5+
const { SESSION_TTL_SECONDS, createSessionConfig } = require('../backend/config/session');
6+
7+
class TestSessionStore extends session.Store {
8+
constructor() {
9+
super();
10+
this.sessions = new Map();
11+
}
12+
13+
get(sid, callback) {
14+
const sessionData = this.sessions.get(sid);
15+
callback(null, sessionData ? JSON.parse(sessionData) : null);
16+
}
17+
18+
set(sid, sessionData, callback) {
19+
this.sessions.set(sid, JSON.stringify(sessionData));
20+
callback(null);
21+
}
22+
23+
destroy(sid, callback) {
24+
this.sessions.delete(sid);
25+
callback(null);
26+
}
27+
}
28+
29+
function createStoreFactory(store = new TestSessionStore()) {
30+
const calls = [];
31+
32+
return {
33+
calls,
34+
store,
35+
create(options) {
36+
calls.push(options);
37+
return store;
38+
},
39+
};
40+
}
41+
42+
describe('Session configuration', () => {
43+
it('initializes connect-mongo with the configured Mongo URL and TTL', () => {
44+
const storeFactory = createStoreFactory();
45+
46+
const config = createSessionConfig({
47+
mongoUrl: 'mongodb://127.0.0.1:27017/github_tracker_test',
48+
sessionSecret: 'test-secret',
49+
storeFactory,
50+
});
51+
52+
expect(storeFactory.calls).toEqual([{
53+
mongoUrl: 'mongodb://127.0.0.1:27017/github_tracker_test',
54+
ttl: SESSION_TTL_SECONDS,
55+
}]);
56+
expect(config.store).toBe(storeFactory.store);
57+
expect(SESSION_TTL_SECONDS).toBe(14 * 24 * 60 * 60);
58+
});
59+
60+
it('does not fall back to express-session MemoryStore', () => {
61+
const config = createSessionConfig({
62+
mongoUrl: 'mongodb://127.0.0.1:27017/github_tracker_test',
63+
sessionSecret: 'test-secret',
64+
storeFactory: createStoreFactory(),
65+
});
66+
67+
expect(config.store).toBeDefined();
68+
expect(config.store instanceof session.MemoryStore).toBeFalse();
69+
});
70+
71+
it('uses secure cookies in production only', () => {
72+
const productionConfig = createSessionConfig({
73+
mongoUrl: 'mongodb://127.0.0.1:27017/github_tracker_test',
74+
sessionSecret: 'test-secret',
75+
nodeEnv: 'production',
76+
storeFactory: createStoreFactory(),
77+
});
78+
const developmentConfig = createSessionConfig({
79+
mongoUrl: 'mongodb://127.0.0.1:27017/github_tracker_test',
80+
sessionSecret: 'test-secret',
81+
nodeEnv: 'development',
82+
storeFactory: createStoreFactory(),
83+
});
84+
85+
expect(productionConfig.cookie.secure).toBeTrue();
86+
expect(developmentConfig.cookie.secure).toBeFalse();
87+
});
88+
89+
it('requires MongoDB configuration for persistent sessions', () => {
90+
expect(() => createSessionConfig({
91+
mongoUrl: '',
92+
sessionSecret: 'test-secret',
93+
storeFactory: createStoreFactory(),
94+
})).toThrowError(/MONGO_URI/);
95+
});
96+
97+
it('persists session data through the configured store', async () => {
98+
const app = express();
99+
100+
app.use(session(createSessionConfig({
101+
mongoUrl: 'mongodb://127.0.0.1:27017/github_tracker_test',
102+
sessionSecret: 'test-secret',
103+
storeFactory: createStoreFactory(),
104+
})));
105+
app.get('/count', (req, res) => {
106+
req.session.views = (req.session.views || 0) + 1;
107+
res.json({ views: req.session.views });
108+
});
109+
110+
const agent = request.agent(app);
111+
112+
await agent.get('/count').expect(200, { views: 1 });
113+
await agent.get('/count').expect(200, { views: 2 });
114+
});
115+
});

0 commit comments

Comments
 (0)