Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
27 changes: 25 additions & 2 deletions backend/src/controllers/authController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -33,10 +35,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);
Expand Down Expand Up @@ -72,10 +84,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);
Expand Down
50 changes: 39 additions & 11 deletions backend/src/middleware/authMiddleware.ts
Original file line number Diff line number Diff line change
@@ -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",
});
}
};
13 changes: 13 additions & 0 deletions backend/src/models/sessionModel.ts
Original file line number Diff line number Diff line change
@@ -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);
23 changes: 22 additions & 1 deletion backend/src/routes/authRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,35 @@
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();

router.post("/signup", registerUser);
router.post("/signin", loginUser);
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",
Expand Down
16 changes: 16 additions & 0 deletions backend/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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`);
Expand Down
8 changes: 6 additions & 2 deletions backend/src/utils/generateToken.ts
Original file line number Diff line number Diff line change
@@ -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;
};
2 changes: 1 addition & 1 deletion backend/tsconfig.tsbuildinfo
Original file line number Diff line number Diff line change
@@ -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"}
{"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"}