From 583e5483a202c9333976d7ef85c109f3b2bb757e Mon Sep 17 00:00:00 2001 From: Sudhanshu Gochar Date: Tue, 18 Nov 2025 19:04:33 +0530 Subject: [PATCH 1/3] feat(notes): Implement private project-specific notes (#249) --- package-lock.json | 241 +++++++++++++++++++++++ package.json | 2 + server/package-lock.json | 108 ++++++++++ server/package.json | 1 + server/src/controllers/noteController.ts | 121 ++++++++++++ server/src/models/NoteModel.ts | 47 +++++ server/src/routes/noteRoutes.ts | 27 +++ server/src/server.js | 6 + 8 files changed, 553 insertions(+) create mode 100644 server/src/controllers/noteController.ts create mode 100644 server/src/models/NoteModel.ts create mode 100644 server/src/routes/noteRoutes.ts diff --git a/package-lock.json b/package-lock.json index 4b3c9ffe..0b6066eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,8 @@ "@eslint/css": "^0.10.0", "@eslint/js": "^9.34.0", "@eslint/json": "^0.13.1", + "@types/mongoose": "^5.11.96", + "@types/node": "^24.10.1", "concurrently": "^9.2.0", "eslint": "^9.34.0", "eslint-plugin-react": "^7.37.5", @@ -308,6 +310,16 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz", + "integrity": "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -322,6 +334,43 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mongoose": { + "version": "5.11.96", + "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.11.96.tgz", + "integrity": "sha512-keiY22ljJtXyM7osgScmZOHV6eL5VFUD5tQumlu+hjS++HND5nM8jNEdj5CSWfKIJpVwQfPuwQ2SfBqUnCAVRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mongoose": "*" + } + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -609,6 +658,16 @@ "node": ">=8" } }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -2404,6 +2463,16 @@ "node": ">=4.0" } }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2544,6 +2613,13 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "dev": true, + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -2584,6 +2660,110 @@ "node": "*" } }, + "node_modules/mongodb": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.20.0.tgz", + "integrity": "sha512-SxqNb8yx+VOjIOx2l7HqkGvYuLC/T85d+jPvqGDdUbKJFz/5PVSsVxQzypQsX7chenYvq5bd8jIr4LtunedE7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.20.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3276,6 +3456,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "dev": true, + "license": "MIT" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -3329,6 +3516,16 @@ "node": ">=0.10.0" } }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -3539,6 +3736,19 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -3666,6 +3876,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -3676,6 +3893,30 @@ "punycode": "^2.1.0" } }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index b04731fc..2dfcb9c4 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,8 @@ "@eslint/css": "^0.10.0", "@eslint/js": "^9.34.0", "@eslint/json": "^0.13.1", + "@types/mongoose": "^5.11.96", + "@types/node": "^24.10.1", "concurrently": "^9.2.0", "eslint": "^9.34.0", "eslint-plugin-react": "^7.37.5", diff --git a/server/package-lock.json b/server/package-lock.json index 47524e38..775bc17f 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -30,6 +30,7 @@ "winston": "^3.18.3" }, "devDependencies": { + "@types/express": "^5.0.5", "nodemon": "^3.1.10" }, "engines": { @@ -102,6 +103,66 @@ "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", "license": "MIT" }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", + "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", + "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.18.13", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.13.tgz", @@ -111,6 +172,53 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", diff --git a/server/package.json b/server/package.json index b5e43771..38198717 100644 --- a/server/package.json +++ b/server/package.json @@ -47,6 +47,7 @@ "winston": "^3.18.3" }, "devDependencies": { + "@types/express": "^5.0.5", "nodemon": "^3.1.10" } } diff --git a/server/src/controllers/noteController.ts b/server/src/controllers/noteController.ts new file mode 100644 index 00000000..2c2dfbdd --- /dev/null +++ b/server/src/controllers/noteController.ts @@ -0,0 +1,121 @@ +import { Request, Response } from 'express'; +import { NoteModel, INote } from '../models/NoteModel'; +import mongoose from 'mongoose'; + +// --- Helper function to get authenticated user ID --- +// NOTE: This assumes an auth middleware attaches the user ID to the request object. +const getUserId = (req: Request): mongoose.Types.ObjectId | undefined => { + // ADJUST THIS LINE if req.user.id is named differently in the project! + return (req as any).user?.id ? new mongoose.Types.ObjectId((req as any).user.id) : undefined; +}; + +// --- 1. GET: Fetch all private notes for a specific project by the authenticated user --- +export const getProjectNotes = async (req: Request, res: Response): Promise => { + try { + const userId = getUserId(req); + if (!userId) { + res.status(401).json({ message: 'User not authenticated.' }); + return; + } + + const { projectId } = req.params; + + const notes: INote[] = await NoteModel.find({ + projectId: projectId, + userId: userId, // CRUCIAL: Only fetch notes belonging to this user + }).sort({ createdAt: -1 }); + + res.status(200).json(notes); + } catch (error) { + console.error('Error fetching notes:', error); + res.status(500).json({ message: 'Server error while fetching notes.' }); + } +}; + +// --- 2. POST: Create a new private note --- +export const createNote = async (req: Request, res: Response): Promise => { + try { + const userId = getUserId(req); + if (!userId) { + res.status(401).json({ message: 'User not authenticated.' }); + return; + } + + const { projectId, content, status } = req.body; + + if (!projectId || !content) { + res.status(400).json({ message: 'Project ID and content are required.' }); + return; + } + + const newNote: INote = await NoteModel.create({ + userId: userId, // Automatically link to the current user + projectId, + content, + status: status || 'todo', + }); + + res.status(201).json(newNote); + } catch (error) { + console.error('Error creating note:', error); + res.status(500).json({ message: 'Server error while creating note.' }); + } +}; + +// --- 3. PUT: Update an existing private note --- +export const updateNote = async (req: Request, res: Response): Promise => { + try { + const userId = getUserId(req); + if (!userId) { + res.status(401).json({ message: 'User not authenticated.' }); + return; + } + + const { noteId } = req.params; + const { content, status } = req.body; + + const updatedNote: INote | null = await NoteModel.findOneAndUpdate( + { _id: noteId, userId: userId }, // CRUCIAL: Find by ID AND User ID for ownership check + { content, status }, + { new: true, runValidators: true } + ); + + if (!updatedNote) { + res.status(404).json({ message: 'Note not found or you do not have permission to update it.' }); + return; + } + + res.status(200).json(updatedNote); + } catch (error) { + console.error('Error updating note:', error); + res.status(500).json({ message: 'Server error while updating note.' }); + } +}; + +// --- 4. DELETE: Delete a private note --- +export const deleteNote = async (req: Request, res: Response): Promise => { + try { + const userId = getUserId(req); + if (!userId) { + res.status(401).json({ message: 'User not authenticated.' }); + return; + } + + const { noteId } = req.params; + + const deletedNote: INote | null = await NoteModel.findOneAndDelete({ + _id: noteId, + userId: userId, // CRUCIAL: Find by ID AND User ID for ownership check + }); + + if (!deletedNote) { + res.status(404).json({ message: 'Note not found or you do not have permission to delete it.' }); + return; + } + + res.status(200).json({ message: 'Note successfully deleted.' }); + } catch (error) { + console.error('Error deleting note:', error); + res.status(500).json({ message: 'Server error while deleting note.' }); + } +}; \ No newline at end of file diff --git a/server/src/models/NoteModel.ts b/server/src/models/NoteModel.ts new file mode 100644 index 00000000..67444c5d --- /dev/null +++ b/server/src/models/NoteModel.ts @@ -0,0 +1,47 @@ +import mongoose, { Document, Schema, Model } from 'mongoose'; + +// 1. Define the TypeScript Interface for the Note Document +export interface INote extends Document { + userId: mongoose.Types.ObjectId; + projectId: mongoose.Types.ObjectId; + content: string; + status: 'todo' | 'in-progress' | 'done'; // Added status for progress tracking +} + +// 2. Define the Mongoose Schema +const NoteSchema: Schema = new Schema( + { + // Links the note to the User who created it (Privacy check) + userId: { + type: Schema.Types.ObjectId, + ref: 'User', // Assuming the User model is named 'User' + required: true, + index: true, + }, + // Links the note to the Project it is related to + projectId: { + type: Schema.Types.ObjectId, + ref: 'Project', // Assuming the Project model is named 'Project' + required: true, + index: true, + }, + // The main content of the private note + content: { + type: String, + required: true, + trim: true, + }, + // Status for tracking progress + status: { + type: String, + enum: ['todo', 'in-progress', 'done'], + default: 'todo', + }, + }, + { + timestamps: true, // Adds createdAt and updatedAt fields + } +); + +// 3. Export the Mongoose Model +export const NoteModel: Model = mongoose.model('Note', NoteSchema); \ No newline at end of file diff --git a/server/src/routes/noteRoutes.ts b/server/src/routes/noteRoutes.ts new file mode 100644 index 00000000..3a2db335 --- /dev/null +++ b/server/src/routes/noteRoutes.ts @@ -0,0 +1,27 @@ +import { Router } from 'express'; +import { + getProjectNotes, + createNote, + updateNote, + deleteNote, +} from '../controllers/noteController'; +// NOTE: Update the path if your authMiddleware is located elsewhere +import authMiddleware from '../middlewares/auth.middleware'; +const router: Router = Router(); + +// All note routes require authentication to ensure privacy +router.use(authMiddleware); + +// GET /api/v1/notes/:projectId -> Fetch all notes for a specific project by the user +router.get('/:projectId', getProjectNotes); + +// POST /api/v1/notes -> Create a new note +router.post('/', createNote); + +// PUT /api/v1/notes/:noteId -> Update a specific note +router.put('/:noteId', updateNote); + +// DELETE /api/v1/notes/:noteId -> Delete a specific note +router.delete('/:noteId', deleteNote); + +export default router; \ No newline at end of file diff --git a/server/src/server.js b/server/src/server.js index 31650e16..aa7863ab 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -14,6 +14,7 @@ import sanitizeInput from './middlewares/sanitize.middleware.js'; // Routes import monitorRoutes from './routes/api/monitor.routes.js'; import router from './routes/index.js'; +import noteRoutes from './routes/noteRoutes.js'; // <--- NEW IMPORT dotenv.config(); @@ -51,6 +52,11 @@ server.use('/monitor', monitorRoutes); // API Routes server.use('/api', router); +// ADDED: Mount the new Note Routes under the /api/v1/notes path +// We use the '/api' prefix here, but your noteRoutes file uses /:projectId etc., +// so the final path will be /api/notes/:projectId +server.use('/api/notes', noteRoutes); // <--- NEW MOUNT + // Error handler (last middleware) server.use(errorHandler); From 0ab143547f42e6fdb5f645c53d7962923619ab4b Mon Sep 17 00:00:00 2001 From: Sudhanshu Gochar Date: Wed, 19 Nov 2025 22:18:14 +0530 Subject: [PATCH 2/3] feat: add role-based access middleware for notes --- server/src/controllers/noteController.ts | 166 +++++++---------------- server/src/middlewares/auth.ts | 16 +++ server/src/middlewares/roleAuth.ts | 45 ++++++ server/src/models/NoteModel.ts | 65 ++++----- server/src/models/user.model.js | 9 ++ server/src/routes/index.js | 4 + server/src/routes/noteRoutes.ts | 55 +++++--- server/src/typings/Express/index.d.ts | 12 ++ 8 files changed, 194 insertions(+), 178 deletions(-) create mode 100644 server/src/middlewares/auth.ts create mode 100644 server/src/middlewares/roleAuth.ts create mode 100644 server/src/typings/Express/index.d.ts diff --git a/server/src/controllers/noteController.ts b/server/src/controllers/noteController.ts index 2c2dfbdd..1a48d4cc 100644 --- a/server/src/controllers/noteController.ts +++ b/server/src/controllers/noteController.ts @@ -1,121 +1,55 @@ -import { Request, Response } from 'express'; -import { NoteModel, INote } from '../models/NoteModel'; -import mongoose from 'mongoose'; - -// --- Helper function to get authenticated user ID --- -// NOTE: This assumes an auth middleware attaches the user ID to the request object. -const getUserId = (req: Request): mongoose.Types.ObjectId | undefined => { - // ADJUST THIS LINE if req.user.id is named differently in the project! - return (req as any).user?.id ? new mongoose.Types.ObjectId((req as any).user.id) : undefined; +import { Request, Response } from "express"; +import NOTE from "../models/NoteModel.js"; + +/** + * GET all notes for a project + */ +export const getNotes = async (req: Request, res: Response) => { + try { + const notes = await NOTE.find({ projectId: req.params.id }) + .populate("authorId", "name email") + .sort({ createdAt: -1 }); + + return res.status(200).json({ success: true, notes }); + } catch (error: any) { + return res.status(500).json({ success: false, message: error.message }); + } }; -// --- 1. GET: Fetch all private notes for a specific project by the authenticated user --- -export const getProjectNotes = async (req: Request, res: Response): Promise => { - try { - const userId = getUserId(req); - if (!userId) { - res.status(401).json({ message: 'User not authenticated.' }); - return; - } - - const { projectId } = req.params; - - const notes: INote[] = await NoteModel.find({ - projectId: projectId, - userId: userId, // CRUCIAL: Only fetch notes belonging to this user - }).sort({ createdAt: -1 }); - - res.status(200).json(notes); - } catch (error) { - console.error('Error fetching notes:', error); - res.status(500).json({ message: 'Server error while fetching notes.' }); - } +/** + * POST: Add a new note + */ +export const addNote = async (req: Request, res: Response) => { + try { + const { content } = req.body; + + const note = await NOTE.create({ + projectId: req.params.id, + authorId: (req as any).user._id, // TypeScript fix + content, + }); + + return res.status(201).json({ success: true, note }); + } catch (error: any) { + return res.status(500).json({ success: false, message: error.message }); + } }; -// --- 2. POST: Create a new private note --- -export const createNote = async (req: Request, res: Response): Promise => { - try { - const userId = getUserId(req); - if (!userId) { - res.status(401).json({ message: 'User not authenticated.' }); - return; - } - - const { projectId, content, status } = req.body; - - if (!projectId || !content) { - res.status(400).json({ message: 'Project ID and content are required.' }); - return; - } - - const newNote: INote = await NoteModel.create({ - userId: userId, // Automatically link to the current user - projectId, - content, - status: status || 'todo', - }); - - res.status(201).json(newNote); - } catch (error) { - console.error('Error creating note:', error); - res.status(500).json({ message: 'Server error while creating note.' }); - } +/** + * PUT: Update a note + */ +export const updateNote = async (req: Request, res: Response) => { + try { + const { content } = req.body; + + const note = await NOTE.findByIdAndUpdate( + req.params.noteId, + { content }, + { new: true } + ); + + return res.status(200).json({ success: true, note }); + } catch (error: any) { + return res.status(500).json({ success: false, message: error.message }); + } }; - -// --- 3. PUT: Update an existing private note --- -export const updateNote = async (req: Request, res: Response): Promise => { - try { - const userId = getUserId(req); - if (!userId) { - res.status(401).json({ message: 'User not authenticated.' }); - return; - } - - const { noteId } = req.params; - const { content, status } = req.body; - - const updatedNote: INote | null = await NoteModel.findOneAndUpdate( - { _id: noteId, userId: userId }, // CRUCIAL: Find by ID AND User ID for ownership check - { content, status }, - { new: true, runValidators: true } - ); - - if (!updatedNote) { - res.status(404).json({ message: 'Note not found or you do not have permission to update it.' }); - return; - } - - res.status(200).json(updatedNote); - } catch (error) { - console.error('Error updating note:', error); - res.status(500).json({ message: 'Server error while updating note.' }); - } -}; - -// --- 4. DELETE: Delete a private note --- -export const deleteNote = async (req: Request, res: Response): Promise => { - try { - const userId = getUserId(req); - if (!userId) { - res.status(401).json({ message: 'User not authenticated.' }); - return; - } - - const { noteId } = req.params; - - const deletedNote: INote | null = await NoteModel.findOneAndDelete({ - _id: noteId, - userId: userId, // CRUCIAL: Find by ID AND User ID for ownership check - }); - - if (!deletedNote) { - res.status(404).json({ message: 'Note not found or you do not have permission to delete it.' }); - return; - } - - res.status(200).json({ message: 'Note successfully deleted.' }); - } catch (error) { - console.error('Error deleting note:', error); - res.status(500).json({ message: 'Server error while deleting note.' }); - } -}; \ No newline at end of file diff --git a/server/src/middlewares/auth.ts b/server/src/middlewares/auth.ts new file mode 100644 index 00000000..4f2dfd34 --- /dev/null +++ b/server/src/middlewares/auth.ts @@ -0,0 +1,16 @@ +import { Request, Response, NextFunction } from "express"; + +export const isAuthenticated = ( + req: Request, + res: Response, + next: NextFunction +) => { + if (!req.user) { + return res.status(401).json({ + success: false, + message: "You must be logged in to access this resource", + }); + } + + next(); +}; diff --git a/server/src/middlewares/roleAuth.ts b/server/src/middlewares/roleAuth.ts new file mode 100644 index 00000000..66e5d236 --- /dev/null +++ b/server/src/middlewares/roleAuth.ts @@ -0,0 +1,45 @@ +import { Request, Response, NextFunction } from "express"; + +export const isCollaborator = ( + req: Request, + res: Response, + next: NextFunction +) => { + const allowed = ["COLLABORATOR", "MAINTAINER", "ADMIN"]; + if (!req.user || !allowed.includes(req.user.role)) { + return res.status(403).json({ + success: false, + message: "Collaborators, Maintainers and Admins can view notes" + }); + } + next(); +}; + +export const isMaintainer = ( + req: Request, + res: Response, + next: NextFunction +) => { + const allowed = ["MAINTAINER", "ADMIN"]; + if (!req.user || !allowed.includes(req.user.role)) { + return res.status(403).json({ + success: false, + message: "Only maintainers or admins can modify notes" + }); + } + next(); +}; + +export const isAdmin = ( + req: Request, + res: Response, + next: NextFunction +) => { + if (!req.user || req.user.role !== "ADMIN") { + return res.status(403).json({ + success: false, + message: "Admin access only" + }); + } + next(); +}; diff --git a/server/src/models/NoteModel.ts b/server/src/models/NoteModel.ts index 67444c5d..ae9cc7ea 100644 --- a/server/src/models/NoteModel.ts +++ b/server/src/models/NoteModel.ts @@ -1,47 +1,30 @@ -import mongoose, { Document, Schema, Model } from 'mongoose'; +import { Schema, model } from "mongoose"; -// 1. Define the TypeScript Interface for the Note Document -export interface INote extends Document { - userId: mongoose.Types.ObjectId; - projectId: mongoose.Types.ObjectId; - content: string; - status: 'todo' | 'in-progress' | 'done'; // Added status for progress tracking -} +const NoteSchema = new Schema( + { + projectId: { + type: Schema.Types.ObjectId, + ref: "Project", + required: true, + }, -// 2. Define the Mongoose Schema -const NoteSchema: Schema = new Schema( - { - // Links the note to the User who created it (Privacy check) - userId: { - type: Schema.Types.ObjectId, - ref: 'User', // Assuming the User model is named 'User' - required: true, - index: true, - }, - // Links the note to the Project it is related to - projectId: { - type: Schema.Types.ObjectId, - ref: 'Project', // Assuming the Project model is named 'Project' - required: true, - index: true, - }, - // The main content of the private note - content: { - type: String, - required: true, - trim: true, - }, - // Status for tracking progress - status: { - type: String, - enum: ['todo', 'in-progress', 'done'], - default: 'todo', - }, + authorId: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, }, - { - timestamps: true, // Adds createdAt and updatedAt fields + + content: { + type: String, + required: true, + trim: true, } + }, + { + timestamps: true, // createdAt & updatedAt + } ); -// 3. Export the Mongoose Model -export const NoteModel: Model = mongoose.model('Note', NoteSchema); \ No newline at end of file +const NOTE = model("Note", NoteSchema); + +export default NOTE; diff --git a/server/src/models/user.model.js b/server/src/models/user.model.js index 6826f01e..7b3601cb 100644 --- a/server/src/models/user.model.js +++ b/server/src/models/user.model.js @@ -2,6 +2,15 @@ import { model } from 'mongoose'; import USER_SCHEMA from '../schemas/user.schema.js'; import { COLLECTION_NAMES } from '../constants/db.js'; +// Add role field to USER_SCHEMA if not already present +USER_SCHEMA.add({ + role: { + type: String, + enum: ["USER", "COLLABORATOR", "MAINTAINER", "ADMIN"], + default: "USER" + } +}); + const USER = model(COLLECTION_NAMES.USERS, USER_SCHEMA); export default USER; diff --git a/server/src/routes/index.js b/server/src/routes/index.js index edd87bce..7e2bad54 100644 --- a/server/src/routes/index.js +++ b/server/src/routes/index.js @@ -15,6 +15,7 @@ import commentRoutes from './api/comment.routes.js'; import notificationRoutes from './api/notification.routes.js'; import collectionRoutes from './api/collections.routes.js'; import collaborationRoutes from './api/collaboration.routes.js'; +import noteRoutes from './api/noteRoutes.js'; // ✅ NEW const router = express.Router(); @@ -29,4 +30,7 @@ router.use('/notification', generalLimiter, notificationRoutes); router.use('/collection', generalLimiter, collectionRoutes); router.use('/collaboration', generalLimiter, collaborationRoutes); +// ✅ NEW: Notes API route +router.use('/', generalLimiter, noteRoutes); + export default router; diff --git a/server/src/routes/noteRoutes.ts b/server/src/routes/noteRoutes.ts index 3a2db335..0d004f89 100644 --- a/server/src/routes/noteRoutes.ts +++ b/server/src/routes/noteRoutes.ts @@ -1,27 +1,40 @@ -import { Router } from 'express'; -import { - getProjectNotes, - createNote, - updateNote, - deleteNote, -} from '../controllers/noteController'; -// NOTE: Update the path if your authMiddleware is located elsewhere -import authMiddleware from '../middlewares/auth.middleware'; -const router: Router = Router(); +import { Router } from "express"; +import { getNotes, addNote, updateNote } from "../controllers/noteController.js"; +import { isAuthenticated } from "../middlewares/auth.js"; +import { isCollaborator, isMaintainer, isAdmin } from "../middlewares/roleAuth"; -// All note routes require authentication to ensure privacy -router.use(authMiddleware); -// GET /api/v1/notes/:projectId -> Fetch all notes for a specific project by the user -router.get('/:projectId', getProjectNotes); -// POST /api/v1/notes -> Create a new note -router.post('/', createNote); +const router = Router(); -// PUT /api/v1/notes/:noteId -> Update a specific note -router.put('/:noteId', updateNote); +/** + * GET notes — COLLABORATOR / MAINTAINER / ADMIN + */ +router.get( + "/projects/:id/notes", + isAuthenticated, + isCollaborator, // 👈 NEW + getNotes +); -// DELETE /api/v1/notes/:noteId -> Delete a specific note -router.delete('/:noteId', deleteNote); +/** + * POST note — MAINTAINER / ADMIN + */ +router.post( + "/projects/:id/notes", + isAuthenticated, + isMaintainer, // 👈 NEW + addNote +); -export default router; \ No newline at end of file +/** + * PUT note — MAINTAINER / ADMIN + */ +router.put( + "/projects/:projectId/notes/:noteId", + isAuthenticated, + isMaintainer, // 👈 NEW + updateNote +); + +export default router; diff --git a/server/src/typings/Express/index.d.ts b/server/src/typings/Express/index.d.ts new file mode 100644 index 00000000..8ea299b5 --- /dev/null +++ b/server/src/typings/Express/index.d.ts @@ -0,0 +1,12 @@ +declare namespace Express { + export interface User { + _id: string; + role: string; + name?: string; + email?: string; + } + + export interface Request { + user?: User; + } +} From 53c8ca282dd615c2f8bcccbaf4f73dd828b73019 Mon Sep 17 00:00:00 2001 From: Sudhanshu Gochar Date: Thu, 20 Nov 2025 02:42:26 +0530 Subject: [PATCH 3/3] refactor: replace TS note backend with JS version + fix routes --- server/src/controllers/noteController.js | 78 ++++++++++++++++++++++++ server/src/controllers/noteController.ts | 55 ----------------- server/src/middlewares/auth.js | 10 +++ server/src/middlewares/auth.limiter.js | 20 +++--- server/src/middlewares/auth.ts | 16 ----- server/src/middlewares/roleAuth.js | 36 +++++++++++ server/src/middlewares/roleAuth.ts | 45 -------------- server/src/models/NoteModel.js | 23 +++++++ server/src/models/NoteModel.ts | 30 --------- server/src/routes/api/noteRoutes.js | 26 ++++++++ server/src/routes/index.js | 9 ++- server/src/routes/noteRoutes.ts | 40 ------------ server/src/server.js | 4 +- 13 files changed, 192 insertions(+), 200 deletions(-) create mode 100644 server/src/controllers/noteController.js delete mode 100644 server/src/controllers/noteController.ts create mode 100644 server/src/middlewares/auth.js delete mode 100644 server/src/middlewares/auth.ts create mode 100644 server/src/middlewares/roleAuth.js delete mode 100644 server/src/middlewares/roleAuth.ts create mode 100644 server/src/models/NoteModel.js delete mode 100644 server/src/models/NoteModel.ts create mode 100644 server/src/routes/api/noteRoutes.js delete mode 100644 server/src/routes/noteRoutes.ts diff --git a/server/src/controllers/noteController.js b/server/src/controllers/noteController.js new file mode 100644 index 00000000..db104b37 --- /dev/null +++ b/server/src/controllers/noteController.js @@ -0,0 +1,78 @@ +import Note from "../models/NoteModel.js"; + + +// GET notes +export const getNotes = async (req, res) => { + try { + const projectId = req.params.id; + const notes = await Note.find({ project: projectId }); + + return res.status(200).json({ + success: true, + notes, + }); + } catch (error) { + return res.status(500).json({ + success: false, + message: "Failed to fetch notes", + error: error.message, + }); + } +}; + +// ADD note +export const addNote = async (req, res) => { + try { + const projectId = req.params.id; + + const newNote = await Note.create({ + project: projectId, + user: req.user._id, + content: req.body.content, + }); + + return res.status(201).json({ + success: true, + message: "Note added successfully", + note: newNote, + }); + } catch (error) { + return res.status(500).json({ + success: false, + message: "Failed to add note", + error: error.message, + }); + } +}; + +// UPDATE note +export const updateNote = async (req, res) => { + try { + const { projectId, noteId } = req.params; + + const note = await Note.findOneAndUpdate( + { _id: noteId, project: projectId }, + { content: req.body.content }, + { new: true } + ); + + if (!note) { + return res.status(404).json({ + success: false, + message: "Note not found", + }); + } + + return res.status(200).json({ + success: true, + message: "Note updated successfully", + note, + }); + } catch (error) { + return res.status(500).json({ + success: false, + message: "Failed to update note", + error: error.message, + }); + } +}; diff --git a/server/src/controllers/noteController.ts b/server/src/controllers/noteController.ts deleted file mode 100644 index 1a48d4cc..00000000 --- a/server/src/controllers/noteController.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Request, Response } from "express"; -import NOTE from "../models/NoteModel.js"; - -/** - * GET all notes for a project - */ -export const getNotes = async (req: Request, res: Response) => { - try { - const notes = await NOTE.find({ projectId: req.params.id }) - .populate("authorId", "name email") - .sort({ createdAt: -1 }); - - return res.status(200).json({ success: true, notes }); - } catch (error: any) { - return res.status(500).json({ success: false, message: error.message }); - } -}; - -/** - * POST: Add a new note - */ -export const addNote = async (req: Request, res: Response) => { - try { - const { content } = req.body; - - const note = await NOTE.create({ - projectId: req.params.id, - authorId: (req as any).user._id, // TypeScript fix - content, - }); - - return res.status(201).json({ success: true, note }); - } catch (error: any) { - return res.status(500).json({ success: false, message: error.message }); - } -}; - -/** - * PUT: Update a note - */ -export const updateNote = async (req: Request, res: Response) => { - try { - const { content } = req.body; - - const note = await NOTE.findByIdAndUpdate( - req.params.noteId, - { content }, - { new: true } - ); - - return res.status(200).json({ success: true, note }); - } catch (error: any) { - return res.status(500).json({ success: false, message: error.message }); - } -}; diff --git a/server/src/middlewares/auth.js b/server/src/middlewares/auth.js new file mode 100644 index 00000000..38a25687 --- /dev/null +++ b/server/src/middlewares/auth.js @@ -0,0 +1,10 @@ +export const isAuthenticated = (req, res, next) => { + if (!req.user) { + return res.status(401).json({ + success: false, + message: "You must be logged in to access this resource" + }); + } + + next(); +}; diff --git a/server/src/middlewares/auth.limiter.js b/server/src/middlewares/auth.limiter.js index db149c81..be9d99c0 100644 --- a/server/src/middlewares/auth.limiter.js +++ b/server/src/middlewares/auth.limiter.js @@ -1,17 +1,21 @@ import { RateLimiterMemory } from 'rate-limiter-flexible'; import { sendResponse } from '../utils/response.js'; +// Rate limit settings const authLimit = new RateLimiterMemory({ - points: 10, - duration: 15 * 60, - blockDuration: 15 * 60, + points: 10, // allowed attempts + duration: 15 * 60, // per 15 minutes + blockDuration: 15 * 60 // block for 15 minutes after limit reached }); -const authLimiter = (req, res, next) => { - authLimit - .consume(req.ip) - .then(() => next()) - .catch(() => sendResponse(res, 429, 'Too many requests to /auth')); +const authLimiter = async (req, res, next) => { + try { + // consume 1 point for this IP + await authLimit.consume(req.ip); + next(); + } catch (error) { + return sendResponse(res, 429, 'Too many requests. Please try again later.'); + } }; export default authLimiter; diff --git a/server/src/middlewares/auth.ts b/server/src/middlewares/auth.ts deleted file mode 100644 index 4f2dfd34..00000000 --- a/server/src/middlewares/auth.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Request, Response, NextFunction } from "express"; - -export const isAuthenticated = ( - req: Request, - res: Response, - next: NextFunction -) => { - if (!req.user) { - return res.status(401).json({ - success: false, - message: "You must be logged in to access this resource", - }); - } - - next(); -}; diff --git a/server/src/middlewares/roleAuth.js b/server/src/middlewares/roleAuth.js new file mode 100644 index 00000000..b4c90fd1 --- /dev/null +++ b/server/src/middlewares/roleAuth.js @@ -0,0 +1,36 @@ +export const isCollaborator = (req, res, next) => { + const allowed = ["user", "maintainer", "admin"]; + + if (!req.user || !allowed.includes(req.user.role)) { + return res.status(403).json({ + success: false, + message: "Users, Maintainers, and Admins can view notes", + }); + } + + next(); +}; + +export const isMaintainer = (req, res, next) => { + const allowed = ["maintainer", "admin"]; + + if (!req.user || !allowed.includes(req.user.role)) { + return res.status(403).json({ + success: false, + message: "Only maintainers or admins can modify notes", + }); + } + + next(); +}; + +export const isAdmin = (req, res, next) => { + if (!req.user || req.user.role !== "admin") { + return res.status(403).json({ + success: false, + message: "Admin access only", + }); + } + + next(); +}; diff --git a/server/src/middlewares/roleAuth.ts b/server/src/middlewares/roleAuth.ts deleted file mode 100644 index 66e5d236..00000000 --- a/server/src/middlewares/roleAuth.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Request, Response, NextFunction } from "express"; - -export const isCollaborator = ( - req: Request, - res: Response, - next: NextFunction -) => { - const allowed = ["COLLABORATOR", "MAINTAINER", "ADMIN"]; - if (!req.user || !allowed.includes(req.user.role)) { - return res.status(403).json({ - success: false, - message: "Collaborators, Maintainers and Admins can view notes" - }); - } - next(); -}; - -export const isMaintainer = ( - req: Request, - res: Response, - next: NextFunction -) => { - const allowed = ["MAINTAINER", "ADMIN"]; - if (!req.user || !allowed.includes(req.user.role)) { - return res.status(403).json({ - success: false, - message: "Only maintainers or admins can modify notes" - }); - } - next(); -}; - -export const isAdmin = ( - req: Request, - res: Response, - next: NextFunction -) => { - if (!req.user || req.user.role !== "ADMIN") { - return res.status(403).json({ - success: false, - message: "Admin access only" - }); - } - next(); -}; diff --git a/server/src/models/NoteModel.js b/server/src/models/NoteModel.js new file mode 100644 index 00000000..019107a1 --- /dev/null +++ b/server/src/models/NoteModel.js @@ -0,0 +1,23 @@ +import mongoose from "mongoose"; + +const NoteSchema = new mongoose.Schema( + { + projectId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Project", + required: true + }, + content: { + type: String, + required: true + }, + createdBy: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true + } + }, + { timestamps: true } +); + +export default mongoose.model("Note", NoteSchema); diff --git a/server/src/models/NoteModel.ts b/server/src/models/NoteModel.ts deleted file mode 100644 index ae9cc7ea..00000000 --- a/server/src/models/NoteModel.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Schema, model } from "mongoose"; - -const NoteSchema = new Schema( - { - projectId: { - type: Schema.Types.ObjectId, - ref: "Project", - required: true, - }, - - authorId: { - type: Schema.Types.ObjectId, - ref: "User", - required: true, - }, - - content: { - type: String, - required: true, - trim: true, - } - }, - { - timestamps: true, // createdAt & updatedAt - } -); - -const NOTE = model("Note", NoteSchema); - -export default NOTE; diff --git a/server/src/routes/api/noteRoutes.js b/server/src/routes/api/noteRoutes.js new file mode 100644 index 00000000..796795c8 --- /dev/null +++ b/server/src/routes/api/noteRoutes.js @@ -0,0 +1,26 @@ +import { Router } from "express"; +import { getNotes, addNote, updateNote } from "../../controllers/noteController.js"; +import { isAuthenticated } from "../../middlewares/auth.js"; +import { isCollaborator, isMaintainer } from "../../middlewares/roleAuth.js"; + +const router = Router(); + +router.get("/projects/:id/notes", + isAuthenticated, + isCollaborator, + getNotes +); + +router.post("/projects/:id/notes", + isAuthenticated, + isMaintainer, + addNote +); + +router.put("/projects/:projectId/notes/:noteId", + isAuthenticated, + isMaintainer, + updateNote +); + +export default router; diff --git a/server/src/routes/index.js b/server/src/routes/index.js index 7e2bad54..d0daa0f7 100644 --- a/server/src/routes/index.js +++ b/server/src/routes/index.js @@ -1,10 +1,8 @@ import express from 'express'; -// Import rate limiting middlewares import authLimiter from '../middlewares/auth.limiter.js'; import generalLimiter from '../middlewares/general.limiter.js'; -// Import route modules import authRoutes from './api/auth.routes.js'; import subscriberRoutes from './api/subscriber.routes.js'; import mediaRoutes from './api/media.routes.js'; @@ -15,7 +13,8 @@ import commentRoutes from './api/comment.routes.js'; import notificationRoutes from './api/notification.routes.js'; import collectionRoutes from './api/collections.routes.js'; import collaborationRoutes from './api/collaboration.routes.js'; -import noteRoutes from './api/noteRoutes.js'; // ✅ NEW +import noteRoutes from './api/noteRoutes.js'; + const router = express.Router(); @@ -30,7 +29,7 @@ router.use('/notification', generalLimiter, notificationRoutes); router.use('/collection', generalLimiter, collectionRoutes); router.use('/collaboration', generalLimiter, collaborationRoutes); -// ✅ NEW: Notes API route -router.use('/', generalLimiter, noteRoutes); +// Notes API +router.use('/notes', generalLimiter, noteRoutes); export default router; diff --git a/server/src/routes/noteRoutes.ts b/server/src/routes/noteRoutes.ts deleted file mode 100644 index 0d004f89..00000000 --- a/server/src/routes/noteRoutes.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Router } from "express"; -import { getNotes, addNote, updateNote } from "../controllers/noteController.js"; -import { isAuthenticated } from "../middlewares/auth.js"; -import { isCollaborator, isMaintainer, isAdmin } from "../middlewares/roleAuth"; - - - -const router = Router(); - -/** - * GET notes — COLLABORATOR / MAINTAINER / ADMIN - */ -router.get( - "/projects/:id/notes", - isAuthenticated, - isCollaborator, // 👈 NEW - getNotes -); - -/** - * POST note — MAINTAINER / ADMIN - */ -router.post( - "/projects/:id/notes", - isAuthenticated, - isMaintainer, // 👈 NEW - addNote -); - -/** - * PUT note — MAINTAINER / ADMIN - */ -router.put( - "/projects/:projectId/notes/:noteId", - isAuthenticated, - isMaintainer, // 👈 NEW - updateNote -); - -export default router; diff --git a/server/src/server.js b/server/src/server.js index aa7863ab..ab213c9a 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -14,7 +14,9 @@ import sanitizeInput from './middlewares/sanitize.middleware.js'; // Routes import monitorRoutes from './routes/api/monitor.routes.js'; import router from './routes/index.js'; -import noteRoutes from './routes/noteRoutes.js'; // <--- NEW IMPORT + +import noteRoutes from "./routes/api/noteRoutes.js"; + dotenv.config();