diff --git a/backend/package-lock.json b/backend/package-lock.json index b5af430..c881206 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -17,6 +17,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", @@ -1617,6 +1618,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", diff --git a/backend/package.json b/backend/package.json index 87cf24e..10d17eb 100644 --- a/backend/package.json +++ b/backend/package.json @@ -20,6 +20,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 77391f0..ffdc9f7 100644 --- a/backend/src/controllers/authController.ts +++ b/backend/src/controllers/authController.ts @@ -8,6 +8,8 @@ import { } 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 } => @@ -67,8 +69,25 @@ export const registerUser = async ( const newUser = await User.create({ name, email, password: hashedPassword }); const typedUser = asTypedUser(newUser); - // Generate and send tokens - sendTokens(res, typedUser); + const token = generateToken(typedUser._id.toString()); +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, + expiresAt, +}); + + res.status(201).json({ + success: true, + message: "User registered successfully", + token, + }); } catch (err) { next(err); } @@ -112,59 +131,26 @@ export const loginUser = async ( const typedUser = asTypedUser(foundUser); - // Generate and send tokens - await typedUser.save(); // Save any potential changes (like refresh tokens) - sendTokens(res, typedUser); - } catch (err) { - next(err); - } -}; + const token = generateToken(typedUser._id.toString()); +const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { exp?: number }; -// โœ… OAUTH CALLBACK CONTROLLER (New) -export const oauthCallback = async ( - req: Request, - res: Response, - next: NextFunction -) => { - try { - if (!req.user) { - return res.status(401).json({ success: false, message: "Authentication failed" }); - } +if (!decoded.exp) { + throw new Error("Token missing expiration claim"); +} - const typedUser = asTypedUser(req.user); +const expiresAt = new Date(decoded.exp * 1000); - // We get the user profile from passport's `done` function - // (which you'd have in src/utils/passport.ts) - // Now we just generate and send tokens +await Session.create({ + userId: typedUser._id, + token, + expiresAt, +}); - // Find the user in DB (req.user is from passport) - const foundUser = await User.findById(typedUser._id); - if (!foundUser) { - return res.status(404).json({ success: false, message: "User not found" }); - } - - const typedFoundUser = asTypedUser(foundUser); - - // We send tokens the same way, but redirect the user - const accessToken = generateAccessToken(typedFoundUser._id.toString()); - const newRefreshToken = generateRefreshToken(typedFoundUser._id.toString()); - - typedFoundUser.refreshTokens = [newRefreshToken]; - await typedFoundUser.save(); - - res.cookie("jwt", newRefreshToken, { - httpOnly: true, - secure: process.env.NODE_ENV !== "development", - sameSite: "strict", - maxAge: 7 * 24 * 60 * 60 * 1000, - }); - // Set the access token in a secure, HTTP-only cookie - res.cookie("access_token", accessToken, { - httpOnly: true, - secure: process.env.NODE_ENV !== "development", - sameSite: "strict", - maxAge: 15 * 60 * 1000, // 15 minutes, adjust as needed + res.json({ + success: true, + message: "Login successful", + token, }); // Redirect to the frontend without passing the token in the URL diff --git a/backend/src/middleware/authMiddleware.ts b/backend/src/middleware/authMiddleware.ts index 5e6a90e..8ba88e9 100644 --- a/backend/src/middleware/authMiddleware.ts +++ b/backend/src/middleware/authMiddleware.ts @@ -1,27 +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 }; - try { - const decoded = jwt.verify( - token, - process.env.JWT_ACCESS_SECRET as string // <-- UPDATED ENV VARIABLE - ) as { id: string }; - req.userId = decoded.id; - next(); - } catch { - // Note: This will now correctly trigger a 401 on an expired access token - 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", + }); } -}; \ No newline at end of file + + + 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..b908954 --- /dev/null +++ b/backend/src/models/sessionModel.ts @@ -0,0 +1,11 @@ +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 }, +}); + + +export const Session = mongoose.model("Session", sessionSchema); diff --git a/backend/src/routes/authRoutes.ts b/backend/src/routes/authRoutes.ts index 62e6926..b8fdb2a 100644 --- a/backend/src/routes/authRoutes.ts +++ b/backend/src/routes/authRoutes.ts @@ -1,7 +1,8 @@ import express from "express"; import { registerUser, loginUser, getUserProfile} from "../controllers/authController.js"; import passport from "passport"; -import { protect } from "../middleware/authMiddleware.js"; +import { Session } from "../models/sessionModel.js"; +import {protect} from "../middleware/authMiddleware.js"; const router = express.Router(); @@ -12,6 +13,26 @@ router.post("/logout", logoutUser); // <-- ADDED router.get("/refresh", handleRefreshToken); // <-- ADDED router.get("/me", protect, getUserProfile); +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..a54ce21 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 { 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 627171f..cad7823 100644 --- a/backend/src/utils/generateToken.ts +++ b/backend/src/utils/generateToken.ts @@ -4,22 +4,12 @@ import dotenv from 'dotenv'; dotenv.config(); const accessTokenSecret = process.env.JWT_ACCESS_SECRET; -const refreshTokenSecret = process.env.JWT_REFRESH_SECRET; - -const parseExpiration = (val: string | undefined, fallback: number | string): number | string => { - if (!val) return fallback; - const trimmed = val.trim(); - return /^\d+$/.test(trimmed) ? Number(trimmed) : trimmed; -}; - -export const generateAccessToken = (id: string) => { - if (!accessTokenSecret) throw new Error("JWT_ACCESS_SECRET is not defined"); - - const options = { - expiresIn: parseExpiration(process.env.JWT_ACCESS_EXPIRATION, 900), - } as SignOptions; - - return jwt.sign({ id }, accessTokenSecret, options); +export const generateToken = (userId: string) => { + const expiresIn = "7d"; + const token = jwt.sign({ id: userId }, process.env.JWT_SECRET as string, { + expiresIn, + }); + return token; }; export const generateRefreshToken = (id: string) => { 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