Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -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",
Expand Down
86 changes: 36 additions & 50 deletions backend/src/controllers/authController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } =>
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down
58 changes: 40 additions & 18 deletions backend/src/middleware/authMiddleware.ts
Original file line number Diff line number Diff line change
@@ -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",
});
}
};


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",
});
}
};
11 changes: 11 additions & 0 deletions backend/src/models/sessionModel.ts
Original file line number Diff line number Diff line change
@@ -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);
23 changes: 22 additions & 1 deletion backend/src/routes/authRoutes.ts
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -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",
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 { 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
22 changes: 6 additions & 16 deletions backend/src/utils/generateToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
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"}
Loading