Skip to content

Commit 61f33d7

Browse files
authored
Merge pull request #51 from anandshukla15/feature/session-expiry
feat: session expiry feature implemented
2 parents afbc425 + feaedf3 commit 61f33d7

File tree

9 files changed

+143
-86
lines changed

9 files changed

+143
-86
lines changed

backend/package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"jsonwebtoken": "^9.0.2",
2121
"mongodb": "^6.20.0",
2222
"mongoose": "^8.19.2",
23+
"node-cron": "^4.2.1",
2324
"passport": "^0.7.0",
2425
"passport-github2": "^0.1.12",
2526
"passport-google-oauth20": "^2.0.0",

backend/src/controllers/authController.ts

Lines changed: 36 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
} from "../utils/generateToken.js";
99
import { userSchema, loginSchema } from "../utils/validateInputs.js";
1010
import dotenv from "dotenv";
11+
import jwt from "jsonwebtoken";
12+
import { Session } from "../models/sessionModel.js";
1113

1214
dotenv.config();
1315
const asTypedUser = (user: any): IUser & { _id: string } =>
@@ -67,8 +69,25 @@ export const registerUser = async (
6769
const newUser = await User.create({ name, email, password: hashedPassword });
6870
const typedUser = asTypedUser(newUser);
6971

70-
// Generate and send tokens
71-
sendTokens(res, typedUser);
72+
const token = generateToken(typedUser._id.toString());
73+
const decoded = jwt.decode(token) as { exp?: number } | null;
74+
75+
if (!decoded || !decoded.exp) {
76+
throw new Error("Invalid token format or missing expiration");
77+
}
78+
79+
const expiresAt = new Date(decoded.exp * 1000);
80+
await Session.create({
81+
userId: typedUser._id,
82+
token,
83+
expiresAt,
84+
});
85+
86+
res.status(201).json({
87+
success: true,
88+
message: "User registered successfully",
89+
token,
90+
});
7291
} catch (err) {
7392
next(err);
7493
}
@@ -112,59 +131,26 @@ export const loginUser = async (
112131

113132
const typedUser = asTypedUser(foundUser);
114133

115-
// Generate and send tokens
116-
await typedUser.save(); // Save any potential changes (like refresh tokens)
117-
sendTokens(res, typedUser);
118-
} catch (err) {
119-
next(err);
120-
}
121-
};
134+
const token = generateToken(typedUser._id.toString());
135+
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { exp?: number };
122136

