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.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/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/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/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/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/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 edd87bce..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,6 +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'; + const router = express.Router(); @@ -29,4 +29,7 @@ router.use('/notification', generalLimiter, notificationRoutes); router.use('/collection', generalLimiter, collectionRoutes); router.use('/collaboration', generalLimiter, collaborationRoutes); +// Notes API +router.use('/notes', generalLimiter, noteRoutes); + export default router; diff --git a/server/src/server.js b/server/src/server.js index 31650e16..ab213c9a 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -15,6 +15,9 @@ import sanitizeInput from './middlewares/sanitize.middleware.js'; import monitorRoutes from './routes/api/monitor.routes.js'; import router from './routes/index.js'; +import noteRoutes from "./routes/api/noteRoutes.js"; + + dotenv.config(); const server = express(); @@ -51,6 +54,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); 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; + } +}