diff --git a/package-lock.json b/package-lock.json index 5c845cf..e960598 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,9 @@ "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", - "helmet": "^8.1.0", "express-async-handler": "^1.2.0", "express-rate-limiter": "^1.3.1", + "helmet": "^8.1.0", "joi": "^18.0.1", "jsonwebtoken": "^9.0.2", "mongoose": "^8.19.1", @@ -20,6 +20,8 @@ "nodemon": "^3.1.10", "readdirp": "^4.1.2", "resend": "^6.1.3", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", "winston": "^3.18.3" }, "devDependencies": { @@ -31,6 +33,50 @@ "prettier": "^3.6.2" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -56,6 +102,15 @@ "node": ">=6.9.0" } }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@commitlint/cli": { "version": "19.8.1", "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-19.8.1.tgz", @@ -592,6 +647,12 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, "node_modules/@mongodb-js/saslprep": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz", @@ -601,6 +662,23 @@ "sparse-bitfield": "^3.0.3" } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, "node_modules/@stablelib/base64": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", @@ -634,7 +712,6 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, "license": "MIT" }, "node_modules/@types/node": { @@ -643,7 +720,6 @@ "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -688,7 +764,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -749,6 +824,26 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansi-styles/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ansi-styles/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -766,7 +861,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/array-ify": { @@ -922,6 +1016,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1022,38 +1122,18 @@ } }, "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-string": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.2.tgz", - "integrity": "sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.2.tgz", + "integrity": "sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg==", "license": "MIT", "dependencies": { "color-name": "^2.0.0" }, "engines": { - "node": ">=18" + "node": ">=14.6" } }, - "node_modules/color-string/node_modules/color-name": { + "node_modules/color-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.0.2.tgz", "integrity": "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==", @@ -1062,25 +1142,25 @@ "node": ">=12.20" } }, - "node_modules/color/node_modules/color-convert": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.2.tgz", - "integrity": "sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg==", + "node_modules/color-string": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.2.tgz", + "integrity": "sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA==", "license": "MIT", "dependencies": { "color-name": "^2.0.0" }, "engines": { - "node": ">=14.6" + "node": ">=18" } }, - "node_modules/color/node_modules/color-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.0.2.tgz", - "integrity": "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==", + "node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", "license": "MIT", "engines": { - "node": ">=12.20" + "node": ">= 6" } }, "node_modules/compare-func": { @@ -1213,7 +1293,6 @@ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -1314,6 +1393,18 @@ "node": ">= 0.8" } }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -1481,7 +1572,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -1776,7 +1866,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -1897,6 +1986,12 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2002,6 +2097,12 @@ "node": ">= 0.8" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2090,6 +2191,27 @@ "node": ">=16" } }, + "node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2303,6 +2425,17 @@ "node": ">=0.8.19" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -2403,6 +2536,18 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-text-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz", @@ -2462,7 +2607,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -2638,6 +2782,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -2650,6 +2801,13 @@ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "license": "MIT" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", @@ -2692,7 +2850,6 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", - "dev": true, "license": "MIT" }, "node_modules/lodash.once": { @@ -2726,8 +2883,8 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", - - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/logform": { "version": "2.7.0", @@ -2746,21 +2903,6 @@ "node": ">= 12.0.0" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - - "dev": true, - "license": "MIT" - - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3105,7 +3247,6 @@ "wrappy": "1" } }, - "node_modules/one-time": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", @@ -3115,22 +3256,13 @@ "fn.name": "1.x.x" } }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true }, - "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3232,6 +3364,15 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3816,6 +3957,62 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "license": "MIT", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.29.5", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.29.5.tgz", + "integrity": "sha512-2zFnjONgLXlz8gLToRKvXHKJdqXF6UGgCmv65i8T6i/UrjDNyV1fIQ7FauZA40SaivlGKEvW2tw9XDyDhfcXqQ==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/text-extensions": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", @@ -3987,13 +4184,6 @@ "punycode": "^2.1.0" } }, - - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - "node_modules/url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", @@ -4004,6 +4194,12 @@ "requires-port": "^1.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", @@ -4016,7 +4212,15 @@ "bin": { "uuid": "dist/bin/uuid" } - + }, + "node_modules/validator": { + "version": "13.15.20", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz", + "integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } }, "node_modules/vary": { "version": "1.1.2", @@ -4101,18 +4305,6 @@ "node": ">= 12.0.0" } }, - "node_modules/winston/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -4157,6 +4349,15 @@ "node": ">=10" } }, + "node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -4198,6 +4399,36 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } } } } diff --git a/package.json b/package.json index d58b811..5969a19 100644 --- a/package.json +++ b/package.json @@ -23,16 +23,18 @@ "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", - "helmet": "^8.1.0", "express-async-handler": "^1.2.0", "express-rate-limiter": "^1.3.1", + "helmet": "^8.1.0", "joi": "^18.0.1", "jsonwebtoken": "^9.0.2", "mongoose": "^8.19.1", "morgan": "^1.10.1", "nodemon": "^3.1.10", "readdirp": "^4.1.2", - "winston": "^3.18.3", - "resend": "^6.1.3" + "resend": "^6.1.3", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "winston": "^3.18.3" } } diff --git a/src/app.js b/src/app.js index ed0d99e..2bb8f15 100644 --- a/src/app.js +++ b/src/app.js @@ -4,7 +4,9 @@ import cookieParser from "cookie-parser"; import helmet from "helmet"; import morgan from "morgan"; import dotenv from "dotenv"; +import swaggerUi from "swagger-ui-express"; import logger from "./config/logger.js"; +import swaggerSpec from "./config/swagger.js"; import authRoutes from "./routes/authRoutes.js"; import rbacRoutes from "./routes/rbacRoutes.js"; import roleRoutes from "./routes/role.routes.js"; @@ -41,6 +43,17 @@ app.use( }) ); +// Swagger API Documentation +app.use( + "/api-docs", + swaggerUi.serve, + swaggerUi.setup(swaggerSpec, { + explorer: true, + customCss: '.swagger-ui .topbar { display: none }', + customSiteTitle: 'RBAC API Docs', + }) +); + // Routes app.use("/api/auth", authRoutes); app.use(rateLimiter); diff --git a/src/config/dbconnection.js b/src/config/dbconnection.js index 00fcdcd..100359a 100644 --- a/src/config/dbconnection.js +++ b/src/config/dbconnection.js @@ -4,8 +4,8 @@ const connectDB=async ()=>{ try { if(!process.env.MONGO_URI){ - console.error('MONGO_URI not found in .env'); - process.exit(1); + console.warn('MONGO_URI not found in .env - database features will be unavailable'); + return; } await mongoose.connect(`${process.env.MONGO_URI}`) @@ -14,8 +14,7 @@ const connectDB=async ()=>{ } catch (error) { - console.log("Mongodb connnection error : ",error); - process.exit(1) + console.warn("Mongodb connection error - database features will be unavailable:",error.message); } } diff --git a/src/config/swagger.js b/src/config/swagger.js new file mode 100644 index 0000000..461d884 --- /dev/null +++ b/src/config/swagger.js @@ -0,0 +1,214 @@ +import swaggerJsDoc from 'swagger-jsdoc'; + +const swaggerOptions = { + definition: { + openapi: '3.0.0', + info: { + title: 'RBAC API Documentation', + version: '1.0.0', + description: + 'Role-Based Access Control (RBAC) API with JWT authentication. This API provides endpoints for user authentication, role management, permission management, and access control.', + contact: { + name: 'API Support', + }, + license: { + name: 'MIT', + }, + }, + servers: [ + { + url: 'http://localhost:5000', + description: 'Development server', + }, + ], + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + description: 'Enter your JWT token', + }, + cookieAuth: { + type: 'apiKey', + in: 'cookie', + name: 'refreshToken', + description: 'Refresh token stored in HTTP-only cookie', + }, + }, + schemas: { + User: { + type: 'object', + required: ['username', 'email', 'password'], + properties: { + _id: { + type: 'string', + description: 'Auto-generated MongoDB ID', + }, + username: { + type: 'string', + description: 'Unique username', + minLength: 3, + maxLength: 30, + }, + email: { + type: 'string', + format: 'email', + description: 'Unique email address', + }, + password: { + type: 'string', + description: 'Hashed password', + minLength: 6, + }, + role: { + type: 'string', + description: 'Reference to Role model', + }, + refreshToken: { + type: 'string', + description: 'JWT refresh token', + }, + createdAt: { + type: 'string', + format: 'date-time', + }, + updatedAt: { + type: 'string', + format: 'date-time', + }, + }, + }, + Role: { + type: 'object', + required: ['name'], + properties: { + _id: { + type: 'string', + description: 'Auto-generated MongoDB ID', + }, + name: { + type: 'string', + description: 'Unique role name', + enum: ['Admin', 'User', 'Moderator'], + }, + description: { + type: 'string', + description: 'Role description', + }, + permissions: { + type: 'array', + items: { + type: 'string', + }, + description: 'Array of permission IDs associated with this role', + }, + createdAt: { + type: 'string', + format: 'date-time', + }, + updatedAt: { + type: 'string', + format: 'date-time', + }, + }, + }, + Permission: { + type: 'object', + required: ['name', 'resource', 'action'], + properties: { + _id: { + type: 'string', + description: 'Auto-generated MongoDB ID', + }, + name: { + type: 'string', + description: 'Unique permission name', + }, + resource: { + type: 'string', + description: 'Resource this permission applies to', + }, + action: { + type: 'string', + description: 'Action allowed on the resource', + }, + description: { + type: 'string', + description: 'Permission description', + }, + createdAt: { + type: 'string', + format: 'date-time', + }, + updatedAt: { + type: 'string', + format: 'date-time', + }, + }, + }, + Error: { + type: 'object', + properties: { + success: { + type: 'boolean', + example: false, + }, + message: { + type: 'string', + description: 'Error message', + }, + statusCode: { + type: 'integer', + description: 'HTTP status code', + }, + }, + }, + Success: { + type: 'object', + properties: { + success: { + type: 'boolean', + example: true, + }, + message: { + type: 'string', + description: 'Success message', + }, + data: { + type: 'object', + description: 'Response data', + }, + }, + }, + }, + }, + tags: [ + { + name: 'Authentication', + description: 'User authentication and authorization endpoints', + }, + { + name: 'Roles', + description: 'Role management endpoints', + }, + { + name: 'Permissions', + description: 'Permission management endpoints', + }, + { + name: 'RBAC Tests', + description: 'Protected endpoints to test role-based access control', + }, + ], + }, + apis: [ + './src/routes/*.js', + './src/controllers/*.js', + './src/models/*.js', + ], +}; + +const swaggerSpec = swaggerJsDoc(swaggerOptions); + +export default swaggerSpec; diff --git a/src/routes/authRoutes.js b/src/routes/authRoutes.js index e8db86e..4a189e8 100644 --- a/src/routes/authRoutes.js +++ b/src/routes/authRoutes.js @@ -6,9 +6,260 @@ import { forgotPassword, resetPassword } from '../controllers/authController.js' const router = express.Router(); +/** + * @swagger + * /api/auth/register: + * post: + * summary: Register a new user + * tags: [Authentication] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - username + * - fullname + * - email + * - password + * properties: + * username: + * type: string + * minLength: 3 + * maxLength: 30 + * example: johndoe + * fullname: + * type: string + * minLength: 3 + * maxLength: 50 + * example: John Doe + * email: + * type: string + * format: email + * example: john@example.com + * password: + * type: string + * minLength: 6 + * example: password123 + * responses: + * 201: + * description: User registered successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: User registered successfully + * data: + * type: object + * properties: + * user: + * type: object + * properties: + * _id: + * type: string + * username: + * type: string + * email: + * type: string + * role: + * type: string + * 400: + * description: Validation error or user already exists + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 500: + * description: Server error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ router.post('/register', validateMiddleware(registerSchema), registerUser); + +/** + * @swagger + * /api/auth/login: + * post: + * summary: Login user + * tags: [Authentication] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * - password + * properties: + * email: + * type: string + * format: email + * example: john@example.com + * password: + * type: string + * example: password123 + * responses: + * 200: + * description: Login successful + * headers: + * Set-Cookie: + * schema: + * type: string + * example: refreshToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...; Path=/; HttpOnly + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Login successful + * data: + * type: object + * properties: + * accessToken: + * type: string + * description: JWT access token + * user: + * type: object + * properties: + * _id: + * type: string + * username: + * type: string + * email: + * type: string + * role: + * type: object + * 401: + * description: Invalid credentials + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 500: + * description: Server error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ router.post('/login', validateMiddleware(loginSchema), loginUser); + +/** + * @swagger + * /api/auth/forgotPassword: + * post: + * summary: Request password reset email + * tags: [Authentication] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * properties: + * email: + * type: string + * format: email + * example: john@example.com + * responses: + * 200: + * description: Password reset email sent + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Password reset email sent + * 404: + * description: User not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 500: + * description: Server error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ router.post('/forgotPassword',forgotPassword); + +/** + * @swagger + * /api/auth/resetPassword/{token}: + * post: + * summary: Reset password with token + * tags: [Authentication] + * parameters: + * - in: path + * name: token + * required: true + * schema: + * type: string + * description: Password reset token from email + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - password + * properties: + * password: + * type: string + * minLength: 6 + * example: newpassword123 + * responses: + * 200: + * description: Password reset successful + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Password reset successful + * 400: + * description: Invalid or expired token + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 500: + * description: Server error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ router.post('/resetPassword/:token',resetPassword); export default router; diff --git a/src/routes/permission.routes.js b/src/routes/permission.routes.js index 54216be..cba8cf8 100644 --- a/src/routes/permission.routes.js +++ b/src/routes/permission.routes.js @@ -9,10 +9,252 @@ import { const router = express.Router(); +/** + * @swagger + * /api/permissions: + * post: + * summary: Create a new permission + * tags: [Permissions] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - name + * - resource + * - action + * properties: + * name: + * type: string + * example: user:read + * resource: + * type: string + * example: user + * action: + * type: string + * example: read + * description: + * type: string + * example: Permission to read user data + * responses: + * 201: + * description: Permission created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Permission created successfully + * data: + * $ref: '#/components/schemas/Permission' + * 400: + * description: Validation error or permission already exists + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ router.post("/", createPermission); + +/** + * @swagger + * /api/permissions: + * get: + * summary: Get all permissions + * tags: [Permissions] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: List of all permissions + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Permissions fetched successfully + * data: + * type: array + * items: + * $ref: '#/components/schemas/Permission' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ router.get("/", getPermissions); + +/** + * @swagger + * /api/permissions/{id}: + * get: + * summary: Get permission by ID + * tags: [Permissions] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Permission ID + * responses: + * 200: + * description: Permission details + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Permission fetched successfully + * data: + * $ref: '#/components/schemas/Permission' + * 404: + * description: Permission not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ router.get("/:id", getPermissionById); + +/** + * @swagger + * /api/permissions/{id}: + * put: + * summary: Update permission + * tags: [Permissions] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Permission ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * resource: + * type: string + * action: + * type: string + * description: + * type: string + * responses: + * 200: + * description: Permission updated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Permission updated successfully + * data: + * $ref: '#/components/schemas/Permission' + * 404: + * description: Permission not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ router.put("/:id", updatePermission); + +/** + * @swagger + * /api/permissions/{id}: + * delete: + * summary: Delete permission + * tags: [Permissions] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Permission ID + * responses: + * 200: + * description: Permission deleted successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Permission deleted successfully + * 404: + * description: Permission not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ router.delete("/:id", deletePermission); export default router; \ No newline at end of file diff --git a/src/routes/rbacRoutes.js b/src/routes/rbacRoutes.js index 890e556..ee0e45b 100644 --- a/src/routes/rbacRoutes.js +++ b/src/routes/rbacRoutes.js @@ -6,12 +6,76 @@ import {validateMiddleware} from '../middlewares/validate.middleware.js'; const router = express.Router(); - +/** + * @swagger + * /api/rbac-test/admin-only: + * get: + * summary: Test endpoint for Admin role only + * description: Protected endpoint that only users with Admin role can access. Used for testing RBAC functionality. + * tags: [RBAC Tests] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Access granted to Admin + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Welcome, Admin + * 401: + * description: Unauthorized - Missing or invalid token + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Forbidden - User does not have Admin role + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ router.get('/admin-only', validateMiddleware(adminAccessSchema, 'headers'), authMiddleware, checkRole(['Admin']), (req, res) => { return res.status(200).json({ message: 'Welcome, Admin' }); }); - +/** + * @swagger + * /api/rbac-test/user-only: + * get: + * summary: Test endpoint for User role only + * description: Protected endpoint that only users with User role can access. Used for testing RBAC functionality. + * tags: [RBAC Tests] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Access granted to User + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Welcome, User + * 401: + * description: Unauthorized - Missing or invalid token + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Forbidden - User does not have User role + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ router.get('/user-only', validateMiddleware(userAccessSchema, 'headers'), authMiddleware, checkRole(['User']), (req, res) => { return res.status(200).json({ message: 'Welcome, User' }); }); diff --git a/src/routes/role.routes.js b/src/routes/role.routes.js index 6e01614..f195aba 100644 --- a/src/routes/role.routes.js +++ b/src/routes/role.routes.js @@ -10,11 +10,311 @@ import { const router = express.Router(); +/** + * @swagger + * /api/roles: + * post: + * summary: Create a new role + * tags: [Roles] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - name + * properties: + * name: + * type: string + * enum: [Admin, User, Moderator] + * example: Moderator + * description: + * type: string + * example: Moderator role with limited permissions + * permissions: + * type: array + * items: + * type: string + * example: [] + * responses: + * 201: + * description: Role created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Role created successfully + * data: + * $ref: '#/components/schemas/Role' + * 400: + * description: Validation error or role already exists + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ router.post("/", createRole); + +/** + * @swagger + * /api/roles: + * get: + * summary: Get all roles + * tags: [Roles] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: List of all roles + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Roles fetched successfully + * data: + * type: array + * items: + * $ref: '#/components/schemas/Role' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ router.get("/", getRoles); + +/** + * @swagger + * /api/roles/{id}: + * get: + * summary: Get role by ID + * tags: [Roles] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Role ID + * responses: + * 200: + * description: Role details + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Role fetched successfully + * data: + * $ref: '#/components/schemas/Role' + * 404: + * description: Role not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ router.get("/:id", getRoleById); + +/** + * @swagger + * /api/roles/{id}: + * put: + * summary: Update role + * tags: [Roles] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Role ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * enum: [Admin, User, Moderator] + * description: + * type: string + * permissions: + * type: array + * items: + * type: string + * responses: + * 200: + * description: Role updated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Role updated successfully + * data: + * $ref: '#/components/schemas/Role' + * 404: + * description: Role not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ router.put("/:id", updateRole); + +/** + * @swagger + * /api/roles/{id}: + * delete: + * summary: Delete role + * tags: [Roles] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Role ID + * responses: + * 200: + * description: Role deleted successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Role deleted successfully + * 404: + * description: Role not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ router.delete("/:id", deleteRole); + +/** + * @swagger + * /api/roles/{id}/permissions: + * put: + * summary: Assign permissions to role + * tags: [Roles] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Role ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - permissions + * properties: + * permissions: + * type: array + * items: + * type: string + * example: ["60d5ecb5c7f6a92c2c9d9c82", "60d5ecb5c7f6a92c2c9d9c83"] + * responses: + * 200: + * description: Permissions assigned successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Permissions assigned successfully + * data: + * $ref: '#/components/schemas/Role' + * 404: + * description: Role not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ router.put("/:id/permissions", assignPermissions); export default router; \ No newline at end of file diff --git a/src/services/authService.js b/src/services/authService.js index ce0b595..fe4870b 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -30,14 +30,12 @@ export const registerUserService = async ({ throw new Error('Default role not found. Please seed roles first.'); } - const salt = await bcrypt.genSalt(10); - const hashedPassword = await bcrypt.hash(password, salt); - + // No need to hash password here - the User model's pre-save hook will handle it const newUser = await User.create({ username, email, fullname, - password: hashedPassword, + password, // Will be hashed by pre-save hook role: userRole._id, }); diff --git a/src/utils/sendEmail.js b/src/utils/sendEmail.js index c6d5b6c..c722910 100644 --- a/src/utils/sendEmail.js +++ b/src/utils/sendEmail.js @@ -1,10 +1,14 @@ import { Resend } from "resend"; import dotenv from "dotenv"; dotenv.config(); -const resend = new Resend(process.env.RESEND_API_KEY); +const resend = process.env.RESEND_API_KEY ? new Resend(process.env.RESEND_API_KEY) : null; export const sendEmail = async (to,html) => { try { + if (!resend) { + console.warn("Resend API key not configured. Email not sent."); + return { message: "Email service not configured" }; + } const response = await resend.emails.send({ from: "Acme ", to: [to],