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
29 changes: 29 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 @@ -17,6 +17,7 @@
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"express-rate-limit": "^8.2.1",
"jsonwebtoken": "^9.0.2",
"mongodb": "^6.20.0",
"mongoose": "^8.19.4",
Expand Down
3 changes: 3 additions & 0 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import path from "path";
dotenv.config();
const app = express();

if (process.env.TRUST_PROXY === "true") {
app.set("trust proxy", true);
}
// Required for __dirname in ES modules / TS
// (TS compiles to CJS so this works fine)
const __dirnameLocal = path.resolve();
Expand Down
30 changes: 30 additions & 0 deletions backend/src/middleware/rateLimiter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import rateLimit from "express-rate-limit";
import type { Request, Response } from "express";
import logger from "../utils/logger.js";

export const authRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
standardHeaders: true,
legacyHeaders: false,
handler: (req: Request, res: Response) => {
const clientIP = req.ip || req.socket.remoteAddress || "unknown";
logger.warn(
`Rate limit exceeded for IP: ${clientIP} on ${req.method} ${req.originalUrl}`
);

const retryAfter = req.rateLimit?.resetTime
? Math.round((req.rateLimit.resetTime - Date.now()) / 1000)
: 900;

res.status(429).json({
success: false,
message: "Too many authentication attempts. Please try again after 15 minutes.",
retryAfter,
});
},
keyGenerator: (req: Request) => {
return req.ip || req.socket.remoteAddress || "unknown";
},
});

2 changes: 2 additions & 0 deletions backend/src/models/sessionModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ const sessionSchema = new mongoose.Schema({
createdAt: { type: Date, default: Date.now },
});

sessionSchema.index({ expiresAt: 1 });
sessionSchema.index({ token: 1 });

export const Session = mongoose.model("Session", sessionSchema);
6 changes: 3 additions & 3 deletions backend/src/routes/authRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import passport from "passport";
import { Session } from "../models/sessionModel.js";
import { protect } from "../middleware/authMiddleware.js";
import { authRateLimiter } from "../middleware/rateLimiter.js";
import { generateToken, generateRefreshToken } from "../utils/generateToken.js";
import jwt from "jsonwebtoken";
import dotenv from "dotenv";
Expand All @@ -20,9 +21,8 @@ dotenv.config();
const router = express.Router();
const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:5173";

// REST endpoints
router.post("/signup", registerUser);
router.post("/signin", loginUser);
router.post("/signup", authRateLimiter, registerUser);
router.post("/signin", authRateLimiter, loginUser);
router.post("/logout", logoutUser);
router.get("/refresh", handleRefreshToken);
router.get("/me", protect, getUserProfile);
Expand Down
2 changes: 2 additions & 0 deletions backend/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ChatMessage } from "./models/chatMessageModel.js";
import Room from "./models/roomModel.js"; // ✅ Import for room events
import app from "./app.js";
import logger from "./utils/logger.js";
import { initializeScheduler } from "./utils/scheduler.js";

dotenv.config();

Expand Down Expand Up @@ -166,6 +167,7 @@ mongoose
.connect(MONGO_URI)
.then(() => {
logger.info("🗄️ MongoDB connected successfully!");
initializeScheduler();
httpServer.listen(PORT, () => {
logger.info(`🚀 Server running on port ${PORT}`);
logger.info(`📡 Socket.io real-time chat ready`);
Expand Down
33 changes: 33 additions & 0 deletions backend/src/utils/scheduler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import cron from "node-cron";
import { cleanupExpiredSessions } from "./sessionCleanup.js";
import logger from "./logger.js";

export const initializeScheduler = () => {
const sessionCleanupCron = process.env.SESSION_CLEANUP_CRON || "0 * * * *";
const sessionCleanupJob = cron.schedule(sessionCleanupCron, async () => {
try {
logger.info("🔄 Running scheduled session cleanup...");
const result = await cleanupExpiredSessions();
logger.info(`✅ Session cleanup completed: ${result.deletedCount} expired session(s) removed`);
} catch (error: any) {
logger.error("Scheduled session cleanup failed:", error);
}
}, {
timezone: "UTC",
});
cleanupExpiredSessions()
.then((result) => {
logger.info(`✅ Initial session cleanup completed on startup: ${result.deletedCount} expired session(s) removed`);
})
.catch((error) => {
logger.error(" Initial session cleanup failed:", error);
});

logger.info("📅 Scheduled tasks initialized:");
logger.info(` - Session cleanup: Cron schedule "${sessionCleanupCron}" (every hour by default)`);

return {
sessionCleanupJob,
};
};

17 changes: 17 additions & 0 deletions backend/src/utils/sessionCleanup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Session } from "../models/sessionModel.js";
import logger from "./logger.js";
export const cleanupExpiredSessions = async (): Promise<{ deletedCount: number }> => {
try {
const now = new Date();
const result = await Session.deleteMany({
expiresAt: { $lt: now },
});
if (result.deletedCount > 0) {
logger.info(`🧹 Cleaned up ${result.deletedCount} expired session(s)`);
}
return { deletedCount: result.deletedCount || 0 };
} catch (error: any) {
logger.error(" Error cleaning up expired sessions:", error);
throw error;
}
};
23 changes: 17 additions & 6 deletions frontend/src/pages/Signin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,23 @@ const SignIn = () => {
reset();
navigate("/"); // redirect to home/dashboard
} catch (error: any) {
toast({
title: "Error",
description:
error.response?.data?.message || "Login failed. Please try again.",
variant: "destructive",
});
// Handle rate limiting errors
if (error.response?.status === 429) {
const retryAfter = error.response?.data?.retryAfter || 15;
const minutes = Math.ceil(retryAfter / 60);
toast({
title: "Too Many Attempts",
description: `Too many login attempts. Please try again after ${minutes} minute${minutes > 1 ? 's' : ''}.`,
variant: "destructive",
});
} else {
toast({
title: "Error",
description:
error.response?.data?.message || "Login failed. Please try again.",
variant: "destructive",
});
}
} finally {
setIsLoading(false);
}
Expand Down
34 changes: 24 additions & 10 deletions frontend/src/pages/Signup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,30 @@ const SignUp = () => {
reset();
setTimeout(() => navigate("/signin"), 1500);
} catch (error: any) {
const msg =
error.response?.data?.message ||
"Registration failed. Please try again.";
toast({
title: "Error",
description: msg,
variant: "destructive",
});
setServerMessage(msg);
setIsError(true);
// Handle rate limiting errors
if (error.response?.status === 429) {
const retryAfter = error.response?.data?.retryAfter || 15;
const minutes = Math.ceil(retryAfter / 60);
const msg = `Too many registration attempts. Please try again after ${minutes} minute${minutes > 1 ? 's' : ''}.`;
toast({
title: "Too Many Attempts",
description: msg,
variant: "destructive",
});
setServerMessage(msg);
setIsError(true);
} else {
const msg =
error.response?.data?.message ||
"Registration failed. Please try again.";
toast({
title: "Error",
description: msg,
variant: "destructive",
});
setServerMessage(msg);
setIsError(true);
}
} finally {
setIsLoading(false);
}
Expand Down
Loading