From e5a65086be822b8d2d532eb08f1654c091887e10 Mon Sep 17 00:00:00 2001 From: Pau Costa Date: Sun, 4 Feb 2024 20:33:26 +0100 Subject: [PATCH] :memo: OpenApi docs and endpoint for the BackEnd Routes Signed-off-by: Pau Costa --- server/package-lock.json | 273 ++++++++++++++++++++ server/package.json | 4 + server/src/controller/UserController.ts | 175 +++++++++++-- server/src/controller/authController.ts | 134 ++++++++-- server/src/controller/postController.ts | 322 ++++++++++++++++++++++-- server/src/index.ts | 5 + server/src/routes/postRoutes.ts | 5 +- server/src/routes/swaggerRoutes.ts | 158 ++++++++++++ server/src/routes/userRoutes.ts | 5 +- 9 files changed, 1011 insertions(+), 70 deletions(-) create mode 100644 server/src/routes/swaggerRoutes.ts diff --git a/server/package-lock.json b/server/package-lock.json index 38db4ec..e5da7ca 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -12,19 +12,65 @@ "body-parser": "^1.19.1", "express": "^4.17.2", "jsonwebtoken": "^9.0.2", + "ms": "^2.1.3", "mysql": "^2.14.1", "reflect-metadata": "^0.1.13", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.0", "typeorm": "0.3.20" }, "devDependencies": { "@types/bcrypt": "^5.0.2", "@types/express": "^4.17.21", "@types/jsonwebtoken": "^9.0.5", + "@types/ms": "^0.7.34", "@types/node": "^16.11.10", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.6", "ts-node": "10.9.1", "typescript": "4.5.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==", + "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==", + "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==" + }, + "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==", + "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/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -78,6 +124,11 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "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==" + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -193,6 +244,11 @@ "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "dev": true }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, "node_modules/@types/jsonwebtoken": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", @@ -253,6 +309,22 @@ "@types/node": "*" } }, + "node_modules/@types/swagger-jsdoc": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", + "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", + "dev": true + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz", + "integrity": "sha512-UVSiGYXa5IzdJJG3hrc86e8KdZWLYxyEsVoUI4iPXc7CO4VZ3AfNP8d/8+hrDRIqz+HAaSMtZSqAsF3Nq2X/Dg==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -397,6 +469,11 @@ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "devOptional": true }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -527,6 +604,11 @@ "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==" + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -748,6 +830,14 @@ "color-support": "bin.js" } }, + "node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "engines": { + "node": ">= 6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -884,6 +974,17 @@ "node": ">=0.3.1" } }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/dotenv": { "version": "16.4.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", @@ -939,6 +1040,14 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1423,6 +1532,17 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -1463,6 +1583,11 @@ "safe-buffer": "^5.0.1" } }, + "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==" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -1473,6 +1598,11 @@ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" }, + "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==" + }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", @@ -1493,6 +1623,11 @@ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" + }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -1770,6 +1905,12 @@ "wrappy": "1" } }, + "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==", + "peer": true + }, "node_modules/parse5": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", @@ -2264,6 +2405,94 @@ "node": ">=8" } }, + "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==", + "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-jsdoc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/swagger-jsdoc/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "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/swagger-jsdoc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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==", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.11.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.2.tgz", + "integrity": "sha512-jQG0cRgJNMZ7aCoiFofnoojeSaa/+KgWaDlfgs8QN+BXoGMpxeMVY5OEnjq4OlNvF3yjftO8c9GRAgcHlO+u7A==" + }, + "node_modules/swagger-ui-express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz", + "integrity": "sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA==", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/tar": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", @@ -2574,6 +2803,14 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "devOptional": true }, + "node_modules/validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -2754,6 +2991,14 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "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==", + "engines": { + "node": ">= 6" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -2824,6 +3069,34 @@ "engines": { "node": ">=6" } + }, + "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==", + "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==", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } } } } diff --git a/server/package.json b/server/package.json index f1b34d5..10a2af7 100644 --- a/server/package.json +++ b/server/package.json @@ -9,6 +9,8 @@ "@types/jsonwebtoken": "^9.0.5", "@types/ms": "^0.7.34", "@types/node": "^16.11.10", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.6", "ts-node": "10.9.1", "typescript": "4.5.2" }, @@ -20,6 +22,8 @@ "ms": "^2.1.3", "mysql": "^2.14.1", "reflect-metadata": "^0.1.13", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.0", "typeorm": "0.3.20" }, "scripts": { diff --git a/server/src/controller/UserController.ts b/server/src/controller/UserController.ts index fec29b0..bf1d2dc 100644 --- a/server/src/controller/UserController.ts +++ b/server/src/controller/UserController.ts @@ -11,8 +11,28 @@ export class UserController { private userRepository = AppDataSource.getRepository(User) private notificationRepository = AppDataSource.getRepository(Notification) - public getAllUsers = catchAsync(async (req: Request, res: Response, next: NextFunction) => { - const users = await this.userRepository.find() + /** + * @swagger + * /users: + * get: + * security: + * - bearerAuth: [] + * tags: + * - Users + * summary: Get all users + * responses: + * 200: + * description: A list of all users + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/User' + */ + public getAllUsers = catchAsync( + async (req: Request, res: Response, next: NextFunction) => { + const users = await this.userRepository.find(); // remove sensitive fields users.forEach(user => { @@ -22,11 +42,36 @@ export class UserController { res.status(200).send(users) }) - public getUser = catchAsync( async (req: Request, res: Response, next: NextFunction) => { - const id = req.params.id - const parsedId = parseInt(id) - // Check if ID is a number - if(isNaN(parsedId)) return next(new AppError('Invalid ID', 400)) + /** + * @swagger + * /users/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: + * - Users + * summary: Get a single user + * parameters: + * - in: path + * name: id + * required: true + * description: ID of the user + * schema: + * type: integer + * responses: + * 200: + * description: A single user + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UserWithRelations' + */ + public getUser = catchAsync( + async (req: Request, res: Response, next: NextFunction) => { + const id = req.params.id; + const parsedId = parseInt(id); + // Check if ID is a number + if (isNaN(parsedId)) return next(new AppError("Invalid ID", 400)); const user = await this.userRepository.findOne({where: {id: parsedId}, relations:{ followed: true, @@ -46,16 +91,34 @@ export class UserController { return res.send(user) }) - public getMe = catchAsync(async (req: AppRequest, res: Response, next: NextFunction) => { - const user = await this.userRepository.findOne({ - where: {id: req.user.id}, - relations:{ - followed: true, - followers: true, - posts: true, - comments: true, - } - }) + /** + * @swagger + * /users/me: + * get: + * tags: + * - Users + * summary: Get the currently logged in user + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: The currently logged in user + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UserWithRelations' + */ + public getMe = catchAsync( + async (req: AppRequest, res: Response, next: NextFunction) => { + const user = await this.userRepository.findOne({ + where: { id: req.user.id }, + relations: { + followed: true, + followers: true, + posts: true, + comments: true, + }, + }); user.followed.forEach(followedUser => followedUser.deleteSensitiveFields()) user.followers.forEach(follower => follower.deleteSensitiveFields()) @@ -63,11 +126,40 @@ export class UserController { return res.status(200).send(user) }) - public followUser = catchAsync(async (req: AppRequest, res: Response, next: NextFunction) => { - const userToFollowId = req.params.id - const parsedId = parseInt(userToFollowId) - // Check if ID is a number - if(isNaN(parsedId)) return next(new AppError('Invalid ID', 400)) + /** + * @swagger + * /users/{id}/follow: + * post: + * security: + * - bearerAuth: [] + * tags: + * - Users + * summary: Follow a user + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The ID of the user to follow + * responses: + * 200: + * description: Successfully followed the user + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UserWithRelations' + * 400: + * description: Invalid ID + * 404: + * description: No user found with that ID + */ + public followUser = catchAsync( + async (req: AppRequest, res: Response, next: NextFunction) => { + const userToFollowId = req.params.id; + const parsedId = parseInt(userToFollowId); + // Check if ID is a number + if (isNaN(parsedId)) return next(new AppError("Invalid ID", 400)); const user = req.user const userToFollow = await this.userRepository.findOne({ @@ -104,11 +196,40 @@ export class UserController { }) }) - public unfollowUser = catchAsync(async (req: AppRequest, res: Response, next: NextFunction) => { - const userToUnfollowId = req.params.id - const parsedId = parseInt(userToUnfollowId) - // Check if ID is a number - if(isNaN(parsedId)) return next(new AppError('Invalid ID', 400)) + /** + * @swagger + * /users/{id}/follow: + * delete: + * security: + * - bearerAuth: [] + * tags: + * - Users + * summary: Unfollow a user + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: The ID of the user to unfollow + * responses: + * 200: + * description: Successfully unfollowed the user + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UserWithRelations' + * 400: + * description: Invalid ID + * 404: + * description: No user found with that ID + */ + public unfollowUser = catchAsync( + async (req: AppRequest, res: Response, next: NextFunction) => { + const userToUnfollowId = req.params.id; + const parsedId = parseInt(userToUnfollowId); + // Check if ID is a number + if (isNaN(parsedId)) return next(new AppError("Invalid ID", 400)); const user = req.user const userToUnfollow = await this.userRepository.findOne({ diff --git a/server/src/controller/authController.ts b/server/src/controller/authController.ts index 2f5f13f..f01ead1 100644 --- a/server/src/controller/authController.ts +++ b/server/src/controller/authController.ts @@ -38,9 +38,48 @@ export class AuthController { } - public handleSignUp = catchAsync(async (req, res, next) => { - const {email, password, passwordValidation, firstName, lastName} = req.body - // Body Validation + /** + * @swagger + * /auth/signup: + * post: + * tags: + * - Authentication + * summary: Sign up a new user + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: string + * description: The email of the user + * password: + * type: string + * description: The password of the user + * passwordValidation: + * type: string + * description: Password validation field + * firstName: + * type: string + * description: The first name of the user + * lastName: + * type: string + * description: The last name of the user + * responses: + * 200: + * description: Successfully signed up the user + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/User' + * 400: + * description: Invalid input or user already exists + */ + public handleSignUp = catchAsync(async (req, res, next) => { + const {email, password, passwordValidation, firstName, lastName} = req.body + // Body Validation if (password != passwordValidation) { return next(new AppError('Passwords do not match', 400)) @@ -64,11 +103,53 @@ export class AuthController { this.sendToken(await this.userRepository.save(newUser), res) }) - public handleLogin = catchAsync(async (req, res, next) => { - const {email, password, longExpiration} = req.body - if (!email || !password) { - return next(new AppError('Please provide email and password', 400)) - } + /** + * @swagger + * /auth/login: + * post: + * tags: + * - Authentication + * summary: Log in a user + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: string + * description: The email of the user + * password: + * type: string + * description: The password of the user + * longExpiration: + * type: boolean + * description: Whether to keep the user logged in for a long time + * responses: + * 200: + * description: Successfully logged in the user + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: "success" + * token: + * type: string + * description: The JWT token for the user + * 400: + * description: Missing email or password + * 401: + * description: Incorrect email or password + */ + public handleLogin = catchAsync(async (req, res, next) => { + const { email, password, longExpiration } = req.body; + if (!email || !password) { + return next(new AppError("Please provide email and password", 400)); + } const user = await this.userRepository.findOne({where: {email}}) if (!user || !(await user.comparePassword(password))) { @@ -79,14 +160,35 @@ export class AuthController { this.sendToken(user, res, {long: longExpiration}) }) - public handleLogout = catchAsync(async (req: AppRequest, res, next) => { - // Set the jwt cookie to a dummy value and set the expiration to a date in the past - res.cookie('jwt', 'loggedout', { - expires: new Date(Date.now() - 10000), - httpOnly: true - }) - res.status(200).json({status: 'success'}) - }) + /** + * @swagger + * /auth/logout: + * get: + * security: + * - bearerAuth: [] + * tags: + * - Authentication + * summary: Log out the user + * responses: + * 200: + * description: Successfully logged out the user + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: "success" + */ + public handleLogout = catchAsync(async (req: AppRequest, res, next) => { + // Set the jwt cookie to a dummy value and set the expiration to a date in the past + res.cookie("jwt", "loggedout", { + expires: new Date(Date.now() - 10000), + httpOnly: true, + }); + res.status(200).json({ status: "success" }); + }); public protect = catchAsync(async (req: AppRequest, res, next) => { let token: string | undefined; diff --git a/server/src/controller/postController.ts b/server/src/controller/postController.ts index 10f18c5..d00de4a 100644 --- a/server/src/controller/postController.ts +++ b/server/src/controller/postController.ts @@ -16,20 +16,72 @@ export class PostController { private userRepository = AppDataSource.getRepository(User) - public getAllPosts = catchAsync(async (_req, res, _next) => { - const posts = await this.postRepository.find({relations: {createdBy: true}}) + /** + * @swagger + * /posts: + * get: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: getAllPosts + * summary: Get all posts + * responses: + * 200: + * description: A list of all posts + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/Post' + */ + public getAllPosts = catchAsync(async (_req, res, _next) => { + const posts = await this.postRepository.find({ + relations: { createdBy: true }, + }); // Remove sensitive fields posts.forEach(post => { post.deleteSensitiveFields() }) - res.status(200).send(posts) - }) - - public getPost = catchAsync(async (req, res, next) => { - const { post, errorMessage} = - await this.validateRequestAndGetEntities(req) + res.status(200).send(posts); + }); + + /** + * @swagger + * /posts/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: getPost + * summary: Get a post by ID + * parameters: + * - in: path + * name: id + * required: true + * description: ID of the post + * schema: + * type: integer + * responses: + * 200: + * description: A post + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Post' + * 400: + * description: Invalid ID + * 404: + * description: No post found with that ID + */ + public getPost = catchAsync(async (req, res, next) => { + const { post, errorMessage } = await this.validateRequestAndGetEntities( + req + ); if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400)) if(errorMessage == 'No post found with that ID'){ @@ -39,8 +91,29 @@ export class PostController { // Remove sensitive fields post.deleteSensitiveFields() - res.send(post) - }) + res.send(post); + }); + + /** + * @swagger + * /posts/followed: + * get: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: getFollowedPosts + * summary: Get posts of followed users + * responses: + * 200: + * description: A list of posts + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/Post' + */ public getFollowedPosts = catchAsync(async (req : AppRequest, res, _next) => { const user = await this.userRepository.findOne({ @@ -54,8 +127,42 @@ export class PostController { post.deleteSensitiveFields() }) - res.send(followedPosts) - }) + res.send(followedPosts); + }); + + /** + * @swagger + * /posts: + * post: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: createPost + * summary: Create a post + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * title: + * type: string + * description: Post title + * content: + * type: string + * description: Post content + * responses: + * 201: + * description: A post + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Post' + * 400: + * description: Title and content are required + */ public createPost = catchAsync(async (req : AppRequest, res, next) => { const user = await this.userRepository.findOne({where: {id: req.user.id}}) @@ -75,8 +182,51 @@ export class PostController { // Remove sensitive fields post.deleteSensitiveFields() - res.status(201).send(post) - }) + res.status(201).send(post); + }); + + /** + * @swagger + * /posts/{id}: + * patch: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: updatePost + * summary: Update a post by ID + * parameters: + * - in: path + * name: id + * required: true + * description: ID of the post + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * title: + * type: string + * description: Post title + * content: + * type: string + * description: Post content + * responses: + * 200: + * description: A post + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Post' + * 400: + * description: Invalid ID or Title and content are required + * 404: + * description: No post found with that ID + */ public updatePost = catchAsync(async (req : AppRequest, res, next) => { const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req) @@ -100,8 +250,34 @@ export class PostController { // Remove sensitive fields post.deleteSensitiveFields() - res.status(200).send(post) - }) + res.status(200).send(post); + }); + + /** + * @swagger + * /posts/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: deletePost + * summary: Delete a post by ID + * parameters: + * - in: path + * name: id + * required: true + * description: ID of the post + * schema: + * type: integer + * responses: + * 204: + * description: No content + * 400: + * description: Invalid ID + * 404: + * description: No post found with that ID + */ public deletePost = catchAsync(async (req : AppRequest, res, next) => { const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req) @@ -113,9 +289,39 @@ export class PostController { return next(new AppError('You are not authorized to delete this post', 403)) } - await this.postRepository.remove(post) - res.status(204).send() - }) + await this.postRepository.remove(post); + res.status(204).send(); + }); + + /** + * @swagger + * /posts/{id}/like: + * post: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: likePost + * summary: Like a post by ID + * parameters: + * - in: path + * name: id + * required: true + * description: ID of the post + * schema: + * type: integer + * responses: + * 200: + * description: A post + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Post' + * 400: + * description: Invalid ID or You have already liked this post + * 404: + * description: No post found with that ID + */ public likePost = catchAsync(async (req : AppRequest, res, next) => { const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req) @@ -146,8 +352,38 @@ export class PostController { // Remove sensitive fields post.deleteSensitiveFields() - res.status(200).send(post) - }) + res.status(200).send(post); + }); + + /** + * @swagger + * /posts/{id}/like: + * delete: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: unlikePost + * summary: Unlike a post by ID + * parameters: + * - in: path + * name: id + * required: true + * description: ID of the post + * schema: + * type: integer + * responses: + * 200: + * description: A post + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Post' + * 400: + * description: Invalid ID or You have not liked this post + * 404: + * description: No post found with that ID + */ public unlikePost = catchAsync(async (req : AppRequest, res, next) => { const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req) @@ -166,8 +402,48 @@ export class PostController { // Remove sensitive fields post.deleteSensitiveFields() - res.status(200).send(post) - }) + res.status(200).send(post); + }); + + /** + * @swagger + * /posts/{id}/comment: + * post: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: commentPost + * summary: Comment on a post by ID + * parameters: + * - in: path + * name: id + * required: true + * description: ID of the post + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * content: + * type: string + * description: Comment content + * responses: + * 201: + * description: A comment + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Comment' + * 400: + * description: Invalid Request + * 404: + * description: No post found with that ID + */ public commentPost = catchAsync(async (req : AppRequest, res, next) => { const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req) diff --git a/server/src/index.ts b/server/src/index.ts index 4882f4d..b059b60 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -6,6 +6,7 @@ import {AuthRoutes} from "./routes/authRoutes"; import {UserRoutes} from "./routes/userRoutes"; import {AuthController} from "./controller/authController"; import {PostRoutes} from "./routes/postRoutes"; +import {swaggerRouter} from "./routes/swaggerRoutes"; AppDataSource.initialize().then(async () => { @@ -16,6 +17,10 @@ AppDataSource.initialize().then(async () => { // register express routes from defined application routes // Auth Routes app.use('/auth', AuthRoutes) + + // Swagger Routes + app.use('/docs', swaggerRouter); + // All routes after this one require authentication const authController = new AuthController(); app.use(authController.protect) diff --git a/server/src/routes/postRoutes.ts b/server/src/routes/postRoutes.ts index 4ea76f8..47ed5d0 100644 --- a/server/src/routes/postRoutes.ts +++ b/server/src/routes/postRoutes.ts @@ -13,6 +13,7 @@ PostRoutes.route("/:id") .get(postController.getPost) .patch(postController.updatePost) .delete(postController.deletePost) -PostRoutes.route("/:id/like").post(postController.likePost) -PostRoutes.route("/:id/unlike").post(postController.unlikePost) +PostRoutes.route("/:id/like") + .post(postController.likePost) + .delete(postController.unlikePost) PostRoutes.route("/:id/comment").post(postController.commentPost) \ No newline at end of file diff --git a/server/src/routes/swaggerRoutes.ts b/server/src/routes/swaggerRoutes.ts new file mode 100644 index 0000000..8e6e28a --- /dev/null +++ b/server/src/routes/swaggerRoutes.ts @@ -0,0 +1,158 @@ +import * as swaggerJSdoc from "swagger-jsdoc"; +import {Router} from "express"; +import * as swaggerUi from "swagger-ui-express"; +import { catchAsync } from "../util/catchAsync"; + +const swaggerOptions = { + swaggerDefinition: { + openapi: "3.0.0", + info: { + title: "DevSpace API", + description: "API for DevSpace", + version: "0.1.0", + }, + + components: { + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + }, + }, + schemas: { + Post: { + type: "object", + properties: { + id: { + type: "integer", + description: "Post ID", + }, + title: { + type: "string", + description: "Post title", + }, + content: { + type: "string", + description: "Post content", + }, + createdAt: { + type: "string", + description: "Post creation date", + }, + createdBy: { + $ref: "#/components/schemas/User", + }, + likedBy: { + type: "array", + items: { + $ref: "#/components/schemas/User", + }, + }, + comments: { + type: "array", + items: { + $ref: "#/components/schemas/Comment", + }, + }, + }, + }, + Comment: { + type: "object", + properties: { + id: { + type: "integer", + description: "Comment ID", + }, + content: { + type: "string", + description: "Comment content", + }, + createdAt: { + type: "string", + description: "Comment creation date", + }, + createdBy: { + $ref: "#/components/schemas/User", + }, + likedBy: { + type: "array", + items: { + $ref: "#/components/schemas/User", + }, + }, + }, + }, + }, + User: { + type: "object", + properties: { + id: { + type: "integer", + description: "User ID", + }, + firstName: { + type: "string", + description: "User first name", + }, + lastName: { + type: "string", + description: "User last name", + }, + }, + }, + UserWithRelations: { + allOf: [ + { + $ref: "#/components/schemas/User", + }, + { + type: "object", + properties: { + posts: { + type: "array", + items: { + $ref: "#/components/schemas/Post", + }, + }, + comments: { + type: "array", + items: { + $ref: "#/components/schemas/Comment", // Assuming you have a Comment schema + }, + }, + followed: { + type: "array", + items: { + $ref: "#/components/schemas/User", + }, + }, + followers: { + type: "array", + items: { + $ref: "#/components/schemas/User", + }, + }, + }, + }, + ], + }, + }, + }, + + apis: ["**/controller/*.ts"], +}; + +const swaggerDocs = swaggerJSdoc(swaggerOptions); + + +console.log(swaggerDocs) +export const swaggerRouter = Router() + + +swaggerRouter.get( + "/json", catchAsync(async (req, res, next) => { + res.json(swaggerDocs); + }) +); +swaggerRouter.use("/", swaggerUi.serve, swaggerUi.setup(swaggerDocs)); \ No newline at end of file diff --git a/server/src/routes/userRoutes.ts b/server/src/routes/userRoutes.ts index cd268dc..c172de0 100644 --- a/server/src/routes/userRoutes.ts +++ b/server/src/routes/userRoutes.ts @@ -8,5 +8,6 @@ const userController = new UserController() UserRoutes.route("/").get(userController.getAllUsers) UserRoutes.route("/me").get(userController.getMe) UserRoutes.route("/:id").get(userController.getUser) -UserRoutes.route("/follow/:id").post(userController.followUser) -UserRoutes.route("/unfollow/:id").post(userController.unfollowUser) \ No newline at end of file +UserRoutes.route("/follow/:id") + .post(userController.followUser) + .delete(userController.unfollowUser) \ No newline at end of file