Skip to content
Open
177 changes: 177 additions & 0 deletions backend/package-lock.json

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

2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
"bcryptjs": "^3.0.3",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"csurf": "^1.11.0",
"dotenv": "^16.5.0",
"express": "^4.18.2",
"express-rate-limit": "^8.6.1",
"jsonwebtoken": "^9.0.3",
"mongodb": "^7.2.0",
"mongoose": "^8.14.3",
Expand Down
4 changes: 4 additions & 0 deletions backend/src/controllers/notesController.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,13 @@ export async function deleteNote(req, res) {
});
}

// Delete all child notes grouped under this note to prevent leaving them orphaned
await Note.deleteMany({ parentId: id, userId: req.user._id });

res.status(200).json({
message: "Note deleted successfully",
});

} catch (error) {
console.error("Error in deleteNote controller:", error);

Expand Down
19 changes: 13 additions & 6 deletions backend/src/middleware/rateLimiter.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@

import rateLimit from "express-rate-limit";
import ratelimit from "../config/upstash.js";

const fallbackLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
standardHeaders: true,
legacyHeaders: false,
message: { message: "Too many requests please try after some time" },
});

const rateLimiter = async (req, res, next) => {

if (process.env.NODE_ENV !== "production") {
return next();
}

try {
const ip = req.headers["x-forwarded-for"]?.split(",")[0].trim() || req.socket.remoteAddress || req.ip;
const identifier = req.user?._id || ip || "global-rate-limit";
const { success, limit, remaining, reset } = await ratelimit.limit(identifier);
const { success } = await ratelimit.limit(identifier);
if (!success) {
return res.status(429).json({ message: "Too many requests please try after some time" });
}
next();
return next();
} catch (error) {
console.error("Rate limiter error (failing open):", error);
next();
console.error("Rate limiter error (failing open to fallback):", error);
return fallbackLimiter(req, res, next);
}
};

Expand Down
15 changes: 6 additions & 9 deletions backend/src/routes/notesRoutes.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import express from "express";
import { getAllNotes, getNoteById, createNote, updateNote, deleteNote } from "../controllers/notesController.js";
import { authenticateUser } from "../middleware/authMiddleware.js";
import rateLimiter from "../middleware/rateLimiter.js";

const router = express.Router();

// Protect all notes routes - only authenticated users can access notes endpoints
router.use(authenticateUser);

router.get("/", getAllNotes);

router.get("/:id", getNoteById);

router.post("/", createNote);

router.put("/:id", updateNote);

router.delete("/:id", deleteNote);
router.get("/", rateLimiter, getAllNotes);
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
router.get("/:id", rateLimiter, getNoteById);
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
router.post("/", rateLimiter, createNote);
router.put("/:id", rateLimiter, updateNote);
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
router.delete("/:id", rateLimiter, deleteNote);
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

export default router;
16 changes: 16 additions & 0 deletions backend/src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import cors from "cors";
import dotenv from "dotenv";
import path from "path";
import cookieParser from "cookie-parser";
import csurf from "csurf";
import { fileURLToPath } from "url";
import dns from "dns";
import jwt from "jsonwebtoken";
Expand Down Expand Up @@ -48,6 +49,21 @@ if (process.env.NODE_ENV !== "production") {
app.use(express.json());
app.use(cookieParser());

// CSRF Protection Middleware
const csrfMiddleware = csurf({ cookie: { httpOnly: true, sameSite: "lax" }, ignoreMethods: ["GET", "HEAD", "OPTIONS"] });
const csrfProtection = (req, res, next) => {
if (process.env.NODE_ENV !== "production" || req.headers.authorization?.startsWith("Bearer ")) {
const origin = req.headers.origin || req.headers.referer;
const allowedOrigin = process.env.CLIENT_URL || "http://localhost:5173";
if (["POST", "PUT", "DELETE", "PATCH"].includes(req.method) && origin && !origin.startsWith(allowedOrigin)) {
return res.status(403).json({ message: "CSRF check failed: unauthorized origin" });
}
return next();
}
return csrfMiddleware(req, res, next);
};
app.use(csrfProtection);

// Optional auth to populate req.user for rateLimiter
const optionalAuthenticateUser = (req, res, next) => {
try {
Expand Down
Loading