From 22a5a87ce85c70dbcac22ab5761b819e5c31a4c0 Mon Sep 17 00:00:00 2001 From: anandshukla15 Date: Sun, 26 Oct 2025 10:55:48 +0530 Subject: [PATCH 1/4] feat: session expiry feature implemented --- backend/package-lock.json | 12 +++++- backend/package.json | 1 + backend/src/controllers/authController.ts | 27 +++++++++++- backend/src/middleware/authMiddleware.ts | 50 ++++++++++++++++++----- backend/src/models/sessionModel.ts | 13 ++++++ backend/src/routes/authRoutes.ts | 22 ++++++++++ backend/src/server.ts | 16 ++++++++ backend/src/utils/generateToken.ts | 8 +++- backend/tsconfig.tsbuildinfo | 2 +- 9 files changed, 133 insertions(+), 18 deletions(-) create mode 100644 backend/src/models/sessionModel.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index 05e6086..bdf3436 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,6 +16,7 @@ "jsonwebtoken": "^9.0.2", "mongodb": "^6.20.0", "mongoose": "^8.19.2", + "node-cron": "^4.2.1", "passport": "^0.7.0", "passport-github2": "^0.1.12", "passport-google-oauth20": "^2.0.0", @@ -220,7 +221,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1585,6 +1585,15 @@ "node": ">= 0.6" } }, + "node_modules/node-cron": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", + "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", @@ -2501,7 +2510,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/backend/package.json b/backend/package.json index 9ecd185..f30a704 100644 --- a/backend/package.json +++ b/backend/package.json @@ -19,6 +19,7 @@ "jsonwebtoken": "^9.0.2", "mongodb": "^6.20.0", "mongoose": "^8.19.2", + "node-cron": "^4.2.1", "passport": "^0.7.0", "passport-github2": "^0.1.12", "passport-google-oauth20": "^2.0.0", diff --git a/backend/src/controllers/authController.ts b/backend/src/controllers/authController.ts index bb61a2b..a41b26d 100644 --- a/backend/src/controllers/authController.ts +++ b/backend/src/controllers/authController.ts @@ -4,6 +4,8 @@ import User, { type IUser } from "../models/userModel.js"; import { generateToken } from "../utils/generateToken.js"; import { userSchema, loginSchema } from "../utils/validateInputs.js"; import dotenv from "dotenv"; +import jwt from "jsonwebtoken"; +import { Session } from "../models/sessionModel.js"; dotenv.config(); const asTypedUser = (user: any): IUser & { _id: string } => user as IUser & { _id: string }; @@ -28,10 +30,20 @@ export const registerUser = async (req: Request, res: Response, next: NextFuncti const newUser = await User.create({ name, email, password: hashedPassword }); const typedUser = asTypedUser(newUser); + const token = generateToken(typedUser._id.toString()); +const decoded = jwt.decode(token) as { exp?: number }; +const expiresAt = new Date((decoded.exp ?? 0) * 1000); + +await Session.create({ + userId: typedUser._id, + token, + expiresAt, +}); + res.status(201).json({ success: true, message: "User registered successfully", - token: generateToken(typedUser._id.toString()), + token, }); } catch (err) { next(err); @@ -66,10 +78,21 @@ export const loginUser = async (req: Request, res: Response, next: NextFunction) const typedUser = asTypedUser(foundUser); + const token = generateToken(typedUser._id.toString()); +const decoded = jwt.decode(token) as { exp?: number }; +const expiresAt = new Date((decoded.exp ?? 0) * 1000); + +await Session.create({ + userId: typedUser._id, + token, + expiresAt, +}); + + res.json({ success: true, message: "Login successful", - token: generateToken(typedUser._id.toString()), + token, }); } catch (err) { next(err); diff --git a/backend/src/middleware/authMiddleware.ts b/backend/src/middleware/authMiddleware.ts index 0465cc5..6a0a13f 100644 --- a/backend/src/middleware/authMiddleware.ts +++ b/backend/src/middleware/authMiddleware.ts @@ -1,21 +1,49 @@ import type { Request, Response, NextFunction } from "express"; import jwt from "jsonwebtoken"; +import { Session } from "../models/sessionModel.js"; interface AuthRequest extends Request { - userId?: string; + userId?: string; } -export const protect = (req: AuthRequest, res: Response, next: NextFunction) => { - let token = req.headers.authorization?.split(" ")[1]; - if (!token) - return res.status(401).json({ success: false, message: "Not authorized, token missing" }); +export const protect = async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const token = req.headers.authorization?.split(" ")[1]; + if (!token) { + return res.status(401).json({ + success: false, + message: "Not authorized โ€” token missing", + }); + } + + + const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { id: string; exp: number }; - try { - const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { id: string }; - req.userId = decoded.id; - next(); - } catch { - res.status(401).json({ success: false, message: "Invalid token" }); + + const activeSession = await Session.findOne({ token }); + if (!activeSession) { + return res.status(401).json({ + success: false, + message: "Session expired or invalid", + }); } + + + if (activeSession.expiresAt < new Date()) { + await Session.deleteOne({ token }); + return res.status(401).json({ + success: false, + message: "Session expired", + }); + } + + req.userId = decoded.id; + next(); + } catch (error) { + return res.status(401).json({ + success: false, + message: "Invalid or expired token", + }); + } }; diff --git a/backend/src/models/sessionModel.ts b/backend/src/models/sessionModel.ts new file mode 100644 index 0000000..cbf02df --- /dev/null +++ b/backend/src/models/sessionModel.ts @@ -0,0 +1,13 @@ +import mongoose from "mongoose"; + +const sessionSchema = new mongoose.Schema({ + userId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, + token: { type: String, required: true }, + expiresAt: { type: Date, required: true }, + createdAt: { type: Date, default: Date.now }, +}); + + +sessionSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 }); + +export const Session = mongoose.model("Session", sessionSchema); diff --git a/backend/src/routes/authRoutes.ts b/backend/src/routes/authRoutes.ts index 5fb34c0..d533e88 100644 --- a/backend/src/routes/authRoutes.ts +++ b/backend/src/routes/authRoutes.ts @@ -1,12 +1,34 @@ import express from "express"; import { registerUser, loginUser, oauthCallback } from "../controllers/authController.js"; import passport from "passport"; +import { Session } from "../models/sessionModel.js"; +import {protect} from "../middleware/authMiddleware.js"; const router = express.Router(); router.post("/signup", registerUser); router.post("/signin", loginUser); +router.post("/logout", protect, async (req, res) => { + try { + const token = req.headers.authorization?.split(" ")[1]; + if (!token) + return res.status(400).json({ success: false, message: "Token missing" }); + + // Delete session for this token + const result = await Session.deleteOne({ token }); + + if (result.deletedCount === 0) { + return res.status(404).json({ success: false, message: "Session not found or already logged out" }); + } + + res.json({ success: true, message: "Logged out successfully" }); + } catch (error) { + console.error("โŒ Logout error:", error); + res.status(500).json({ success: false, message: "Server error during logout" }); + } +}); + // Google OAuth router.get( "/google", diff --git a/backend/src/server.ts b/backend/src/server.ts index 8767644..434b5c8 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -4,6 +4,8 @@ import { createServer } from "http"; import { Server as SocketIOServer } from "socket.io"; import dotenv from "dotenv"; import cors from "cors"; +import cron from "node-cron"; +import { Session } from "./models/sessionModel.js"; import { ChatMessage } from "./models/chatMessageModel.js"; // <-- make sure this file exists and exports model import app from "./app.js"; @@ -101,6 +103,20 @@ mongoose .connect(MONGO_URI) .then(() => { console.log("๐Ÿ—„๏ธ MongoDB connected successfully!"); + + + cron.schedule("0 2 * * *", async () => { + const expiryDate = new Date(); + expiryDate.setDate(expiryDate.getDate() - 7); + try { + const result = await Session.deleteMany({ createdAt: { $lt: expiryDate } }); + console.log(`๐Ÿงน Cleanup complete โ€” ${result.deletedCount} expired sessions removed`); + } catch (error) { + console.error("โŒ Session cleanup failed:", error); + } + }); + + httpServer.listen(PORT, () => { console.log(`๐Ÿš€ Server running on port ${PORT}`); console.log(`๐Ÿ“ก Socket.io real-time chat ready`); diff --git a/backend/src/utils/generateToken.ts b/backend/src/utils/generateToken.ts index 7aa3f73..cf2a370 100644 --- a/backend/src/utils/generateToken.ts +++ b/backend/src/utils/generateToken.ts @@ -1,5 +1,9 @@ import jwt from "jsonwebtoken"; -export const generateToken = (id: string) => { - return jwt.sign({ id }, process.env.JWT_SECRET as string, { expiresIn: "7d" }); +export const generateToken = (userId: string) => { + const expiresIn = "7d"; + const token = jwt.sign({ id: userId }, process.env.JWT_SECRET as string, { + expiresIn, + }); + return token; }; diff --git a/backend/tsconfig.tsbuildinfo b/backend/tsconfig.tsbuildinfo index 6ad5b5b..163f914 100644 --- a/backend/tsconfig.tsbuildinfo +++ b/backend/tsconfig.tsbuildinfo @@ -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"} \ No newline at end of file +{"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"} \ No newline at end of file From af96509340cf78c0cb26606ba150590fb48a125c Mon Sep 17 00:00:00 2001 From: Anand Kumar Shukla <153289958+anandshukla15@users.noreply.github.com> Date: Sun, 26 Oct 2025 16:52:21 +0530 Subject: [PATCH 2/4] Update backend/src/middleware/authMiddleware.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- backend/src/middleware/authMiddleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/middleware/authMiddleware.ts b/backend/src/middleware/authMiddleware.ts index 6a0a13f..8ba88e9 100644 --- a/backend/src/middleware/authMiddleware.ts +++ b/backend/src/middleware/authMiddleware.ts @@ -18,7 +18,7 @@ export const protect = async (req: AuthRequest, res: Response, next: NextFunctio } - const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { id: string; exp: number }; + const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { id: string }; const activeSession = await Session.findOne({ token }); From 1bb1873dfa0cc813af3d7d4b90a55210415d3fb6 Mon Sep 17 00:00:00 2001 From: Anand Kumar Shukla <153289958+anandshukla15@users.noreply.github.com> Date: Sun, 26 Oct 2025 16:53:05 +0530 Subject: [PATCH 3/4] Update backend/src/models/sessionModel.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- backend/src/models/sessionModel.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/src/models/sessionModel.ts b/backend/src/models/sessionModel.ts index cbf02df..b908954 100644 --- a/backend/src/models/sessionModel.ts +++ b/backend/src/models/sessionModel.ts @@ -8,6 +8,4 @@ const sessionSchema = new mongoose.Schema({ }); -sessionSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 }); - export const Session = mongoose.model("Session", sessionSchema); From 140bb1a1b2b70a448afd99f86d50c451df0d966a Mon Sep 17 00:00:00 2001 From: anandshukla15 Date: Sun, 26 Oct 2025 16:54:33 +0530 Subject: [PATCH 4/4] clear conflict --- backend/src/controllers/authController.ts | 17 +++++++++++++---- backend/src/server.ts | 22 +++++++++++----------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/backend/src/controllers/authController.ts b/backend/src/controllers/authController.ts index 1ddccc7..37c20c9 100644 --- a/backend/src/controllers/authController.ts +++ b/backend/src/controllers/authController.ts @@ -36,9 +36,13 @@ export const registerUser = async (req: Request, res: Response, next: NextFuncti const typedUser = asTypedUser(newUser); const token = generateToken(typedUser._id.toString()); -const decoded = jwt.decode(token) as { exp?: number }; -const expiresAt = new Date((decoded.exp ?? 0) * 1000); +const decoded = jwt.decode(token) as { exp?: number } | null; +if (!decoded || !decoded.exp) { + throw new Error("Invalid token format or missing expiration"); +} + +const expiresAt = new Date(decoded.exp * 1000); await Session.create({ userId: typedUser._id, token, @@ -85,8 +89,13 @@ export const loginUser = async (req: Request, res: Response, next: NextFunction) const typedUser = asTypedUser(foundUser); const token = generateToken(typedUser._id.toString()); -const decoded = jwt.decode(token) as { exp?: number }; -const expiresAt = new Date((decoded.exp ?? 0) * 1000); +const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { exp?: number }; + +if (!decoded.exp) { + throw new Error("Token missing expiration claim"); +} + +const expiresAt = new Date(decoded.exp * 1000); await Session.create({ userId: typedUser._id, diff --git a/backend/src/server.ts b/backend/src/server.ts index 434b5c8..a54ce21 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -4,7 +4,7 @@ import { createServer } from "http"; import { Server as SocketIOServer } from "socket.io"; import dotenv from "dotenv"; import cors from "cors"; -import cron from "node-cron"; + import { Session } from "./models/sessionModel.js"; import { ChatMessage } from "./models/chatMessageModel.js"; // <-- make sure this file exists and exports model import app from "./app.js"; @@ -105,16 +105,16 @@ mongoose console.log("๐Ÿ—„๏ธ MongoDB connected successfully!"); - cron.schedule("0 2 * * *", async () => { - const expiryDate = new Date(); - expiryDate.setDate(expiryDate.getDate() - 7); - try { - const result = await Session.deleteMany({ createdAt: { $lt: expiryDate } }); - console.log(`๐Ÿงน Cleanup complete โ€” ${result.deletedCount} expired sessions removed`); - } catch (error) { - console.error("โŒ Session cleanup failed:", error); - } - }); + // cron.schedule("0 2 * * *", async () => { + // const expiryDate = new Date(); + // expiryDate.setDate(expiryDate.getDate() - 7); + // try { + // const result = await Session.deleteMany({ createdAt: { $lt: expiryDate } }); + // console.log(`๐Ÿงน Cleanup complete โ€” ${result.deletedCount} expired sessions removed`); + // } catch (error) { + // console.error("โŒ Session cleanup failed:", error); + // } + // }); httpServer.listen(PORT, () => {