diff --git a/.env.sample b/.env.sample index 86160f9a44..e7bbe204c3 100644 --- a/.env.sample +++ b/.env.sample @@ -19,6 +19,9 @@ MATCH_DB_LOCAL_URI=mongodb://match-db:27017/match MATCH_DB_USERNAME=user MATCH_DB_PASSWORD=password +# Broker +BROKER_URL=amqp://broker:5672 + # Secret for creating JWT signature JWT_SECRET=you-can-replace-this-with-your-own-secret diff --git a/compose.dev.yml b/compose.dev.yml index f0ddd5746b..edc16997ea 100644 --- a/compose.dev.yml +++ b/compose.dev.yml @@ -40,7 +40,7 @@ services: ports: - 27019:27017 - match-broker: + broker: ports: - 5672:5672 diff --git a/compose.yml b/compose.yml index 92626e918b..4b7024e1f9 100644 --- a/compose.yml +++ b/compose.yml @@ -34,6 +34,7 @@ services: DB_LOCAL_URI: ${QUESTION_DB_LOCAL_URI} DB_USERNAME: ${QUESTION_DB_USERNAME} DB_PASSWORD: ${QUESTION_DB_PASSWORD} + JWT_SECRET: ${JWT_SECRET} networks: - gateway-network - question-db-network @@ -94,8 +95,10 @@ services: DB_LOCAL_URI: ${MATCH_DB_LOCAL_URI} DB_USERNAME: ${MATCH_DB_USERNAME} DB_PASSWORD: ${MATCH_DB_PASSWORD} + JWT_SECRET: ${JWT_SECRET} + BROKER_URL: ${BROKER_URL} depends_on: - match-broker: + broker: condition: service_healthy networks: - gateway-network @@ -114,13 +117,13 @@ services: - match-db-network restart: always - match-broker: - container_name: match-broker - hostname: match-broker + broker: + container_name: broker + hostname: broker image: rabbitmq:4.0.2 user: rabbitmq networks: - - match-db-network + - gateway-network healthcheck: test: rabbitmq-diagnostics check_port_connectivity interval: 30s diff --git a/services/match/.env.sample b/services/match/.env.sample index 86bc9196be..c88c405e05 100644 --- a/services/match/.env.sample +++ b/services/match/.env.sample @@ -4,4 +4,7 @@ DB_CLOUD_URI= DB_LOCAL_URI=mongodb://match-db:27017/match DB_USERNAME=user DB_PASSWORD=password -PORT=8083 \ No newline at end of file +BROKER_URL=amqp://broker:5672 +JWT_SECRET=you-can-replace-this-with-your-own-secret +PORT=8083 +NODE_ENV=development \ No newline at end of file diff --git a/services/match/package-lock.json b/services/match/package-lock.json index 5eb089a86e..286f4156bd 100644 --- a/services/match/package-lock.json +++ b/services/match/package-lock.json @@ -10,22 +10,22 @@ "license": "ISC", "dependencies": { "amqplib": "^0.10.4", - "axios": "^1.7.7", "body-parser": "^1.20.3", "cors": "^2.8.5", "express": "^4.21.0", - "joi": "^17.13.3", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.7.0", - "morgan": "^1.10.0" + "morgan": "^1.10.0", + "zod": "^3.23.8", + "zod-validation-error": "^3.4.0" }, "devDependencies": { "@eslint/js": "^9.10.0", "@types/amqplib": "^0.10.5", - "@types/axios": "^0.9.36", "@types/cors": "^2.8.17", "@types/eslint__js": "^8.42.3", "@types/express": "^4.17.21", - "@types/joi": "^17.2.2", + "@types/jsonwebtoken": "^9.0.7", "@types/mongoose": "^5.11.96", "@types/morgan": "^1.9.9", "@types/node": "^22.5.4", @@ -258,21 +258,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -451,27 +436,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/@sideway/address": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", - "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", - "license": "BSD-3-Clause" - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "license": "BSD-3-Clause" - }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -517,13 +481,6 @@ "@types/node": "*" } }, - "node_modules/@types/axios": { - "version": "0.9.36", - "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.9.36.tgz", - "integrity": "sha512-NLOpedx9o+rxo/X5ChbdiX6mS1atE4WHmEEIcR9NLenRVa5HoVjAvjafwU3FPTqnZEstpoqCaW7fagqSoTDNeg==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -616,16 +573,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/joi": { - "version": "17.2.2", - "resolved": "https://registry.npmjs.org/@types/joi/-/joi-17.2.2.tgz", - "integrity": "sha512-vPvPwxn0Y4pQyqkEcMCJYxXCMYcrHqdfFX4SpF4zcqYioYexmDyxtM3OK+m/ZwGBS8/dooJ0il9qCwAdd6KFtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "joi": "*" - } - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -633,6 +580,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -1184,23 +1141,6 @@ "node": ">=8" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1296,6 +1236,12 @@ "node": ">=16.20.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-more-ints": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", @@ -1425,18 +1371,6 @@ "dev": true, "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/common-tags": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", @@ -1564,15 +1498,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1635,6 +1560,15 @@ "node": ">=6.0.0" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2114,40 +2048,6 @@ "dev": true, "license": "ISC" }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2563,19 +2463,6 @@ "dev": true, "license": "ISC" }, - "node_modules/joi": { - "version": "17.13.3", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", - "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.3.0", - "@hapi/topo": "^5.1.0", - "@sideway/address": "^4.1.5", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2610,6 +2497,55 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kareem": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", @@ -2666,6 +2602,42 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2673,6 +2645,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/loglevel": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", @@ -3773,12 +3751,6 @@ "node": ">= 0.10" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -3998,7 +3970,6 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -4669,6 +4640,27 @@ "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" + } + }, + "node_modules/zod-validation-error": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.4.0.tgz", + "integrity": "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.18.0" + } } } } diff --git a/services/match/package.json b/services/match/package.json index 9cc2dbc358..9a19897aec 100644 --- a/services/match/package.json +++ b/services/match/package.json @@ -14,22 +14,22 @@ "description": "", "dependencies": { "amqplib": "^0.10.4", - "axios": "^1.7.7", "body-parser": "^1.20.3", "cors": "^2.8.5", "express": "^4.21.0", - "joi": "^17.13.3", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.7.0", - "morgan": "^1.10.0" + "morgan": "^1.10.0", + "zod": "^3.23.8", + "zod-validation-error": "^3.4.0" }, "devDependencies": { "@eslint/js": "^9.10.0", "@types/amqplib": "^0.10.5", - "@types/axios": "^0.9.36", "@types/cors": "^2.8.17", "@types/eslint__js": "^8.42.3", "@types/express": "^4.17.21", - "@types/joi": "^17.2.2", + "@types/jsonwebtoken": "^9.0.7", "@types/mongoose": "^5.11.96", "@types/morgan": "^1.9.9", "@types/node": "^22.5.4", diff --git a/services/match/src/app.ts b/services/match/src/app.ts index fb15a8c8b7..0106e46f5f 100644 --- a/services/match/src/app.ts +++ b/services/match/src/app.ts @@ -5,6 +5,7 @@ import router from './routes'; import matchRequestRouter from './routes/matchRequestRoutes'; import bodyParser from 'body-parser'; import { verifyAccessToken } from './middleware/jwt'; +import config from './config'; const app: Express = express(); @@ -15,7 +16,7 @@ app.use(bodyParser.json()); app.use( cors({ - origin: process.env.CORS_ORIGIN ?? true, + origin: config.CORS_ORIGIN, methods: ['GET', 'POST', 'DELETE', 'PUT', 'PATCH'], allowedHeaders: ['Origin', 'X-Request-With', 'Content-Type', 'Accept', 'Authorization'], }), diff --git a/services/match/src/config.ts b/services/match/src/config.ts new file mode 100644 index 0000000000..ecc3747819 --- /dev/null +++ b/services/match/src/config.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; + +const envSchema = z + .object({ + DB_USERNAME: z.string().trim().min(1), + DB_PASSWORD: z.string().trim().min(1), + DB_CLOUD_URI: z.string().trim().optional(), + DB_LOCAL_URI: z.string().trim().optional(), + BROKER_URL: z.string().url(), + NODE_ENV: z.enum(['development', 'production']).default('development'), + CORS_ORIGIN: z.union([z.string().url(), z.literal('*')]).default('*'), + JWT_SECRET: z.string().trim().min(32), + PORT: z.coerce.number().min(1024).default(8083), + }) + .superRefine((data, ctx) => { + const isUrl = z.string().url(); + const cloudRes = isUrl.safeParse(data.DB_CLOUD_URI); + const localRes = isUrl.safeParse(data.DB_LOCAL_URI); + if (data.NODE_ENV === 'production') { + cloudRes.error?.issues.forEach(i => ctx.addIssue({ ...i, path: ['DB_CLOUD_URI'] })); + } else if (data.NODE_ENV === 'development') { + localRes.error?.issues.forEach(i => ctx.addIssue({ ...i, path: ['DB_LOCAL_URI'] })); + } + }); + +const result = envSchema.safeParse(process.env); +if (!result.success) { + console.error('There is an error with the environment variables:', result.error.issues); + process.exit(1); +} + +const { NODE_ENV, DB_CLOUD_URI, DB_LOCAL_URI } = result.data; +const DB_URI = (NODE_ENV === 'production' ? DB_CLOUD_URI : DB_LOCAL_URI) as string; +const config = { ...result.data, DB_URI }; + +export default config; diff --git a/services/match/src/controllers/matchRequestController.ts b/services/match/src/controllers/matchRequestController.ts index 4600dc6940..79e0b486cb 100644 --- a/services/match/src/controllers/matchRequestController.ts +++ b/services/match/src/controllers/matchRequestController.ts @@ -10,6 +10,7 @@ import { } from '../models/repository'; import { produceMatchUpdatedRequest } from '../events/producer'; import { getStatus } from '../models/matchRequestModel'; +import { fromError } from 'zod-validation-error'; /** * Creates a match request. @@ -17,13 +18,15 @@ import { getStatus } from '../models/matchRequestModel'; * @param res */ export const createMatchRequest = async (req: Request, res: Response) => { - const { error, value } = createMatchRequestSchema.validate(req.body); - if (error) { - return handleBadRequest(res, error.message); + const result = createMatchRequestSchema.safeParse(req.body); + + if (!result.success) { + const formattedError = fromError(result.error).toString(); + return handleBadRequest(res, formattedError); } const { id: userId, username } = req.user; - const { topics, difficulty } = value; + const { topics, difficulty } = result.data; try { const matchRequest = await _createMatchRequest(userId, username, topics, difficulty); await produceMatchUpdatedRequest(matchRequest.id, userId, username, topics, difficulty); diff --git a/services/match/src/events/broker.ts b/services/match/src/events/broker.ts index 21e1bd2802..bf816301e0 100644 --- a/services/match/src/events/broker.ts +++ b/services/match/src/events/broker.ts @@ -1,4 +1,5 @@ import client, { Channel, Connection } from 'amqplib'; +import config from '../config'; // TODO: Add authentication @@ -17,7 +18,7 @@ class MessageBroker { } try { - this.connection = await client.connect('amqp://match-broker'); + this.connection = await client.connect(config.BROKER_URL); console.log('Connected to RabbitMQ'); this.channel = await this.connection.createChannel(); this.connected = true; diff --git a/services/match/src/events/producer.ts b/services/match/src/events/producer.ts index 080ea8cc2f..e217ef8da7 100644 --- a/services/match/src/events/producer.ts +++ b/services/match/src/events/producer.ts @@ -1,11 +1,12 @@ import { Difficulty } from '../models/matchRequestModel'; import { MatchUpdatedEvent, MatchFoundEvent } from '../types/event'; +import { IdType } from '../types/request'; import messageBroker from './broker'; import { Queues } from './queues'; export async function produceMatchUpdatedRequest( - requestId: string, - userId: string, + requestId: IdType, + userId: IdType, username: string, topics: string[], difficulty: Difficulty, diff --git a/services/match/src/index.ts b/services/match/src/index.ts index 60fed0b15a..f9e96ffd64 100644 --- a/services/match/src/index.ts +++ b/services/match/src/index.ts @@ -1,9 +1,10 @@ import app from './app'; +import config from './config'; import messageBroker from './events/broker'; import { initializeConsumers } from './events/consumer'; import { connectToDB } from './models/repository'; -const port = process.env.PORT || 8083; +const port = config.PORT; connectToDB() .then(() => console.log('MongoDB connected successfully')) diff --git a/services/match/src/middleware/jwt.ts b/services/match/src/middleware/jwt.ts index 371c9c31e2..660766b68d 100644 --- a/services/match/src/middleware/jwt.ts +++ b/services/match/src/middleware/jwt.ts @@ -1,7 +1,8 @@ +import jwt from 'jsonwebtoken'; import { NextFunction, Request, Response } from 'express'; -import { handleInternalError, handleUnauthorized } from '../utils/responses'; -import { VerifyTokenResponse } from '../types/response'; -import axios from 'axios'; +import { handleUnauthorized } from '../utils/responses'; +import config from '../config'; +import { userSchema } from '../types/request'; export async function verifyAccessToken(req: Request, res: Response, next: NextFunction) { const authHeader = req.headers['authorization']; @@ -9,20 +10,17 @@ export async function verifyAccessToken(req: Request, res: Response, next: NextF handleUnauthorized(res, 'Authenticated failed'); return; } - try { - const response = await axios.get('http://user:8082/auth/verify-token', { - headers: { authorization: authHeader }, - }); - req.user = response.data.data; - next(); - } catch (error: any) { - if (error?.response?.status == 401) { - handleUnauthorized(res); + + const token = authHeader.split(' ')[1]; + + jwt.verify(token, config.JWT_SECRET, async (err, user) => { + const result = userSchema.safeParse(user); + if (err || result.error) { + handleUnauthorized(res, 'Authentication failed'); return; } - console.error(error); - handleInternalError(res); - return; - } + req.user = result.data; + next(); + }); } diff --git a/services/match/src/models/repository.ts b/services/match/src/models/repository.ts index a0baee367d..b654123156 100644 --- a/services/match/src/models/repository.ts +++ b/services/match/src/models/repository.ts @@ -1,24 +1,14 @@ -import mongoose, { Types } from 'mongoose'; +import mongoose from 'mongoose'; import { Difficulty, MatchRequestModel } from './matchRequestModel'; import { oneMinuteAgo } from '../utils/date'; - -type IdType = string | Types.ObjectId; +import config from '../config'; +import { IdType } from '../types/request'; export async function connectToDB() { - const mongoURI = process.env.NODE_ENV === 'production' ? process.env.DB_CLOUD_URI : process.env.DB_LOCAL_URI; - - console.log('MongoDB URI:', mongoURI); - - if (!mongoURI) { - throw new Error('MongoDB URI not specified'); - } else if (!process.env.DB_USERNAME || !process.env.DB_PASSWORD) { - throw Error('MongoDB credentials not specified'); - } - - await mongoose.connect(mongoURI, { + await mongoose.connect(config.DB_URI, { authSource: 'admin', - user: process.env.DB_USERNAME, - pass: process.env.DB_PASSWORD, + user: config.DB_USERNAME, + pass: config.DB_PASSWORD, }); } diff --git a/services/match/src/types/express.d.ts b/services/match/src/types/express.d.ts index ca510382a8..92949f41ce 100644 --- a/services/match/src/types/express.d.ts +++ b/services/match/src/types/express.d.ts @@ -1,6 +1,6 @@ // https://blog.logrocket.com/extend-express-request-object-typescript/ -import { RequestUser } from '../custom'; +import { RequestUser } from './request'; export {}; diff --git a/services/match/src/types/request.ts b/services/match/src/types/request.ts index 6644aae8b7..4f4a1a509b 100644 --- a/services/match/src/types/request.ts +++ b/services/match/src/types/request.ts @@ -1,8 +1,21 @@ import { Types } from 'mongoose'; +import { z } from 'zod'; + +export type IdType = string | Types.ObjectId; + +enum Role { + Admin = 'admin', + User = 'user', +} export interface RequestUser { - id: Types.ObjectId | string; + id: IdType; username: string; - email: string; - isAdmin: boolean; + role: Role; } + +export const userSchema = z.object({ + id: z.string(), + username: z.string(), + role: z.nativeEnum(Role), +}); diff --git a/services/match/src/types/response.ts b/services/match/src/types/response.ts deleted file mode 100644 index d5a63921ce..0000000000 --- a/services/match/src/types/response.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { MatchRequest, MatchRequestStatus } from '../models/matchRequestModel'; -import { RequestUser } from './request'; - -export interface BaseResponse { - status: string; - message: string; -} - -export interface VerifyTokenResponse extends BaseResponse { - data: RequestUser; -} - -export interface MatchRequestWithStatus extends MatchRequest { - status: MatchRequestStatus; -} diff --git a/services/match/src/validation/matchRequestValidation.ts b/services/match/src/validation/matchRequestValidation.ts index 9c3aa4d767..7450b64816 100644 --- a/services/match/src/validation/matchRequestValidation.ts +++ b/services/match/src/validation/matchRequestValidation.ts @@ -1,9 +1,7 @@ -import Joi from 'joi'; +import { z } from 'zod'; import { Difficulty } from '../models/matchRequestModel'; -export const createMatchRequestSchema = Joi.object({ - topics: Joi.array().items(Joi.string()).min(1).required(), - difficulty: Joi.string() - .valid(...Object.values(Difficulty)) - .required(), +export const createMatchRequestSchema = z.object({ + topics: z.array(z.string().min(1)).min(1), + difficulty: z.nativeEnum(Difficulty), }); diff --git a/services/question/.env.sample b/services/question/.env.sample index 5613b8f4a7..a7547df62d 100644 --- a/services/question/.env.sample +++ b/services/question/.env.sample @@ -4,6 +4,7 @@ DB_CLOUD_URI= DB_LOCAL_URI=mongodb://question-db:27017/question DB_USERNAME=user DB_PASSWORD=password +JWT_SECRET=you-can-replace-this-with-your-own-secret CORS_ORIGIN=* PORT=8081 NODE_ENV=development \ No newline at end of file diff --git a/services/question/package-lock.json b/services/question/package-lock.json index 9c6f712439..5f8d4f4a77 100644 --- a/services/question/package-lock.json +++ b/services/question/package-lock.json @@ -12,14 +12,17 @@ "body-parser": "^1.20.3", "cors": "^2.8.5", "express": "^4.21.0", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.6.2", - "morgan": "^1.10.0" + "morgan": "^1.10.0", + "zod": "^3.23.8" }, "devDependencies": { "@eslint/js": "^9.10.0", "@types/cors": "^2.8.17", "@types/eslint__js": "^8.42.3", "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.7", "@types/morgan": "^1.9.9", "@types/node": "^22.5.4", "@typescript-eslint/eslint-plugin": "^8.5.0", @@ -520,6 +523,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -1141,6 +1154,12 @@ "node": ">=16.20.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1447,6 +1466,15 @@ "node": ">=6.0.0" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2369,6 +2397,55 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kareem": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", @@ -2425,6 +2502,42 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2432,6 +2545,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/loglevel": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", @@ -3727,7 +3846,6 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -4382,6 +4500,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/services/question/package.json b/services/question/package.json index f7a57cd3d3..7dda995f07 100644 --- a/services/question/package.json +++ b/services/question/package.json @@ -4,8 +4,8 @@ "main": "index.js", "scripts": { "build": "npx tsc", - "start": "npm run build && node dist/index.js", - "dev": "nodemon src/index.ts", + "start": "npm run build && node ./dist/index.js", + "dev": "nodemon --files ./src/index.ts", "lint": "npx eslint .", "lint:fix": "npx eslint . --fix" }, @@ -16,14 +16,17 @@ "body-parser": "^1.20.3", "cors": "^2.8.5", "express": "^4.21.0", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.6.2", - "morgan": "^1.10.0" + "morgan": "^1.10.0", + "zod": "^3.23.8" }, "devDependencies": { "@eslint/js": "^9.10.0", "@types/cors": "^2.8.17", "@types/eslint__js": "^8.42.3", "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.7", "@types/morgan": "^1.9.9", "@types/node": "^22.5.4", "@typescript-eslint/eslint-plugin": "^8.5.0", diff --git a/services/question/src/app.ts b/services/question/src/app.ts index 53a5b4acc0..6bc6da6bcf 100644 --- a/services/question/src/app.ts +++ b/services/question/src/app.ts @@ -4,6 +4,7 @@ import cors from 'cors'; import router from './routes'; import questionRouter from './routes/questionRoutes'; import bodyParser from 'body-parser'; +import config from './config'; const app: Express = express(); @@ -14,7 +15,7 @@ app.use(bodyParser.json()); app.use( cors({ - origin: process.env.CORS_ORIGIN ?? true, + origin: config.CORS_ORIGIN, methods: ['GET', 'POST', 'DELETE', 'PUT', 'PATCH'], allowedHeaders: ['Origin', 'X-Request-With', 'Content-Type', 'Accept', 'Authorization'], }), diff --git a/services/question/src/config.ts b/services/question/src/config.ts new file mode 100644 index 0000000000..8fa9be31cd --- /dev/null +++ b/services/question/src/config.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; + +const envSchema = z + .object({ + DB_USERNAME: z.string().trim().min(1), + DB_PASSWORD: z.string().trim().min(1), + DB_CLOUD_URI: z.string().trim().optional(), + DB_LOCAL_URI: z.string().trim().optional(), + NODE_ENV: z.enum(['development', 'production']).default('development'), + CORS_ORIGIN: z.union([z.string().url(), z.literal('*')]).default('*'), + JWT_SECRET: z.string().trim().min(32), + PORT: z.coerce.number().min(1024).default(8081), + }) + .superRefine((data, ctx) => { + const isUrl = z.string().url(); + const cloudRes = isUrl.safeParse(data.DB_CLOUD_URI); + const localRes = isUrl.safeParse(data.DB_LOCAL_URI); + if (data.NODE_ENV === 'production') { + cloudRes.error?.issues.forEach(i => ctx.addIssue({ ...i, path: ['DB_CLOUD_URI'] })); + } else if (data.NODE_ENV === 'development') { + localRes.error?.issues.forEach(i => ctx.addIssue({ ...i, path: ['DB_LOCAL_URI'] })); + } + }); + +const result = envSchema.safeParse(process.env); +if (!result.success) { + console.error('There is an error with the environment variables:', result.error.issues); + process.exit(1); +} + +const { NODE_ENV, DB_CLOUD_URI, DB_LOCAL_URI } = result.data; +const DB_URI = (NODE_ENV === 'production' ? DB_CLOUD_URI : DB_LOCAL_URI) as string; +const config = { ...result.data, DB_URI }; + +export default config; diff --git a/services/question/src/index.ts b/services/question/src/index.ts index 4802d8777b..105d538608 100644 --- a/services/question/src/index.ts +++ b/services/question/src/index.ts @@ -1,9 +1,10 @@ import app from './app'; +import config from './config'; import { connectToDB, upsertManyQuestions } from './models'; import { getDemoQuestions } from './utils/data'; import { initializeCounter } from './utils/sequence'; -const port = process.env.PORT || 8081; +const port = config.PORT; connectToDB() .then(async () => { diff --git a/services/question/src/middleware/jwt.ts b/services/question/src/middleware/jwt.ts new file mode 100644 index 0000000000..dec2175628 --- /dev/null +++ b/services/question/src/middleware/jwt.ts @@ -0,0 +1,34 @@ +import jwt from 'jsonwebtoken'; +import { NextFunction, Request, Response } from 'express'; +import config from '../config'; +import { Role, userSchema } from '../types/request'; +import { handleForbidden, handleUnauthorized } from '../utils/helpers'; + +export async function verifyAccessToken(req: Request, res: Response, next: NextFunction) { + const authHeader = req.headers['authorization']; + if (!authHeader) { + handleUnauthorized(res, 'Authenticated failed'); + return; + } + + const token = authHeader.split(' ')[1]; + + jwt.verify(token, config.JWT_SECRET, async (err, user) => { + const result = userSchema.safeParse(user); + if (err || result.error) { + handleUnauthorized(res, 'Authentication failed'); + return; + } + + req.user = result.data; + next(); + }); +} + +export function verifyIsAdmin(req: Request, res: Response, next: NextFunction) { + if (req.user.role === Role.Admin) { + next(); + return; + } + handleForbidden(res, 'Not authorized to access this resource'); +} diff --git a/services/question/src/models/index.ts b/services/question/src/models/index.ts index 90596e07ce..2d54e72f47 100644 --- a/services/question/src/models/index.ts +++ b/services/question/src/models/index.ts @@ -1,21 +1,12 @@ import mongoose from 'mongoose'; import { IQuestion, Question } from './questionModel'; +import config from '../config'; export async function connectToDB() { - const mongoURI = process.env.NODE_ENV === 'production' ? process.env.DB_CLOUD_URI : process.env.DB_LOCAL_URI; - - console.log('MongoDB URI:', mongoURI); - - if (!mongoURI) { - throw new Error('MongoDB URI not specified'); - } else if (!process.env.DB_USERNAME || !process.env.DB_PASSWORD) { - throw Error('MongoDB credentials not specified'); - } - - await mongoose.connect(mongoURI, { + await mongoose.connect(config.DB_URI, { authSource: 'admin', - user: process.env.DB_USERNAME, - pass: process.env.DB_PASSWORD, + user: config.DB_USERNAME, + pass: config.DB_PASSWORD, }); } diff --git a/services/question/src/routes/questionRoutes.ts b/services/question/src/routes/questionRoutes.ts index c48e61860b..2fcdb6343f 100644 --- a/services/question/src/routes/questionRoutes.ts +++ b/services/question/src/routes/questionRoutes.ts @@ -9,6 +9,7 @@ import { updateQuestion, deleteQuestions, } from '../controllers/questionController'; +import { verifyAccessToken, verifyIsAdmin } from '../middleware/jwt'; /** * Router for question endpoints. @@ -27,21 +28,21 @@ questionRouter.get('/:id', getQuestionById); /** * Add a new question to the database. */ -questionRouter.post('/', addQuestion); +questionRouter.post('/', verifyAccessToken, verifyIsAdmin, addQuestion); /** * Update a question in the database. */ -questionRouter.put('/:id', updateQuestion); +questionRouter.put('/:id', verifyAccessToken, verifyIsAdmin, updateQuestion); /** * Delete a question from the database. */ -questionRouter.delete('/:id', deleteQuestion); +questionRouter.delete('/:id', verifyAccessToken, verifyIsAdmin, deleteQuestion); /** * Delete questions from the database. */ -questionRouter.post('/delete', deleteQuestions); +questionRouter.post('/delete', verifyAccessToken, verifyIsAdmin, deleteQuestions); export default questionRouter; diff --git a/services/question/src/types/express.d.ts b/services/question/src/types/express.d.ts new file mode 100644 index 0000000000..92949f41ce --- /dev/null +++ b/services/question/src/types/express.d.ts @@ -0,0 +1,13 @@ +// https://blog.logrocket.com/extend-express-request-object-typescript/ + +import { RequestUser } from './request'; + +export {}; + +declare global { + namespace Express { + export interface Request { + user: RequestUser; + } + } +} diff --git a/services/question/src/types/request.ts b/services/question/src/types/request.ts new file mode 100644 index 0000000000..0fa4ce321e --- /dev/null +++ b/services/question/src/types/request.ts @@ -0,0 +1,21 @@ +import { Types } from 'mongoose'; +import { z } from 'zod'; + +export type IdType = string | Types.ObjectId; + +export enum Role { + Admin = 'admin', + User = 'user', +} + +export interface RequestUser { + id: IdType; + username: string; + role: Role; +} + +export const userSchema = z.object({ + id: z.string(), + username: z.string(), + role: z.nativeEnum(Role), +}); diff --git a/services/question/src/utils/helpers.ts b/services/question/src/utils/helpers.ts index 58b1dfcfce..b2f08f9a68 100644 --- a/services/question/src/utils/helpers.ts +++ b/services/question/src/utils/helpers.ts @@ -24,6 +24,30 @@ export const handleBadRequest = (res: Response, message = 'Bad Request') => { }); }; +/** + * Handles unauthorized requests and sends a 401 response with a custom message. + * @param res + * @param message + */ +export const handleUnauthorized = (res: Response, message = 'Unauthorized Request') => { + res.status(401).json({ + status: 'Error', + message, + }); +}; + +/** + * Handles forbidden requests and sends a 403 response with a custom message. + * @param res + * @param message + */ +export const handleForbidden = (res: Response, message = 'Forbidden Request') => { + res.status(403).json({ + status: 'Error', + message, + }); +}; + /** * Handles not found errors and sends a 404 response with a custom message. * @param res diff --git a/services/user/package-lock.json b/services/user/package-lock.json index e416db5e6c..509c3a52f4 100644 --- a/services/user/package-lock.json +++ b/services/user/package-lock.json @@ -16,7 +16,8 @@ "express": "^4.21.0", "http": "^0.0.1-security", "jsonwebtoken": "^9.0.2", - "mongoose": "^8.5.4" + "mongoose": "^8.5.4", + "zod": "^3.23.8" }, "devDependencies": { "@eslint/js": "^9.11.1", @@ -5115,6 +5116,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/services/user/package.json b/services/user/package.json index 55112d18b9..36a14097a2 100644 --- a/services/user/package.json +++ b/services/user/package.json @@ -45,6 +45,7 @@ "express": "^4.21.0", "http": "^0.0.1-security", "jsonwebtoken": "^9.0.2", - "mongoose": "^8.5.4" + "mongoose": "^8.5.4", + "zod": "^3.23.8" } } diff --git a/services/user/src/config.ts b/services/user/src/config.ts new file mode 100644 index 0000000000..7c8e2191e1 --- /dev/null +++ b/services/user/src/config.ts @@ -0,0 +1,34 @@ +import { z } from 'zod'; + +const envSchema = z + .object({ + DB_USERNAME: z.string().trim().min(1), + DB_PASSWORD: z.string().trim().min(1), + DB_CLOUD_URI: z.string().trim().optional(), + DB_LOCAL_URI: z.string().trim().optional(), + NODE_ENV: z.enum(['development', 'production']).default('development'), + JWT_SECRET: z.string().trim().min(32), + PORT: z.coerce.number().min(1024).default(8082), + }) + .superRefine((data, ctx) => { + const isUrl = z.string().url(); + const cloudRes = isUrl.safeParse(data.DB_CLOUD_URI); + const localRes = isUrl.safeParse(data.DB_LOCAL_URI); + if (data.NODE_ENV === 'production') { + cloudRes.error?.issues.forEach(i => ctx.addIssue({ ...i, path: ['DB_CLOUD_URI'] })); + } else if (data.NODE_ENV === 'development') { + localRes.error?.issues.forEach(i => ctx.addIssue({ ...i, path: ['DB_LOCAL_URI'] })); + } + }); + +const result = envSchema.safeParse(process.env); +if (!result.success) { + console.error('There is an error with the environment variables:', result.error.issues); + process.exit(1); +} + +const { NODE_ENV, DB_CLOUD_URI, DB_LOCAL_URI } = result.data; +const DB_URI = (NODE_ENV === 'production' ? DB_CLOUD_URI : DB_LOCAL_URI) as string; +const config = { ...result.data, DB_URI }; + +export default config; diff --git a/services/user/src/controller/auth-controller.ts b/services/user/src/controller/auth-controller.ts index 641061dcaa..98d06e1b98 100644 --- a/services/user/src/controller/auth-controller.ts +++ b/services/user/src/controller/auth-controller.ts @@ -4,6 +4,8 @@ import { findUserByUsername as _findUserByUsername } from '../model/repository'; import { formatUserResponse } from './user-controller'; import { Request, Response } from 'express'; import { handleBadRequest, handleInternalError, handleSuccess, handleUnauthorized } from '../utils/helper'; +import config from '../config'; +import { Role } from '../model/user-model'; export async function handleLogin(req: Request, res: Response) { const { username, password } = req.body; @@ -25,16 +27,16 @@ export async function handleLogin(req: Request, res: Response) { return; } - if (!process.env.JWT_SECRET) { - handleInternalError(res, 'JWT secret not specified'); - return; - } + const role = user.isAdmin ? Role.Admin : Role.User; + console.log({ id: user.id, username: user.username, role }); const accessToken = jwt.sign( { id: user.id, + username: user.username, + role, }, - process.env.JWT_SECRET, + config.JWT_SECRET, { expiresIn: '1d', }, diff --git a/services/user/src/middleware/basic-access-control.ts b/services/user/src/middleware/basic-access-control.ts index a9ea0e9702..11fc3a7c8f 100644 --- a/services/user/src/middleware/basic-access-control.ts +++ b/services/user/src/middleware/basic-access-control.ts @@ -1,7 +1,8 @@ import jwt from 'jsonwebtoken'; import { findUserById as _findUserById } from '../model/repository'; import { NextFunction, Request, Response } from 'express'; -import { handleForbidden, handleInternalError, handleUnauthorized } from '../utils/helper'; +import { handleForbidden, handleUnauthorized } from '../utils/helper'; +import config from '../config'; export function verifyAccessToken(req: Request, res: Response, next: NextFunction) { const authHeader = req.headers['authorization']; @@ -13,12 +14,7 @@ export function verifyAccessToken(req: Request, res: Response, next: NextFunctio // request auth header: `Authorization: Bearer + ` const token = authHeader.split(' ')[1]; - if (!process.env.JWT_SECRET) { - handleInternalError(res, 'JWT Secret not provided'); - return; - } - - jwt.verify(token, process.env.JWT_SECRET, async (err, user) => { + jwt.verify(token, config.JWT_SECRET, async (err, user) => { if (err || typeof user !== 'object' || !user?.id) { handleUnauthorized(res, 'Authentication failed'); return; diff --git a/services/user/src/model/repository.ts b/services/user/src/model/repository.ts index 8559d1c227..c86f8b5cfa 100644 --- a/services/user/src/model/repository.ts +++ b/services/user/src/model/repository.ts @@ -1,18 +1,13 @@ import UserModel from './user-model'; import 'dotenv/config'; import { connect } from 'mongoose'; +import config from '../config'; export async function connectToDB() { - const mongoUri = process.env.NODE_ENV === 'production' ? process.env.DB_CLOUD_URI : process.env.DB_LOCAL_URI; - - if (!mongoUri) { - throw new Error('MongoDB URI not specified'); - } - - await connect(mongoUri, { + await connect(config.DB_URI, { authSource: 'admin', - user: process.env.DB_USERNAME, - pass: process.env.DB_PASSWORD, + user: config.DB_USERNAME, + pass: config.DB_PASSWORD, }); } diff --git a/services/user/src/model/user-model.ts b/services/user/src/model/user-model.ts index 9c22ba20db..0860017ce2 100644 --- a/services/user/src/model/user-model.ts +++ b/services/user/src/model/user-model.ts @@ -2,6 +2,11 @@ import mongoose, { Types } from 'mongoose'; const Schema = mongoose.Schema; +export enum Role { + Admin = 'admin', + User = 'user', +} + export interface User { id: Types.ObjectId; username: string; diff --git a/services/user/src/server.ts b/services/user/src/server.ts index e85572cc4c..474653552a 100644 --- a/services/user/src/server.ts +++ b/services/user/src/server.ts @@ -2,8 +2,9 @@ import http from 'http'; import index from './index'; import 'dotenv/config'; import { connectToDB } from './model/repository'; +import config from './config'; -const port = process.env.PORT || 8082; +const port = config.PORT; const server = http.createServer(index);