diff --git a/package-lock.json b/package-lock.json index 4af3d4ef..de37e77d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,6 @@ "@octokit/graphql": "^7.0.1", "@octokit/rest": "^19.0.13", "@types/node-cron": "^3.0.11", - "apollo-server": "^3.13.0", "axios": "^1.7.7", "bcryptjs": "^2.4.3", "cloudinary": "^1.30.1", @@ -44,7 +43,8 @@ "ts-node-dev": "^2.0.0", "tslog": "^4.9.2", "ws": "^8.11.0", - "xlsx": "^0.18.5" + "xlsx": "^0.18.5", + "zod": "^3.23.8" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.1", @@ -327,17 +327,6 @@ "node": ">=14" } }, - "node_modules/@apollo/utils.dropunuseddefinitions": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@apollo/utils.dropunuseddefinitions/-/utils.dropunuseddefinitions-1.1.0.tgz", - "integrity": "sha512-jU1XjMr6ec9pPoL+BFWzEPW7VHHulVdGKMkPAMiCigpVIT11VmCbnij0bWob8uS3ODJ65tZLYKAh/55vLw2rbg==", - "engines": { - "node": ">=12.13.0" - }, - "peerDependencies": { - "graphql": "14.x || 15.x || 16.x" - } - }, "node_modules/@apollo/utils.fetcher": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@apollo/utils.fetcher/-/utils.fetcher-2.0.1.tgz", @@ -354,94 +343,6 @@ "node": ">=14" } }, - "node_modules/@apollo/utils.keyvaluecache": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@apollo/utils.keyvaluecache/-/utils.keyvaluecache-1.0.2.tgz", - "integrity": "sha512-p7PVdLPMnPzmXSQVEsy27cYEjVON+SH/Wb7COyW3rQN8+wJgT1nv9jZouYtztWW8ZgTkii5T6tC9qfoDREd4mg==", - "dependencies": { - "@apollo/utils.logger": "^1.0.0", - "lru-cache": "7.10.1 - 7.13.1" - } - }, - "node_modules/@apollo/utils.keyvaluecache/node_modules/lru-cache": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.13.1.tgz", - "integrity": "sha512-CHqbAq7NFlW3RSnoWXLJBxCWaZVBrfa9UEHId2M3AW8iEBurbqduNexEUCGc3SHc6iCYXNJCDi903LajSVAEPQ==", - "engines": { - "node": ">=12" - } - }, - "node_modules/@apollo/utils.logger": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@apollo/utils.logger/-/utils.logger-1.0.1.tgz", - "integrity": "sha512-XdlzoY7fYNK4OIcvMD2G94RoFZbzTQaNP0jozmqqMudmaGo2I/2Jx71xlDJ801mWA/mbYRihyaw6KJii7k5RVA==" - }, - "node_modules/@apollo/utils.printwithreducedwhitespace": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@apollo/utils.printwithreducedwhitespace/-/utils.printwithreducedwhitespace-1.1.0.tgz", - "integrity": "sha512-GfFSkAv3n1toDZ4V6u2d7L4xMwLA+lv+6hqXicMN9KELSJ9yy9RzuEXaX73c/Ry+GzRsBy/fdSUGayGqdHfT2Q==", - "engines": { - "node": ">=12.13.0" - }, - "peerDependencies": { - "graphql": "14.x || 15.x || 16.x" - } - }, - "node_modules/@apollo/utils.removealiases": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@apollo/utils.removealiases/-/utils.removealiases-1.0.0.tgz", - "integrity": "sha512-6cM8sEOJW2LaGjL/0vHV0GtRaSekrPQR4DiywaApQlL9EdROASZU5PsQibe2MWeZCOhNrPRuHh4wDMwPsWTn8A==", - "engines": { - "node": ">=12.13.0" - }, - "peerDependencies": { - "graphql": "14.x || 15.x || 16.x" - } - }, - "node_modules/@apollo/utils.sortast": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@apollo/utils.sortast/-/utils.sortast-1.1.0.tgz", - "integrity": "sha512-VPlTsmUnOwzPK5yGZENN069y6uUHgeiSlpEhRnLFYwYNoJHsuJq2vXVwIaSmts015WTPa2fpz1inkLYByeuRQA==", - "dependencies": { - "lodash.sortby": "^4.7.0" - }, - "engines": { - "node": ">=12.13.0" - }, - "peerDependencies": { - "graphql": "14.x || 15.x || 16.x" - } - }, - "node_modules/@apollo/utils.stripsensitiveliterals": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@apollo/utils.stripsensitiveliterals/-/utils.stripsensitiveliterals-1.2.0.tgz", - "integrity": "sha512-E41rDUzkz/cdikM5147d8nfCFVKovXxKBcjvLEQ7bjZm/cg9zEcXvS6vFY8ugTubI3fn6zoqo0CyU8zT+BGP9w==", - "engines": { - "node": ">=12.13.0" - }, - "peerDependencies": { - "graphql": "14.x || 15.x || 16.x" - } - }, - "node_modules/@apollo/utils.usagereporting": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@apollo/utils.usagereporting/-/utils.usagereporting-1.0.1.tgz", - "integrity": "sha512-6dk+0hZlnDbahDBB2mP/PZ5ybrtCJdLMbeNJD+TJpKyZmSY6bA3SjI8Cr2EM9QA+AdziywuWg+SgbWUF3/zQqQ==", - "dependencies": { - "@apollo/usage-reporting-protobuf": "^4.0.0", - "@apollo/utils.dropunuseddefinitions": "^1.1.0", - "@apollo/utils.printwithreducedwhitespace": "^1.1.0", - "@apollo/utils.removealiases": "1.0.0", - "@apollo/utils.sortast": "^1.1.0", - "@apollo/utils.stripsensitiveliterals": "^1.2.0" - }, - "engines": { - "node": ">=12.13.0" - }, - "peerDependencies": { - "graphql": "14.x || 15.x || 16.x" - } - }, "node_modules/@apollo/utils.withrequired": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@apollo/utils.withrequired/-/utils.withrequired-2.0.1.tgz", @@ -450,26 +351,6 @@ "node": ">=14" } }, - "node_modules/@apollographql/apollo-tools": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@apollographql/apollo-tools/-/apollo-tools-0.5.4.tgz", - "integrity": "sha512-shM3q7rUbNyXVVRkQJQseXv6bnYM3BUma/eZhwXR4xsuM+bqWnJKvW7SAfRjP7LuSCocrexa5AXhjjawNHrIlw==", - "engines": { - "node": ">=8", - "npm": ">=6" - }, - "peerDependencies": { - "graphql": "^14.2.1 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@apollographql/graphql-playground-html": { - "version": "1.6.29", - "resolved": "https://registry.npmjs.org/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.29.tgz", - "integrity": "sha512-xCcXpoz52rI4ksJSdOCxeOCn2DLocxwHf9dVT/Q90Pte1LX+LY+91SFtJF3KXVHH8kEin+g1KKCQPKBjZJfWNA==", - "dependencies": { - "xss": "^1.0.8" - } - }, "node_modules/@aws-crypto/sha256-browser": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", @@ -1773,32 +1654,6 @@ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-tools/mock": { - "version": "8.7.20", - "resolved": "https://registry.npmjs.org/@graphql-tools/mock/-/mock-8.7.20.tgz", - "integrity": "sha512-ljcHSJWjC/ZyzpXd5cfNhPI7YljRVvabKHPzKjEs5ElxWu2cdlLGvyNYepApXDsM/OJG/2xuhGM+9GWu5gEAPQ==", - "dependencies": { - "@graphql-tools/schema": "^9.0.18", - "@graphql-tools/utils": "^9.2.1", - "fast-json-stable-stringify": "^2.1.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/mock/node_modules/@graphql-tools/utils": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.2.1.tgz", - "integrity": "sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==", - "dependencies": { - "@graphql-typed-document-node/core": "^3.1.1", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, "node_modules/@graphql-tools/schema": { "version": "9.0.19", "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-9.0.19.tgz", @@ -1984,11 +1839,6 @@ "node": ">=8" } }, - "node_modules/@josephg/resolvable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@josephg/resolvable/-/resolvable-1.0.1.tgz", - "integrity": "sha512-CtzORUwWTTOTqfVtHaKRJ0I1kNQd1bpn3sUh8I3nJDVY+5/M/Oe1DnEWzPQvqq/xPIIkzzzIP7mfCoAjFRvDhg==" - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -2952,14 +2802,6 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, - "node_modules/@types/accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/bcryptjs": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", @@ -3470,240 +3312,6 @@ "node": ">= 8" } }, - "node_modules/apollo-datasource": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-3.3.2.tgz", - "integrity": "sha512-L5TiS8E2Hn/Yz7SSnWIVbZw0ZfEIXZCa5VUiVxD9P53JvSrf4aStvsFDlGWPvpIdCR+aly2CfoB79B9/JjKFqg==", - "deprecated": "The `apollo-datasource` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023 and October 22nd 2024, respectively). See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details.", - "dependencies": { - "@apollo/utils.keyvaluecache": "^1.0.1", - "apollo-server-env": "^4.2.1" - }, - "engines": { - "node": ">=12.0" - } - }, - "node_modules/apollo-reporting-protobuf": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/apollo-reporting-protobuf/-/apollo-reporting-protobuf-3.4.0.tgz", - "integrity": "sha512-h0u3EbC/9RpihWOmcSsvTW2O6RXVaD/mPEjfrPkxRPTEPWqncsgOoRJw+wih4OqfH3PvTJvoEIf4LwKrUaqWog==", - "deprecated": "The `apollo-reporting-protobuf` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023 and October 22nd 2024, respectively). This package's functionality is now found in the `@apollo/usage-reporting-protobuf` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details.", - "dependencies": { - "@apollo/protobufjs": "1.2.6" - } - }, - "node_modules/apollo-reporting-protobuf/node_modules/@apollo/protobufjs": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@apollo/protobufjs/-/protobufjs-1.2.6.tgz", - "integrity": "sha512-Wqo1oSHNUj/jxmsVp4iR3I480p6qdqHikn38lKrFhfzcDJ7lwd7Ck7cHRl4JE81tWNArl77xhnG/OkZhxKBYOw==", - "hasInstallScript": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.0", - "@types/node": "^10.1.0", - "long": "^4.0.0" - }, - "bin": { - "apollo-pbjs": "bin/pbjs", - "apollo-pbts": "bin/pbts" - } - }, - "node_modules/apollo-reporting-protobuf/node_modules/@types/node": { - "version": "10.17.60", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", - "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" - }, - "node_modules/apollo-server": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/apollo-server/-/apollo-server-3.13.0.tgz", - "integrity": "sha512-hgT/MswNB5G1r+oBhggVX4Fjw53CFLqG15yB5sN+OrYkCVWF5YwPbJWHfSWa7699JMEXJGaoVfFzcvLZK0UlDg==", - "dependencies": { - "@types/express": "4.17.14", - "apollo-server-core": "^3.13.0", - "apollo-server-express": "^3.13.0", - "express": "^4.17.1" - }, - "peerDependencies": { - "graphql": "^15.3.0 || ^16.0.0" - } - }, - "node_modules/apollo-server-core": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-3.13.0.tgz", - "integrity": "sha512-v/g6DR6KuHn9DYSdtQijz8dLOkP78I5JSVJzPkARhDbhpH74QNwrQ2PP2URAPPEDJ2EeZNQDX8PvbYkAKqg+kg==", - "dependencies": { - "@apollo/utils.keyvaluecache": "^1.0.1", - "@apollo/utils.logger": "^1.0.0", - "@apollo/utils.usagereporting": "^1.0.0", - "@apollographql/apollo-tools": "^0.5.3", - "@apollographql/graphql-playground-html": "1.6.29", - "@graphql-tools/mock": "^8.1.2", - "@graphql-tools/schema": "^8.0.0", - "@josephg/resolvable": "^1.0.0", - "apollo-datasource": "^3.3.2", - "apollo-reporting-protobuf": "^3.4.0", - "apollo-server-env": "^4.2.1", - "apollo-server-errors": "^3.3.1", - "apollo-server-plugin-base": "^3.7.2", - "apollo-server-types": "^3.8.0", - "async-retry": "^1.2.1", - "fast-json-stable-stringify": "^2.1.0", - "graphql-tag": "^2.11.0", - "loglevel": "^1.6.8", - "lru-cache": "^6.0.0", - "node-abort-controller": "^3.0.1", - "sha.js": "^2.4.11", - "uuid": "^9.0.0", - "whatwg-mimetype": "^3.0.0" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "graphql": "^15.3.0 || ^16.0.0" - } - }, - "node_modules/apollo-server-core/node_modules/@graphql-tools/merge": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.3.1.tgz", - "integrity": "sha512-BMm99mqdNZbEYeTPK3it9r9S6rsZsQKtlqJsSBknAclXq2pGEfOxjcIZi+kBSkHZKPKCRrYDd5vY0+rUmIHVLg==", - "dependencies": { - "@graphql-tools/utils": "8.9.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/apollo-server-core/node_modules/@graphql-tools/schema": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.5.1.tgz", - "integrity": "sha512-0Esilsh0P/qYcB5DKQpiKeQs/jevzIadNTaT0jeWklPMwNbT7yMX4EqZany7mbeRRlSRwMzNzL5olyFdffHBZg==", - "dependencies": { - "@graphql-tools/merge": "8.3.1", - "@graphql-tools/utils": "8.9.0", - "tslib": "^2.4.0", - "value-or-promise": "1.0.11" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/apollo-server-core/node_modules/@graphql-tools/utils": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.9.0.tgz", - "integrity": "sha512-pjJIWH0XOVnYGXCqej8g/u/tsfV4LvLlj0eATKQu5zwnxd/TiTHq7Cg313qUPTFFHZ3PP5wJ15chYVtLDwaymg==", - "dependencies": { - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/apollo-server-core/node_modules/value-or-promise": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.11.tgz", - "integrity": "sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg==", - "engines": { - "node": ">=12" - } - }, - "node_modules/apollo-server-env": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-4.2.1.tgz", - "integrity": "sha512-vm/7c7ld+zFMxibzqZ7SSa5tBENc4B0uye9LTfjJwGoQFY5xsUPH5FpO5j0bMUDZ8YYNbrF9SNtzc5Cngcr90g==", - "deprecated": "The `apollo-server-env` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023 and October 22nd 2024, respectively). This package's functionality is now found in the `@apollo/utils.fetcher` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details.", - "dependencies": { - "node-fetch": "^2.6.7" - }, - "engines": { - "node": ">=12.0" - } - }, - "node_modules/apollo-server-errors": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-3.3.1.tgz", - "integrity": "sha512-xnZJ5QWs6FixHICXHxUfm+ZWqqxrNuPlQ+kj5m6RtEgIpekOPssH/SD9gf2B4HuWV0QozorrygwZnux8POvyPA==", - "deprecated": "The `apollo-server-errors` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023 and October 22nd 2024, respectively). This package's functionality is now found in the `@apollo/server` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details.", - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "graphql": "^15.3.0 || ^16.0.0" - } - }, - "node_modules/apollo-server-express": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-3.13.0.tgz", - "integrity": "sha512-iSxICNbDUyebOuM8EKb3xOrpIwOQgKxGbR2diSr4HP3IW8T3njKFOoMce50vr+moOCe1ev8BnLcw9SNbuUtf7g==", - "dependencies": { - "@types/accepts": "^1.3.5", - "@types/body-parser": "1.19.2", - "@types/cors": "2.8.12", - "@types/express": "4.17.14", - "@types/express-serve-static-core": "4.17.31", - "accepts": "^1.3.5", - "apollo-server-core": "^3.13.0", - "apollo-server-types": "^3.8.0", - "body-parser": "^1.19.0", - "cors": "^2.8.5", - "parseurl": "^1.3.3" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "express": "^4.17.1", - "graphql": "^15.3.0 || ^16.0.0" - } - }, - "node_modules/apollo-server-express/node_modules/@types/cors": { - "version": "2.8.12", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", - "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" - }, - "node_modules/apollo-server-plugin-base": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/apollo-server-plugin-base/-/apollo-server-plugin-base-3.7.2.tgz", - "integrity": "sha512-wE8dwGDvBOGehSsPTRZ8P/33Jan6/PmL0y0aN/1Z5a5GcbFhDaaJCjK5cav6npbbGL2DPKK0r6MPXi3k3N45aw==", - "deprecated": "The `apollo-server-plugin-base` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023 and October 22nd 2024, respectively). This package's functionality is now found in the `@apollo/server` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details.", - "dependencies": { - "apollo-server-types": "^3.8.0" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "graphql": "^15.3.0 || ^16.0.0" - } - }, - "node_modules/apollo-server-types": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/apollo-server-types/-/apollo-server-types-3.8.0.tgz", - "integrity": "sha512-ZI/8rTE4ww8BHktsVpb91Sdq7Cb71rdSkXELSwdSR0eXu600/sY+1UXhTWdiJvk+Eq5ljqoHLwLbY2+Clq2b9A==", - "deprecated": "The `apollo-server-types` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023 and October 22nd 2024, respectively). This package's functionality is now found in the `@apollo/server` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details.", - "dependencies": { - "@apollo/utils.keyvaluecache": "^1.0.1", - "@apollo/utils.logger": "^1.0.0", - "apollo-reporting-protobuf": "^3.4.0", - "apollo-server-env": "^4.2.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "graphql": "^15.3.0 || ^16.0.0" - } - }, "node_modules/append-transform": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", @@ -4265,11 +3873,6 @@ "node": ">= 0.8" } }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -4372,11 +3975,6 @@ "node": ">= 8" } }, - "node_modules/cssfilter": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", - "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==" - }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -5047,7 +4645,8 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -6443,17 +6042,6 @@ "get-func-name": "^2.0.1" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -8897,21 +8485,6 @@ "node": ">=0.8" } }, - "node_modules/xss": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", - "integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==", - "dependencies": { - "commander": "^2.20.3", - "cssfilter": "0.0.10" - }, - "bin": { - "xss": "bin/xss" - }, - "engines": { - "node": ">= 0.10.0" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -8930,11 +8503,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -9021,6 +8589,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index d0c45618..fdc77c2e 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,6 @@ "@octokit/graphql": "^7.0.1", "@octokit/rest": "^19.0.13", "@types/node-cron": "^3.0.11", - "apollo-server": "^3.13.0", "axios": "^1.7.7", "bcryptjs": "^2.4.3", "cloudinary": "^1.30.1", @@ -88,7 +87,8 @@ "ts-node-dev": "^2.0.0", "tslog": "^4.9.2", "ws": "^8.11.0", - "xlsx": "^0.18.5" + "xlsx": "^0.18.5", + "zod": "^3.23.8" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.1", diff --git a/src/helpers/generateRandomPassword.ts b/src/helpers/generateRandomPassword.ts index 1218e1c1..f4cace89 100644 --- a/src/helpers/generateRandomPassword.ts +++ b/src/helpers/generateRandomPassword.ts @@ -4,5 +4,6 @@ export default function generateRandomPassword(length = 8) { return generator.generate({ length, numbers: true, + symbols: true, }); } diff --git a/src/helpers/isAssignedToProgramOrCohort.ts b/src/helpers/isAssignedToProgramOrCohort.ts deleted file mode 100644 index 6178a95e..00000000 --- a/src/helpers/isAssignedToProgramOrCohort.ts +++ /dev/null @@ -1,56 +0,0 @@ -import Cohort from '../models/cohort.model' -import { Organization } from '../models/organization.model' -import Program from '../models/program.model' -import { User } from '../models/user' - -export default async function isAssigned(organName: string, userId: string) { - // Fetch programs and populate organization - const programs: any = await Program.find().populate({ - path: 'organization', - model: Organization, - strictPopulate: false, - }) - - const cohorts: any = await Cohort.find().populate({ - path: 'program', - model: Program, - strictPopulate: false, - populate: [ - { - path: 'organization', - model: Organization, - strictPopulate: false, - }, - { - path: 'users', - model: User, - strictPopulate: false, - }, - ], - }) - let isAssignedToProgramOrCohort = false - - // Check if the user is assigned to any program associated with the organization - for (const element of programs) { - if (element.organization?.name === organName) { - isAssignedToProgramOrCohort = true - return isAssignedToProgramOrCohort - } - } - - // Check if the user is assigned to any cohort associated with the organization - for (const cohort of cohorts) { - if (cohort.program.organization?.name === organName) { - if ( - cohort.users.some( - (user: { _id: { toString: () => string } }) => - user._id.toString() === userId - ) - ) { - isAssignedToProgramOrCohort = true - return isAssignedToProgramOrCohort - } - } - } - return isAssignedToProgramOrCohort -} diff --git a/src/helpers/logintracker.ts b/src/helpers/logintracker.ts deleted file mode 100644 index 4eadcae8..00000000 --- a/src/helpers/logintracker.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { User } from '../models/user' -import { sendEmail } from '../utils/sendEmail' - -export const checkloginAttepmts = async (Profile: any, user: any) => { - try { - const { activity } = await Profile.findOne({ user }) - if (activity && activity?.length > 1) { - const inline = activity[activity.length - 1] - const recent = Number(inline.failed) + 1 || 0 - if ( - recent >= 3 || - inline.country_name != activity[activity.length - 2].country_name - ) { - await sendEmail( - user.email, - 'SUSPICIOUS ACTIVITY DETECTED ON YOUR ACCOUNT', - emailtemp(recent, inline.date, inline.country_name, inline.city), - '', - process.env.COORDINATOR_EMAIL, - process.env.COORDINATOR_PASS - ) - } - return recent - } - } catch (error) { - console.log(error) - } - - return 1 -} - -export async function checkUserAccountStatus( - userId: string -): Promise { - const user = await User.findById(userId) - if (!user) { - return false - } - return user?.status?.status -} - -const emailtemp = (trials: any, date: any, country: any, city: any) => { - return ` - - - - Suspicious Activity Detected - - - -
-

