Skip to content

Commit 0ce6ca8

Browse files
Merge branch 'main' into fix-password-validation
2 parents 30f1584 + 987908b commit 0ce6ca8

47 files changed

Lines changed: 2959 additions & 333 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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/passportConfig.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ passport.serializeUser((user, done) => {
3838
passport.deserializeUser(async (id, done) => {
3939
try {
4040
const user = await User.findById(id);
41-
done(null, user);
41+
done(null, user ? {
42+
id: user._id.toString(),
43+
username: user.username,
44+
email: user.email
45+
} : null);
4246
} catch (err) {
4347
done(err, null);
4448
}

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/routes/auth.js

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const router = express.Router();
99
router.post("/signup", validateRequest(signupSchema), async (req, res) => {
1010

1111
const { username, email, password } = req.body;
12-
12+
1313
try {
1414
const existingUser = await User.findOne({
1515
$or: [{ email }, { username }],
@@ -30,6 +30,17 @@ router.post("/signup", validateRequest(signupSchema), async (req, res) => {
3030
}
3131
});
3232

33+
// Session status route
34+
router.get("/me", (req, res) => {
35+
const isAuthenticated = typeof req.isAuthenticated === "function" && req.isAuthenticated();
36+
37+
if (!isAuthenticated) {
38+
return res.status(200).json({ authenticated: false, user: null });
39+
}
40+
41+
return res.status(200).json({ authenticated: true, user: req.user });
42+
});
43+
3344
// Login route
3445
router.post("/login", validateRequest(loginSchema), passport.authenticate('local'), (req, res) => {
3546
res.status(200).json( { message: 'Login successful', user: req.user } );
@@ -40,10 +51,18 @@ router.get("/logout", (req, res) => {
4051

4152
req.logout((err) => {
4253

43-
if (err)
54+
if (err) {
4455
return res.status(500).json({ message: 'Logout failed', error: err.message });
45-
else
46-
res.status(200).json({ message: 'Logged out successfully' });
56+
}
57+
58+
req.session.destroy((sessionErr) => {
59+
if (sessionErr) {
60+
return res.status(500).json({ message: 'Logout failed', error: sessionErr.message });
61+
}
62+
63+
res.clearCookie('connect.sid');
64+
return res.status(200).json({ message: 'Logged out successfully' });
65+
});
4766
});
4867
});
4968

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: 2 additions & 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",
@@ -31,6 +32,7 @@
3132
"postcss": "^8.4.47",
3233
"react": "^18.3.1",
3334
"react-dom": "^18.3.1",
35+
"react-github-calendar": "^5.0.6",
3436
"react-hot-toast": "^2.4.1",
3537
"react-icons": "^5.3.0",
3638
"react-router-dom": "^6.28.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+
});

src/App.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111
will-change: filter;
1212
transition: filter 300ms;
1313
}
14+
1415
.logo:hover {
1516
filter: drop-shadow(0 0 2em #646cffaa);
1617
}
18+
1719
.logo.react:hover {
1820
filter: drop-shadow(0 0 2em #61dafbaa);
1921
}
@@ -22,6 +24,7 @@
2224
from {
2325
transform: rotate(0deg);
2426
}
27+
2528
to {
2629
transform: rotate(360deg);
2730
}
@@ -40,3 +43,11 @@
4043
.read-the-docs {
4144
color: #888;
4245
}
46+
47+
.calendar-container svg text {
48+
fill: #1f2937 !important;
49+
}
50+
51+
.dark .calendar-container svg text {
52+
fill: #d1d5db !important;
53+
}

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useLocation } from "react-router-dom";
22
import Navbar from "./components/Navbar";
33
import Footer from "./components/Footer";
44
import ScrollProgressBar from "./components/ScrollProgressBar";
5+
import ScrollNavigator from "./components/ScrollNavigator";
56
import { Toaster } from "react-hot-toast";
67
import Router from "./Routes/Router";
78

@@ -16,6 +17,7 @@ function App() {
1617
{!isFullscreen && <ScrollProgressBar />}
1718

1819
{!isFullscreen && <Navbar />}
20+
<ScrollNavigator />
1921

2022
<main className={`flex justify-center items-center ${isFullscreen ? "flex-1" : "flex-grow bg-gray-50 dark:bg-gray-800"}`}>
2123
<Router />

0 commit comments

Comments
 (0)