diff --git a/package-lock.json b/package-lock.json index a7c070ca..b3b47479 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "jest": "^29.7.0", "joi": "^17.13.1", "jsonwebtoken": "^9.0.2", + "mailgun-js": "^0.22.0", "morgan": "^1.10.0", "nodemailer": "^6.9.13", "nodemon": "^3.1.0", @@ -40,12 +41,14 @@ "typeorm": "^0.3.20" }, "devDependencies": { + "@types/bcrypt": "^5.0.2", "@types/bcryptjs": "^2.4.6", "@types/cookie-session": "^2.0.49", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/jest": "^29.5.12", "@types/jsonwebtoken": "^9.0.6", + "@types/mailgun-js": "^0.22.18", "@types/morgan": "^1.9.9", "@types/nodemailer": "^6.4.15", "@types/passport": "^1.0.16", @@ -1645,6 +1648,15 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/bcryptjs": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", @@ -1813,6 +1825,30 @@ "@types/node": "*" } }, + "node_modules/@types/mailgun-js": { + "version": "0.22.18", + "resolved": "https://registry.npmjs.org/@types/mailgun-js/-/mailgun-js-0.22.18.tgz", + "integrity": "sha512-xp1QwOjlgvbV9MmKEEmmXN0PkfDxkVQjq0IU1c9DL7VBIQCPxnrqLTaCVm40Ig+YxUnt6e2dT/dNUhaKGQ+V6A==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/mailgun-js/node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -2519,6 +2555,17 @@ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" }, + "node_modules/ast-types": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.2.tgz", + "integrity": "sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/async": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", @@ -3181,7 +3228,6 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, "engines": { "iojs": ">= 1.0.0", "node": ">= 0.12.0" @@ -3331,6 +3377,11 @@ "node": ">= 0.8" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -3383,6 +3434,11 @@ "node": ">= 8" } }, + "node_modules/data-uri-to-buffer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz", + "integrity": "sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ==" + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -3497,8 +3553,7 @@ "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, "node_modules/deepmerge": { "version": "4.3.1", @@ -3550,6 +3605,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/degenerator": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-1.0.4.tgz", + "integrity": "sha512-EMAC+riLSC64jKfOs1jp8J7M4ZXstUUwTdwFBEv6HOzL/Ae+eAzMKEK0nJnpof2fnw9IOjmE6u6qXFejVyk8AA==", + "dependencies": { + "ast-types": "0.x.x", + "escodegen": "1.x.x", + "esprima": "3.x.x" + } + }, + "node_modules/degenerator/node_modules/esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha512-AWwVMNxwhN8+NIPQzAQZCm7RkLC4RbM3B1OobMuyp3i+w73X57KCKaVIxaRZb+DYCojq7rspo+fmuQfAboyhFg==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3878,6 +3955,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, + "node_modules/es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "dependencies": { + "es6-promise": "^4.0.3" + } + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -3903,6 +3993,82 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/escodegen/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/eslint": { "version": "8.57.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", @@ -4200,7 +4366,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -4384,6 +4549,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4433,8 +4603,7 @@ "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, "node_modules/fast-safe-stringify": { "version": "2.1.1", @@ -4471,6 +4640,11 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -4695,6 +4869,39 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, + "node_modules/ftp": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", + "integrity": "sha512-faFVML1aBx2UoDStmLwv2Wptt4vw5x03xxX172nhA5Y5HBshW5JweqQ2W4xL4dezQTG8inJsuYcpPHHU3X5OTQ==", + "dependencies": { + "readable-stream": "1.1.x", + "xregexp": "2.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/ftp/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "node_modules/ftp/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/ftp/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4824,6 +5031,64 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-uri": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-2.0.4.tgz", + "integrity": "sha512-v7LT/s8kVjs+Tx0ykk1I+H/rbpzkHvuIq87LmeXptcf5sNWm9uQiwjNAt94SJPA1zOlCntmnOlJvVWKmzsxG8Q==", + "dependencies": { + "data-uri-to-buffer": "1", + "debug": "2", + "extend": "~3.0.2", + "file-uri-to-path": "1", + "ftp": "~0.3.10", + "readable-stream": "2" + } + }, + "node_modules/get-uri/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/get-uri/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/get-uri/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/get-uri/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/get-uri/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/get-uri/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -5116,6 +5381,42 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", + "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", + "dependencies": { + "agent-base": "4", + "debug": "3.1.0" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "dependencies": { + "es6-promisify": "^5.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", @@ -5237,6 +5538,14 @@ "node": ">=0.8.19" } }, + "node_modules/inflection": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", + "integrity": "sha512-lRy4DxuIFWXlJU7ed8UiTJOSTqStqYdEb4CEbtXfNbkdj3nH1L+reUWiE10VWcJS2yR7tge8Z74pJjtBjNwj0w==", + "engines": [ + "node >= 0.4.0" + ] + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -5265,6 +5574,11 @@ "node": ">= 0.4" } }, + "node_modules/ip": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -6540,11 +6854,59 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, "dependencies": { "yallist": "^3.0.2" } }, + "node_modules/mailgun-js": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/mailgun-js/-/mailgun-js-0.22.0.tgz", + "integrity": "sha512-a2alg5nuTZA9Psa1pSEIEsbxr1Zrmqx4VkgGCQ30xVh0kIH7Bu57AYILo+0v8QLSdXtCyLaS+KVmdCrQo0uWFA==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dependencies": { + "async": "^2.6.1", + "debug": "^4.1.0", + "form-data": "^2.3.3", + "inflection": "~1.12.0", + "is-stream": "^1.1.0", + "path-proxy": "~1.0.0", + "promisify-call": "^2.0.2", + "proxy-agent": "^3.0.3", + "tsscmp": "^1.0.6" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/mailgun-js/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/mailgun-js/node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/mailgun-js/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -6820,6 +7182,14 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node_modules/netmask": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz", + "integrity": "sha512-3DWDqAtIiPSkBXZyYEjwebfK56nrlQfRGt642fu8RPaL+ePu750+HCMHxjJCG3iEHq/0aeMvX6KIzlv7nuhfrA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/node-addon-api": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", @@ -7205,6 +7575,64 @@ "node": ">=6" } }, + "node_modules/pac-proxy-agent": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-3.0.1.tgz", + "integrity": "sha512-44DUg21G/liUZ48dJpUSjZnFfZro/0K5JTyFYLBcmh9+T6Ooi4/i4efwUiEy0+4oQusCBqWdhv16XohIj1GqnQ==", + "dependencies": { + "agent-base": "^4.2.0", + "debug": "^4.1.1", + "get-uri": "^2.0.0", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^3.0.0", + "pac-resolver": "^3.0.0", + "raw-body": "^2.2.0", + "socks-proxy-agent": "^4.0.1" + } + }, + "node_modules/pac-proxy-agent/node_modules/agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "dependencies": { + "es6-promisify": "^5.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", + "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", + "dependencies": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/pac-proxy-agent/node_modules/https-proxy-agent/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/pac-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-3.0.0.tgz", + "integrity": "sha512-tcc38bsjuE3XZ5+4vP96OfhOugrX+JcnpUbhfuc4LuXBLQhoTthOstZeoQJBDnQUDYzYmdImKsbz0xSl1/9qeA==", + "dependencies": { + "co": "^4.6.0", + "degenerator": "^1.0.4", + "ip": "^1.1.5", + "netmask": "^1.0.6", + "thunkify": "^2.1.2" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7400,6 +7828,22 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-proxy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-proxy/-/path-proxy-1.0.0.tgz", + "integrity": "sha512-p9IuY9FRY1nU59RDW+tnLL6qMxmBnY03WGYxzy1FcqE5OMO5ggz7ahmOBH0JBS+9f95Yc7V5TZ+kHpTeFWaLQA==", + "dependencies": { + "inflection": "~1.3.0" + } + }, + "node_modules/path-proxy/node_modules/inflection": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.3.8.tgz", + "integrity": "sha512-xRvG6XhAkbneGO5BXP0uKyGkzmZ2bBbrFkx4ZVNx2TmsECbiq/pJapbbx/NECh+E85IfZwW5+IeVNJfkQgavag==", + "engines": [ + "node >= 0.4.0" + ] + }, "node_modules/path-scurry": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", @@ -7719,6 +8163,22 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/promisify-call": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/promisify-call/-/promisify-call-2.0.4.tgz", + "integrity": "sha512-ZX68J1+1Pe0I8NC0P6Ji3fDDcJceVfpoygfDLgdb1fp5vW9IRlwSpDaxe1T5HgwchyHV2DsL/pWzWikUiWEbLQ==", + "dependencies": { + "with-callback": "^1.0.2" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -7744,6 +8204,55 @@ "node": ">= 0.10" } }, + "node_modules/proxy-agent": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-3.1.1.tgz", + "integrity": "sha512-WudaR0eTsDx33O3EJE16PjBRZWcX8GqCEeERw1W3hZJgH/F2a46g7jty6UGty6NeJ4CKQy8ds2CJPMiyeqaTvw==", + "dependencies": { + "agent-base": "^4.2.0", + "debug": "4", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^3.0.0", + "lru-cache": "^5.1.1", + "pac-proxy-agent": "^3.0.1", + "proxy-from-env": "^1.0.0", + "socks-proxy-agent": "^4.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/proxy-agent/node_modules/agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "dependencies": { + "es6-promisify": "^5.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/proxy-agent/node_modules/https-proxy-agent": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", + "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", + "dependencies": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/proxy-agent/node_modules/https-proxy-agent/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -8299,6 +8808,56 @@ "node": ">=8" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.3.3.tgz", + "integrity": "sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==", + "dependencies": { + "ip": "1.1.5", + "smart-buffer": "^4.1.0" + }, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz", + "integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==", + "dependencies": { + "agent-base": "~4.2.1", + "socks": "~2.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", + "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", + "dependencies": { + "es6-promisify": "^5.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/socks/node_modules/ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha512-rBtCAQAJm8A110nbwn6YdveUnuZH3WrC36IwkRXxDnq53JvXA2NVQvB7IHyKomxK1MJ4VDNw3UtFDdXQ+AvLYA==" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -8769,6 +9328,14 @@ "node": ">=0.2.6" } }, +<<<<<<< HEAD +======= + "node_modules/thunkify": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/thunkify/-/thunkify-2.1.2.tgz", + "integrity": "sha512-w9foI80XcGImrhMQ19pxunaEC5Rp2uzxZZg4XBAFRfiLOplk3F0l7wo+bO16vC2/nlQfR/mXZxcduo0MF2GWLg==" + }, +>>>>>>> 87b26945c015e1ffac3453c190f06c1f6d096b48 "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -9579,11 +10146,18 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/with-callback": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/with-callback/-/with-callback-1.0.2.tgz", + "integrity": "sha512-zaUhn7OWgikdqWlPYpZ4rTX/6IAV0czMVyd+C6QLVrif2tATF28CYUnHBmHs2a5EaZo7bB1+plBUPHto+HW8uA==", + "engines": { + "node": ">=4" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -9644,6 +10218,14 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xregexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", + "integrity": "sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA==", + "engines": { + "node": "*" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -9663,8 +10245,7 @@ "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/yaml": { "version": "2.0.0-1", diff --git a/package.json b/package.json index 94062fc1..c3b9ae67 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "build": "tsc", "lint": "eslint --config .eslintrc.json .", "format": "prettier --write .", - "test": "jest --no-cache", - "test:ci": "jest --coverage" + "test": "jest --no-cache --detectOpenHandles", + "test:ci": "jest --coverage --detectOpenHandles" }, "repository": { "type": "git", @@ -40,6 +40,7 @@ "jest": "^29.7.0", "joi": "^17.13.1", "jsonwebtoken": "^9.0.2", + "mailgun-js": "^0.22.0", "morgan": "^1.10.0", "nodemailer": "^6.9.13", "nodemon": "^3.1.0", @@ -65,12 +66,14 @@ ] }, "devDependencies": { + "@types/bcrypt": "^5.0.2", "@types/bcryptjs": "^2.4.6", "@types/cookie-session": "^2.0.49", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/jest": "^29.5.12", "@types/jsonwebtoken": "^9.0.6", + "@types/mailgun-js": "^0.22.18", "@types/morgan": "^1.9.9", "@types/nodemailer": "^6.4.15", "@types/passport": "^1.0.16", diff --git a/src/__test__/category.test.ts b/src/__test__/category.test.ts new file mode 100644 index 00000000..21b161bc --- /dev/null +++ b/src/__test__/category.test.ts @@ -0,0 +1,183 @@ +import request from 'supertest'; +import app from '../app'; +import { afterAllHook, beforeAllHook, getVendorToken } from './testSetup'; + +beforeAll(beforeAllHook); +afterAll(afterAllHook); + +describe('Category Creation Tests', () => { + beforeAll(async () => { + token = await getVendorToken(); + }); + let token: string; + let categoryId: number; + + it('should create a new category with valid data', async () => { + const categoryData = { + name: 'Test Category', + description: 'Test category description', + }; + + const response = await request(app) + .post('/api/v1/category') + .set('Authorization', `Bearer ${token}`) + .send(categoryData); + expect(response.status).toBe(201); + expect(response.body.message).toBe('Category successfully created'); + expect(response.body.data).toHaveProperty('id'); + expect(response.body.data).toHaveProperty('name', categoryData.name); + expect(response.body.data).toHaveProperty( + 'description', + categoryData.description + ); + categoryId = response.body.data.id; + }); + + it('should return a 400 status code if name is missing', async () => { + const invalidData = { + description: 'Test category description', + }; + + const response = await request(app) + .post('/api/v1/category') + .set('Authorization', `Bearer ${token}`) + .send(invalidData); + + expect(response.status).toBe(400); + expect(response.body.errors[0].msg).toBe('Category name is required'); + }); + + it('should return 400 if request data is invalid', async () => { + const invalidData = {}; + + const response = await request(app) + .put(`/api/v1/category/${categoryId}`) + .set('Authorization', `Bearer ${token}`) + .send(invalidData); + + expect(response.status).toBe(400); + expect(response.body.errors).toBeDefined(); + }); + + it('should return a 409 status code if category name already exists', async () => { + const existingCategoryData = { + name: 'Existing Category', + description: 'Existing category description', + }; + await request(app) + .post('/api/v1/category') + .set('Authorization', `Bearer ${token}`) + .send(existingCategoryData); + + const newCategoryData = { + name: 'Existing Category', + description: 'Existing category description', + }; + const response = await request(app) + .post('/api/v1/category') + .set('Authorization', `Bearer ${token}`) + .send(newCategoryData); + + expect(response.status).toBe(409); + expect(response.body.message).toBe('Category name already exists'); + }); + + it('should return all categories with status 200', async () => { + const response = await request(app).get('/api/v1/category'); + expect(response.status).toBe(200); + expect(response.body.message).toBe('Data retrieved successfully'); + expect(response.body.data).toBeDefined; + }); + + it('should return a category by ID with status 200', async () => { + const response = await request(app).get(`/api/v1/category/${categoryId}`); + + expect(response.status).toBe(200); + expect(response.body.message).toBe('Data retrieved successfully'); + expect(response.body.data).toBeDefined; + }); + + it('should return 404 if category is not found', async () => { + const nonExistentCategoryId = 9999; + + const response = await request(app).get( + `/api/v1/category/${nonExistentCategoryId}` + ); + + expect(response.status).toBe(404); + expect(response.body.message).toBe('Category Not Found'); + }); + + it('should update the category with status 200', async () => { + const updatedCategoryData = { + name: 'Updated Category Name', + description: 'Updated category description', + }; + + const response = await request(app) + .put(`/api/v1/category/${categoryId}`) + .set('Authorization', `Bearer ${token}`) + .send(updatedCategoryData); + + expect(response.status).toBe(200); + expect(response.body.message).toBe('Category successfully updated'); + expect(response.body.data.name).toBe(updatedCategoryData.name); + expect(response.body.data.description).toBe( + updatedCategoryData.description + ); + }); + + it('should return a 409 status code if category update name already exists', async () => { + const existingCategoryData = { + name: 'Existing Category', + description: 'Existing category description', + }; + await request(app) + .post('/api/v1/category') + .set('Authorization', `Bearer ${token}`) + .send(existingCategoryData); + + const updateCategoryData = { + name: 'Existing Category', + description: 'Existing category description', + }; + const response = await request(app) + .put(`/api/v1/category/${categoryId}`) + .set('Authorization', `Bearer ${token}`) + .send(updateCategoryData); + + expect(response.status).toBe(409); + expect(response.body.message).toBe('Category name already exists'); + }); + + it('should return 404 if category is not found', async () => { + const response = await request(app) + .put('/api/v1/category/9999') + .set('Authorization', `Bearer ${token}`) + .send({ + name: 'Updated Category Name', + description: 'Updated category description', + }); + + expect(response.status).toBe(404); + expect(response.body.message).toBe('Category Not Found'); + }); + + it('should delete the category with status 200', async () => { + const response = await request(app) + .delete(`/api/v1/category/${categoryId}`) + .set('Authorization', `Bearer ${token}`); + + expect(response.status).toBe(200); + expect(response.body.message).toBe('Category deleted successfully'); + }); + + it('should return 404 if category is not found', async () => { + const response = await request(app) + .delete('/api/v1/category/9999') + .set('Authorization', `Bearer ${token}`); + + expect(response.status).toBe(404); + expect(response.body.message).toBe('Category Not Found'); + }); +}); diff --git a/src/__test__/product.test.ts b/src/__test__/product.test.ts new file mode 100644 index 00000000..fc976ed8 --- /dev/null +++ b/src/__test__/product.test.ts @@ -0,0 +1,263 @@ +import request from 'supertest'; +import app from '../app'; +import { getVendorToken, afterAllHook, beforeAllHook } from './testSetup'; + +beforeAll(beforeAllHook); +afterAll(afterAllHook); + +describe('Product Controller Tests', () => { + let token: string; + let productId: number; + let categoryId: number; + + beforeAll(async () => { + token = await getVendorToken(); + }); + + it('should create a new product with valid data', async () => { + // create a category + const categoryData = { + name: 'Category', + description: 'category description', + }; + + const categoryResponse = await request(app) + .post('/api/v1/category') + .set('Authorization', `Bearer ${token}`) + .send(categoryData); + + categoryId = categoryResponse.body.data.id; + + const productData = { + name: 'New Product', + image: 'new_product.jpg', + gallery: [], + shortDesc: 'This is a new product', + longDesc: 'Detailed description of the new product', + categoryId: categoryId, + quantity: 10, + regularPrice: 5, + salesPrice: 4, + tags: ['tag1', 'tag2'], + type: 'Simple', + isAvailable: true, + }; + + const response = await request(app) + .post('/api/v1/product') + .set('Authorization', `Bearer ${token}`) + .send(productData); + expect(response.statusCode).toEqual(201); + expect(response.body.message).toEqual('Product successfully created'); + expect(response.body.data).toBeDefined(); + productId = response.body.data.id; + }); + + it('should return 409 if product name already exists', async () => { + const productData = { + name: 'New Product', + image: 'new_product.jpg', + gallery: [], + shortDesc: 'This is a new product', + longDesc: 'Detailed description of the new product', + categoryId: categoryId, + quantity: 10, + regularPrice: 5, + salesPrice: 4, + tags: ['tag1', 'tag2'], + type: 'Simple', + isAvailable: true, + }; + + const response = await request(app) + .post('/api/v1/product') + .set('Authorization', `Bearer ${token}`) + .send(productData); + + expect(response.statusCode).toEqual(409); + expect(response.body.message).toEqual('Product name already exists'); + }); + + it('should return 404 if category not found', async () => { + const nonExistentCategoryId = 999; + + const productData = { + name: 'Test Product', + image: 'test.jpg', + gallery: [], + shortDesc: 'A test product', + longDesc: 'Description of a test product', + categoryId: nonExistentCategoryId, + quantity: 10, + regularPrice: 20, + salesPrice: 18, + tags: ['tag1', 'tag2'], + type: 'Simple', + isAvailable: true, + }; + + const response = await request(app) + .post('/api/v1/product') + .set('Authorization', `Bearer ${token}`) + .send(productData); + + expect(response.statusCode).toBe(404); + expect(response.body.message).toBe('Category not found'); + }); + + it('should return validation errors for invalid product data', async () => { + const invalidProductData = { + name: '', + image: '', + }; + const response = await request(app) + .post('/api/v1/product') + .set('Authorization', `Bearer ${token}`) + .send(invalidProductData); + expect(response.statusCode).toEqual(400); + expect(response.body.errors).toBeDefined(); + }); + + it('should retrieve all products', async () => { + const response = await request(app).get('/api/v1/product'); + + expect(response.statusCode).toEqual(200); + expect(response.body.message).toEqual('Data retrieved successfully'); + expect(Array.isArray(response.body.data)).toBeTruthy(); + }); + + it('should retrieve a single product by ID', async () => { + const response = await request(app) + .get(`/api/v1/product/${productId}`) + .set('Authorization', `Bearer ${token}`); + + expect(response.statusCode).toEqual(200); + expect(response.body.message).toEqual('Data retrieved successfully'); + expect(response.body.data).toBeDefined(); + }); + + it('should update a product by ID', async () => { + const updatedProductData = { + name: 'Updated Product Name', + image: 'Updated.jpg', + gallery: [], + shortDesc: 'This is a updated', + longDesc: 'Detailed description of the Updated product', + categoryId: categoryId, + quantity: 3, + regularPrice: 10, + salesPrice: 7, + tags: ['tag1', 'tag2'], + type: 'Variable', + isAvailable: true, + }; + + const response = await request(app) + .put(`/api/v1/product/${productId}`) + .set('Authorization', `Bearer ${token}`) + .send(updatedProductData); + + expect(response.statusCode).toEqual(200); + expect(response.body.message).toEqual('Product successfully updated'); + expect(response.body.data).toBeDefined(); + }); + + it('should return a 404 for a non-existent product while updating', async () => { + const updatedProductData = { + name: 'Updated Product Name', + image: 'Updated.jpg', + gallery: [], + shortDesc: 'This is a updated', + longDesc: 'Detailed description of the Updated product', + categoryId: categoryId, + quantity: 3, + regularPrice: 10, + salesPrice: 7, + tags: ['tag1', 'tag2'], + type: 'Variable', + isAvailable: true, + }; + const nonExistentProductId = -999; + const response = await request(app) + .put(`/api/v1/product/${nonExistentProductId}`) + .set('Authorization', `Bearer ${token}`) + .send(updatedProductData); + + expect(response.statusCode).toEqual(404); + expect(response.body.message).toEqual('Product not found'); + }); + + it('should return 404 if category not found while updating', async () => { + const nonExistentCategoryId = 999; + + const productData = { + name: 'Test Product', + image: 'test.jpg', + gallery: [], + shortDesc: 'A test product', + longDesc: 'Description of a test product', + categoryId: nonExistentCategoryId, + quantity: 10, + regularPrice: 20, + salesPrice: 18, + tags: ['tag1', 'tag2'], + type: 'Simple', + isAvailable: true, + }; + + const response = await request(app) + .put(`/api/v1/product/${productId}`) + .set('Authorization', `Bearer ${token}`) + .send(productData); + + expect(response.statusCode).toBe(404); + expect(response.body.message).toBe('Category not found'); + }); + it('should return a 404 for a non-existent product', async () => { + const nonExistentProductId = -999; + const response = await request(app) + .get(`/api/v1/product/${nonExistentProductId}`) + .set('Authorization', `Bearer ${token}`); + expect(response.statusCode).toEqual(404); + expect(response.body.message).toEqual('Product not found'); + }); + + it('should return validation errors for invalid update data', async () => { + const invalidUpdateData = { + name: '', + }; + const response = await request(app) + .put(`/api/v1/product/${productId}`) + .set('Authorization', `Bearer ${token}`) + .send(invalidUpdateData); + expect(response.statusCode).toEqual(400); + expect(response.body.errors).toBeDefined(); + }); + + it('should delete a product by ID', async () => { + const response = await request(app) + .delete(`/api/v1/product/${productId}`) + .set('Authorization', `Bearer ${token}`); + + expect(response.statusCode).toEqual(200); + expect(response.body.message).toEqual('Product deleted successfully'); + }); + + it('should return a 404 for a non-existent product', async () => { + const nonExistentProductId = -999; + const response = await request(app) + .delete(`/api/v1/product/${nonExistentProductId}`) + .set('Authorization', `Bearer ${token}`); + expect(response.statusCode).toEqual(404); + expect(response.body.message).toEqual('Product Not Found'); + }); + + it('should delete all products', async () => { + const response = await request(app) + .delete('/api/v1/product') + .set('Authorization', `Bearer ${token}`); + + expect(response.statusCode).toEqual(200); + expect(response.body.message).toEqual('All product deleted successfully'); + }); +}); diff --git a/src/__test__/testSetup.ts b/src/__test__/testSetup.ts index dc34aeaf..969af324 100644 --- a/src/__test__/testSetup.ts +++ b/src/__test__/testSetup.ts @@ -1,20 +1,91 @@ import { DbConnection } from '../database/index'; import UserModel from '../database/models/userModel'; - import { Role } from '../database/models'; +import Category from '../database/models/categoryEntity'; +import Product from '../database/models/productEntity'; +import request from 'supertest'; +import app from '../app'; export async function beforeAllHook() { await DbConnection.instance.initializeDb(); - // removing all data from role table + + // Get repositories + const userRepository = await DbConnection.connection.getRepository(UserModel); const roleRepository = await DbConnection.connection.getRepository(Role); + const categoryRepository = + await DbConnection.connection.getRepository(Category); + const productRepository = + await DbConnection.connection.getRepository(Product); + + // Delete all users,roles and categories + await userRepository.createQueryBuilder().delete().execute(); await roleRepository.createQueryBuilder().delete().execute(); + await categoryRepository.createQueryBuilder().delete().execute(); + await productRepository.createQueryBuilder().delete().execute(); +} + +// Get Vendor Token function +export async function getVendorToken() { + const userRepository = await DbConnection.connection.getRepository(UserModel); + + const formData = { + name: 'Vendor', + permissions: ['test-permission1', 'test-permission2'], + }; + await request(app).post('/api/v1/roles/create_role').send(formData); + + const userData = { + firstName: 'Test', + lastName: 'User', + email: 'test1@gmail.com', + password: 'TestPassword123', + userType: 'vendor', + }; + await request(app).post('/api/v1/register').send(userData); + + const updatedUser = await userRepository.findOne({ + where: { email: userData.email }, + }); + if (updatedUser) { + updatedUser.isVerified = true; + await userRepository.save(updatedUser); + } + + const loginResponse = await request(app).post('/api/v1/login').send({ + email: userData.email, + password: userData.password, + }); + + expect(loginResponse.status).toBe(200); + expect(loginResponse.body.message).toBe( + 'Please provide the 2FA code sent to your email.' + ); + + const user = await userRepository.findOne({ + where: { email: userData.email }, + }); + if (!user) throw new Error('User not found'); + + const verifyResponse = await request(app) + .post(`/api/v1/verify2FA/${user.id}`) + .send({ + code: user.twoFactorCode, + }); + expect(verifyResponse.status).toBe(200); + expect(verifyResponse.body).toHaveProperty('token'); + expect(verifyResponse.body.token).toBeDefined(); + return verifyResponse.body.token; } export async function afterAllHook() { - const userRepository = DbConnection.connection.getRepository(UserModel); - const repository = await userRepository.clear(); - // eslint-disable-next-line no-console - console.log(repository); + await DbConnection.connection.transaction(async (transactionManager) => { + const userRepository = transactionManager.getRepository(UserModel); + const categoryRepository = transactionManager.getRepository(Category); + const productRepository = transactionManager.getRepository(Product); + await userRepository.createQueryBuilder().delete().execute(); + await categoryRepository.createQueryBuilder().delete().execute(); + await productRepository.createQueryBuilder().delete().execute(); + }); await DbConnection.instance.disconnectDb(); } diff --git a/src/__test__/userController.test.ts b/src/__test__/userController.test.ts index cd49bb8e..1a48549d 100644 --- a/src/__test__/userController.test.ts +++ b/src/__test__/userController.test.ts @@ -3,7 +3,7 @@ import app from '../app'; import { afterAllHook, beforeAllHook } from './testSetup'; import jwt from 'jsonwebtoken'; import dbConnection from '../database'; -import UserModel from '../database/models/userModel'; +import UserModel from '../database/models/userModel'; const userRepository = dbConnection.getRepository(UserModel); beforeAll(beforeAllHook); @@ -25,7 +25,10 @@ describe('User Registration Tests', () => { expect(response.body.user).toHaveProperty('firstName', userData.firstName); expect(response.body.user).toHaveProperty('lastName', userData.lastName); expect(response.body.user).toHaveProperty('email', userData.email); - expect(response.body.user).toHaveProperty('userType', response.body.user.userType); + expect(response.body.user).toHaveProperty( + 'userType', + response.body.user.userType + ); }); it('should return a 400 status code if validation fails', async () => { @@ -146,93 +149,185 @@ describe('User Registration Tests', () => { }); }); +describe('User Login Tests', () => { + it('should log in a vendor with valid credentials', async () => { + const formData = { + name: 'Vendor', + permissions: ['test-permission1', 'test-permission2'], + }; + + await request(app).post('/api/v1/roles/create_role').send(formData); + const userData = { + firstName: 'Test', + lastName: 'User', + email: 'test1@gmail.com', + password: 'TestPassword123', + userType: 'vendor', + }; + await request(app).post('/api/v1/register').send(userData); + const updatedUser = await userRepository.findOne({ + where: { email: userData.email }, + }); + if (updatedUser) { + updatedUser.isVerified = true; + await userRepository.save(updatedUser); + const loginResponse = await request(app).post('/api/v1/login').send({ + email: userData.email, + password: userData.password, + }); -describe('User Login Tests', () => { - it('should log in a user with valid credentials', async () => { - const userData = { - firstName: 'Test', - lastName: 'User', - email: 'test@gmail.com', - password: 'TestPassword123', - userType: 'buyer', - }; - await request(app).post('/api/v1/register').send(userData); - const updatedUser = await userRepository.findOne({ where: { email: userData.email } }); - if (updatedUser) { - updatedUser.isVerified = true; - await userRepository.save(updatedUser); - - const loginResponse = await request(app).post('/api/v1/login').send({ - email: userData.email, - password: userData.password, - }); - - expect(loginResponse.status).toBe(200); - expect(loginResponse.body.token).toBeDefined(); - expect(loginResponse.body.message).toBe('Successfully Logged in'); - } else { - throw new Error('User not found'); - } + expect(loginResponse.status).toBe(200); + expect(loginResponse.body.message).toBe( + 'Please provide the 2FA code sent to your email.' + ); + } }); - + + it('should verify the 2FA code for a vendor user', async () => { + const userData = { + firstName: 'Test', + lastName: 'User', + email: 'test1@gmail.com', + password: 'TestPassword123', + userType: 'vendor', + }; + + // Register the user + await request(app).post('/api/v1/register').send(userData); + + // Verify the user + let user = await userRepository.findOne({ + where: { email: userData.email }, + }); + if (user) { + user.isVerified = true; + await userRepository.save(user); + } + const loginResponse = await request(app).post('/api/v1/login').send({ + email: userData.email, + password: userData.password, + }); + + expect(loginResponse.status).toBe(200); + expect(loginResponse.body.message).toBe( + 'Please provide the 2FA code sent to your email.' + ); + + user = await userRepository.findOne({ where: { email: userData.email } }); + + if (user) { + const verifyResponse = await request(app) + .post(`/api/v1/verify2FA/${user.id}`) + .send({ + code: user.twoFactorCode, + }); + + expect(verifyResponse.status).toBe(200); + expect(verifyResponse.body).toHaveProperty('token'); + } + }); + + it('should log in a buyer with valid credentials', async () => { + const formData = { + name: 'Buyer', + permissions: ['test-permission1', 'test-permission2'], + }; + + // Create the role first + const roleResponse = await request(app) + .post('/api/v1/roles/create_role') + .send(formData); + + const userData = { + firstName: 'Test', + lastName: 'User', + email: 'test2@gmail.com', + password: 'TestPassword123', + userType: roleResponse.body.id, + }; + await request(app).post('/api/v1/register').send(userData); + + const updatedUser = await userRepository.findOne({ + where: { email: userData.email }, + }); + if (updatedUser) { + updatedUser.isVerified = true; + await userRepository.save(updatedUser); + + const loginResponse = await request(app).post('/api/v1/login').send({ + email: userData.email, + password: userData.password, + }); + + expect(loginResponse.status).toBe(200); + expect(loginResponse.body.token).toBeDefined(); + expect(loginResponse.body.message).toBe('Buyer Logged in successfully'); + + // Decode the token and check its properties + const decodedToken = jwt.decode(loginResponse.body.token); + expect(decodedToken).toHaveProperty('user'); + expect(decodedToken).toHaveProperty('iat'); + expect(decodedToken).toHaveProperty('exp'); + } + }); + it('should return a 401 status code if the email is not verified', async () => { - const userData = { - firstName: 'Test', - lastName: 'User', - email: 'test@gmail.com', - password: 'TestPassword123', - userType: 'buyer', - }; - await request(app).post('/api/v1/register').send(userData); - const updatedUser = await userRepository.findOne({ - where: { email: userData.email }, - }); - - if (updatedUser) { - updatedUser.isVerified = false; - await userRepository.save(updatedUser); - const loginResponse = await request(app).post('/api/v1/login').send({ - email: userData.email, - password: userData.password, - }); - - expect(loginResponse.status).toBe(401); - expect(loginResponse.body.message).toBe('Please verify your email. Confirmation link has been sent.'); // Corrected message - } else { - throw new Error('User not found'); - } + const userData = { + firstName: 'Test', + lastName: 'User', + email: 'test@gmail.com', + password: 'TestPassword123', + userType: 'buyer', + }; + await request(app).post('/api/v1/register').send(userData); + const updatedUser = await userRepository.findOne({ + where: { email: userData.email }, + }); + + if (updatedUser) { + updatedUser.isVerified = false; + await userRepository.save(updatedUser); + const loginResponse = await request(app).post('/api/v1/login').send({ + email: userData.email, + password: userData.password, + }); + + expect(loginResponse.status).toBe(401); + expect(loginResponse.body.message).toBe( + 'Please verify your email. Confirmation link has been sent.' + ); // Corrected message + } }); - + it('should return a 401 status code if the password does not match', async () => { - const userData = { - firstName: 'Test', - lastName: 'User', - email: 'test@gmail.com', - password: 'TestPassword123', - userType: 'buyer', - }; - await request(app).post('/api/v1/register').send(userData); - - const loginResponse = await request(app).post('/api/v1/login').send({ - email: userData.email, - password: 'IncorrectPassword', - }); - expect(loginResponse.status).toBe(401); - expect(loginResponse.body.message).toBe('Password does not match'); + const userData = { + firstName: 'Test', + lastName: 'User', + email: 'test@gmail.com', + password: 'TestPassword123', + userType: 'buyer', + }; + await request(app).post('/api/v1/register').send(userData); + + const loginResponse = await request(app).post('/api/v1/login').send({ + email: userData.email, + password: 'IncorrectPassword', + }); + expect(loginResponse.status).toBe(401); + expect(loginResponse.body.message).toBe('Password does not match'); }); it('should return a 404 status code if the user is not found', async () => { - const nonExistentEmail = 'nonexistent@example.com'; const loginResponse = await request(app).post('/api/v1/login').send({ - email: nonExistentEmail, - password: 'TestPassword123', + email: nonExistentEmail, + password: 'TestPassword123', }); - + expect(loginResponse.status).toBe(404); expect(loginResponse.body.message).toBe('User Not Found'); - }); - }); + }); +}); diff --git a/src/app.ts b/src/app.ts index 7c3b5d2f..821bbf85 100644 --- a/src/app.ts +++ b/src/app.ts @@ -4,14 +4,14 @@ import morgan from 'morgan'; import swaggerUi from 'swagger-ui-express'; import swaggerSpec from './docs/swaggerconfig'; import 'reflect-metadata'; -import userRoute from './routes/userRoutes'; -import roleRoutes from './routes/roleRoutes'; +import router from './routes/index'; import fs from 'fs'; import path from 'path'; import authRoutes from './routes/auth-routes'; import cookieSession from 'cookie-session'; import passport from 'passport'; +import userRouter from './routes/userRoutes'; // Require Passport midleware require('./middlewares/passport-setup'); @@ -25,9 +25,7 @@ const logStream = fs.createWriteStream(path.join(__dirname, 'output.log'), { flags: 'a', }); -//Data Sanitation Against SQL injection - -//Data Sanitation Against SiteScripts +//Data Sanitation Against SQL injection morgan.token('type', function (req: Request) { return req.headers['content-type']; @@ -80,8 +78,8 @@ app.get('/', (req: Request, res: Response) => { }); // Middleware to handle all endpoint routes -app.use('/api/v1', userRoute); -app.use('/api/v1/roles', roleRoutes); +app.use('/api/v1', router); +app.use('/api/v1', userRouter); // Endpoints for serving social login app.use('/auth', authRoutes); diff --git a/src/controller/categoryController.ts b/src/controller/categoryController.ts new file mode 100644 index 00000000..92bddbff --- /dev/null +++ b/src/controller/categoryController.ts @@ -0,0 +1,128 @@ +import { Request, Response } from 'express'; +import dbConnection from '../database'; +import Category from '../database/models/categoryEntity'; +import { check, validationResult } from 'express-validator'; +import errorHandler from '../middlewares/errorHandler'; + +const categoryRepository = dbConnection.getRepository(Category); + +interface categoryRequestBody { + name: string; + description: string; +} + +const createCategoryRules = [ + check('name').isLength({ min: 1 }).withMessage('Category name is required'), + check('description') + .isLength({ min: 1 }) + .withMessage('Category description is required'), +]; + +export const createCategory = [ + ...createCategoryRules, + errorHandler(async (req: Request, res: Response) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + const { name, description } = req.body as categoryRequestBody; + + const existingCategory = await categoryRepository.findOne({ + where: { name }, + }); + if (existingCategory) { + return res.status(409).json({ message: 'Category name already exists' }); + } + const newCategory = new Category({ + name: name, + description: description, + }); + const updatedCategory = await categoryRepository.save(newCategory); + return res.status(201).json({ + message: 'Category successfully created', + data: updatedCategory, + }); + }), +]; + +export const getAllCategories = errorHandler( + async (req: Request, res: Response) => { + const categories = await categoryRepository.find(); + return res + .status(200) + .json({ message: 'Data retrieved successfully', data: categories }); + } +); + +export const getCategory = errorHandler(async (req: Request, res: Response) => { + const categoryId: number = parseInt(req.params.categoryId); + + const category = await categoryRepository.findOne({ + where: { id: categoryId }, + }); + + if (!category) { + return res.status(404).json({ message: 'Category Not Found' }); + } + + res + .status(200) + .json({ message: 'Data retrieved successfully', data: category }); +}); + +export const updateCategory = [ + ...createCategoryRules, + errorHandler(async (req: Request, res: Response) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const categoryId: number = parseInt(req.params.categoryId); + const { name, description } = req.body as categoryRequestBody; + + const category = await categoryRepository.findOne({ + where: { id: categoryId }, + }); + + if (!category) { + return res.status(404).json({ message: 'Category Not Found' }); + } + + const existingCategory = await categoryRepository.findOne({ + where: { name }, + }); + + if (existingCategory && existingCategory.id !== categoryId) { + return res.status(409).json({ message: 'Category name already exists' }); + } + + category.name = name; + category.description = description; + + const updatedCategory = await categoryRepository.save(category); + + return res.status(200).json({ + message: 'Category successfully updated', + data: updatedCategory, + }); + }), +]; + +export const deleteCategory = errorHandler( + async (req: Request, res: Response) => { + const categoryId: number = parseInt(req.params.categoryId); + + const category = await categoryRepository.findOne({ + where: { id: categoryId }, + }); + + if (!category) { + return res.status(404).json({ message: 'Category Not Found' }); + } + + await categoryRepository.delete(categoryId); + + res.status(200).json({ message: 'Category deleted successfully' }); + } +); diff --git a/src/controller/productController.ts b/src/controller/productController.ts new file mode 100644 index 00000000..3fe2f009 --- /dev/null +++ b/src/controller/productController.ts @@ -0,0 +1,290 @@ +import { Request, Response } from 'express'; +import Product from '../database/models/productEntity'; +import Category from '../database/models/categoryEntity'; +import UserModel from '../database/models/userModel'; +import dbConnection from '../database'; +import { check, validationResult } from 'express-validator'; +import errorHandler from '../middlewares/errorHandler'; + +const productRepository = dbConnection.getRepository(Product); +const categoryRepository = dbConnection.getRepository(Category); +const userRepository = dbConnection.getRepository(UserModel); + +interface ProductRequestBody { + name: string; + image: string; + gallery: string[]; + shortDesc: string; + longDesc: string; + categoryId: number; + quantity: number; + regularPrice: number; + salesPrice: number; + tags: string[]; + type: 'Simple' | 'Grouped' | 'Variable'; + isAvailable: boolean; +} + +const createProductRules = [ + check('name').isLength({ min: 1 }).withMessage('Product name is required'), + check('image').isLength({ min: 1 }).withMessage('Product image is required'), + check('gallery').isArray().withMessage('Gallery must be an array'), + check('shortDesc') + .isLength({ min: 1 }) + .withMessage('Short description is required'), + check('longDesc') + .isLength({ min: 1 }) + .withMessage('Long description is required'), + check('categoryId') + .isInt({ min: 1 }) + .withMessage('Valid category ID is required'), + check('quantity') + .isInt({ min: 0 }) + .withMessage('Quantity must be a non-negative integer'), + check('regularPrice') + .isFloat({ min: 0 }) + .withMessage('Regular price must be a non-negative number'), + check('salesPrice') + .isFloat({ min: 0 }) + .withMessage('Sales price must be a non-negative number'), + check('tags').isArray().withMessage('Tags must be an array'), + check('type') + .isIn(['Simple', 'Grouped', 'Variable']) + .withMessage('Invalid product type'), +]; + +export const createProduct = [ + ...createProductRules, + errorHandler(async (req: Request, res: Response) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + const vendorId = req.user!.id; + + const { + name, + image, + gallery, + shortDesc, + longDesc, + categoryId, + quantity, + regularPrice, + salesPrice, + tags, + type, + } = req.body as ProductRequestBody; + + const category = await categoryRepository.findOne({ + where: { id: categoryId }, + }); + if (!category) { + return res.status(404).json({ message: 'Category not found' }); + } + + const vendor = await userRepository.findOne({ + where: { id: vendorId }, + select: { + id: true, + firstName: true, + }, + }); + + if (!vendor) { + return res.status(404).json({ message: 'Vendor not found' }); + } + + const existingProduct = await productRepository.findOne({ + where: { name }, + }); + + if (existingProduct) { + return res.status(409).json({ message: 'Product name already exists' }); + } + + const newProduct = new Product({ + name, + image, + gallery, + shortDesc, + longDesc, + category, + vendor, + quantity, + regularPrice, + salesPrice, + tags, + type, + }); + const updatedProduct = await productRepository.save(newProduct); + return res.status(201).json({ + message: 'Product successfully created', + data: updatedProduct, + }); + }), +]; + +const updateProductRules = [ + check('name').isLength({ min: 1 }).withMessage('Product name is required'), + check('image').isLength({ min: 1 }).withMessage('Product image is required'), + check('gallery').isArray().withMessage('Gallery must be an array'), + check('shortDesc') + .isLength({ min: 1 }) + .withMessage('Short description is required'), + check('longDesc') + .isLength({ min: 1 }) + .withMessage('Long description is required'), + check('categoryId') + .isInt({ min: 1 }) + .withMessage('Valid category ID is required'), + check('quantity') + .isInt({ min: 0 }) + .withMessage('Quantity must be a non-negative integer'), + check('regularPrice') + .isFloat({ min: 0 }) + .withMessage('Regular price must be a non-negative number'), + check('salesPrice') + .isFloat({ min: 0 }) + .withMessage('Sales price must be a non-negative number'), + check('tags').isArray().withMessage('Tags must be an array'), + check('type') + .isIn(['Simple', 'Grouped', 'Variable']) + .withMessage('Invalid product type'), + check('isAvailable') + .isBoolean() + .withMessage('isAvailable must be a boolean value'), +]; +export const updateProduct = [ + ...updateProductRules, + errorHandler(async (req: Request, res: Response) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const productId: number = parseInt(req.params.productId); + const { + name, + image, + gallery, + shortDesc, + longDesc, + categoryId, + quantity, + regularPrice, + salesPrice, + tags, + type, + isAvailable, + } = req.body as ProductRequestBody; + + const product = await productRepository.findOne({ + where: { id: productId }, + }); + + if (!product) { + return res.status(404).json({ message: 'Product not found' }); + } + + const category = await categoryRepository.findOne({ + where: { id: categoryId }, + }); + + if (!category) { + return res.status(404).json({ message: 'Category not found' }); + } + + product.name = name; + product.image = image; + product.gallery = gallery; + product.shortDesc = shortDesc; + product.longDesc = longDesc; + product.category = category; + product.quantity = quantity; + product.regularPrice = regularPrice; + product.salesPrice = salesPrice; + product.tags = tags; + product.type = type; + product.isAvailable = isAvailable; + + const updatedProduct = await productRepository.save(product); + + return res.status(200).json({ + message: 'Product successfully updated', + data: updatedProduct, + }); + }), +]; + +export const getAllProducts = errorHandler( + async (req: Request, res: Response) => { + const products = await productRepository.find({ + select: { + category: { + name: true, + }, + vendor: { + firstName: true, + }, + }, + relations: ['category', 'vendor'], + }); + return res + .status(200) + .json({ message: 'Data retrieved successfully', data: products }); + } +); + +export const getProduct = errorHandler(async (req: Request, res: Response) => { + const productId: number = parseInt(req.params.productId); + + const product = await productRepository.findOne({ + where: { id: productId }, + select: { + category: { + name: true, + }, + vendor: { + firstName: true, + }, + }, + relations: ['category', 'vendor'], + }); + + if (!product) { + return res.status(404).json({ message: 'Product not found' }); + } + + return res + .status(200) + .json({ message: 'Data retrieved successfully', data: product }); +}); + +export const deleteProduct = errorHandler( + async (req: Request, res: Response) => { + const productId: number = parseInt(req.params.productId); + + const product = await productRepository.findOne({ + where: { id: productId }, + }); + + if (!product) { + return res.status(404).json({ message: 'Product Not Found' }); + } + + await productRepository.delete(productId); + + return res.status(200).json({ message: 'Product deleted successfully' }); + } +); + +export const deleteAllProduct = errorHandler( + async (req: Request, res: Response) => { + const deletedProducts = await productRepository.delete({}); + return res.status(200).json({ + message: 'All product deleted successfully', + count: deletedProducts.affected, + }); + } +); diff --git a/src/controller/userController.ts b/src/controller/userController.ts index 5642d758..62597b3c 100644 --- a/src/controller/userController.ts +++ b/src/controller/userController.ts @@ -7,6 +7,7 @@ import UserModel from '../database/models/userModel'; import sendEmail from '../emails/index'; import { sendCode } from '../emails/mailer'; import jwt from 'jsonwebtoken'; +import errorHandler from '../middlewares/errorHandler'; // Assuming dbConnection.getRepository(UserModel) returns a repository instance const userRepository = dbConnection.getRepository(UserModel); @@ -122,6 +123,7 @@ export const getAllUsers = async (req: Request, res: Response) => { email: true, userType: { id: true, + name: true, }, }, relations: ['userType'], @@ -162,80 +164,76 @@ export const deleteUser = async (req: Request, res: Response) => { return res .status(500) .json({ error: 'An error occurred while deleting the record.' }); + } +}; - }} - - - - export const Login = async (req: Request, res: Response) => { - try { - const user = await userRepository.findOne({ where: { email: req.body['email'] } }); - if (!user) { - return res.status(404).send({ message: 'User Not Found' }); - } - const passwordMatch = await bcrypt.compare(req.body.password, user.password); // Compare with hashed password from the database - if (!passwordMatch) { - return res.status(401).send({ message: 'Password does not match' }); - } - if (!user.isVerified) { - // Send confirmation email if user is not verified - const token = jwt.sign( - { userId: user.id, email: user.email }, - process.env.JWT_SECRET as jwt.Secret, - { expiresIn: '1d' } - ); - const confirmLink = `${process.env.APP_URL}/api/v1/confirm?token=${token}`; - await sendEmail('confirm', user.email, { name: user.firstName, link: confirmLink }); - return res.status(401).send({ message: 'Please verify your email. Confirmation link has been sent.' }); - } +export const Login = errorHandler(async (req: Request, res: Response) => { + const user = await userRepository.findOne({ + where: { email: req.body['email'] }, + relations: ['userType'], + }); + if (!user) { + return res.status(404).send({ message: 'User Not Found' }); + } + const passwordMatch = await bcrypt.compare(req.body.password, user.password); // Compare with hashed password from the database + if (!passwordMatch) { + return res.status(401).send({ message: 'Password does not match' }); + } + if (!user.isVerified) { + const token = jwt.sign( + { userId: user.id, email: user.email }, + process.env.JWT_SECRET as jwt.Secret, + { expiresIn: '1d' } + ); + const confirmLink = `${process.env.APP_URL}/api/v1/confirm?token=${token}`; + await sendEmail('confirm', user.email, { + name: user.firstName, + link: confirmLink, + }); + return res.status(401).send({ + message: 'Please verify your email. Confirmation link has been sent.', + }); + } - // Generate a new 2FA code + if (user.userType.name === 'Vendor') { const twoFactorCode = Math.floor(100000 + Math.random() * 900000); - // Store the 2FA code in the user's record in the database await userRepository.update(user.id, { twoFactorCode }); - // Send 2FA code to user's email - await sendCode( - user.email, - 'Your 2FA Code', - './templates/2fa.html', - { name: user.firstName, twoFactorCode: twoFactorCode.toString() } - ); - - // Respond with a message asking for the 2FA code - res.status(200).json({ message: 'Please provide the 2FA code sent to your email.' }); + await sendCode(user.email, 'Your 2FA Code', './templates/2fa.html', { + name: user.firstName, + twoFactorCode: twoFactorCode.toString(), + }); - } catch (error) { - console.error('Error occurred while logging in:', error); - res.status(500).send(error); + res + .status(200) + .json({ message: 'Please provide the 2FA code sent to your email.' }); + } else { + const token = jwt.sign({ user }, process.env.JWT_SECRET as jwt.Secret, { + expiresIn: '1h', + }); + res.status(200).json({ token, message: 'Buyer Logged in successfully' }); } -} - -export const verify2FA = async (req: Request, res: Response): Promise => { - try { - const { code } = req.body; - const { userId } = req.params; - - // Use the repository to find the user by their id - const user = await userRepository.findOne({ where: { id: Number(userId) } }); +}); - if (!user) { - res.status(401).json({ error: 'Invalid credentials' }); - return; - } - if (code !== user.twoFactorCode) { - res.status(401).json({ error: 'Invalid code' }); - return; - } +export const verify2FA = errorHandler(async (req: Request, res: Response) => { + const { code } = req.body; + const { userId } = req.params; - // Generate JWT - const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET as jwt.Secret, { expiresIn: '1h' }); + const user = await userRepository.findOne({ + where: { id: Number(userId) }, + relations: ['userType'], + }); - // Send JWT to the user - res.status(200).json({ token }); - - } catch (error) { - res.status(500).json({ error: (error as Error).message }); + if (!user) { + return res.status(401).json({ error: 'Invalid credentials' }); } -}; \ No newline at end of file + if (code !== user.twoFactorCode) { + return res.status(401).json({ error: 'Invalid code' }); + } + + const token = jwt.sign({ user }, process.env.JWT_SECRET as jwt.Secret, { + expiresIn: '1h', + }); + return res.status(200).json({ token }); +}); diff --git a/src/database/models/categoryEntity.ts b/src/database/models/categoryEntity.ts new file mode 100644 index 00000000..31a5dbf8 --- /dev/null +++ b/src/database/models/categoryEntity.ts @@ -0,0 +1,36 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + OneToMany, +} from 'typeorm'; +import Product from './productEntity'; + +@Entity() +export default class Category { + @PrimaryGeneratedColumn() + id: number; + + @Column({ unique: true }) + name: string; + + @Column({ length: 250 }) + description: string; + + @OneToMany(() => Product, (product) => product.category, { + cascade: ['update'], + }) + products: Product[]; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; + + constructor(category: Partial) { + Object.assign(this, category); + } +} diff --git a/src/database/models/index.ts b/src/database/models/index.ts index 42df6b1e..000308b3 100644 --- a/src/database/models/index.ts +++ b/src/database/models/index.ts @@ -1,2 +1,3 @@ export * from './userModel'; export * from './roleEntity'; +export * from './productEntity'; diff --git a/src/database/models/productEntity.ts b/src/database/models/productEntity.ts new file mode 100644 index 00000000..4c2fa97c --- /dev/null +++ b/src/database/models/productEntity.ts @@ -0,0 +1,65 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, +} from 'typeorm'; +import Category from './categoryEntity'; +import UserModel from './userModel'; + +@Entity() +export default class Product { + @PrimaryGeneratedColumn() + id: number; + + @Column({ length: 200 }) + name: string; + + @Column() + image: string; + + @Column('simple-array') + gallery: string[]; + + @Column({ length: 250 }) + shortDesc: string; + + @Column() + longDesc: string; + + @ManyToOne(() => Category) + category: Category; + + @Column() + quantity: number; + + @Column() + regularPrice: number; + + @Column() + salesPrice: number; + + @Column('simple-array') + tags: string[]; + + @Column({ default: 'Simple' }) + type: 'Simple' | 'Grouped' | 'Variable'; + + @Column({ default: true }) + isAvailable: boolean; + + @ManyToOne(() => UserModel) + vendor: UserModel; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; + + constructor(product: Partial) { + Object.assign(this, product); + } +} diff --git a/src/database/models/roleEntity.ts b/src/database/models/roleEntity.ts index 8fcf0706..d7c76ad6 100644 --- a/src/database/models/roleEntity.ts +++ b/src/database/models/roleEntity.ts @@ -1,4 +1,5 @@ -import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'; +import UserModel from './userModel'; @Entity() export class Role { @@ -8,6 +9,9 @@ export class Role { @Column({ unique: true }) name: string; + @OneToMany(() => UserModel, (user) => user.userType, { cascade: ['update'] }) + users: UserModel[]; + @Column('simple-array') permissions: string[]; } diff --git a/src/database/models/userModel.ts b/src/database/models/userModel.ts index 0500ca47..f71170e2 100644 --- a/src/database/models/userModel.ts +++ b/src/database/models/userModel.ts @@ -1,13 +1,6 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - OneToOne, - JoinColumn, -} from 'typeorm'; +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; import { Role } from './roleEntity'; - @Entity() export default class UserModel { @PrimaryGeneratedColumn() @@ -19,14 +12,13 @@ export default class UserModel { @Column() lastName: string; - @Column() + @Column({ unique: true }) email: string; @Column({ nullable: true }) password: string; - @OneToOne(() => Role, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) - @JoinColumn() + @ManyToOne(() => Role) userType: Role; @Column({ nullable: true }) @@ -44,9 +36,11 @@ export default class UserModel { @Column({ default: false }) isVerified: boolean; + @Column({ default: 'active' }) + status: 'active' | 'inactive'; - @Column({ nullable: true }) - twoFactorCode: Number; + @Column({ nullable: true }) + twoFactorCode: number; constructor(user: Partial) { Object.assign(this, user); diff --git a/src/docs/2Fadocs.ts b/src/docs/2Fadocs.ts index 510122d5..e5ac8bff 100644 --- a/src/docs/2Fadocs.ts +++ b/src/docs/2Fadocs.ts @@ -1,6 +1,6 @@ /** * @swagger - * /api/v1/verify2FA/{userId}: + * /api/v1/user/verify2FA/{userId}: * post: * summary: Verify 2FA code * tags: [Login] @@ -52,4 +52,4 @@ * error: * type: string * description: An error message indicating internal server error - */ \ No newline at end of file + */ diff --git a/src/docs/categoryDocs.ts b/src/docs/categoryDocs.ts new file mode 100644 index 00000000..5891db19 --- /dev/null +++ b/src/docs/categoryDocs.ts @@ -0,0 +1,207 @@ +/** + * @swagger + * tags: + * name: Category + * description: Category management + */ +/** + * @swagger + * /api/v1/category/: + * post: + * summary: create a new category + * tags: [Category] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * description: + * type: string + * responses: + * '201': + * description: Category successfully created + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: A message indicating successful registration + * data: + * type: object + * properties: + * id: + * type: integer + * description: The unique identifier of the category + * name: + * type: string + * description: The name of the category + * description: + * type: string + * description: description of the category + * '400': + * description: Bad request + * content: + * application/json: + * schema: + * type: object + * properties: + * errors: + * type: array + * items: + * type: object + * properties: + * msg: + * type: string + * description: The error message + * '409': + * description: Conflict - Category name already exists + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: A message indicating the name of category already exists + */ +/** +/** + * @swagger + * /api/v1/category/{categoryId}: + * put: + * summary: update an existing category + * tags: [Category] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: categoryId + * type: string + * required: true + * description: ID of the category + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * description: + * type: string + * responses: + * '200': + * description: Category successfully updated + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: A message indicating successful updation + * data: + * type: object + * properties: + * id: + * type: integer + * description: The unique identifier of the category + * name: + * type: string + * description: The name of the category + * description: + * type: string + * description: description of the category + * '400': + * description: Bad request + * content: + * application/json: + * schema: + * type: object + * properties: + * errors: + * type: array + * items: + * type: object + * properties: + * msg: + * type: string + * description: The error message + * '409': + * description: Conflict - Category name already exists + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: A message indicating the name of category already exists + */ +/** +/** + * @swagger + * /api/v1/category/: + * get: + * summary: Get all categories + * tags: [Category] + * responses: + * '200': + * description: Successful + * '404': + * description: Categories not found + * '500': + * description: Internal Server Error + */ +/** + * @swagger + * /api/v1/category/{categoryId}: + * get: + * summary: Get an existing category + * tags: [Category] + * parameters: + * - in: path + * name: categoryId + * type: string + * required: true + * description: ID of the category + * responses: + * '200': + * description: Successful + * '404': + * description: category not found + * '500': + * description: Internal Server Error + */ + +/** + * @swagger + * /api/v1/category/{categoryId}: + * delete: + * summary: Deletes an existing category + * tags: [Category] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: categoryId + * type: string + * required: true + * description: ID of the category to delete + * responses: + * '200': + * description: Delete Successful + * '404': + * description: category not found + * '500': + * description: Internal Server Error + */ diff --git a/src/docs/productDoc.ts b/src/docs/productDoc.ts new file mode 100644 index 00000000..f2c0bd27 --- /dev/null +++ b/src/docs/productDoc.ts @@ -0,0 +1,186 @@ +/** + * @swagger + * tags: + * name: Product + * description: Operations related to products + */ + +/** + * @swagger + * /api/v1/product: + * get: + * summary: Get all products + * tags: [Product] + * responses: + * '200': + * description: Successful operation + * '500': + * description: Internal server error + * + * delete: + * summary: Delete all products + * tags: [Product] + * security: + * - bearerAuth: [] + * responses: + * '200': + * description: Products deleted successfully + * '500': + * description: Failed to delete products + */ + +/** + * @swagger + * /api/v1/product/{productId}: + * get: + * summary: Get a product by ID + * tags: [Product] + * parameters: + * - in: path + * name: productId + * type: integer + * required: true + * description: ID of the product to retrieve + * responses: + * '200': + * description: Successful operation + * '404': + * description: Product not found + * '500': + * description: Internal server error + * + * delete: + * summary: Delete a product by ID + * tags: [Product] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: productId + * type: integer + * required: true + * description: ID of the product to delete + * responses: + * '200': + * description: Product deleted successfully + * '404': + * description: Product not found + * '500': + * description: Internal server error + */ + +/** + * @swagger + * /api/v1/product: + * post: + * summary: Create a new product + * tags: [Product] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * image: + * type: string + * gallery: + * type: array + * items: + * type: string + * shortDesc: + * type: string + * longDesc: + * type: string + * categoryId: + * type: integer + * quantity: + * type: integer + * regularPrice: + * type: number + * salesPrice: + * type: number + * tags: + * type: array + * items: + * type: string + * type: + * type: string + * enum: ['Simple', 'Grouped', 'Variable'] + * isAvailable: + * type: boolean + * responses: + * '201': + * description: Product successfully created + * '400': + * description: Bad request + * '404': + * description: Category not found + * '409': + * description: Product name already exists + */ + +/** + * @swagger + * /api/v1/product/{productId}: + * put: + * summary: Update an existing product + * tags: [Product] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: productId + * type: integer + * required: true + * description: ID of the product to update + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * image: + * type: string + * gallery: + * type: array + * items: + * type: string + * shortDesc: + * type: string + * longDesc: + * type: string + * categoryId: + * type: integer + * quantity: + * type: integer + * regularPrice: + * type: number + * salesPrice: + * type: number + * tags: + * type: array + * items: + * type: string + * type: + * type: string + * enum: ['Simple', 'Grouped', 'Variable'] + * isAvailable: + * type: boolean + * responses: + * '200': + * description: Product successfully updated + * '400': + * description: Bad request + * '404': + * description: Product not found + * '500': + * description: Internal server error + */ diff --git a/src/docs/userAuth.ts b/src/docs/userAuth.ts index c9a3d2eb..4787521f 100644 --- a/src/docs/userAuth.ts +++ b/src/docs/userAuth.ts @@ -1,6 +1,6 @@ /** * @swagger - * /api/v1/login: + * /api/v1/user/login: * post: * summary: Login user * tags: [Login] @@ -90,4 +90,4 @@ * message: * type: string * description: An error message indicating internal server error - */ \ No newline at end of file + */ diff --git a/src/docs/userRegisterDocs.ts b/src/docs/userRegisterDocs.ts index de7bda60..893944d4 100644 --- a/src/docs/userRegisterDocs.ts +++ b/src/docs/userRegisterDocs.ts @@ -6,7 +6,7 @@ */ /** * @swagger - * /api/v1/register: + * /api/v1/user/register: * post: * summary: Register a new user * tags: [User] @@ -90,7 +90,7 @@ /** * @swagger - * /api/v1/confirm: + * /api/v1/user/confirm: * get: * summary: Confirm user email * tags: [User] @@ -133,3 +133,40 @@ * type: string * description: An error message indicating user not found */ + +/** + * @swagger + * /api/v1/user/getAllUsers: + * get: + * summary: Get all Users + * tags: [User] + * responses: + * '200': + * description: Successful + * '500': + * description: Internal Server Error + */ + +/** + * @swagger + * /api/v1/user/delete/{id}: + * delete: + * summary: Deletes an existing User + * tags: [User] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * type: string + * required: true + * description: ID of the user to delete + * responses: + * '200': + * description: Record deleted successfully. + * '404': + * description: Record not found. + * '500': + * description: An error occurred while deleting the record. + */ + diff --git a/src/emails/mailer.ts b/src/emails/mailer.ts index 5fa5e048..f6a1b712 100644 --- a/src/emails/mailer.ts +++ b/src/emails/mailer.ts @@ -1,30 +1,35 @@ -import nodemailer from 'nodemailer'; +import mailgun from 'mailgun-js'; import fs from 'fs'; import path from 'path'; +import dotenv from 'dotenv'; +dotenv.config() +const mg = mailgun({ + apiKey: process.env.MAILGUN_TOKEN || 'default_api_key', + domain: process.env.MAILGUN_DOMAIN || 'default_domain' +}); export async function sendCode(to: string, subject: string, htmlTemplatePath: string, replacements: Record) { const template = await fs.promises.readFile(path.resolve(__dirname, htmlTemplatePath), 'utf8'); - let html = template; for (const placeholder in replacements) { html = html.replace(new RegExp(`{{${placeholder}}}`, 'g'), replacements[placeholder]); } - const transporter = nodemailer.createTransport({ - service: 'gmail', - auth: { - user: process.env.EMAIL_USER, - pass: process.env.EMAIL_PASS - } - }); - const mailOptions = { - from: "dynamitesecommerce@gmail.com", + from: 'dynamitesecommerce@gmail.com', to, subject, html }; // Send the email - return transporter.sendMail(mailOptions); -} \ No newline at end of file + return new Promise((resolve, reject) => { + mg.messages().send(mailOptions, (error, body) => { + if (error) { + reject(error); + } else { + resolve(body); + } + }); + }); +} diff --git a/src/middlewares/authorize.ts b/src/middlewares/authorize.ts index 4f368c77..c5c9984c 100644 --- a/src/middlewares/authorize.ts +++ b/src/middlewares/authorize.ts @@ -6,24 +6,37 @@ const roleRepository = dbConnection.getRepository(Role); export const checkRole = (roles: string[]) => { return async (req: Request, res: Response, next: NextFunction) => { - if (req.user && roles.includes(req.user.userType.name)) { - next(); - } else { - res.status(403).json({msg:'Forbidden'}); + try { + // Assuming req.user contains the user information after authentication + if ( + req.user && + req.user.userType && + roles.includes(req.user.userType.name) + ) { + return next(); + } else { + return res.status(403).json({ msg: 'Forbidden' }); + } + } catch (error) { + return res.status(500).json({ message: 'Internal server error' }); } }; }; export const checkPermissions = async (permission: string) => { return async (req: Request, res: Response, next: NextFunction) => { - const userRole = await roleRepository.findOneBy({ - name: req.user!.userType.name, - }); + try { + const userRole = await roleRepository.findOne({ + where: { name: req.user!.userType.name }, + }); - if (userRole && userRole.permissions.includes(permission)) { - next(); - } else { - res.status(403).json({msg:'Forbidden'}); + if (userRole && userRole.permissions.includes(permission)) { + return next(); + } else { + return res.status(403).json({ msg: 'Forbidden' }); + } + } catch (error) { + return res.status(500).json({ message: 'Internal server error' }); } }; }; diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts index 9837635f..86b8a0c6 100644 --- a/src/middlewares/errorHandler.ts +++ b/src/middlewares/errorHandler.ts @@ -1,16 +1,20 @@ -import { Request, Response, NextFunction } from 'express'; +import { Request, Response } from 'express'; -type MiddlewareFunction = (req: Request, res: Response, next: NextFunction) => Promise; +type MiddlewareFunction = ( + req: Request, + res: Response +) => Promise> | undefined>; -function errorHandler(func: MiddlewareFunction) { - return async (req: Request, res: Response, next: NextFunction) => { - try { - await func(req, res, next); - } catch (error) { // Removed the type annotation from the catch clause variable because it caused liting errors - const message = error.detail || 'Internal Server Error'; - res.status(500).send(message); - } - }; - } +function errorHandler(func: MiddlewareFunction): MiddlewareFunction { + return async (req: Request, res: Response) => { + try { + return await func(req, res); + } catch (error) { + const message = + (error as { detail?: string }).detail || 'Internal Server Error'; + return res.status(500).send(message); + } + }; +} -export default errorHandler; +export default errorHandler; \ No newline at end of file diff --git a/src/middlewares/isLoggedIn.ts b/src/middlewares/isLoggedIn.ts new file mode 100644 index 00000000..17fea984 --- /dev/null +++ b/src/middlewares/isLoggedIn.ts @@ -0,0 +1,33 @@ +import { Request, Response, NextFunction } from 'express'; +import jwt from 'jsonwebtoken'; + +declare module 'express-serve-static-core' { + interface Request { + userId?: number; + userEmail?: string; + userType: { + id: number; + name: string; + permissions: []; + }; + } +} + +export const IsLoggedIn = (req: Request, res: Response, next: NextFunction) => { + const token = req.headers.authorization?.split(' ')[1]; + + if (!token) { + return res.status(401).json({ message: 'Unauthorized: No token provided' }); + } + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET as jwt.Secret); + + // @ts-expect-error this is because ts + req.user = decoded.user; + + next(); + } catch (error) { + return res.status(401).json({ message: 'Unauthorized: Invalid token' }); + } +}; diff --git a/src/middlewares/passport-setup.ts b/src/middlewares/passport-setup.ts index 5e95b267..23e86496 100644 --- a/src/middlewares/passport-setup.ts +++ b/src/middlewares/passport-setup.ts @@ -2,7 +2,7 @@ import passport from 'passport'; import GooglePassport from 'passport-google-oauth20'; import FacebookPassport from 'passport-facebook'; import dbConnection from '../database'; -import UserModel from '../database/models/userModel'; +import UserModel from '../database/models/userModel'; import dotenv from 'dotenv'; dotenv.config(); diff --git a/src/routes/categoryRoutes.ts b/src/routes/categoryRoutes.ts new file mode 100644 index 00000000..f28acf61 --- /dev/null +++ b/src/routes/categoryRoutes.ts @@ -0,0 +1,23 @@ +import { Router } from 'express'; +import { + createCategory, + deleteCategory, + getAllCategories, + getCategory, + updateCategory, +} from '../controller/categoryController'; +import { IsLoggedIn } from '../middlewares/isLoggedIn'; + +const categoryRouter = Router(); + +categoryRouter + .route('/') + .post(IsLoggedIn, createCategory) + .get(getAllCategories); +categoryRouter + .route('/:categoryId') + .get(getCategory) + .put(IsLoggedIn, updateCategory) + .delete(IsLoggedIn, deleteCategory); + +export default categoryRouter; diff --git a/src/routes/index.ts b/src/routes/index.ts index f6117b86..da75aa8f 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,8 +1,14 @@ -// import roleRoutes from './roleRoutes' -// import { Router } from 'express' +import { Router } from 'express'; +import userRouter from './userRoutes'; +import roleRoutes from './roleRoutes'; +import productRoutes from './productRoutes'; +import categoryRoutes from './categoryRoutes'; -// const router = Router() +const router = Router(); -// router.use('/roles', roleRoutes) +router.use('/user', userRouter); +router.use('/roles', roleRoutes); +router.use('/product', productRoutes); +router.use('/category', categoryRoutes); -// export default router +export default router; diff --git a/src/routes/productRoutes.ts b/src/routes/productRoutes.ts new file mode 100644 index 00000000..d1f51f25 --- /dev/null +++ b/src/routes/productRoutes.ts @@ -0,0 +1,26 @@ +import { Router } from 'express'; +import { + createProduct, + deleteAllProduct, + deleteProduct, + getAllProducts, + getProduct, + updateProduct, +} from '../controller/productController'; +import { IsLoggedIn } from '../middlewares/isLoggedIn'; +import { checkRole } from '../middlewares/authorize'; + +const productRouter = Router(); + +productRouter + .route('/') + .post(IsLoggedIn, checkRole(['Vendor']), createProduct) + .get(getAllProducts) + .delete(IsLoggedIn, deleteAllProduct); +productRouter + .route('/:productId') + .get(getProduct) + .put(IsLoggedIn, checkRole(['Vendor']), updateProduct) + .delete(IsLoggedIn, deleteProduct); + +export default productRouter; diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index f1b1ac6a..5e89371a 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -6,18 +6,16 @@ import { getAllUsers, deleteUser, Login, - verify2FA + verify2FA, } from '../controller/userController'; -const route = Router(); -route.post('/register', registerUser); -route.get('/getAllUsers', getAllUsers); -route.get('/confirm', confirmEmail); -route.delete('/delete/:id', deleteUser); -route.delete('/deleteAllUsers', deleteAllUsers); -route.post('/login',Login) -route.get('/all-users', getAllUsers); -route.post('/verify2FA/:userId', verify2FA); - -export default route; +const userRouter = Router(); +userRouter.post('/register', registerUser); +userRouter.get('/getAllUsers', getAllUsers); +userRouter.get('/confirm', confirmEmail); +userRouter.delete('/delete/:id', deleteUser); +userRouter.delete('/deleteAllUsers', deleteAllUsers); +userRouter.post('/login', Login); +userRouter.post('/verify2FA/:userId', verify2FA); +export default userRouter;