Suspicious Activity Detected

-

We have detected some suspicious activity on your account. For your security, we recommend taking immediate action to ensure the safety of your account.

-

If you believe this activity was unauthorized, please click the button below to reset your password:

- Reset Password -
- -
  • date:${date}
  • -
  • country name: ${country}
  • -
  • city: ${city}
  • -
  • failed attempts:${trials}
  • -
    - - -

    If you recognize this activity and believe it was performed by you, you can safely ignore this message.

    -

    If you have any questions or need further assistance, please contact our support team.

    -

    Best regards,

    -

    Pulse Team

    -
    - - -` -} diff --git a/src/helpers/organization.helper.ts b/src/helpers/organization.helper.ts index aa904db2..8cdb7bcf 100644 --- a/src/helpers/organization.helper.ts +++ b/src/helpers/organization.helper.ts @@ -1,7 +1,9 @@ import { GraphQLError } from 'graphql' import 'dotenv/config' import { JwtPayload, verify } from 'jsonwebtoken' -import { Organization } from '../models/organization.model' +import { IOrganization, Organization } from '../models/organization.model' +import { IOrgUserData, IUser, IUserMethods} from '../models/user' +import { HydratedDocument } from 'mongoose' export async function checkLoggedInOrganization(token?: string) { const SECRET = process.env.SECRET as string @@ -27,6 +29,14 @@ export async function checkLoggedInOrganization(token?: string) { }) } + if(org.isDeleted){ + throw new GraphQLError(`Organization named ${name} was deleted`, { + extensions: { + code: 'AUTHENTICATION_ERROR', + }, + }) + } + return org } catch (error: any) { if (error.message === 'invalid signature') { @@ -50,3 +60,15 @@ export async function checkLoggedInOrganization(token?: string) { } } } + +export function isPartOfOrganization(user: HydratedDocument, org: HydratedDocument): HydratedDocument{ + const orgUserData = user.organizations.find(data=>data.orgId.toString()===org._id.toString()) + if(!orgUserData){ + throw new GraphQLError(`User ${user.email} is not part of ${org.name}`,{ + extensions: { + code: "FORBIDDEN" + } + }) + } + return orgUserData +} diff --git a/src/helpers/user.helpers.ts b/src/helpers/user.helpers.ts index e1651ffb..5f444f88 100644 --- a/src/helpers/user.helpers.ts +++ b/src/helpers/user.helpers.ts @@ -1,31 +1,50 @@ import { GraphQLError } from 'graphql' -import { RoleOfUser, User } from '../models/user' +import User, { IUser, IUserMethods, IOrgUserData, RoleOfUser } from '../models/user' import { Context } from './../context' import * as jwt from 'jsonwebtoken' +import { IOrganization } from '../models/organization.model' +import { HydratedDocument } from 'mongoose' +import Cohort from '../models/cohort.model' +import Program from '../models/program.model' +import { Profile } from '../models/profile.model' +import { sendEmail } from '../utils/sendEmail' +import suspiciousActivityTemplate from '../utils/templates/suspiciousActivityTemplate' -const SECRET: string = process.env.SECRET as string +const SECRET: string = process.env.SECRET || "secret" -export const generateToken = (userId: string, role: string) => { - return jwt.sign({ userId, role }, SECRET, { expiresIn: '2h' }) -} -export const generateTokenUserExists = (email: string) => { - return jwt.sign({ email }, SECRET, { expiresIn: '2d' }) -} -export const generateTokenOrganization = (name: string) => { - return jwt.sign({ name }, SECRET, { expiresIn: '336h' }) +export const generateToken = (payload: Object, expiresIn: number) => { + try{ + return jwt.sign(payload, SECRET, { expiresIn }) + }catch(err: any){ + console.log(err) + throw new GraphQLError("Server error",{ + extensions: { + code: "SERVER_ERROR" + } + }) + } } - export const genericToken=(playLoad:any)=>{ - return jwt.sign({...playLoad},SECRET) - } +export const verifyToken = (token: string)=>{ + try{ + return jwt.verify(token, SECRET) + }catch(err: any){ + throw new GraphQLError(err.message,{ + extensions: { + code: "SERVER_ERROR" + } + }) + } +} export const emailExpression = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ export async function checkUserLoggedIn( + org: HydratedDocument, context: Context -): Promise<(a?: Array) => Context> { +): Promise<(a?: Array) => { user: HydratedDocument, orgUserData: HydratedDocument }> { const { userId, role } = context if (!userId) { @@ -45,7 +64,16 @@ export async function checkUserLoggedIn( }) } - if (user.status?.status !== 'active') { + const orgUserData = user.organizations.find(data=>data.orgId.toString()===org._id.toString()) + if(!orgUserData){ + throw new GraphQLError(`User ${user.email} is not part of ${org.name}`, { + extensions: { + CODE: 'FORBIDDEN', + }, + }) + } + + if (orgUserData.status?.status !== 'active') { throw new GraphQLError('User is not active', { extensions: { CODE: 'USER_NOT_ACTIVE', @@ -65,6 +93,160 @@ export async function checkUserLoggedIn( ) } - return { userId, role } + return { user, orgUserData } + } +} + +export async function isAssignedToAnEntity(user: HydratedDocument, org: HydratedDocument) { + const orgUserData = user.organizations.find(data=>data.orgId.toString()===org._id.toString()) + if(!orgUserData){ + throw new GraphQLError(`User ${user.email} is not part of ${org.name}`,{ + extensions:{ + code: "FORBIDDEN" + } + }) + } + switch(orgUserData.role){ + case RoleOfUser.SUPER_ADMIN: + case RoleOfUser.ADMIN: + break + case RoleOfUser.MANAGER: + const programs = await Program.find({ + manager: user._id, + organization: org._id, + }) + if(!programs.length){ + throw new GraphQLError(`User ${user.email} does not manage any programs`,{ + extensions: { + code: "FORBIDDEN" + } + }) + } + break + case RoleOfUser.COORDINATOR: + // if(!orgUserData.program){ + // throw new GraphQLError(`User ${user.email} is not assigned to a program`,{ + // extensions: { + // code: "FORBIDDEN" + // } + // }) + // } + const cohorts = await Cohort.find({ + coordinator: user._id, + organization: org._id, + }) + if(!cohorts.length){ + throw new GraphQLError(`User ${user.email} does not manage any cohorts`,{ + extensions: { + code: "FORBIDDEN" + } + }) + } + break + case RoleOfUser.TTL: + if(!orgUserData.program){ + throw new GraphQLError(`User ${user.email} is not assigned to a program`,{ + extensions: { + code: "FORBIDDEN" + } + }) + } + if(!orgUserData.cohort){ + throw new GraphQLError(`User ${user.email} is not assigned to a cohort`,{ + extensions: { + code: "FORBIDDEN" + } + }) + } + if(!orgUserData.team){ + throw new GraphQLError(`User ${user.email} does not manage any team`,{ + extensions: { + code: "FORBIDDEN" + } + }) + } + break + case RoleOfUser.TRAINEE: + if(!orgUserData.program){ + throw new GraphQLError(`User ${user.email} is not assigned to a program`,{ + extensions: { + code: "FORBIDDEN" + } + }) + } + if(!orgUserData.cohort){ + throw new GraphQLError(`User ${user.email} is not assigned to a cohort`,{ + extensions: { + code: "FORBIDDEN" + } + }) + } + if(!orgUserData.team){ + throw new GraphQLError(`User ${user.email} is not assigned to a team`,{ + extensions: { + code: "FORBIDDEN" + } + }) + } + break + default: + throw new GraphQLError("User role is not valid",{ + extensions: { + code: "FORBIDDEN" + } + }) + } +} + +export const checkloginAttempts = async (user: HydratedDocument, org: HydratedDocument) => { + const orgUserData = user.organizations.find(data=>data.orgId.toString()===org._id.toString()) + if(!orgUserData){ + throw new GraphQLError(`User ${user.email} is not part of ${org.name}`,{ + extensions: { + code: "FORBIDDEN" + } + }) + } + const profile = await Profile.findOne({ + user: user._id, + orgId: org._id, + }) + if(!profile){ + throw new GraphQLError(`User ${user.email} does not have a profile associated with ${org.name}`,{ + extensions: { + code: "FORBIDDEN" + } + }) + } + const { activity } = profile + if (activity && activity?.length > 1) { + const inline = activity[activity.length - 1] + const recent = Number(inline.failed) + 1 || 0 + if ( + recent >= 3 || + inline.country_name != activity[activity.length - 2].country_name + ) { + await sendEmail( + user.email, + 'SUSPICIOUS ACTIVITY DETECTED ON YOUR ACCOUNT', + suspiciousActivityTemplate(recent, inline.date, inline.country_name, inline.city), + '', + process.env.COORDINATOR_EMAIL, + process.env.COORDINATOR_PASS + ) + } + return recent } } + +export async function checkUserAccountStatus(user: HydratedDocument, org:HydratedDocument): Promise<'active' | 'drop' | 'suspended' | any> { +const orgUserData = user.organizations.find(data=> data.orgId.toString()===org._id.toString()) +if(!orgUserData){ + throw new GraphQLError(`User ${user.email} is not part of ${org.name}`,{ + extensions:{ + code: "FORBIDDEN" + } + }) +} +return orgUserData.status?.status +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index f5762ea8..020f7f6c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,10 @@ import { WebSocketServer } from 'ws' import { useServer } from 'graphql-ws/lib/use/ws' import { graphqlUploadExpress } from 'graphql-upload-ts' +import mongoose from 'mongoose' +import { mongooseSoftDelete } from './plugins/mongooseSoftDelete' +mongoose.plugin(mongooseSoftDelete) + // Import resolvers, schemas, utilities import { connect } from './database/db.config' import { context } from './context' @@ -44,13 +48,14 @@ import ticketSchema from './schema/ticket.shema' import notificationSchema from './schema/notification.schema' import statisticsSchema from './schema/invitationStatics.schema' -import StatisticsResolvers from './resolvers/invitationStatics.resolvers' +// import StatisticsResolvers from './resolvers/invitationStatics.resolvers' import { IResolvers } from '@graphql-tools/utils' import invitationSchema from './schema/invitation.schema' -import TableViewInvitationResolver from './resolvers/TableViewInvitationResolver' +// import TableViewInvitationResolver from './resolvers/TableViewInvitationResolver' import eventSchema from './schema/event.schema' import './utils/cron-jobs/team-jobs' +import userSchema from './schema/user.schema' const PORT: number = parseInt(process.env.PORT!) || 4000 @@ -66,30 +71,31 @@ export const typeDefs = mergeTypeDefs([ notificationSchema, statisticsSchema, eventSchema, + userSchema ]) export const resolvers = mergeResolvers([ userResolvers, - profileResolvers, - programResolvers, - cohortResolvers, - createRatingSystemresolver, - manageStudentResolvers, - ratingResolvers, - replyResolver, - phaseResolver, - teamResolver, - notificationResolver, - eventResolvers, - ticketResolver, - DocumentationResolvers, - attendanceResolver, - Sessionresolvers, - - StatisticsResolvers, - - invitationResolvers, - TableViewInvitationResolver, + // profileResolvers, + // programResolvers, + // cohortResolvers, + // createRatingSystemresolver, + // manageStudentResolvers, + // ratingResolvers, + // replyResolver, + // phaseResolver, + // teamResolver, + // notificationResolver, + // eventResolvers, + // ticketResolver, + // DocumentationResolvers, + // attendanceResolver, + // Sessionresolvers, + + // StatisticsResolvers, + + // invitationResolvers, + // TableViewInvitationResolver, ]) async function startApolloServer( diff --git a/src/models/attendance.model.ts b/src/models/attendance.model.ts index f1417e2b..15842a6e 100644 --- a/src/models/attendance.model.ts +++ b/src/models/attendance.model.ts @@ -6,12 +6,12 @@ const AttendanceSchema = new Schema({ required: true, }, phase: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'Phase', required: true, }, cohort: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'Cohort', required: true, }, @@ -31,14 +31,14 @@ const AttendanceSchema = new Schema({ }, }, team: { - type: mongoose.Schema.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'Team', required: true }, trainees: [ { trainee: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'User', }, status: [ diff --git a/src/models/cohort.model.ts b/src/models/cohort.model.ts index 4cb877a6..93b097f4 100644 --- a/src/models/cohort.model.ts +++ b/src/models/cohort.model.ts @@ -1,11 +1,10 @@ -import mongoose, { Schema } from 'mongoose' -import { User } from './user' -import { PhaseInterface } from './phase.model'; +import mongoose, { Schema, Document } from 'mongoose' +import User from './user' -export interface CohortInterface { - _id: mongoose.Types.ObjectId; +export interface CohortInterface extends Document { + id?: string; name: string; - phase: PhaseInterface; + phase: mongoose.Types.ObjectId; coordinator: mongoose.Types.ObjectId; members: mongoose.Types.ObjectId[]; program: mongoose.Types.ObjectId; @@ -14,6 +13,7 @@ export interface CohortInterface { startDate: Date; endDate?: Date; // Optional organization: mongoose.Types.ObjectId; + isDeleted?: Boolean; } const cohortSchema = new Schema( @@ -24,21 +24,20 @@ const cohortSchema = new Schema( required: true, }, phase: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, required: true, ref: 'Phase', }, coordinator: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'User', - required: true, }, members: { - type: [mongoose.Types.ObjectId], + type: [Schema.Types.ObjectId], ref: 'User', }, program: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, required: true, ref: 'Program', }, @@ -59,7 +58,7 @@ const cohortSchema = new Schema( type: Date, }, organization: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'Organization', required: true, }, diff --git a/src/models/event.model.ts b/src/models/event.model.ts index 4691fa2a..9de36650 100644 --- a/src/models/event.model.ts +++ b/src/models/event.model.ts @@ -10,7 +10,7 @@ const Event = mongoose.model( 'Event', new Schema({ user: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'User', required: true, }, diff --git a/src/models/invitation.model.ts b/src/models/invitation.model.ts index 231527de..7b2fc2c9 100644 --- a/src/models/invitation.model.ts +++ b/src/models/invitation.model.ts @@ -16,7 +16,7 @@ const ROLE = { } const InvitationSchema = new Schema({ inviterId: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'User', required: true, }, diff --git a/src/models/notification.model.ts b/src/models/notification.model.ts index 17cb1ac2..a7c2fc47 100644 --- a/src/models/notification.model.ts +++ b/src/models/notification.model.ts @@ -5,7 +5,7 @@ const Notification = mongoose.model( new Schema( { receiver: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'User', required: true, }, @@ -14,7 +14,7 @@ const Notification = mongoose.model( required: true, }, sender: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'User', required: true, }, diff --git a/src/models/organization.model.ts b/src/models/organization.model.ts index 753d634a..14c34ae9 100644 --- a/src/models/organization.model.ts +++ b/src/models/organization.model.ts @@ -1,6 +1,29 @@ -import mongoose, { model, Schema } from 'mongoose' +import mongoose, { model, Schema, Document } from 'mongoose' +import Program from './program.model' +import Phase from './phase.model' +import Cohort from './cohort.model' +import Team from './team.model' +import User from './user' +import { Profile } from './profile.model' -const organizationSchema = new Schema({ +export interface IOrganization{ + id?: string, + name: string, + description?: string, + gitHubOrganisation?: string, + activeRepos: string[], + admin: mongoose.Types.ObjectId[], + status: string, + isDeleted: Boolean, +} + +export enum ORG_STATUS{ + ACTIVE='active', + PENDING='pending', + REJECTED='rejected' +} + +const organizationSchema = new Schema({ name: { type: String, unique: true, @@ -16,16 +39,46 @@ const organizationSchema = new Schema({ type: [String], }, admin: { - type: [mongoose.Types.ObjectId], + type: [Schema.Types.ObjectId], ref: 'User', required: true, }, status: { type: String, - enum: ['active', 'pending', 'rejected'], - default: 'active', + enum: Object.values(ORG_STATUS), + default: ORG_STATUS.PENDING, }, }) -const Organization = model('Organization', organizationSchema) +organizationSchema.pre('save',async function(next){ + if(this.isDeleted){ + await Program.updateMany({ organization: this._id },{ + $set: { isDeleted: true } + }) + await Cohort.updateMany({ organization: this._id },{ + $set: { isDeleted: true } + }) + await Team.updateMany({ organization: this._id },{ + $set: { isDeleted: true } + }) + await Phase.updateMany({ organization: this._id },{ + $set: { isDeleted: true } + }) + await Profile.updateMany({ orgId: this._id },{ + $set: { isDeleted: true } + }) + const orgUsers = await User.find({"organizations.orgId": this._id}) + for(const user of orgUsers){ + const orgData = user.organizations.find(data=>data.orgId.toString()===this._id.toString()) + if(!orgData){ + continue + } + orgData.isDeleted = true + await user.save() + } + } + next() +}) + +const Organization = model('Organization', organizationSchema) export { Organization } diff --git a/src/models/phase.model.ts b/src/models/phase.model.ts index ad9c2754..baaae87c 100644 --- a/src/models/phase.model.ts +++ b/src/models/phase.model.ts @@ -1,10 +1,10 @@ import mongoose, { Schema } from 'mongoose'; export interface PhaseInterface { - _id: mongoose.Types.ObjectId; name: string; description: string; organization: mongoose.Types.ObjectId; + isDeleted?: Boolean; } const phaseSchema = new Schema({ @@ -18,10 +18,10 @@ const phaseSchema = new Schema({ required: true, }, organization: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'Organization', required: true, - }, + } }); const Phase = mongoose.model('Phase', phaseSchema); diff --git a/src/models/profile.model.ts b/src/models/profile.model.ts index d6bbe47e..53fc102c 100644 --- a/src/models/profile.model.ts +++ b/src/models/profile.model.ts @@ -1,4 +1,31 @@ -import mongoose, { model, Schema } from 'mongoose' +import mongoose, { model, Schema, Document } from 'mongoose' + +interface IActivity extends mongoose.Types.Subdocument{ + id?: string, + country_code?: string , + country_name?: string, + IPv4?: string, + city?: string, + state?: string, + postal?: string, + latitude?: number, + longitude?: number, + failed?: number, + date?: Date, +} + +export interface IProfile extends Document{ + id?: string, + orgId: mongoose.Types.ObjectId, + biography?: string, + avatar?: string, + cover?: string, + activity: IActivity[], + user: mongoose.Types.ObjectId, + githubUsername?: string, + resume?: string, + isDeleted?: Boolean; +} const ActivitySchema = new mongoose.Schema({ country_code: { type: String }, @@ -15,6 +42,11 @@ const ActivitySchema = new mongoose.Schema({ const profileSchema = new Schema( { + orgId: { + type: Schema.Types.ObjectId, + ref: 'Organization', + required: true, + }, biography: { type: String, }, @@ -26,17 +58,16 @@ const profileSchema = new Schema( }, activity: [ActivitySchema], user: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'User', required: true, - unique: true, }, githubUsername: { type: String, }, resume: { type: String, - }, + } }, { toJSON: { virtuals: true }, diff --git a/src/models/program.model.ts b/src/models/program.model.ts index d46a1ebf..29ae37d7 100644 --- a/src/models/program.model.ts +++ b/src/models/program.model.ts @@ -13,12 +13,11 @@ const programSchema = new Schema( default: '', }, manager: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'User', - required: true, }, organization: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'Organization', required: true, }, diff --git a/src/models/ratingSystem.ts b/src/models/ratingSystem.ts index 17169078..e0067710 100644 --- a/src/models/ratingSystem.ts +++ b/src/models/ratingSystem.ts @@ -29,7 +29,7 @@ const systemRating = mongoose.model( default: false, }, organization: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'Organization', required: true, }, diff --git a/src/models/ratings.ts b/src/models/ratings.ts index 0bc5c513..7be64923 100644 --- a/src/models/ratings.ts +++ b/src/models/ratings.ts @@ -3,7 +3,7 @@ import mongoose, { Schema } from 'mongoose' const RatingSchema = new Schema( { user: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'User', required: true, }, @@ -22,7 +22,7 @@ const RatingSchema = new Schema( feedbacks: [ { sender: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'User', }, content: { @@ -47,12 +47,12 @@ const RatingSchema = new Schema( default: true, }, coordinator: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'User', required: true, }, cohort: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'Cohort', required: true, }, @@ -61,7 +61,7 @@ const RatingSchema = new Schema( required: false, }, organization: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'Organization', required: true, }, @@ -76,7 +76,7 @@ const TempData = mongoose.model( new Schema( { user: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'User', required: true, }, @@ -99,7 +99,7 @@ const TempData = mongoose.model( feedbacks: [ { sender: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'User', }, content: { @@ -116,12 +116,12 @@ const TempData = mongoose.model( default: [], }, coordinator: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'Cohort', required: true, }, cohort: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'Cohort', required: true, }, @@ -134,7 +134,7 @@ const TempData = mongoose.model( required: false, }, organization: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'Organization', required: true, }, diff --git a/src/models/reply.model.ts b/src/models/reply.model.ts index 67274cd4..ffa47d52 100644 --- a/src/models/reply.model.ts +++ b/src/models/reply.model.ts @@ -10,7 +10,7 @@ mongoose.set('toJSON', { const ReplySchema = new Schema( { user: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'User', required: true, }, diff --git a/src/models/team.model.ts b/src/models/team.model.ts index 5c140f02..faaf33b1 100644 --- a/src/models/team.model.ts +++ b/src/models/team.model.ts @@ -1,18 +1,17 @@ import mongoose, { Schema } from 'mongoose' -import { User } from './user' -import { CohortInterface } from './cohort.model'; -import { PhaseInterface } from './phase.model'; +import User from './user' export interface TeamInterface { - _id: mongoose.Types.ObjectId; + id?: string; name: string; - cohort?: CohortInterface; - phase?: PhaseInterface; + cohort?: mongoose.Types.ObjectId; + phase?: mongoose.Types.ObjectId; ttl?: mongoose.Types.ObjectId; members: mongoose.Types.ObjectId[]; startingPhase: Date; active: boolean; organization: mongoose.Types.ObjectId; + isDeleted?: Boolean; } const teamSchema = new Schema( @@ -23,15 +22,15 @@ const teamSchema = new Schema( required: true, }, cohort: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'Cohort', }, ttl: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'User', }, members: { - type: [mongoose.Types.ObjectId], + type: [Schema.Types.ObjectId], ref: 'User', }, startingPhase: { @@ -44,20 +43,20 @@ const teamSchema = new Schema( default: true, }, organization: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'Organization', required: true, }, manager: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'User', }, phase: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'Phase', }, program: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'Program', }, isJobActive: { diff --git a/src/models/ticket.model.ts b/src/models/ticket.model.ts index 542498bd..775a748f 100644 --- a/src/models/ticket.model.ts +++ b/src/models/ticket.model.ts @@ -3,12 +3,12 @@ import mongoose, { Schema } from 'mongoose' const ticketSchema = new Schema( { user: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'User', required: true, }, assignee: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'User', required: false, }, @@ -28,12 +28,12 @@ const ticketSchema = new Schema( replies: [ { sender: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'User', required: true, }, receiver: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, ref: 'User', required: true, }, diff --git a/src/models/user.ts b/src/models/user.ts index 424836d5..aeb52e55 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -1,40 +1,58 @@ import bcrypt from 'bcryptjs' -import mongoose, { model, Schema } from 'mongoose' +import mongoose, { model, Types, Schema, Model } from 'mongoose' import { Profile } from './profile.model' +import { Rating } from './ratings'; + +export enum USER_STATUS_ENUM{ + ACTIVE="active", + DROP="drop", + SUSPENDED="suspended" +} export interface UserStatus { - status: 'active' | 'drop' | 'suspended' - reason?: string - date?: Date + status: USER_STATUS_ENUM, + reason?: string, + date?: Date, } -export interface OrgUserDataInterface{ - _id: mongoose.Types.ObjectId; - orgId: mongoose.Types.ObjectId; +export interface IOrgUserData{ + _id: Types.ObjectId, + id?: string; + orgId: Types.ObjectId; role: string; - team?: mongoose.Types.ObjectId; - status: UserStatus; - cohort?: mongoose.Types.ObjectId; - program?: mongoose.Types.ObjectId; - phase?: mongoose.Types.ObjectId; + team?: Types.ObjectId; + status?: UserStatus; + cohort?: Types.ObjectId; + program?: Types.ObjectId; + phase?: Types.ObjectId; + profile?: Types.ObjectId; pushNotifications: boolean; emailNotifications: boolean; - ratings?: mongoose.Types.ObjectId[]; + isDeleted: boolean; } -export interface UserInterface { - _id: mongoose.Types.ObjectId; +export interface IUser{ + id?: string; email: string; password: string; - organizations: mongoose.Types.ObjectId[]; - firstName: String; - lastName: String; - name: String; - address: String; - city: String; - country: String; - phoneNumber: String; + organizations: Types.DocumentArray; + firstName?: string; + lastName?: string; + gender?: string; + name?: string; + address?: string; + city?: string; + country?: string; + phoneNumber?: string; + dateOfBirth?: Date; + isDeleted: boolean +} + +export interface IUserMethods { + checkPass(password: string): Promise } +type UserModel = Model + export enum RoleOfUser { TRAINEE = 'trainee', COORDINATOR = 'coordinator', @@ -51,9 +69,9 @@ mongoose.set('toJSON', { }, }) -const orgUserDataSchema = new Schema({ +const orgUserDataSchema = new Schema({ orgId: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, required: true, ref: 'Organization', }, @@ -62,15 +80,15 @@ const orgUserDataSchema = new Schema({ default: 'user', }, team: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, required: false, ref: 'Team', }, status: { status: { type: String, - enum: ['active', 'drop', 'suspended'], - default: 'active', + enum: Object.values(USER_STATUS_ENUM), + default: USER_STATUS_ENUM.ACTIVE, }, reason: String, date: { @@ -78,20 +96,25 @@ const orgUserDataSchema = new Schema({ }, }, cohort: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, required: false, ref: 'Cohort', }, program: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, required: false, ref: 'Program', }, phase: { - type: mongoose.Types.ObjectId, + type: Schema.Types.ObjectId, required: false, ref: 'Phase', }, + profile: { + type: Schema.Types.ObjectId, + required: false, + ref: 'Profile', + }, pushNotifications: { type: Boolean, default: true, @@ -101,7 +124,7 @@ const orgUserDataSchema = new Schema({ default: true, }, }) -const userSchema = new Schema( +const userSchema = new Schema( { email: { type: String, @@ -170,15 +193,18 @@ userSchema.methods.checkPass = async function (password: string) { return pass } -userSchema.pre( - 'deleteOne', - { document: true, query: false }, - async function (next) { - const prof = await Profile.findOne({ user: this._id }) - if (prof) await prof.remove() - return next() +userSchema.pre('save', async function (next) { + if (!this.isModified('isDeleted')) return next() + if(this.isDeleted){ + await Profile.updateMany({user: this._id},{ + $set: {isDeleted: true} + }) + await Rating.updateMany({user: this._id},{ + $set: {isDeleted: true} + }) } -) + next() +}) userSchema.pre('save', async function (next) { if (!this.isModified('password')) return next() @@ -187,19 +213,6 @@ userSchema.pre('save', async function (next) { return next() }) -const UserRole = mongoose.model( - 'UserRole', - new Schema({ - name: { - type: String, - ref: 'User', - required: true, - unique: true, - }, - }) -) - -const User = model('User', userSchema) -const OrgUserData = model('OrgUserData', orgUserDataSchema) +const User = model('User', userSchema) -export { User, UserRole, OrgUserData } +export default User diff --git a/src/plugins/mongooseSoftDelete.ts b/src/plugins/mongooseSoftDelete.ts new file mode 100644 index 00000000..e61a2531 --- /dev/null +++ b/src/plugins/mongooseSoftDelete.ts @@ -0,0 +1,15 @@ +import {Schema} from "mongoose"; + +export const mongooseSoftDelete=(schema: Schema, options: any)=>{ + schema.add({ + isDeleted: { + type: Boolean, + required: true, + default: false + } + }) + schema.pre(['find', 'findOne', 'findOneAndDelete', 'findOneAndUpdate'],function(next){ + this.where({ isDeleted: false}) + next() + }) +} \ No newline at end of file diff --git a/src/resolvers/DocumentationResolvers.ts b/src/resolvers/DocumentationResolvers.ts index 69cd2a1c..5c520eed 100644 --- a/src/resolvers/DocumentationResolvers.ts +++ b/src/resolvers/DocumentationResolvers.ts @@ -1,3 +1,4 @@ +//@ts-nocheck import { DocumentNode, GraphQLError } from 'graphql' import { Documentation } from '../models/documentation.model' diff --git a/src/resolvers/TableViewInvitationResolver.ts b/src/resolvers/TableViewInvitationResolver.ts index c7a94965..f6f4cab9 100644 --- a/src/resolvers/TableViewInvitationResolver.ts +++ b/src/resolvers/TableViewInvitationResolver.ts @@ -1,3 +1,4 @@ +//@ts-nocheck /* eslint-disable indent */ import { Invitation } from '../models/invitation.model'; import { ApolloError } from 'apollo-server'; diff --git a/src/resolvers/attendance.resolvers.ts b/src/resolvers/attendance.resolvers.ts index 0c592123..89ae4cb4 100644 --- a/src/resolvers/attendance.resolvers.ts +++ b/src/resolvers/attendance.resolvers.ts @@ -1,3 +1,4 @@ +//@ts-nocheck /* eslint-disable indent */ import { Attendance } from '../models/attendance.model' import { ObjectId } from 'mongodb' diff --git a/src/resolvers/cohort.resolvers.ts b/src/resolvers/cohort.resolvers.ts index d381f3d3..a418f407 100644 --- a/src/resolvers/cohort.resolvers.ts +++ b/src/resolvers/cohort.resolvers.ts @@ -1,3 +1,4 @@ +//@ts-nocheck import { GraphQLError } from 'graphql' import { isAfter } from 'date-fns' import { checkLoggedInOrganization } from '../helpers/organization.helper' diff --git a/src/resolvers/coordinatorResolvers.ts b/src/resolvers/coordinatorResolvers.ts index 090c2b32..eb4ca6b8 100644 --- a/src/resolvers/coordinatorResolvers.ts +++ b/src/resolvers/coordinatorResolvers.ts @@ -1,3 +1,4 @@ +//@ts-nocheck import { GraphQLError } from 'graphql' import * as jwt from 'jsonwebtoken' import { Types } from 'mongoose' diff --git a/src/resolvers/createRatingSystemresolver.ts b/src/resolvers/createRatingSystemresolver.ts index c2a705c9..534cc0bc 100644 --- a/src/resolvers/createRatingSystemresolver.ts +++ b/src/resolvers/createRatingSystemresolver.ts @@ -1,3 +1,4 @@ +//@ts-nocheck import { systemRating } from '../models/ratingSystem' import { Context } from './../context' import { checkUserLoggedIn } from '../helpers/user.helpers' diff --git a/src/resolvers/eventResolver.ts b/src/resolvers/eventResolver.ts index fed3f813..6a2ce6f7 100644 --- a/src/resolvers/eventResolver.ts +++ b/src/resolvers/eventResolver.ts @@ -1,3 +1,4 @@ +//@ts-nocheck /* eslint-disable quotes */ import { GraphQLError } from 'graphql' diff --git a/src/resolvers/invitation.resolvers.ts b/src/resolvers/invitation.resolvers.ts index 2f228bf1..09338edb 100644 --- a/src/resolvers/invitation.resolvers.ts +++ b/src/resolvers/invitation.resolvers.ts @@ -1,3 +1,4 @@ +//@ts-nocheck import { GraphQLUpload } from 'graphql-upload-ts' import { GraphQLError } from 'graphql' import { Invitation } from '../models/invitation.model' diff --git a/src/resolvers/invitationStatics.resolvers.ts b/src/resolvers/invitationStatics.resolvers.ts index 44ef2bfd..0aae62fe 100644 --- a/src/resolvers/invitationStatics.resolvers.ts +++ b/src/resolvers/invitationStatics.resolvers.ts @@ -1,3 +1,4 @@ +//@ts-nocheck import { checkUserLoggedIn } from '../helpers/user.helpers' import { Invitation } from '../models/invitation.model' import { GraphQLError } from 'graphql' diff --git a/src/resolvers/notification.resolvers.ts b/src/resolvers/notification.resolvers.ts index 5d1af81f..0c990a13 100644 --- a/src/resolvers/notification.resolvers.ts +++ b/src/resolvers/notification.resolvers.ts @@ -1,3 +1,4 @@ +//@ts-nocheck import { Notification } from '../models/notification.model' import { Context } from './../context' import { checkUserLoggedIn } from '../helpers/user.helpers' diff --git a/src/resolvers/phase.resolver.ts b/src/resolvers/phase.resolver.ts index f645bda1..9ac181fa 100644 --- a/src/resolvers/phase.resolver.ts +++ b/src/resolvers/phase.resolver.ts @@ -1,3 +1,4 @@ +//@ts-nocheck import { GraphQLError } from 'graphql' import { checkUserLoggedIn } from '../helpers/user.helpers' import { checkLoggedInOrganization } from '../helpers/organization.helper' diff --git a/src/resolvers/profileResolver.ts b/src/resolvers/profileResolver.ts index 08b390dd..c287c204 100644 --- a/src/resolvers/profileResolver.ts +++ b/src/resolvers/profileResolver.ts @@ -1,3 +1,4 @@ +//@ts-nocheck import { Context } from '../context' import { checkLoggedInOrganization } from '../helpers/organization.helper' import { checkUserLoggedIn } from '../helpers/user.helpers' diff --git a/src/resolvers/program.resolvers.ts b/src/resolvers/program.resolvers.ts index 15dc4c03..29939fea 100644 --- a/src/resolvers/program.resolvers.ts +++ b/src/resolvers/program.resolvers.ts @@ -1,3 +1,4 @@ +//@ts-nocheck import { GraphQLError } from 'graphql' import { ObjectId } from 'mongodb' import { checkLoggedInOrganization } from '../helpers/organization.helper' diff --git a/src/resolvers/ratingsResolvers.ts b/src/resolvers/ratingsResolvers.ts index 0a5452cc..2be1cefe 100644 --- a/src/resolvers/ratingsResolvers.ts +++ b/src/resolvers/ratingsResolvers.ts @@ -1,3 +1,4 @@ +//@ts-nocheck import { Rating, TempData } from '../models/ratings' import { RoleOfUser, User } from '../models/user' import { Organization } from '../models/organization.model' diff --git a/src/resolvers/reply.resolver.ts b/src/resolvers/reply.resolver.ts index 5063ec7c..e7856daa 100644 --- a/src/resolvers/reply.resolver.ts +++ b/src/resolvers/reply.resolver.ts @@ -1,3 +1,4 @@ +//@ts-nocheck import { Notifications } from '../models/reply.model' import { Rating } from '../models/ratings' import { RoleOfUser, User } from '../models/user' diff --git a/src/resolvers/resolver.ts b/src/resolvers/resolver.ts index bb2f9e0b..51824f3f 100644 --- a/src/resolvers/resolver.ts +++ b/src/resolvers/resolver.ts @@ -1,3 +1,4 @@ +//@ts-nocheck import bcrypt from 'bcryptjs' import * as jwt from 'jsonwebtoken' import mongoose from 'mongoose' diff --git a/src/resolvers/session.resolver.ts b/src/resolvers/session.resolver.ts index f86a3416..c01c3ab7 100644 --- a/src/resolvers/session.resolver.ts +++ b/src/resolvers/session.resolver.ts @@ -1,7 +1,7 @@ +//ts-nocheck import Session from '../models/session.model' import { Notification } from '../models/notification.model' import { pubSubPublish } from './notification.resolvers' -import { ApolloError } from 'apollo-server' const Sessionresolvers = { Query: { diff --git a/src/resolvers/team.resolvers.ts b/src/resolvers/team.resolvers.ts index 8fb30cce..f58fc8b9 100644 --- a/src/resolvers/team.resolvers.ts +++ b/src/resolvers/team.resolvers.ts @@ -1,3 +1,4 @@ +//@ts-nocheck import { ApolloError, ValidationError } from 'apollo-server' import { checkLoggedInOrganization } from '../helpers/organization.helper' import { checkUserLoggedIn } from '../helpers/user.helpers' diff --git a/src/resolvers/ticket.resolver.ts b/src/resolvers/ticket.resolver.ts index 83e8e2b5..b006ef75 100644 --- a/src/resolvers/ticket.resolver.ts +++ b/src/resolvers/ticket.resolver.ts @@ -1,3 +1,4 @@ +//@ts-nocheck import { GraphQLError } from 'graphql' import Ticket from '../models/ticket.model' import { Context } from '../context' diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index 94101e3d..84266115 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -1,31 +1,28 @@ /* eslint-disable prefer-const */ import { Octokit } from '@octokit/rest' -import bcrypt from 'bcryptjs' +import { compareSync, hashSync } from 'bcryptjs' import { GraphQLError } from 'graphql' // import * as jwt from 'jsonwebtoken' import { JwtPayload, verify } from 'jsonwebtoken' -import mongoose, { Error } from 'mongoose' +import { HydratedDocument } from 'mongoose' import generateRandomPassword from '../helpers/generateRandomPassword' -import isAssigned from '../helpers/isAssignedToProgramOrCohort' -import { checkloginAttepmts } from '../helpers/logintracker' -import { checkLoggedInOrganization } from '../helpers/organization.helper' +import { checkLoggedInOrganization, isPartOfOrganization } from '../helpers/organization.helper' import { checkUserLoggedIn, emailExpression, generateToken, - generateTokenOrganization, - generateTokenUserExists, - genericToken, + verifyToken, + isAssignedToAnEntity, + checkloginAttempts, + checkUserAccountStatus } from '../helpers/user.helpers' import Cohort from '../models/cohort.model' import { Invitation } from '../models/invitation.model' -import { Organization } from '../models/organization.model' -import Phase from '../models/phase.model' +import { IOrganization, ORG_STATUS, Organization } from '../models/organization.model' import { Profile } from '../models/profile.model' import Program from '../models/program.model' -import { Rating } from '../models/ratings' import Team from '../models/team.model' -import { RoleOfUser, User, UserRole } from '../models/user' +import User, { IUser, IUserMethods, RoleOfUser, USER_STATUS_ENUM} from '../models/user' import { pushNotification } from '../utils/notification/pushNotification' import { sendEmail } from '../utils/sendEmail' import forgotPasswordTemplate from '../utils/templates/forgotPasswordTemplate' @@ -33,27 +30,30 @@ import organizationApprovedTemplate from '../utils/templates/organizationApprove import organizationCreatedTemplate from '../utils/templates/organizationCreatedTemplate' import organizationRejectedTemplate from '../utils/templates/organizationRejectedTemplate' import registrationRequest from '../utils/templates/registrationRequestTemplate' -import { EmailPattern } from '../utils/validation.utils' import { Context } from './../context' -import { UserInputError } from 'apollo-server' +import { validateEmail, validatePasswordField, validateStringField, validateURLField } from '../validations' const octokit = new Octokit({ auth: `${process.env.Org_Repo_Access}` }) -const SECRET: string = process.env.SECRET as string -export type OrganizationType = InstanceType -export type UserType = InstanceType +const SECRET: string = process.env.SECRET || "secret" +// export type OrganizationType = InstanceType +// export type UserType = InstanceType -enum Status { - pending = 'pending', - approved = 'approved', - rejected = 'rejected', +enum ACTION_ENUM{ + APPROVE='approve', + REJECT='reject' } -async function logGeoActivity(user: any, clientIpAdress: string) { - const response = await fetch(`https://ipapi.co/${clientIpAdress}/json/`) +interface InvitationToken{ + email: string + role: string + orgName: string + userExists: boolean +} +async function logGeoActivity(user: HydratedDocument,org: HydratedDocument,clientIpAdress?: string) { + const response = await fetch(`https://ipapi.co/${clientIpAdress}/json/`) const geoData = await response.json() - - const profile = await Profile.findOne({ user: user._id }) + const profile = await Profile.findOne({user: user._id, orgId: org._id}) if (!profile) { return } @@ -85,144 +85,129 @@ async function logGeoActivity(user: any, clientIpAdress: string) { } const resolvers: any = { - Query: { - async getOrganizations(_: any, __: any, context: Context) { - ;(await checkUserLoggedIn(context))([RoleOfUser.SUPER_ADMIN]) - - return Organization.find() - }, - async getUpdatedEmailNotifications(_: any, { id }: any, context: Context) { - const user: any = await User.findOne({ _id: id }) - if (!user) { - throw new Error('User not found') - } - return user.emailNotifications - }, - async getUpdatedPushNotifications(_: any, { id }: any, context: Context) { - const user: any = await User.findOne({ _id: id }) - if (!user) { - throw new Error('User not found') - } - return user.pushNotifications + Query: { + async getOrganizations(_: any, {orgToken}: {orgToken: string}, context: Context) { + const org = await checkLoggedInOrganization(orgToken) + ;(await checkUserLoggedIn(org, context))([RoleOfUser.SUPER_ADMIN]) + const organizations = await Organization.find() + return organizations }, - async getOrganization(_: any, { name }: any, context: Context) { - const { userId, role } = (await checkUserLoggedIn(context))([ +// async getUpdatedEmailNotifications(_: any, { id }: any, context: Context) { +// const user: any = await User.findOne({ _id: id }) +// if (!user) { +// throw new Error('User not found') +// } +// return user.emailNotifications +// }, +// async getUpdatedPushNotifications(_: any, { id }: any, context: Context) { +// const user: any = await User.findOne({ _id: id }) +// if (!user) { +// throw new Error('User not found') +// } +// return user.pushNotifications +// }, + async getOrganization(_: any, { name, orgToken }: {name: string, orgToken: string}, context: Context) { + const org = await checkLoggedInOrganization(orgToken) + ;(await checkUserLoggedIn(org, context))([ RoleOfUser.SUPER_ADMIN, - RoleOfUser.ADMIN, - 'trainee', ]) - const organization = await Organization.findOne({ name }) + const organization = await Organization.findOne({ name }).populate('admin') if (!organization) { - throw new Error('Organization not found') + throw new GraphQLError('Organization not found',{ + extensions: { + code: "ORG_NOT_FOUND" + } + }) } - - const org = organization.toObject() - const orgAdmin = await User.findById(org.admin) - return { ...org, admin: orgAdmin } + return organization }, - async getSignupOrganization(_: any, { orgToken }: any) { + async getCurrentOrganization(_: any, { orgToken }: {orgToken: string}, context: Context) { const org: InstanceType = await checkLoggedInOrganization(orgToken) - return Organization.findOne({ name: org.name }) - }, - async verifyResetPasswordToken(_: any, { token }: any) { - const { email } = verify(token, SECRET) as JwtPayload - const user: any = await User.findOne({ email }) - if (!user) { - throw new Error('Unauthorized to access the page! ') - } + ;(await checkUserLoggedIn(org, context))(Object.values(RoleOfUser)) + + return org }, +// async verifyResetPasswordToken(_: any, { token }: any) { +// const { email } = verify(token, SECRET) as JwtPayload +// const user: any = await User.findOne({ email }) +// if (!user) { +// throw new Error('Unauthorized to access the page! ') +// } +// }, async gitHubActivity( _: any, - { organisation, username }: any, + { orgToken}: { orgToken: string}, context: Context ) { - ;(await checkUserLoggedIn(context))([ + const org = await checkLoggedInOrganization(orgToken) + const {user, orgUserData} = (await checkUserLoggedIn(org, context))([ RoleOfUser.ADMIN, RoleOfUser.COORDINATOR, - 'trainee', RoleOfUser.MANAGER, - 'ttl', + RoleOfUser.TTL, + RoleOfUser.TRAINEE, ]) - const organisationExists = await Organization.findOne({ - name: organisation, - }) - if (!organisationExists) - throw new Error("This Organization doesn't exist") - - organisation = organisationExists.gitHubOrganisation + await orgUserData.populate('profile') - const { data: checkOrg } = await octokit.orgs.get({ org: organisation }) + const { data: checkOrg } = await octokit.orgs.get({ org: org.gitHubOrganisation || "" }) if (!checkOrg) { throw new GraphQLError('Organization Not found On GitHub', { extensions: { - code: 'UserInputError', + code: 'ORG_NOT_FOUND', }, }) } const { data: checkUser } = await octokit.users.getByUsername({ - username: username, + username: (orgUserData.profile as any).githubUsername || "", }) if (!checkUser) { throw new GraphQLError('User Not found On Github', { extensions: { - code: 'UserInputError', + code: 'USER_NOT_FOUND', }, }) } - let allRepos: any = [] - - allRepos = organisationExists.activeRepos - let pullRequestOpen = 0 let pullRequestClosed = 0 let pullRequestMerged = 0 let pullRequestTotal = 0 - let allCommits = 0 - try { - for (const repo of allRepos) { - try { - const response = await octokit.pulls.list({ - owner: organisation, - repo: repo, - state: 'all', - sort: 'created', - direction: 'desc', - per_page: 200, - }) - - const pullRequests = response.data.filter( - (pullRequest: any) => pullRequest.user.login === username - ) - pullRequestTotal += pullRequests.length - pullRequestOpen += pullRequests.filter( - (pullRequest: any) => pullRequest.state === 'open' - ).length - pullRequestClosed += pullRequests.filter( - (pullRequest: any) => pullRequest.state === 'closed' - ).length - pullRequestMerged += pullRequests.filter( - (pullRequest: any) => pullRequest.merged_at != null - ).length - } catch (error) { - console.error( - `Error retrieving commits for repository ${repo.name}:`, - error - ) - throw new GraphQLError( - 'Error retrieving commits for repository ${repo.name}:' - ) - } + for (const repo of org.activeRepos) { + const response = await octokit.pulls.list({ + owner: org.gitHubOrganisation || "", + repo: repo, + state: 'all', + sort: 'created', + direction: 'desc', + per_page: 200, + }) + const pullRequests = response.data.filter( + (pullRequest: any) => pullRequest.user.login === (orgUserData.profile as any).githubUsername + ) + pullRequestTotal += pullRequests.length + pullRequestOpen += pullRequests.filter( + (pullRequest: any) => pullRequest.state === 'open' + ).length + pullRequestClosed += pullRequests.filter( + (pullRequest: any) => pullRequest.state === 'closed' + ).length + pullRequestMerged += pullRequests.filter( + (pullRequest: any) => pullRequest.merged_at != null + ).length } } catch (error) { - console.error('Error retrieving repositories:', error) + throw new GraphQLError("Error retrieving repository information", { + extensions: { + code: "SERVER_ERROR" + } + }) } return { totalCommits: pullRequestMerged, @@ -233,12 +218,11 @@ const resolvers: any = { }, } }, - }, - Login: { - user: async (parent: any) => { - const user = await User.findById(parent.user.id) - return user - }, +// Login: { +// user: async (parent: any) => { +// const user = await User.findById(parent.user.id) +// return user +// }, }, Mutation: { async createUser( @@ -246,734 +230,685 @@ const resolvers: any = { { email, password, - role, + verifyPassword, firstName, lastName, dateOfBirth, gender, orgToken, - }: any + }: { + email: string, + password: string, + verifyPassword: string, + firstName: string, + lastName: string, + dateOfBirth: string, + gender: string, + orgToken: string + } ) { // checkLoggedInOrganization checks if the organization token passed was valid const org: InstanceType = await checkLoggedInOrganization(orgToken) - const userExists = await User.findOne({ email: email }) + const userExists = await User.findOne({ email: email, organizations: { + $elemMatch: { + orgId: org._id + } + } }) if (userExists) - throw new GraphQLError('User with a such email already exists', { + throw new GraphQLError(`User already exists in ${org.name}`, { extensions: { - code: 'UserInputError', + code: 'USER_INPUT_ERROR', }, - }) + }) const isValidEmail = emailExpression.test(String(email).toLowerCase()) if (!isValidEmail) throw new GraphQLError('invalid email format', { extensions: { - code: 'ValidationError', + code: 'VALIDATION_ERROR', }, }) + // use regular expression to validate password if (password.length < 6) throw new GraphQLError('password should be minimum 6 characters', { extensions: { - code: 'ValidationError', + code: 'VALIDATION_ERROR', }, - }) - let invitee + }) + if (password !== verifyPassword) + throw new GraphQLError('passwords do not match', { + extensions: { + code: 'VALIDATION_ERROR', + }, + }) const invitation = await Invitation.findOne({ 'invitees.email': email }) .sort({ createdAt: -1 }) .exec() - - if (invitation) { - invitee = invitation.invitees.find((invitee) => invitee.email === email) + if (!invitation) { + throw new GraphQLError("User was not invitated",{ + extensions: { + code: 'INVITATION_NOT_FOUND' + } + }) + } + const invitee = invitation.invitees.find((invitee) => invitee.email === email) + if(!invitee){ + throw new GraphQLError("User is not an invitee",{ + extensions: { + code: "FORBIDDEN" + } + }) } - const user = await User.create({ - role: role || invitee?.role || 'user', - email: email, + email, password, - organizations: org.name, + firstName, + lastName, + gender, + dateOfBirth, + organizations: [ + { + orgId: org._id, + role: invitee.role + } + ] }) - const token = generateToken(user._id.toString(), user?.role) + const token = generateToken({ + userId: user._id.toString(), + role:user.organizations[0].role + }, 7200) if (user && invitation) { invitation.status = 'accepted' await invitation.save() } - const newProfile = await Profile.create({ - user, - firstName, - lastName, - dateOfBirth, - gender, + user: user._id, + orgId: org._id, }) + user.organizations[0].profile = newProfile._id + await user.save() + return { token, user } + }, - const newUser: string | null = await User.findByIdAndUpdate( - user.id, - { - profile: newProfile, - }, - { new: true } - ) - - return { token, user: newUser } + addUserToOrganization: async(_: any, {invitationToken}: {invitationToken: string})=>{ + const {email, role, orgName, userExists} = verifyToken(invitationToken) as InvitationToken + if(typeof email !=="string" || !email || typeof role!=="string" || !role || typeof orgName !=="string" || !orgName || typeof userExists !== "boolean"){ + throw new GraphQLError("Invalid token payload",{ + extensions:{ + code: "USER_INPUT_ERROR" + } + }) + } + const user = await User.findOne({ + email + }) + if(!user){ + throw new GraphQLError("No such user was found",{ + extensions: { + code: "USER_NOT_FOUND" + } + }) + } + const organization = await Organization.findOne({ + name: orgName + }) + if(!organization){ + throw new GraphQLError("No such organization was found",{ + extensions: { + code: "ORG_NOT_FOUND" + } + }) + } + const invitation = await Invitation.findOne({ + invitees: { + $elemMatch:{ + email: user.email, + exists: true, + } + } + }) + if(!invitation){ + throw new GraphQLError("No such invitation was found",{ + extensions: { + code: "INVITATION_NOT_FOUND" + } + }) + } + user.organizations.push({ + orgId: organization._id, + role: role + }) + await user.save() }, - async createProfile(_: any, args: any, context: { userId: any }) { - if (!context.userId) throw new Error('Unauthorized') - if (!mongoose.isValidObjectId(context.userId)) - throw new Error('Invalid user id') - const userExists = await User.findOne({ _id: context.userId }) - if (!userExists) throw new Error('This user does not exists') + async updateProfile(_: any, {biography, cover, avatar, githubUsername, resume, orgToken}: + { + biography: string, + cover: string, + avatar: string, + githubUsername: string, + resume: string, + orgToken: string + }, + context: Context) + { + validateStringField(biography, "Please enter a valid biography") + validateURLField(cover, "Please enter a valid cover url") + validateURLField(avatar, "Please enter a valid avatar url") + validateStringField(githubUsername, "please enter a valid githubUsername") + validateURLField(resume, "Please enter a valid resume url") + const org = await checkLoggedInOrganization(orgToken) + const {user} = (await checkUserLoggedIn(org, context))(Object.values(RoleOfUser)) + const profile = await Profile.findOneAndUpdate( - { user: context.userId }, - args, + { user: user._id }, + { + biography, + cover, + avatar, + githubUsername, + resume, + }, { upsert: true, new: true, } ) - return profile.toJSON() + return profile }, async loginUser( _: any, - { loginInput: { email, password, orgToken } }: any, - context: any + { email, password, orgToken }: {email: string, password: string, orgToken: string}, + context: Context ) { + validateEmail(email, "Please enter a valid email address") const { clientIpAdress } = context - // get the organization if someone logs in const org: InstanceType = await checkLoggedInOrganization(orgToken) - - const user: any = await User.findOne({ email }).populate({ - path: 'cohort', - model: Cohort, - strictPopulate: false, - populate: { - path: 'program', - model: Program, - strictPopulate: false, - populate: { - path: 'organization', - model: Organization, - strictPopulate: false, - }, - }, - }) - + const user: any = await User.findOne({ email }) if (!user) { - throw new GraphQLError('invalid credential', { + throw new GraphQLError(`User ${email} does not exist`, { extensions: { - code: 'AccountNotFound', + code: 'USER_NOT_FOUND', }, }) - } else if (user?.status?.status !== 'active') { + } + const orgUserData = user.organizations.find((data: any)=>data.orgId.toString()===org._id.toString()) + if(!orgUserData){ + throw new GraphQLError(`User ${user.email} is not part of ${org.name}`,{ + extensions: { + code: "FORBIDDEN" + } + }) + } + if (orgUserData.status?.status !== 'active') { throw new GraphQLError( `Your account have been ${ - user?.status?.status ?? user?.status + orgUserData.status?.status }, please contact your organization admin for assistance`, { extensions: { - code: 'AccountInactive', + code: 'ACCOUNT_INACTIVE', }, } ) } - let attempts = await checkloginAttepmts(Profile, user) - - if (await user?.checkPass(password)) { - if ( - user?.role === RoleOfUser.TRAINEE && - user?.organizations?.includes(org?.name) - ) { - if ( - !user.cohort || - user.cohort === null || - !user.team || - user.team === null - ) { - throw new Error('Please wait to be added to a program or cohort') - } - if (await isAssigned(org?.name, user._id)) { - const token = generateToken(user._id, user._doc?.role || 'user') - - const geoData = await logGeoActivity(user, clientIpAdress) - - const data = { - token: token, - user: user.toJSON(), - geoData, - } - return data - } else { - throw new Error( - 'You are not assigned to any valid program or cohort in this organization.' - ) - } - } else if ( - user?.role === RoleOfUser.TTL && - user?.organizations?.includes(org?.name) - ) { - if (user.cohort && user.team) { - const token = generateToken(user._id, user._doc?.role || 'user') - - const geoData = await logGeoActivity(user, clientIpAdress) - - const data = { - token: token, - user: user.toJSON(), - geoData, - } - return data - } else { - throw new Error('You are not assigned to any cohort ot team yet.') - } - } - const organization: any = await Organization.findOne({ - name: org?.name, - admin: user.id, - }) + await checkloginAttempts(user, org) + const geoData = await logGeoActivity(user,org,clientIpAdress) - if (user?.role === RoleOfUser.ADMIN) { - if (user?.organizations?.includes(org?.name)) { - const token = generateToken(user._id, user._doc?.role || 'user') - - const geoData = await logGeoActivity(user, clientIpAdress) - - const data = { - token: token, - user: user.toJSON(), - geoData, - } - return data - } else { - throw new Error('You do not have access to this organization.') - } - } else if (user?.role === RoleOfUser.MANAGER) { - const program: any = await Program.find({ - manager: user.id, - }).populate({ - path: 'organization', - model: Organization, - strictPopulate: false, - }) - let checkProgramOrganization: any = false - - for (const element of program) { - if (element.organization.name == org?.name) { - checkProgramOrganization = true - } - } - if (checkProgramOrganization) { - const managerToken = generateToken( - user._id, - user._doc?.role || 'user' - ) - - const geoData = await logGeoActivity(user, clientIpAdress) - - const managerData = { - token: managerToken, - user: user.toJSON(), - geoData, - } - return managerData - } else { - throw new Error('You are not assigned to any program yet.') - } - } else if (user?.role === RoleOfUser.COORDINATOR) { - const cohort: any = await Cohort.find({ - coordinator: user.id, - }).populate({ - path: 'program', - model: Program, - strictPopulate: false, - populate: { - path: 'organization', - model: Organization, - strictPopulate: false, + if(!await user?.checkPass(password)) { + throw new GraphQLError('Invalid credential', { + extensions: { + code: 'UserInputError', }, }) - let checkCohortOrganization: any = false - - for (const element of cohort) { - if (element.program.organization.name == org?.name) { - checkCohortOrganization = true - } - } - - if (checkCohortOrganization) { - const coordinatorToken = generateToken( - user._id, - user._doc?.role || 'user' - ) - - const geoData = await logGeoActivity(user, clientIpAdress) - - const coordinatorData = { - token: coordinatorToken, - user: user.toJSON(), - geoData, - } - return coordinatorData - } else { - throw new Error('You are not assigned to any cohort yet.') - } - } else if (user?.role === RoleOfUser.SUPER_ADMIN) { - const superAdminToken = generateToken( - user._id, - user._doc?.role || 'user' - ) + } - const geoData = await logGeoActivity(user, clientIpAdress) + await isAssignedToAnEntity(user,org) - const superAdminData = { - token: superAdminToken, - user: user.toJSON(), - geoData, - } - return superAdminData - } else { - throw new Error('Please wait to be added to a program or cohort') - } - } else { - throw new GraphQLError('Invalid credential', { - extensions: { - code: 'UserInputError', - }, - }) + return { + token: generateToken({ userId: user._id, role: orgUserData.role}, 7200), + user: user.toJSON(), + geoData, } }, - async deleteUser(_: any, { input }: any, context: { userId: any }) { - const requester = await User.findById(context.userId) - if (!requester) { - throw new Error('Requester does not exist') + async deleteUser(_: any, { userId, reason , orgToken}: {userId: string, reason?: string, orgToken: string}, context: Context) { + if(reason){ + validateStringField(reason, "Please provide a valid reason") } - if ( - requester.role !== RoleOfUser.ADMIN && - requester.role !== RoleOfUser.SUPER_ADMIN - ) { - throw new Error('You do not have permission to delete users') + const org = await checkLoggedInOrganization(orgToken) + const {user} = (await checkUserLoggedIn(org,context))([RoleOfUser.ADMIN, RoleOfUser.SUPER_ADMIN]) + if (!user) { + throw new GraphQLError('Requester does not exist',{ + extensions: { + code: 'USER_NOT_FOUND' + } + }) } - const userToSuspend = await User.findById(input.id) + const userToSuspend = await User.findById(userId) if (!userToSuspend) { - throw new Error('User to be suspended does not exist') + throw new GraphQLError('User to be suspended does not exist',{ + extensions: { + code: 'USER_NOT_FOUND' + } + }) } - if (userToSuspend?.status?.status == 'suspended') { - throw new Error('User is already suspended') + const userData = isPartOfOrganization(userToSuspend, org) + if (userData.status?.status == 'suspended') { + throw new GraphQLError('User is already suspended',{ + extensions: { + code: 'FORBIDDEN' + } + }) } - // Handle coordinator suspension - if (userToSuspend.role === RoleOfUser.COORDINATOR) { - const hasCohort = await Cohort.findOne({ coordinator: input.id }) - if (hasCohort) { - await Cohort.findOneAndReplace( - { coordinator: input.id }, - { coordinator: null } - ) + if (userData.role === RoleOfUser.COORDINATOR) { + const cohorts = await Cohort.find({ coordinator: userToSuspend._id, organization: org._id }) + for(const cohort of cohorts){ + cohort.coordinator = undefined + await cohort.save() await pushNotification( - context.userId, - `The coordinator of ${hasCohort.name} has been suspended. Please assign a new coordinator.`, - context.userId + user._id, + `The coordinator of ${cohort.name} has been suspended. Please assign a new coordinator.`, + user._id, + org._id ) } } // Handle TTL suspension - else if (userToSuspend.role === 'ttl') { - const hasTeam = await Team.findOne({ ttl: input.id }) + else if (userData.role === RoleOfUser.TTL) { + const hasTeam = await Team.findOne({ ttl: userToSuspend._id, organization: org._id }) if (hasTeam) { - await Team.findOneAndReplace({ ttl: input.id }, { ttl: null }) + hasTeam.ttl = undefined + await hasTeam.save() await pushNotification( - context.userId, + user._id, `The TTL of ${hasTeam.name} has been suspended. Please assign a new TTL.`, - context.userId + user._id, + org._id ) } } - // Suspend user by updating their status - await User.findByIdAndUpdate(input.id, { - status: { status: 'suspended' }, - }) - + userData.status={ + status: USER_STATUS_ENUM.SUSPENDED, + reason: reason ? reason : 'Not specified', + date: new Date() + } + await userToSuspend.save() // Send suspension notification to the user await pushNotification( - input.id, + userToSuspend._id, 'Your account has been suspended and will no longer be able to access the system.', - input.id + user._id, + org._id ) - // Send confirmation notification to the requester/admin await pushNotification( - context.userId, - `You have successfully suspended the user with ID: ${input.id}.`, - context.userId + user._id, + `You have successfully suspended the user with email: ${userToSuspend.email}.`, + user._id, + org._id, ) - return { message: 'User suspended successfully' } + return { + message: 'User suspended successfully' + } }, - async updateUserRole(_: any, { id, name, orgToken }: any) { - const allRoles = [ - RoleOfUser.TRAINEE, - RoleOfUser.COORDINATOR, - RoleOfUser.MANAGER, - RoleOfUser.ADMIN, - RoleOfUser.SUPER_ADMIN, - RoleOfUser.TTL, - ] + async updateUserRole(_: any, { userId, role, orgToken }: {userId: string, role: RoleOfUser, orgToken: string}, context: Context) { const org = await checkLoggedInOrganization(orgToken) - const roleExists = allRoles.includes(name) - if (!roleExists) throw new Error("This role doesn't exist") - const userExists = await User.findById(id) - if (!userExists) throw new Error("User doesn't exist") - - const getAllUsers = await User.find({ - role: RoleOfUser.ADMIN, - }) - - let checkUserOrganization = 0 - - getAllUsers.forEach((user) => { - if (user.organizations.includes(org?.name || '')) { - checkUserOrganization++ - } - }) + const { user }= (await checkUserLoggedIn(org, context))([RoleOfUser.ADMIN, RoleOfUser.SUPER_ADMIN]) + if (!Object.values(RoleOfUser).includes(role)) { + throw new GraphQLError("This role doesn't exist", { + extensions: { + code: "FORBIDDEN" + } + }) + } + const userExists = await User.findById(userId) + if (!userExists){ + throw new GraphQLError("User doesn't exist",{ + extensions: { + code: "FORBIDDEN" + } + }) + } - if (checkUserOrganization == 1 && userExists.role == RoleOfUser.ADMIN) { - throw new Error('There must be at least one admin in the organization') + if(user._id.toString()===userExists._id.toString()){ + throw new GraphQLError("User cannot edit their own role",{ + extensions: { + code: "FORBIDDEN" + } + }) } - if (userExists.role == RoleOfUser.COORDINATOR) { - const userCohort: any = await Cohort.find({ - coordinator: userExists?.id, + const userData = isPartOfOrganization(userExists, org) + + if (userData.role == RoleOfUser.COORDINATOR) { + const cohorts = await Cohort.find({ + coordinator: userExists._id, + organization: org._id, }) - if (userCohort) { - await Cohort.updateMany( - { coordinator: userExists?.id }, - { - $set: { - coordinator: null, - }, - } - ) + for(const cohort of cohorts){ + cohort.coordinator = undefined + await cohort.save() } - } else if (userExists.role == RoleOfUser.MANAGER) { - const userProgram: any = await Program.find({ manager: userExists?.id }) - if (userProgram) { - await Program.updateMany( - { manager: userExists?.id }, - { - $set: { - manager: null, - }, - } - ) + } + if (userData.role == RoleOfUser.MANAGER) { + const userPrograms = await Program.find({ manager: userExists._id, organization: org._id }) + for(const userProgram of userPrograms) { + userProgram.manager = undefined + await userProgram.save() } - } else if (userExists.role == 'ttl') { - let teamttl: any = await Team.find({ ttl: userExists?.id }) + } + if (userData.role == RoleOfUser.TTL) { + const teamttl = await Team.findOne({ ttl: userExists._id, organization: org._id }) if (teamttl) { - await Team.updateMany( - { ttl: userExists?.id }, - { - $set: { - ttl: null, - }, - } - ) + teamttl.ttl = undefined + await teamttl.save() } - } else if (userExists.role == RoleOfUser.ADMIN) { - const userOrg: any = await Organization.find({ admin: userExists?.id }) - if (userOrg) { - await Organization.findByIdAndUpdate(userOrg.id, { - admin: userOrg[0].admin.filter( - (item: any) => item != userExists.id - ), - }) - } - } - if (name == RoleOfUser.ADMIN) { - org?.admin?.push(id) - org.save() + } + if (userData.role == RoleOfUser.ADMIN) { + const admins = org.admin.filter(admin=>admin.toString()!==userExists._id.toString()) + org.admin = admins + await org.save() } - const updatedUser = await User.findOneAndUpdate( - { - _id: id, - }, - { - $set: { - role: name, - }, - }, - { new: true } - ) - return updatedUser - }, - - async createUserRole(_: any, { name }: any) { - const newRole = await UserRole.create({ name }) - return newRole + userData.role = role + userExists.save() + return userExists }, //This section is to make org name login to be case insensitive - async loginOrg(_: any, { orgInput: { name } }: any) { + async loginOrg(_: any, { name }: { name: String }) { const organization: any = await Organization.findOne({ name: { $regex: new RegExp('^' + name + '$', 'i') }, }) - - if (organization) { - if ( - organization.status == Status.pending || - organization.status == Status.rejected - ) { - throw new GraphQLError('Your organization is not approved yet', { - extensions: { - code: 'UserInputError', - }, - }) - } - } - - if (organization) { - const token = generateTokenOrganization(organization.name) - const data = { - token: token, - organization: organization.toJSON(), - } - return data - } else { + if (!organization) { throw new GraphQLError( `We do not recognize this organization ${name}`, { extensions: { - code: 'UserInputError', + code: 'FORBIDDEN', }, } ) } + if (organization.status == ORG_STATUS.PENDING) { + throw new GraphQLError('Your organization is not approved yet', { + extensions: { + code: 'FORBIDDEN', + }, + }) + } + if (organization.status == ORG_STATUS.REJECTED) { + throw new GraphQLError('Your organization was not approved', { + extensions: { + code: 'FORBIDDEN', + }, + }) + } + const token = generateToken({ name: organization.name }, 7200) + const data = { + token: token, + organization: organization.toJSON(), + } + return data }, // end of making org name to be case insensitive async requestOrganization( _: any, - { organizationInput: { name, email, description } }: any + { organizationInput: { name, email, description } }: { organizationInput: {name: string, email: string, description: string}} ) { - try { + + validateEmail(email, "Please enter a valid email address") + validateStringField(name, "Please enter a valid organization name") + validateStringField(description, "Please enter a valid organization description") // Check if organization name already exists const orgExists = await Organization.findOne({ name }) if (orgExists) { throw new GraphQLError(`Organization name '${name}' already taken`, { extensions: { - code: 'UserInputError', + code: 'FORBIDDEN', }, }) } - - // Validate email format - const emailExpression = EmailPattern - const isValidEmail = emailExpression.test(String(email).toLowerCase()) - if (!isValidEmail) { - throw new GraphQLError('Invalid email format', { + // + const existingUser = await User.findOne({email}) + if(existingUser){ + throw new GraphQLError(`User ${existingUser.email} already exists, please use another email`,{ extensions: { - code: 'ValidationError', - }, + code: "FORBIDDEN" + } }) } - const existingUser = await User.findOne({ - email, - role: { $ne: RoleOfUser.ADMIN }, + // Create the organization with 'pending' status + const newOrg =await Organization.create({ + name, + description, + status: ORG_STATUS.PENDING, }) - const admin = await User.findOne({ email, role: RoleOfUser.ADMIN }) - if (existingUser) { - throw new GraphQLError( - `User with email '${email}' exists and is not an admin. Please use another email.`, - { - extensions: { - code: 'UserInputError', - }, - } - ) - } - if (admin) - throw new GraphQLError( - `User with ${email} exists. Please use another email`, - { - extensions: { - code: 'UserInputError', - }, - } - ) + //Create admin and assign them to the requested organization const password: any = generateRandomPassword() - let newAdmin: any = undefined - if (!admin) { - newAdmin = await User.create({ + const admin = await User.create({ email: email, password: password, - role: RoleOfUser.ADMIN, - organizations: name, - }) - - // Create the organization with 'pending' status -const {name:nm,admin:adm,description:desc}=await Organization.create({ - admin: newAdmin._id, - name, - description, - status: 'pending', - }) - const newOrgToken=genericToken({nm,adm,desc,email}) - - const superAdmin = await User.find({ role: RoleOfUser.SUPER_ADMIN }) - // Get the email content - const link = process.env.FRONTEND_LINK ?? "" - const content = registrationRequest(email, name, description,link,newOrgToken) - - - // Send registration request email to super admin - await sendEmail( - superAdmin[0].email, - 'Organization registration request', - content, - link, - process.env.ADMIN_EMAIL, - process.env.ADMIN_PASS - ) + organizations: [{ + orgId: newOrg._id, + role: RoleOfUser.ADMIN + }] + }) - return 'Organization registration request sent successfully' + const profile = await Profile.create({ + orgId: newOrg._id, + user: admin._id, + }) + admin.organizations[0].profile = profile._id + await admin.save() + + // add admin to new organization + newOrg.admin = [admin._id] + await newOrg.save() + await newOrg.populate('admin') + + //add new org to all superAdmins + const superAdmins = await User.find({ + organizations: { + $elemMatch: { role: RoleOfUser.SUPER_ADMIN } } - } catch (error) { - throw error + }) + if (!superAdmins.length) { + throw new GraphQLError("Server Error", { + extensions: { + code: "SERVER_ERROR" + } + }) + } + for (const superAdmin of superAdmins) { + const profile = await Profile.create({ + orgId: newOrg._id, + user: superAdmin._id, + }) + superAdmin.organizations.push({ + orgId: newOrg._id, + role: RoleOfUser.SUPER_ADMIN, + profile: profile._id + }) + await superAdmin.save() + } + + const newOrgToken = generateToken({ + name: newOrg.name, + admin: newOrg.admin[0], + description: newOrg.description, + email: admin.email + }, 7200) + + // Get the email content + const link = process.env.FRONTEND_LINK ?? "" + const content = registrationRequest(email, name, description, link, newOrgToken) + + // Send registration request email to super admin + for(const superAdmin of superAdmins){ + await sendEmail( + superAdmin.email, + 'Organization registration request', + content, + link, + process.env.ADMIN_EMAIL, + process.env.ADMIN_PASS + ) + } + + return { + message: 'Organization registration request sent successfully', + org: newOrg.toJSON() } }, async RegisterNewOrganization( _: any, - { organizationInput: { name, email }, action }: any, + { name, action , orgToken}: { name: string, action: ACTION_ENUM, orgToken: string}, context: Context ) { + validateStringField(name, "Please enter a valid organization name") + const org = await checkLoggedInOrganization(orgToken) // check if requester is super admin - ;(await checkUserLoggedIn(context))([RoleOfUser.SUPER_ADMIN]) - const orgExists = await Organization.findOne({ name: name }) - if (action == 'approve') { - if (!orgExists) { - throw new GraphQLError('Organization Not found ', { + const user = (await checkUserLoggedIn(org, context))([RoleOfUser.SUPER_ADMIN]) + const orgExists = await Organization.findOne({ name: name }).populate('admin') + if(!orgExists){ + throw new GraphQLError("No such organization found",{ + extensions: { + code: "ORG_NOT_FOUND" + } + }) + } + if(action === ACTION_ENUM.APPROVE){ + if(orgExists.status === ORG_STATUS.ACTIVE){ + throw new GraphQLError("Organization is already active",{ extensions: { - code: 'UserInputError', - }, + code: "FORBIDDEN" + } }) } - const adminUser = await User.findOne({ _id: orgExists.admin[0] }) - if (!adminUser || adminUser.email !== email) { - throw new GraphQLError('Admin email does not match', { + const admin = (orgExists as any).admin[0] + const password = generateRandomPassword() + admin.password = hashSync(password, 10) + await admin.save() + orgExists.status = ORG_STATUS.ACTIVE + await orgExists.save() + await sendEmail( + admin.email, + 'Organization Approved and created notice', + organizationApprovedTemplate( + orgExists.name, + admin.email, + password, + ), + process.env.FRONTEND_LINK || "link", + process.env.ADMIN_EMAIL, + process.env.ADMIN_PASS + ) + }else if(action === ACTION_ENUM.REJECT){ + if(orgExists.status === ORG_STATUS.REJECTED){ + throw new GraphQLError("Organization was already rejected",{ extensions: { - code: 'UserInputError', - }, + code: "FORBIDDEN" + } }) } - if (orgExists) { - const password: any = generateRandomPassword() - const adminID = orgExists.admin - const admin = await User.findOne({ _id: adminID }) - admin?.organizations.push(name) - - const hash = await bcrypt.hash(password, 10) - await User.updateOne({ email: email }, { $set: { password: hash } }) - - orgExists.status = 'active' - await orgExists.save() - - const content = organizationApprovedTemplate( - orgExists?.name as string, - email, - password - ) - const link: any = process.env.FRONTEND_LINK - await sendEmail( - email, - 'Organization Approved and created notice', - content, - link, - process.env.ADMIN_EMAIL, - process.env.ADMIN_PASS - ) - } - } - if (orgExists && action == 'reject') { - orgExists.status = 'rejected' + const admin = (orgExists as any).admin[0] + orgExists.status = ORG_STATUS.REJECTED await orgExists.save() - const content = organizationRejectedTemplate(name) - const link: any = process.env.FRONTEND_LINK await sendEmail( - email, + admin.email, 'Organization Request rejected notice', - content, - link, + organizationRejectedTemplate(orgExists.name), + process.env.FRONTEND_LINK || 'link', process.env.ADMIN_EMAIL, process.env.ADMIN_PASS ) + }else{ + throw new GraphQLError("Invalid action, Please approve or reject an organization",{ + extensions: { + code: "USER_INPUT_ERROR" + } + }) } - - return orgExists + return orgExists.toJSON() }, async addOrganization( _: any, - { organizationInput: { name, email, description }, action: action }: any, + { organizationInput: { name, email, description }, orgToken }: {organizationInput: { name: string, email: string, description: string}, orgToken: string}, context: Context ) { - // the below commented line help to know if the user is an superAdmin to perform an action of creating an organization - ;(await checkUserLoggedIn(context))([RoleOfUser.SUPER_ADMIN]) - if (action == 'new') { - const orgExists = await Organization.findOne({ name: name }) - if (orgExists) { - throw new GraphQLError('Organization Name already taken ' + name, { - extensions: { - code: 'UserInputError', - }, - }) - } - } + validateEmail(email, "Please enter a valid email address") + validateStringField(name, "Please enter a valid organization name") + validateStringField(description, "Please enter a valid organization description") - // check if the requester is already an admin, if not create him - const admin = await User.findOne({ email, role: RoleOfUser.ADMIN }) - // if (!admin) { - // console.log('admin exist') - // } - const password: any = generateRandomPassword() - let newAdmin: any = undefined - if (!admin) { - newAdmin = await User.create({ - email, - password, + const org = await checkLoggedInOrganization(orgToken) + ;(await checkUserLoggedIn(org, context))([RoleOfUser.SUPER_ADMIN]) + const orgExists = await Organization.findOne({ name: name }) + if (orgExists) { + throw new GraphQLError(`Organization Name ${orgExists.name} already taken`, { + extensions: { + code: 'FORBIDDEN', + }, + }) + } + const newOrg = await Organization.create({ + name, + description, + status: ORG_STATUS.ACTIVE + }) + const userExists = await User.findOne({email}) + if (userExists) { + const userExistsProfile = await Profile.create({ + user: userExists._id, + org: newOrg._id, + }) + userExists.organizations.push({ + orgId: org._id, role: RoleOfUser.ADMIN, + profile: userExistsProfile._id, }) + await userExists.save() + return newOrg.toJSON() } + const password: string = generateRandomPassword() - let org: any = await Organization.findOne({ admin: admin?._id }) + const admin = await User.create({ + email, + password: password, + organizations: [{ + orgId: newOrg._id, + role: RoleOfUser.ADMIN + }] + }) + const profile = await Profile.create({ + user: admin._id, + orgId: newOrg._id + }) - if (action == 'new') { - // create the organization - org = await Organization.create({ - admin: admin ? admin._id : newAdmin?._id, - name, - email, - description, - status: 'active', - }) - } - if (action !== 'new') { - const hash = await bcrypt.hash(password, 10) - await User.updateOne({ email: email }, { $set: { password: hash } }) - } + admin.organizations[0].profile = profile._id + await admin.save() + newOrg.admin = [admin._id] + await newOrg.save() + await newOrg.populate('admin') // send the requester an email with his password const content = organizationCreatedTemplate(org.name, email, password) @@ -988,140 +923,97 @@ const {name:nm,admin:adm,description:desc}=await Organization.create({ process.env.ADMIN_PASS ) - return org + return org.toJSON() }, async updateGithubOrganisation( _: any, - { name, gitHubOrganisation }: any, + { gitHubOrganisation, orgToken }: { gitHubOrganisation: string, orgToken: string}, context: Context ) { - ;(await checkUserLoggedIn(context))([ + validateStringField(gitHubOrganisation, "Please enter a valid organization githubOrganization") + const org = await checkLoggedInOrganization(orgToken) + ;(await checkUserLoggedIn(org, context))([ RoleOfUser.ADMIN, RoleOfUser.SUPER_ADMIN, ]) - - const org = await Organization.findOne({ name: name }) - if (!org) { - throw new GraphQLError('Organization Not found', { - extensions: { - code: 'UserInputError', - }, - }) - } - org.gitHubOrganisation = gitHubOrganisation await org.save() - - return { - admin: org.admin, - name: org.name, - description: org.description, - status: org.status, - gitHubOrganisation: org.gitHubOrganisation, - } + await org.populate('admin') + return org.toJSON() }, - async addActiveRepostoOrganization(_: any, { name, repoUrl }: any) { - const checkOrg = await Organization.findOne({ name: name }) - if (!checkOrg) { - throw new GraphQLError('Organization Not found', { - extensions: { - code: 'UserInputError', - }, - }) - } + async addActiveRepostoOrganization(_: any, { repoUrl, orgToken}: {repoUrl: string, orgToken: string}, context: Context) { + validateURLField(repoUrl, "Please provide a valid repoUrl") + const org = await checkLoggedInOrganization(orgToken) + ;(await checkUserLoggedIn(org, context))([RoleOfUser.ADMIN, RoleOfUser.SUPER_ADMIN]) - const allRepos = checkOrg.activeRepos + const allRepos = org.activeRepos if (allRepos.includes(repoUrl)) { throw new GraphQLError('Repository Already Exists', { extensions: { - code: 'UserInputError', + code: 'FORBIDDEN', }, }) } - checkOrg.activeRepos.push(repoUrl) - await checkOrg.save() + org.activeRepos.push(repoUrl) + await org.save() - return { - admin: checkOrg.admin, - name: checkOrg.name, - activeRepos: checkOrg.activeRepos, - description: checkOrg.description, - status: checkOrg.status, - } + return org.toJSON() }, - async deleteActiveRepostoOrganization(_: any, { name, repoUrl }: any) { - // const { userId } = (await checkUserLoggedIn(context))([RoleOfUser.ADMIN,RoleOfUser.SUPER_ADMIN]); - - const org = await Organization.findOne({ name: name }) - if (!org) { - throw new GraphQLError('Organization Not found', { - extensions: { - code: 'UserInputError', - }, - }) - } + async deleteActiveRepostoOrganization(_: any, { repoUrl, orgToken}: {repoUrl: string, orgToken: string}, context: Context) { + validateURLField(repoUrl, "Please provide a valid repo URL") + const org = await checkLoggedInOrganization(orgToken) + ;(await checkUserLoggedIn(org,context))([RoleOfUser.ADMIN,RoleOfUser.SUPER_ADMIN]); const allRepos = org.activeRepos if (!allRepos.includes(repoUrl)) { throw new GraphQLError('Repository Not Found', { extensions: { - code: 'UserInputError', + code: 'REPO_NOT_FOUND', }, }) } const index = allRepos.indexOf(repoUrl) - allRepos.splice(index, 1) await org.save() - return { - admin: org.admin, - name: org.name, - description: org.description, - status: org.status, - } + return org.toJSON() }, - async deleteOrganization(_: any, { id }: any, context: Context) { - ;(await checkUserLoggedIn(context))([ - RoleOfUser.ADMIN, + async deleteOrganization(_: any, { orgId, orgToken }: {orgId: string, orgToken: string}, context: Context) { + const org = await checkLoggedInOrganization(orgToken) + ;(await checkUserLoggedIn(org, context))([ RoleOfUser.SUPER_ADMIN, ]) - - const organizationExists = await Organization.findOne({ _id: id }) - - if (!organizationExists) - throw new Error("This Organization doesn't exist") - await Cohort.deleteMany({ organization: id }) - await Team.deleteMany({ organization: id }) - await Phase.deleteMany({ organization: id }) - await User.deleteMany({ - organizations: organizationExists.name, - role: { $ne: RoleOfUser.SUPER_ADMIN }, - }) - await User.deleteOne({ _id: organizationExists.admin[0] }) - const deleteOrg = await Organization.findOneAndDelete({ - _id: id, - }) - - if (!deleteOrg) - throw new Error( - 'Not deleted, something went wrong, please try again later' - ) - return deleteOrg + const organizationExists = await Organization.findById(orgId) + if (!organizationExists){ + throw new GraphQLError("This Organization doesn't exist",{ + extensions: { + code: "ORG_NOT_FOUND" + } + }) + } + organizationExists.isDeleted = true + await organizationExists.save() + return organizationExists.toJSON() }, - async forgotPassword(_: any, { email }: any) { + async forgotPassword(_: any, { email }: { email: string }) { + validateEmail(email, "Please provide a valid email") const userExists: any = await User.findOne({ email }) - - if (userExists) { - const token: any = generateTokenUserExists(email) + if (!userExists) { + throw new GraphQLError("No such user found",{ + extensions: { + code: "USER_NOT_FOUND" + } + }) + } + const token: any = generateToken({ email }, 7200) const newToken: any = token.replaceAll('.', '*') const webLink = `${process.env.FRONTEND_LINK}/forgot-password/${newToken}` const appLink = `${process.env.FRONTEND_LINK}/redirect?dest=app&path=/auth/reset-password&fallback=${webLink}&token=${newToken}` @@ -1136,144 +1028,156 @@ const {name:nm,admin:adm,description:desc}=await Organization.create({ process.env.ADMIN_PASS ) - return 'Check Your Email To Proceed!' - } else { - throw new Error('Something went wrong!\nCheck your credentials') - } + return { + message: 'Check Your Email To Proceed!' + } }, - async updateEmailNotifications(_: any, { id }: any, context: Context) { - const user: any = await User.findOne({ _id: id }) - if (!user) { - throw new Error('User not found') + async updateEmailNotifications(_: any, { orgToken }: { orgToken: string}, context: Context) { + const org = await checkLoggedInOrganization(orgToken) + const { user, orgUserData } = (await checkUserLoggedIn(org, context))(Object.values(RoleOfUser)) + orgUserData.emailNotifications = !orgUserData.emailNotifications + await user.save() + + return { + message: 'updated successfully' } - const updatedEmailNotifications = !user.emailNotifications - const updateEmailPreference = await User.updateOne( - { _id: id }, - { emailNotifications: updatedEmailNotifications } - ) - return 'updated successful' }, - async updatePushNotifications(_: any, { id }: any, context: Context) { - const user: any = await User.findOne({ _id: id }) - if (!user) { - throw new Error('User not found') - } - user.pushNotifications = !user.pushNotifications + async updatePushNotifications(_: any, { orgToken}: {orgToken: string}, context: Context) { + const org = await checkLoggedInOrganization(orgToken) + const { user, orgUserData } = (await checkUserLoggedIn(org, context))(Object.values(RoleOfUser)) + orgUserData.pushNotifications = !orgUserData.pushNotifications await user.save() - const updatedPushNotifications = user.pushNotifications - return 'updated successful' + return { + message: 'updated successfully' + } }, async resetUserPassword( _: any, - { password, confirmPassword, token }: any, - context: any + { password, confirmPassword, resetToken }: { password: string, confirmPassword: string, resetToken: string} ) { - const { email } = verify(token, SECRET) as JwtPayload - if (password === confirmPassword) { - const user: any = await User.findOne({ email }) + validatePasswordField(password, "Please enter a valid new password") + validatePasswordField(confirmPassword, "Please enter a valid confirmation password") + const { email } = verify(resetToken, SECRET) as JwtPayload + if (password !== confirmPassword) { + throw new GraphQLError("Passwords do not match",{ + extensions: { + code: "USER_INPUT_ERROR" + } + }) + } + const user = await User.findOne({ email }) if (!user) { - throw new Error("User doesn't exist! ") + throw new GraphQLError("User doesn't exist! ",{ + extensions: { + code: "USER_NOT_FOUND" + } + }) } user.password = password await user.save() - return 'Your password was reset successfully! ' - } else if (password !== confirmPassword) { - throw new Error('Password mismatch! ') - } else { - throw new Error('Oopps! something went wrong') - } + + return { + message: 'Your password was reset successfully! ' + } }, async changeUserPassword( _: any, - { currentPassword, newPassword, confirmPassword, token }: any, - context: any + { currentPassword, newPassword, confirmPassword, orgToken }: { currentPassword: string, newPassword: string, confirmPassword: string, orgToken: string}, + context: Context ) { - const { userId } = verify(token, SECRET) as JwtPayload - if (newPassword === confirmPassword) { - const user: any = await User.findById(userId) - if (!user) { - throw new Error("User doesn't exist! ") - } - - if (bcrypt.compareSync(currentPassword, user.password)) { - user.password = newPassword - await user.save() - return 'Your password was reset successfully! ' - } else { - throw new Error('Current Password is incorrect') - } - } else if (newPassword !== confirmPassword) { - throw new Error('New password mismatch!') - } else { - throw new Error('Oopps! something went wrong') + validatePasswordField(currentPassword, "Please enter a valid current password") + validatePasswordField(newPassword, "Please enter a valid new password") + validatePasswordField(confirmPassword, "Please enter a valid confirmation password") + const org = await checkLoggedInOrganization(orgToken) + const { user } = (await checkUserLoggedIn(org, context))(Object.values(RoleOfUser)) + if (newPassword !== confirmPassword) { + throw new GraphQLError("Passwords do not match",{ + extensions: { + code: "USER_INPUT_ERROR" + } + }) } - }, - }, - User: { - async profile(parent: any) { - const profile = await Profile.findOne({ user: parent.id.toString() }) - if (!profile) { - return null - } else { - return profile + if (!compareSync(currentPassword, user.password)) { + throw new GraphQLError("Incorrect credentials", { + extensions: { + code: "FORBIDDEN" + } + }) } - }, - async program(parent: any) { - const program = await Program.findOne({ manager: parent.id.toString() }) - if (!program) { - return null - } else { - return program + user.password = newPassword + await user.save() + return { + message: 'Your password was reset successfully! ' } + } }, - async cohort(parent: any) { - const cohort = await Cohort.findById(parent.cohort.toString()) + } - if (!cohort) { - return null - } else { - return cohort - } - }, - async team(parent: UserType) { - const team = await Team.findOne({ members: parent._id }) - if (!team) { - return null - } else { - return team - } - }, - async ratings(parent: UserType) { - const ratings = await Rating.find({ user: parent._id }).populate([ - 'user', - 'cohort', - { - path: 'feedbacks', - populate: 'sender', - }, - ]) - if (!ratings) { - return null - } else { - return ratings - } - }, - }, - Profile: { - async user(parent: any) { - const user = await User.findOne({ _id: parent.user.toString() }) - if (!user) return null - return user - }, - }, - Organization: { - async admin(parent: any) { - return User.findById(parent.admin) - }, - }, -} +// User: { +// async profile(parent: any) { +// const profile = await Profile.findOne({ user: parent.id.toString() }) +// if (!profile) { +// return null +// } else { +// return profile +// } +// }, +// async program(parent: any) { +// const program = await Program.findOne({ manager: parent.id.toString() }) +// if (!program) { +// return null +// } else { +// return program +// } +// }, +// async cohort(parent: any) { +// const cohort = await Cohort.findById(parent.cohort.toString()) + +// if (!cohort) { +// return null +// } else { +// return cohort +// } +// }, +// async team(parent: UserType) { +// const team = await Team.findOne({ members: parent._id }) +// if (!team) { +// return null +// } else { +// return team +// } +// }, +// async ratings(parent: UserType) { +// const ratings = await Rating.find({ user: parent._id }).populate([ +// 'user', +// 'cohort', +// { +// path: 'feedbacks', +// populate: 'sender', +// }, +// ]) +// if (!ratings) { +// return null +// } else { +// return ratings +// } +// }, +// }, +// Profile: { +// async user(parent: any) { +// const user = await User.findOne({ _id: parent.user.toString() }) +// if (!user) return null +// return user +// }, +// }, +// Organization: { +// async admin(parent: any) { +// return User.findById(parent.admin) +// }, +// }, +//} export default resolvers diff --git a/src/schema/index.ts b/src/schema/index.ts index d779d76e..47c64c7d 100644 --- a/src/schema/index.ts +++ b/src/schema/index.ts @@ -85,26 +85,6 @@ const Schema = gql` password: String! role: String } - input LoginInput { - email: String - password: String - orgToken: String - } - input OrgInput { - name: String - } - - input DeleteUserInput { - id: ID! - } - - type DeleteUserPayload { - message: String! - } - - type Mutation { - deleteUser(input: DeleteUserInput!): DeleteUserPayload! - } type Profile { id: ID! @@ -134,24 +114,12 @@ const Schema = gql` id: ID! name: String! } - type RegisteredUser { - token: String - user: User - } - type Login { - token: String - user: User - } - type OrgLogin { - token: String - organization: Organization - } type Organization { id: ID! name: String! description: String - admin: User + admin: [User]! status: String gitHubOrganisation: String activeRepos: [String] @@ -172,23 +140,6 @@ const Schema = gql` emailNotifications: Boolean! } - type GitHubActivity { - totalCommits: String! - pullRequest: pullRequest! - } - - type pullRequest { - merged: String! - closed: String! - opened: String! - } - - input OrganizationInput { - email: String! - name: String! - description: String - } - type Rating { id: ID! user: User! @@ -275,9 +226,6 @@ const Schema = gql` getProfile: Profile getAllRoles: [UserRole] getRole(id: ID!): UserRole - getOrganizations: [Organization]! - getOrganization(name: String!): Organization - getSignupOrganization(orgToken: String!): Organization fetchRatings(orgToken: String): [Rating] fetchTrainees: [Cohort] fetchRatingsForAdmin(orgToken: String): [FetchRatingForAdmin] @@ -289,7 +237,7 @@ const Schema = gql` getTTLTeams(orgToken: String): [Team!]! getAllTeams(orgToken: String): [Team!]! getAllTeamInCohort(orgToken: String, cohort: String): [Team!] - gitHubActivity(organisation: String!, username: String!): GitHubActivity! + gitHubActivity(orgToken: String!): GitHubActivity! getRatingsByCohort(cohortId: String!, orgToken: String!): [Rating]! getTeamsByCohort(cohortId: String!,orgToken: String!): [Team]! } @@ -308,45 +256,15 @@ const Schema = gql` RejectedRatings: [RejectedRows]! } + type Message{ + message: String! + } + type Mutation { createUserRole(name: String!): UserRole! uploadResume(userId: ID!, resume: String!): Profile dropTTLUser(email: String!, reason: String!): String! undropTTLUser(email: String!): String! - createUser( - firstName: String! - lastName: String! - dateOfBirth: DateTime! - gender: String! - email: String! - password: String! - orgToken: String! - role: String - ): RegisteredUser! - loginUser(loginInput: LoginInput): Login! - loginOrg(orgInput: OrgInput): OrgLogin! - requestOrganization(organizationInput: OrganizationInput!): String! - addOrganization( - organizationInput: OrganizationInput - action: String - ): Organization! - RegisterNewOrganization( - organizationInput: OrganizationInput - action: String - ): Organization! - updateProfile( - lastName: String - firstName: String - address: String - city: String - country: String - phoneNumber: String - biography: String - fileName: String - cover: String - githubUsername: String - ): Profile - createProfile( lastName: String firstName: String @@ -361,12 +279,6 @@ const Schema = gql` ): Profile updateAvatar(avatar: String): Profile updateCoverImage(cover: String): Profile - updateUserRole(id: ID!, name: String, orgToken: String): User! - deleteOrganization(id: ID!): Organization - updateGithubOrganisation( - name: String! - gitHubOrganisation: String! - ): Organization addRatings( user: String! sprint: Int! @@ -404,28 +316,8 @@ const Schema = gql` user: String content: String ): RatingMessageTemp - approveRating(user: String!, sprint: Int!): ApproveRating rejectRating(user: String!, sprint: Int!): String! - forgotPassword(email: String!): String! - resetUserPassword( - password: String! - confirmPassword: String! - token: String! - ): String! - changeUserPassword( - currentPassword: String! - newPassword: String! - confirmPassword: String! - token: String! - ): String! - - addActiveRepostoOrganization(name: String!, repoUrl: String!): Organization! - - deleteActiveRepostoOrganization( - name: String! - repoUrl: String! - ): Organization! addTeam( name: String! cohortName: String! @@ -564,10 +456,6 @@ const Schema = gql` description: String! ): Documentation! } - type Mutation { - updatePushNotifications(id: ID!): String - updateEmailNotifications(id: ID!): String - } type Query { getUpdatedEmailNotifications(id: ID!): Boolean! getUpdatedPushNotifications(id: ID!): Boolean! diff --git a/src/schema/invitationStatics.schema.ts b/src/schema/invitationStatics.schema.ts index 6417ea62..bda88ffc 100644 --- a/src/schema/invitationStatics.schema.ts +++ b/src/schema/invitationStatics.schema.ts @@ -1,4 +1,4 @@ -import { gql } from 'apollo-server' +import gql from 'graphql-tag' const statisticsSchema = gql` type Statistics { diff --git a/src/schema/user.schema.ts b/src/schema/user.schema.ts new file mode 100644 index 00000000..c4d48720 --- /dev/null +++ b/src/schema/user.schema.ts @@ -0,0 +1,148 @@ +import gql from "graphql-tag"; + +const userSchema = gql` + +type PullRequest { + merged: String! + closed: String! + opened: String! +} + +type GitHubActivity { + totalCommits: String! + pullRequest: PullRequest! +} + +type RegisteredUser { + token: String + user: User + } + +type Query { + getOrganizations(orgToken: String!): [Organization]! + getOrganization(name: String!, orgToken: String!): Organization + getCurrentOrganization(orgToken: String!): Organization + gitHubActivity(orgToken: String!): GitHubActivity! +} + +type OrgLogin { + token: String + organization: Organization +} + +type Login { + token: String! + user: User! + geoData: Activity! +} + +input OrganizationInput { + email: String! + name: String! + description: String! +} + +type RequestedOrganization{ + message: String!, + org: Organization! +} + +type Mutation { + createUser( + firstName: String! + lastName: String! + dateOfBirth: DateTime! + gender: String! + email: String! + password: String! + orgToken: String! + role: String + ): RegisteredUser! + + addUserToOrganization( + invitationToken: String! + ): User! + + updateProfile( + biography: String + cover: String + avatar: String + githubUsername: String + resume: String + orgToken: String! + ): Profile + + loginUser( + email: String + password: String + orgToken: String + ): Login! + + deleteUser( + userId: String! + reason: String + orgToken: String! + ): Message! + + updateUserRole( + userId: ID!, + role: String!, + orgToken: String! + ): User! + + loginOrg(name: String!): OrgLogin! + + requestOrganization(organizationInput: OrganizationInput!): RequestedOrganization! + + RegisterNewOrganization( + name: String!, + action: String!, + orgToken: String!, + ): Organization! + + addOrganization( + organizationInput: OrganizationInput + orgToken: String! + ): Organization! + + updateGithubOrganisation( + gitHubOrganisation: String! + orgToken: String! + ): Organization + + deleteOrganization( + orgId: ID! + orgToken: String! + ): Organization + + addActiveRepostoOrganization( + repoUrl: String! + orgToken: String! + ): Organization! + + deleteActiveRepostoOrganization( + repoUrl: String! + orgToken: String! + ): Organization! + + updatePushNotifications(orgToken: String!): Message! + updateEmailNotifications(orgToken: String!): Message! + + forgotPassword(email: String!): Message! + + resetUserPassword( + password: String! + confirmPassword: String! + resetToken: String! + ): Message! + + changeUserPassword( + currentPassword: String! + newPassword: String! + confirmPassword: String! + orgToken: String! + ): Message! +} + +` +export default userSchema \ No newline at end of file diff --git a/src/seeders/attendance.seed.ts b/src/seeders/attendance.seed.ts index a77125fb..2e51cbd7 100644 --- a/src/seeders/attendance.seed.ts +++ b/src/seeders/attendance.seed.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -import { RoleOfUser, User } from '../models/user' +import User, { RoleOfUser } from '../models/user' import { Attendance } from '../models/attendance.model' const seedAttendance = async () => { diff --git a/src/seeders/cohorts.seed.ts b/src/seeders/cohorts.seed.ts index 32dba16e..a5fbf3f8 100644 --- a/src/seeders/cohorts.seed.ts +++ b/src/seeders/cohorts.seed.ts @@ -1,105 +1,116 @@ import Cohort from '../models/cohort.model' import Phase from '../models/phase.model' import Program from '../models/program.model' -import { RoleOfUser, User } from '../models/user' +import User,{ RoleOfUser } from '../models/user' import { Organization } from '../models/organization.model' const seedCohorts = async () => { // Organization - const andelaOrg = await Organization.find({ name: 'Andela' }) - const iremboOrg = await Organization.find({ name: 'Irembo' }) + const andelaOrg = await Organization.findOne({ name: 'Andela' }) + const iremboOrg = await Organization.findOne({ name: 'Irembo' }) // Programs const andelaPrograms = await Program.find({ - $or: [{ name: { $eq: 'Atlp 1' } }, { name: { $eq: 'Atlp 2' } }], + organization: andelaOrg?._id, }) const iremboProgams = await Program.find({ - $or: [ - { name: { $eq: 'Brainly Developers Program' } }, - { name: { $eq: 'Rwema' } }, - ], + organization: iremboOrg?._id, }) // Phases - const phases = await Phase.find() + const andelaPhases = await Phase.find({ + organization: andelaOrg?._id, + }) + + const iremboPhases = await Phase.find({ + organization: andelaOrg?._id, + }) // Coordinators - const andelCoord = await User.find({ - role: RoleOfUser.COORDINATOR, - organizations: { $in: ['Andela'] }, + const andelaCoord = await User.find({ + organizations: { + $elemMatch: { + orgId: andelaOrg?._id, + role: RoleOfUser.COORDINATOR, + } + }, }) const iremboCoord = await User.find({ - role: RoleOfUser.COORDINATOR, - organizations: { $in: ['Irembo'] }, + organizations: { + $elemMatch: { + orgId: iremboOrg?._id, + role: RoleOfUser.COORDINATOR, + } + }, }) const cohorts = [ // Andela { name: 'cohort 1', - phase: phases[0]._id.toHexString(), - coordinator: andelCoord[0]._id.toHexString(), + phase: andelaPhases[0]._id.toHexString(), + coordinator: andelaCoord[0]._id.toHexString(), program: andelaPrograms[0]._id.toHexString(), teams: 1, active: true, startDate: new Date(), endDate: new Date(), - organization: andelaOrg[0]._id.toHexString(), + organization: andelaOrg?._id.toHexString(), }, { name: 'cohort 2', - phase: phases[1]._id.toHexString(), - coordinator: andelCoord[0]._id.toHexString(), + phase: andelaPhases[1]._id.toHexString(), + coordinator: andelaCoord[0]._id.toHexString(), program: andelaPrograms[0]._id.toHexString(), teams: 1, active: true, startDate: new Date(), endDate: new Date(), - organization: andelaOrg[0]._id.toHexString(), + organization: andelaOrg?._id.toHexString(), }, { name: 'Ndevu 12', - phase: phases[0]._id.toHexString(), - coordinator: andelCoord[0]._id.toHexString(), + phase: andelaPhases[0]._id.toHexString(), + coordinator: andelaCoord[0]._id.toHexString(), program: andelaPrograms[0]._id.toHexString(), teams: 1, active: true, startDate: new Date(), endDate: new Date(), - organization: andelaOrg[0]._id.toHexString(), + organization: andelaOrg?._id.toHexString(), }, { name: 'Ndevu Project', - phase: phases[1]._id.toHexString(), - coordinator: andelCoord[0]._id.toHexString(), + phase: andelaPhases[1]._id.toHexString(), + coordinator: andelaCoord[0]._id.toHexString(), program: andelaPrograms[0]._id.toHexString(), teams: 1, active: true, startDate: new Date(), endDate: new Date(), - organization: andelaOrg[0]._id.toHexString(), + organization: andelaOrg?._id.toHexString(), }, // Irembo { name: 'cohort 1', - phase: phases[1]._id.toHexString(), + phase: iremboPhases[0]._id.toHexString(), coordinator: iremboCoord[0]._id.toHexString(), program: iremboProgams[0]._id.toHexString(), active: true, startDate: new Date(), endDate: new Date(), - organization: iremboOrg[0]._id.toHexString(), + organization: iremboOrg?._id.toHexString(), }, { name: 'cohort 2', - phase: phases[2]._id.toHexString(), + phase: iremboPhases[1]._id.toHexString(), coordinator: iremboCoord[0]._id.toHexString(), program: iremboProgams[1]._id.toHexString(), startDate: new Date(), endDate: new Date(), - organization: iremboOrg[0]._id.toHexString(), + organization: iremboOrg?._id.toHexString(), }, ] diff --git a/src/seeders/index.ts b/src/seeders/index.ts index 57109b71..feb8625f 100644 --- a/src/seeders/index.ts +++ b/src/seeders/index.ts @@ -1,3 +1,7 @@ +import mongoose from 'mongoose' +import { mongooseSoftDelete } from '../plugins/mongooseSoftDelete' +mongoose.plugin(mongooseSoftDelete) + import { connect } from './../database/db.config' import logger from '../utils/logger.utils' import seedCohorts from './cohorts.seed' @@ -15,8 +19,8 @@ import seedTickets from './ticket.seed' connect().then(async () => { try { await seedUsers() - await seedTickets() await seedOrganizations() + await seedTickets() await seedPrograms() await seedPhases() await seedCohorts() @@ -24,7 +28,7 @@ connect().then(async () => { await seedNotification() await seedsystemRatings() await seedRatings() - // await seedAttendance() + //await seedAttendance() logger.info('Database seeded Successfully') process.exit() diff --git a/src/seeders/organization.seed.ts b/src/seeders/organization.seed.ts index 6796112d..e6b1eb46 100644 --- a/src/seeders/organization.seed.ts +++ b/src/seeders/organization.seed.ts @@ -1,36 +1,88 @@ /* eslint-disable */ -import { RoleOfUser, User } from '../models/user' -import { Organization } from '../models/organization.model' +import User, { RoleOfUser } from '../models/user' +import { ORG_STATUS, Organization } from '../models/organization.model' +import { Profile } from '../models/profile.model' +import { users as dbUsers } from './users.seed' + +type UserRole={ + admin: number, + manager: number, + coordinator: number, + trainee: number, + user: number, + ttl: number +} + +const usersTypes: UserRole = { + admin: 1, + manager: 1, + coordinator: 1, + trainee: 2, + user: 2, + ttl: 2, +} const seedOrganizations = async () => { - const andelaAdmins = await User.find({ - role: RoleOfUser.ADMIN, - organizations: { $in: ['Andela'] }, - }) - const IremboAdmins = await User.find({ - role: RoleOfUser.ADMIN, - organizations: { $in: ['Irembo'] }, - }) + try { + await Organization.deleteMany({}) + const users = await User.find() + const organizations = [ + { + name: 'Andela', + description: + 'Master the professional and technical skills needed to accelerate your career and use technology to change the world.', + gitHubOrganisation: 'atlp-rwanda', + activeRepos: ['atlp-pulse-bn', 'atlp-pulse-fn'], + status: ORG_STATUS.ACTIVE, + }, + { + name: 'Irembo', + description: 'Organization 2 description', + status: ORG_STATUS.ACTIVE, + }, + ] + const orgs = await Organization.insertMany(organizations) + let userCount = 1 + for (const org of orgs) { + for (const role in usersTypes) { + for (const user of users.slice(userCount, userCount+usersTypes[role as keyof UserRole])) { + const profile = await Profile.create({ + user: user._id, + githubUserName: dbUsers.find(dbUser => dbUser.email === user.email).githubUserName || 'unavailable', + orgId: org._id, + }) + user.organizations.push({ + orgId: org._id, + role: role, + profile: profile?._id + }) + await user.save() + if(role === RoleOfUser.ADMIN){ + org.admin.push(user.id) + await org.save() + } + } + userCount+=usersTypes[role as keyof UserRole] + } + } - const organizations = [ - { - name: 'Andela', - description: - 'Master the professional and technical skills needed to accelerate your career and use technology to change the world.', - admin: [...andelaAdmins.map((admin) => admin._id.toHexString())], - gitHubOrganisation: 'atlp-rwanda', - activeRepos: ['atlp-pulse-bn', 'atlp-pulse-fn'], - }, - { - name: 'Irembo', - description: 'Organization 2 description', - admin: [...IremboAdmins.map((admin) => admin._id.toHexString())], - }, - ] + // add super admin to all organizations + for(const org of orgs){ + const profile = await Profile.create({ + user: users[0]._id, + orgId: org._id, + }) - await Organization.deleteMany({}) - await Organization.insertMany(organizations) - return null + users[0].organizations.push({ + orgId: org._id, + role: RoleOfUser.SUPER_ADMIN, + profile: profile._id, + }) + } + await users[0].save() + } catch (err: any) { + console.log(err) + } } export default seedOrganizations diff --git a/src/seeders/phases.seed.ts b/src/seeders/phases.seed.ts index 00bf946c..65843393 100644 --- a/src/seeders/phases.seed.ts +++ b/src/seeders/phases.seed.ts @@ -2,26 +2,27 @@ import Phase from '../models/phase.model' import { Organization } from '../models/organization.model' const seedPhases = async () => { + const organizations = await Organization.find() const phases = [ { name: 'Phase I', description: 'Core concept', - organization: (await Organization.find())[0]?.id, + organization: organizations[0]?.id, }, { name: 'Phase II', description: 'Team project', - organization: (await Organization.find())[0]?.id, + organization: organizations[0]?.id, }, { name: 'Phase I', description: 'Core Concept phase', - organization: (await Organization.find())[1]?.id, + organization: organizations[1]?.id, }, { name: 'Phase II', description: 'Team project phase', - organization: (await Organization.find())[1]?.id, + organization: organizations[1]?.id, }, ] diff --git a/src/seeders/programs.seed.ts b/src/seeders/programs.seed.ts index 35202ba0..90891c4a 100644 --- a/src/seeders/programs.seed.ts +++ b/src/seeders/programs.seed.ts @@ -1,33 +1,41 @@ import Program from '../models/program.model' -import { RoleOfUser, User } from '../models/user' +import User, { RoleOfUser } from '../models/user' import { Organization } from '../models/organization.model' const seedPrograms = async () => { + const andelaOrg = await Organization.findOne({ name: 'Andela' }) + const iremboOrg = await Organization.findOne({ name: 'Irembo' }) const andelaManagers = await User.find({ - role: RoleOfUser.MANAGER, - organizations: { $in: ['Andela'] }, + organizations: { + $elemMatch: { + orgId: andelaOrg?._id, + role: RoleOfUser.MANAGER, + } + }, }) const IremboManagers = await User.find({ - role: RoleOfUser.MANAGER, - organizations: { $in: ['Irembo'] }, + organizations: { + $elemMatch: { + orgId: iremboOrg?._id, + role: RoleOfUser.MANAGER, + } + }, }) - const andelaOrg = await Organization.find({ name: 'Andela' }) - const iremboOrg = await Organization.find({ name: 'Irembo' }) const programs = [ // Andela { name: 'Atlp 1', description: 'none', manager: andelaManagers[0]._id.toHexString(), - organization: andelaOrg[0]._id.toHexString(), + organization: andelaOrg?._id.toHexString(), }, { name: 'Atlp 2', description: 'none', manager: andelaManagers[0]._id.toHexString(), - organization: andelaOrg[0]._id.toHexString(), + organization: andelaOrg?._id.toHexString(), }, // Irembo @@ -35,14 +43,14 @@ const seedPrograms = async () => { name: 'Brainly Developers Program', description: 'This belong to cohort 7', manager: IremboManagers[0]._id.toHexString(), - organization: iremboOrg[0]._id.toHexString(), + organization: iremboOrg?._id.toHexString(), }, { name: 'Rwema', description: 'none', manager: IremboManagers[0]._id.toHexString(), - organization: iremboOrg[0]._id.toHexString(), + organization: iremboOrg?._id.toHexString(), }, ] diff --git a/src/seeders/ticket.seed.ts b/src/seeders/ticket.seed.ts index 92c84948..f475cf55 100644 --- a/src/seeders/ticket.seed.ts +++ b/src/seeders/ticket.seed.ts @@ -1,5 +1,5 @@ import Ticket from '../models/ticket.model' -import { RoleOfUser, User } from '../models/user' +import User, { RoleOfUser } from '../models/user' const generateSubject = (userId: string): string => { const subjects = [ @@ -30,12 +30,22 @@ const generateMessage = (userId: string): string => { const seedTickets = async (): Promise => { try { await Ticket.deleteMany({}) - const assignees = await User.find({ role: 'user' }).select('_id') + const assignees = await User.find({ + organizations: { + $elemMatch: { + role: 'user', + } + }, + }).select('_id') if (assignees.length === 0) { throw new Error('No assignees found with the role "user".') } const user = await User.findOne({ - role: { $in: [RoleOfUser.ADMIN, RoleOfUser.COORDINATOR] }, + organizations: { + $elemMatch: { + role: {$in: [RoleOfUser.ADMIN, RoleOfUser.COORDINATOR]}, + } + }, }).select('_id') if (!user) { throw new Error('No user found with the role "admin" or "coordinator".') diff --git a/src/seeders/userRoles.seed.ts b/src/seeders/userRoles.seed.ts deleted file mode 100644 index 0fe639ad..00000000 --- a/src/seeders/userRoles.seed.ts +++ /dev/null @@ -1,35 +0,0 @@ -import mongoose from 'mongoose' -import { RoleOfUser, UserRole } from '../models/user' - -// Create seed data for user roles with explicit IDs -const userRolesSeed = [ - { - name: RoleOfUser.SUPER_ADMIN, - }, - { - name: RoleOfUser.ADMIN, - }, - { - name: RoleOfUser.COORDINATOR, - }, - { - name: RoleOfUser.TTL, - }, - { - name: RoleOfUser.MANAGER, - }, - { - name: RoleOfUser.TRAINEE, - }, -] - -async function seedUserRoles() { - try { - //Clear Existing data in the collection before seeding (Optional) - await UserRole.deleteMany({}) - const roles = await UserRole.insertMany(userRolesSeed) - } catch (err) { - console.error('Error seeding user Roles:', err) - } -} -export default seedUserRoles diff --git a/src/seeders/users.seed.ts b/src/seeders/users.seed.ts index 659e56d1..4c9fb43c 100644 --- a/src/seeders/users.seed.ts +++ b/src/seeders/users.seed.ts @@ -1,5 +1,5 @@ import { hashSync } from 'bcryptjs' -import { RoleOfUser, User } from '../models/user' +import User, { RoleOfUser } from '../models/user' import { Profile } from '../models/profile.model' const organizations: any = { @@ -7,6 +7,87 @@ const organizations: any = { Irembo: [], } +export const users: Array = [ + { + firstName: 'ATLP', + lastName: 'Devpulse', + email: 'devpulse@proton.me', + githubUserName: 'atlp-rwanda', + }, + { + firstName: 'Muhawenimana', + lastName: 'Lydia', + email: 'gatarelydie370@gmail.com', + githubUserName: '', + }, + { + firstName: 'paciFique', + lastName: 'Mbonimana', + email: 'pacifiquemboni123@gmail.com', + githubUserName: '', + }, + { + firstName: 'Muheto', + lastName: 'Darius', + email: 'muhedarius@gmail.com', + githubUserName: '', + }, + { + firstName: 'Kagabo', + lastName: 'Darius', + email: 'kagabodarius@gmail.com', + githubUserName: '', + }, + { + firstName: 'Ndayambaje', + lastName: 'Virgile', + email: 'ndayambajevgschooling@gmail.com', + githubUserName: '', + }, + { + firstName: 'NDATIMANA', + lastName: 'Samuel', + email: 'ndatimanasamuel1@gmail.com', + githubUserName: 'blackd44', + }, + { + firstName: 'Ken', + lastName: 'Mugisha', + email: 'keneon2003@gmail.com', + githubUserName: '', + }, + { + firstName: 'Kevin', + lastName: 'Rukundo', + email: 'kevinrukundo1@gmail.com', + githubUserName: '', + }, + { + firstName: 'Patrick', + lastName: 'Mugwaneza', + email: 'mugwanezapatrick6@gmail.com', + githubUserName: '', + }, + { + firstName: 'Elissa', + lastName: 'NTIHINDUKA', + email: 'ntihindukaelissa77@gmail.com', + githubUserName: '', + }, + { + firstName: 'Serge', + lastName: 'Shema', + email: 'shemaserge@gmail.com', + githubUserName: '', + }, + { + firstName: 'David', + lastName: 'Irankunda', + email: 'irankundadavid@gmail.com', + githubUserName: '', + }, +] + const seedUsers = async () => { try { // Clear existing users and profiles @@ -14,216 +95,43 @@ const seedUsers = async () => { await Profile.deleteMany({}) // Define sample users - const users: Array = [ - { - firstName: 'ATLP', - lastName: 'Devpulse', - email: 'devpulse@proton.me', - githubUserName: 'atlp-rwanda', - }, - { - firstName: 'Muhawenimana', - lastName: 'Lydia', - email: 'gatarelydie370@gmail.com', - githubUserName: '', - }, - { - firstName: 'paciFique', - lastName: 'Mbonimana', - email: 'pacifiquemboni123@gmail.com', - githubUserName: '', - }, - { - firstName: 'Muheto', - lastName: 'Darius', - email: 'muhedarius@gmail.com', - githubUserName: '', - }, - { - firstName: 'Kagabo', - lastName: 'Darius', - email: 'kagabodarius@gmail.com', - githubUserName: '', - }, - { - firstName: 'Ndayambaje', - lastName: 'Virgile', - email: 'ndayambajevgschooling@gmail.com', - githubUserName: '', - }, - { - firstName: 'NDATIMANA', - lastName: 'Samuel', - email: 'ndatimanasamuel1@gmail.com', - githubUserName: 'blackd44', - }, - { - firstName: 'Ken', - lastName: 'Mugisha', - email: 'keneon2003@gmail.com', - githubUserName: '', - }, - { - firstName: 'Kevin', - lastName: 'Rukundo', - email: 'kevinrukundo1@gmail.com', - githubUserName: '', - }, - { - firstName: 'Patrick', - lastName: 'Mugwaneza', - email: 'mugwanezapatrick6@gmail.com', - githubUserName: '', - }, - { - firstName: 'Elissa', - lastName: 'NTIHINDUKA', - email: 'ntihindukaelissa77@gmail.com', - githubUserName: '', - }, - ] - - // Distribute users among organizations - organizations.Andela = [...users.slice(0, 7)] - organizations.Irembo = [...users.slice(7)] - - // Define the number of users per role - const usersTypes = { - admin: 1, - manager: 1, - coordinators: 1, - users: 2, - ttl: 2, - } - - // Create an array of users to be registered - const registerUsers: Array = [] - - // Populate registerUsers with users for each organization - Object.entries(organizations).forEach(([orgName, orgUsers]: any) => { - // Admins - for (const element of orgUsers) { - if (registerUsers.find((user) => user.email === element.email)) continue - if ( - registerUsers.filter( - (user) => - user.organizations.includes(orgName) && - user.role === RoleOfUser.ADMIN - ).length === usersTypes.admin - ) - break - registerUsers.push({ - email: element.email, - password: hashSync('Test@12345'), - role: RoleOfUser.ADMIN, - organizations: [orgName], - }) - } - for (const element of orgUsers) { - if (registerUsers.find((user) => user.email === element.email)) continue - if ( - registerUsers.filter( - (user) => - user.organizations.includes(orgName) && - user.role === RoleOfUser.MANAGER - ).length === usersTypes.manager - ) - break - registerUsers.push({ - email: element.email, - password: hashSync('Test@12345'), - role: RoleOfUser.MANAGER, - organizations: [orgName], - }) - } + const registerUsers:Array= [] - // Coordinators - for (const element of orgUsers) { - if (registerUsers.find((user) => user.email === element.email)) continue - if ( - registerUsers.filter( - (user) => - user.organizations.includes(orgName) && - user.role === RoleOfUser.COORDINATOR - ).length === usersTypes.coordinators - ) - break - registerUsers.push({ - email: element.email, - password: hashSync('Test@12345'), - role: RoleOfUser.COORDINATOR, - organizations: [orgName], - }) - } - - // Users - for (const element of orgUsers) { - if (registerUsers.find((user) => user.email === element.email)) continue - if ( - registerUsers.filter( - (user) => - user.organizations.includes(orgName) && user.role === 'user' - ).length === usersTypes.users - ) - break - registerUsers.push({ - email: element.email, - password: hashSync('Test@12345'), - role: 'user', - organizations: [orgName], - }) - } - - // TTL - for (const element of orgUsers) { - if (registerUsers.find((user) => user.email === element.email)) continue - if ( - registerUsers.filter( - (user) => - user.organizations.includes(orgName) && user.role === 'ttl' - ).length === usersTypes.ttl - ) - break - registerUsers.push({ - email: element.email, - password: hashSync('Test@12345'), - role: 'ttl', - organizations: [orgName], - }) - } + users.forEach((user: any)=>{ + if(registerUsers.find(registerUser=>registerUser.email===user.email)) return + registerUsers.push({ + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + password: hashSync('Test@12345') + }) }) - // Add SuperAdmin + //Add SUPER_ADMIN registerUsers.unshift({ + firstName: 'samuel', + lastName: 'Nishimwe', email: 'samuel.nishimwe@andela.com', password: hashSync('Test@12345'), - role: RoleOfUser.SUPER_ADMIN, - organizations: ['Andela'], }) - // Save users to the database await User.insertMany(registerUsers) - // Generate profiles for registered users - const profiles = [] - const dbUsers = await User.find().select('_id email') - - for (const element of dbUsers) { - const userProfile = users.find((user) => user.email === element.email) - - if (userProfile) { - profiles.push({ - user: element._id, - firstName: userProfile.firstName, - lastName: userProfile.lastName, - githubUsername: userProfile.githubUserName || 'unavailable', - }) - } - } - - // Save profiles to the database - await Profile.insertMany(profiles) + // // Generate profiles for registered users + // const profiles = [] + // for (const element of dbUsers) { + // const userProfile = users.find((user) => user.email === element.email) + // if (userProfile) { + // profiles.push({ + // user: element._id, + // githubUsername: userProfile.githubUserName || 'unavailable', + // }) + // } + // } + + // // Save profiles to the database + // await Profile.insertMany(profiles) } catch (error) { throw new Error('Failed to seed users and profiles') } diff --git a/src/utils/cron-jobs/team-jobs.ts b/src/utils/cron-jobs/team-jobs.ts index 489a9f39..3576dee0 100644 --- a/src/utils/cron-jobs/team-jobs.ts +++ b/src/utils/cron-jobs/team-jobs.ts @@ -35,8 +35,8 @@ export const addNewAttendanceWeek = async () => { const teams = await Team.find({ active: true, isJobActive: true }).populate('cohort'); for (const team of teams) { - const phase = team.phase || (team.cohort as CohortInterface).phase - const attendances = await Attendance.find({ cohort: (team.cohort as CohortInterface)._id, phase: phase }); + const phase = team.phase || (team.cohort as unknown as CohortInterface).phase + const attendances = await Attendance.find({ cohort: (team.cohort as unknown as CohortInterface)._id, phase: phase }); let lastWeek = 0; let attendanceIndex: number | undefined; @@ -64,7 +64,7 @@ export const addNewAttendanceWeek = async () => { } ); if (!isInSameWeek && (new Date().getTime() > new Date(teamAttendanceDate).getTime())) { - const attendanceExist = await Attendance.findOne({ week: (lastWeek + 1), phase: phase, cohort: (team.cohort as CohortInterface)._id }) + const attendanceExist = await Attendance.findOne({ week: (lastWeek + 1), phase: phase, cohort: (team.cohort as unknown as CohortInterface)._id }) if (attendanceExist) { completedTeamsId.push(team._id.toString()); attendanceExist.teams.push({ @@ -73,11 +73,11 @@ export const addNewAttendanceWeek = async () => { }) await attendanceExist.save() } else { - const tempTeams = await Team.find({ active: true, isJobActive: true, cohort: (team.cohort as CohortInterface)._id }).populate('cohort') + const tempTeams = await Team.find({ active: true, isJobActive: true, cohort: (team.cohort as unknown as CohortInterface)._id }).populate('cohort') await Attendance.create({ week: (lastWeek + 1), phase, - cohort: (team.cohort as CohortInterface)._id, + cohort: (team.cohort as unknown as CohortInterface)._id, teams: tempTeams.map(team => { if (!completedTeamsId.includes(team._id.toString()) && team.phase && (team.phase as mongoose.Types.ObjectId).equals(phase.toString())) { return { team, trainees: [] }; @@ -91,7 +91,7 @@ export const addNewAttendanceWeek = async () => { } } if (attendances.length && attendanceIndex === undefined) { - const tempAttendance = await Attendance.findOne({ week: (lastWeek + 1), phase, cohort: (team.cohort as CohortInterface)._id }).populate('cohort') + const tempAttendance = await Attendance.findOne({ week: (lastWeek + 1), phase, cohort: (team.cohort as unknown as CohortInterface)._id }).populate('cohort') tempAttendance?.teams.push({ team: team._id, trainees: [] @@ -99,13 +99,13 @@ export const addNewAttendanceWeek = async () => { await tempAttendance?.save(); } if (!attendances.length) { - const tempTeams = await Team.find({ active: true, isJobActive: true, cohort: (team.cohort as CohortInterface)._id }).populate('cohort') + const tempTeams = await Team.find({ active: true, isJobActive: true, cohort: (team.cohort as unknown as CohortInterface)._id }).populate('cohort') await Attendance.create({ week: (lastWeek + 1), phase, - cohort: (team.cohort as CohortInterface)._id, + cohort: (team.cohort as unknown as CohortInterface)._id, teams: tempTeams.map(team => { - const isPhaseTrue = (team.phase && (team.phase as mongoose.Types.ObjectId).equals(phase.toString())) || ((team.cohort as CohortInterface).phase as unknown as mongoose.Types.ObjectId).equals(phase.toString()) + const isPhaseTrue = (team.phase && (team.phase as mongoose.Types.ObjectId).equals(phase.toString())) || ((team.cohort as unknown as CohortInterface).phase as unknown as mongoose.Types.ObjectId).equals(phase.toString()) if (!completedTeamsId.includes(team._id.toString()) && isPhaseTrue) { return { team, trainees: [] }; } diff --git a/src/utils/notification/pushNotification.ts b/src/utils/notification/pushNotification.ts index 5b9ae24a..b57df2c2 100644 --- a/src/utils/notification/pushNotification.ts +++ b/src/utils/notification/pushNotification.ts @@ -1,13 +1,16 @@ import mongoose from 'mongoose' import { Notification } from '../../models/notification.model' import { pubSubPublish } from '../../resolvers/notification.resolvers' -import { User } from '../../models/user' +import User from '../../models/user' import { Profile } from '../../models/profile.model' +import { Organization } from '../../models/organization.model' +import { GraphQLError } from 'graphql' export const pushNotification = async ( receiver: mongoose.Types.ObjectId, message: string, sender: mongoose.Types.ObjectId, + orgId: mongoose.Types.ObjectId, type?: | 'rating' | 'performance' @@ -29,8 +32,31 @@ export const pushNotification = async ( id: notification.id, sender: { profile: profile?.toObject() }, } + const org = await Organization.findOne({ _id: orgId}) + if(!org){ + throw new GraphQLError("No such organization found",{ + extensions:{ + code: "ORG_NOT_FOUND" + } + }) + } const userExists = await User.findOne({ _id: receiver }) - if (userExists && userExists.pushNotifications) { + if(!userExists){ + throw new GraphQLError("No such user exists",{ + extensions:{ + code: "USER_NOT_FOUND" + } + }) + } + const orgUserData = userExists?.organizations.find(data=>data.orgId.toString()===org._id.toString()) + if(!orgUserData){ + throw new GraphQLError(`User ${userExists.email} is not part ${org.name}`,{ + extensions:{ + code: "FORBIDDEN" + } + }) + } + if (orgUserData.pushNotifications) { pubSubPublish(sanitizedNotification) } } diff --git a/src/utils/templates/suspiciousActivityTemplate.ts b/src/utils/templates/suspiciousActivityTemplate.ts new file mode 100644 index 00000000..9d3c87ed --- /dev/null +++ b/src/utils/templates/suspiciousActivityTemplate.ts @@ -0,0 +1,87 @@ +const suspiciousActivityTemplate = (trials: number, date: Date, country?: string, city?: string) => { + return ` + + + + Suspicious Activity Detected + + + +
    +

    Suspicious Activity Detected

    +

    We have detected some suspicious activity on your account. For your security, we recommend taking immediate action to ensure the safety of your account.

    +

    If you believe this activity was unauthorized, please click the button below to reset your password:

    + Reset Password +
    + +
  • date:${date.toString()}
  • +
  • country name: ${country || "unknown"}
  • +
  • city: ${city || "unknown"}
  • +
  • failed attempts:${trials}
  • +
    + + +

    If you recognize this activity and believe it was performed by you, you can safely ignore this message.

    +

    If you have any questions or need further assistance, please contact our support team.

    +

    Best regards,

    +

    Pulse Team

    +
    + + + ` + } + + export default suspiciousActivityTemplate \ No newline at end of file diff --git a/src/validations/index.ts b/src/validations/index.ts new file mode 100644 index 00000000..daf50187 --- /dev/null +++ b/src/validations/index.ts @@ -0,0 +1,46 @@ +import { GraphQLError } from "graphql" +import { z } from "zod" + +export function validateEmail(email: string, errorMsg: string){ + const data = z.string().email().safeParse(email) + if(data.error){ + throw new GraphQLError(errorMsg,{ + extensions: { + code: "VALIDATION_ERROR" + } + }) + } +} + +export function validateStringField(field: string, errorMsg: string){ + const data = z.string().min(2).safeParse(field) + if(data.error){ + throw new GraphQLError(errorMsg,{ + extensions: { + code: "VALIDATION_ERROR" + } + }) + } +} + +export function validateURLField(field: string, errorMsg: string){ + const data = z.string().url().safeParse(field) + if(data.error){ + throw new GraphQLError(errorMsg,{ + extensions: { + code: "VALIDATION_ERROR" + } + }) + } +} + +export function validatePasswordField(field: string, errorMsg: string){ + const data = z.string().min(6).regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])[A-Za-z\d\W_]{6,}$/).safeParse(field) + if(data.error){ + throw new GraphQLError(errorMsg,{ + extensions: { + code: "VALIDATION_ERROR" + } + }) + } +} \ No newline at end of file