123-
// ✅ OAUTH CALLBACK CONTROLLER (New)
124-
export const oauthCallback = async (
125-
req: Request,
126-
res: Response,
127-
next: NextFunction
128-
) => {
129-
try {
130-
if (!req.user) {
131-
return res.status(401).json({ success: false, message: "Authentication failed" });
132-
}
137+
if (!decoded.exp) {
138+
throw new Error("Token missing expiration claim");
139+
}
133140

134-
const typedUser = asTypedUser(req.user);
141+
const expiresAt = new Date(decoded.exp * 1000);
135142

136-
// We get the user profile from passport's `done` function
137-
// (which you'd have in src/utils/passport.ts)
138-
// Now we just generate and send tokens
143+
await Session.create({
144+
userId: typedUser._id,
145+
token,
146+
expiresAt,
147+
});
139148

140-
// Find the user in DB (req.user is from passport)
141-
const foundUser = await User.findById(typedUser._id);
142-
if (!foundUser) {
143-
return res.status(404).json({ success: false, message: "User not found" });
144-
}
145-
146-
const typedFoundUser = asTypedUser(foundUser);
147-
148-
// We send tokens the same way, but redirect the user
149-
const accessToken = generateAccessToken(typedFoundUser._id.toString());
150-
const newRefreshToken = generateRefreshToken(typedFoundUser._id.toString());
151-
152-
typedFoundUser.refreshTokens = [newRefreshToken];
153-
await typedFoundUser.save();
154-
155-
res.cookie("jwt", newRefreshToken, {
156-
httpOnly: true,
157-
secure: process.env.NODE_ENV !== "development",
158-
sameSite: "strict",
159-
maxAge: 7 * 24 * 60 * 60 * 1000,
160-
});
161149

162-
// Set the access token in a secure, HTTP-only cookie
163-
res.cookie("access_token", accessToken, {
164-
httpOnly: true,
165-
secure: process.env.NODE_ENV !== "development",
166-
sameSite: "strict",
167-
maxAge: 15 * 60 * 1000, // 15 minutes, adjust as needed
150+
res.json({
151+
success: true,
152+
message: "Login successful",
153+
token,
168154
});
169155

170156
// Redirect to the frontend without passing the token in the URL
Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,49 @@
11
import type { Request, Response, NextFunction } from "express";
22
import jwt from "jsonwebtoken";
3+
import { Session } from "../models/sessionModel.js";
34

45
interface AuthRequest extends Request {
5-
userId?: string;
6+
userId?: string;
67
}
78

8-
export const protect = (req: AuthRequest, res: Response, next: NextFunction) => {
9-
let token = req.headers.authorization?.split(" ")[1];
109

11-
if (!token)
12-
return res
13-
.status(401)
14-
.json({ success: false, message: "Not authorized, token missing" });
10+
export const protect = async (req: AuthRequest, res: Response, next: NextFunction) => {
11+
try {
12+
const token = req.headers.authorization?.split(" ")[1];
13+
if (!token) {
14+
return res.status(401).json({
15+
success: false,
16+
message: "Not authorized — token missing",
17+
});
18+
}
19+
20+
21+
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { id: string };
1522

16-
try {
17-
const decoded = jwt.verify(
18-
token,
19-
process.env.JWT_ACCESS_SECRET as string // <-- UPDATED ENV VARIABLE
20-
) as { id: string };
21-
req.userId = decoded.id;
22-
next();
23-
} catch {
24-
// Note: This will now correctly trigger a 401 on an expired access token
25-
res.status(401).json({ success: false, message: "Invalid token" });
23+
24+
const activeSession = await Session.findOne({ token });
25+
if (!activeSession) {
26+
return res.status(401).json({
27+
success: false,
28+
message: "Session expired or invalid",
29+
});
2630
}
27-
};
31+
32+
33+
if (activeSession.expiresAt < new Date()) {
34+
await Session.deleteOne({ token });
35+
return res.status(401).json({
36+
success: false,
37+
message: "Session expired",
38+
});
39+
}
40+
41+
req.userId = decoded.id;
42+
next();
43+
} catch (error) {
44+
return res.status(401).json({
45+
success: false,
46+
message: "Invalid or expired token",
47+
});
48+
}
49+
};

backend/src/models/sessionModel.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import mongoose from "mongoose";
2+
3+
const sessionSchema = new mongoose.Schema({
4+
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
5+
token: { type: String, required: true },
6+
expiresAt: { type: Date, required: true },
7+
createdAt: { type: Date, default: Date.now },
8+
});
9+
10+
11+
export const Session = mongoose.model("Session", sessionSchema);

backend/src/routes/authRoutes.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import express from "express";
22
import { registerUser, loginUser, getUserProfile} from "../controllers/authController.js";
33
import passport from "passport";
4-
import { protect } from "../middleware/authMiddleware.js";
4+
import { Session } from "../models/sessionModel.js";
5+
import {protect} from "../middleware/authMiddleware.js";
56

67
const router = express.Router();
78

@@ -12,6 +13,26 @@ router.post("/logout", logoutUser); // <-- ADDED
1213
router.get("/refresh", handleRefreshToken); // <-- ADDED
1314
router.get("/me", protect, getUserProfile);
1415

16+
router.post("/logout", protect, async (req, res) => {
17+
try {
18+
const token = req.headers.authorization?.split(" ")[1];
19+
if (!token)
20+
return res.status(400).json({ success: false, message: "Token missing" });
21+
22+
// Delete session for this token
23+
const result = await Session.deleteOne({ token });
24+
25+
if (result.deletedCount === 0) {
26+
return res.status(404).json({ success: false, message: "Session not found or already logged out" });
27+
}
28+
29+
res.json({ success: true, message: "Logged out successfully" });
30+
} catch (error) {
31+
console.error("❌ Logout error:", error);
32+
res.status(500).json({ success: false, message: "Server error during logout" });
33+
}
34+
});
35+
1536
// Google OAuth
1637
router.get(
1738
"/google",

backend/src/server.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { createServer } from "http";
44
import { Server as SocketIOServer } from "socket.io";
55
import dotenv from "dotenv";
66
import cors from "cors";
7+
8+
import { Session } from "./models/sessionModel.js";
79
import { ChatMessage } from "./models/chatMessageModel.js"; // <-- make sure this file exists and exports model
810
import app from "./app.js";
911

@@ -101,6 +103,20 @@ mongoose
101103
.connect(MONGO_URI)
102104
.then(() => {
103105
console.log("🗄️ MongoDB connected successfully!");
106+
107+
108+
// cron.schedule("0 2 * * *", async () => {
109+
// const expiryDate = new Date();
110+
// expiryDate.setDate(expiryDate.getDate() - 7);
111+
// try {
112+
// const result = await Session.deleteMany({ createdAt: { $lt: expiryDate } });
113+
// console.log(`🧹 Cleanup complete — ${result.deletedCount} expired sessions removed`);
114+
// } catch (error) {
115+
// console.error("❌ Session cleanup failed:", error);
116+
// }
117+
// });
118+
119+
104120
httpServer.listen(PORT, () => {
105121
console.log(`🚀 Server running on port ${PORT}`);
106122
console.log(`📡 Socket.io real-time chat ready`);

backend/src/utils/generateToken.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,12 @@ import dotenv from 'dotenv';
44
dotenv.config();
55
const accessTokenSecret = process.env.JWT_ACCESS_SECRET;
66

7-
const refreshTokenSecret = process.env.JWT_REFRESH_SECRET;
8-
9-
const parseExpiration = (val: string | undefined, fallback: number | string): number | string => {
10-
if (!val) return fallback;
11-
const trimmed = val.trim();
12-
return /^\d+$/.test(trimmed) ? Number(trimmed) : trimmed;
13-
};
14-
15-
export const generateAccessToken = (id: string) => {
16-
if (!accessTokenSecret) throw new Error("JWT_ACCESS_SECRET is not defined");
17-
18-
const options = {
19-
expiresIn: parseExpiration(process.env.JWT_ACCESS_EXPIRATION, 900),
20-
} as SignOptions;
21-
22-
return jwt.sign({ id }, accessTokenSecret, options);
7+
export const generateToken = (userId: string) => {
8+
const expiresIn = "7d";
9+
const token = jwt.sign({ id: userId }, process.env.JWT_SECRET as string, {
10+
expiresIn,
11+
});
12+
return token;
2313
};
2414

2515
export const generateRefreshToken = (id: string) => {

backend/tsconfig.tsbuildinfo

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"root":["./src/app.ts","./src/server.ts","./src/controllers/authcontroller.ts","./src/controllers/healthcontroller.ts","./src/controllers/roomcontroller.ts","./src/middleware/authmiddleware.ts","./src/middleware/errorhandler.ts","./src/models/chatmessagemodel.ts","./src/models/roommodel.ts","./src/models/usermodel.ts","./src/routes/authroutes.ts","./src/routes/healthroutes.ts","./src/routes/roomroutes.ts","./src/utils/generatetoken.ts","./src/utils/passport.ts","./src/utils/validateinputs.ts"],"version":"5.9.3"}
1+
{"root":["./src/app.ts","./src/server.ts","./src/controllers/authcontroller.ts","./src/controllers/healthcontroller.ts","./src/controllers/roomcontroller.ts","./src/middleware/authmiddleware.ts","./src/middleware/errorhandler.ts","./src/models/chatmessagemodel.ts","./src/models/roommodel.ts","./src/models/sessionmodel.ts","./src/models/usermodel.ts","./src/routes/authroutes.ts","./src/routes/healthroutes.ts","./src/routes/roomroutes.ts","./src/utils/generatetoken.ts","./src/utils/passport.ts","./src/utils/validateinputs.ts"],"version":"5.9.3"}

0 commit comments

Comments
 (0)