From 530189c4e6cb423d9ae43bedecfa4927a423bfcc Mon Sep 17 00:00:00 2001 From: Bespalov Tymofii Date: Thu, 29 Jan 2026 14:59:43 +0100 Subject: [PATCH 01/14] add body parser, req validation, router map, constants & vocab --- .env | 6 + .github/workflows/test.yml-template | 23 ++ .gitignore | 4 - package-lock.json | 422 ++++++++++++++++++++++++++-- package.json | 9 +- src/createServer.ts | 35 +++ src/db/db.ts | 31 ++ src/errors/errors.ts | 80 ++++++ src/index.js | 10 + src/middleware/bodyParser.ts | 64 +++++ src/middleware/index.ts | 3 + src/model/index.ts | 5 + src/model/socialAccounts.model.ts | 44 +++ src/model/token.model.ts | 45 +++ src/model/user.model.ts | 52 ++++ src/router/router.ts | 91 ++++++ src/static/endpoints.ts | 16 ++ src/static/index.ts | 5 + src/static/types.ts | 4 + src/static/vocab/dbVocab.ts | 37 +++ src/static/vocab/httpVocab.ts | 29 ++ src/utils/jwt.ts | 0 src/validation/index.ts | 3 + src/validation/validateRequest.ts | 40 +++ 24 files changed, 1026 insertions(+), 32 deletions(-) create mode 100644 .env create mode 100644 .github/workflows/test.yml-template create mode 100644 src/createServer.ts create mode 100644 src/db/db.ts create mode 100644 src/errors/errors.ts create mode 100644 src/middleware/bodyParser.ts create mode 100644 src/middleware/index.ts create mode 100644 src/model/index.ts create mode 100644 src/model/socialAccounts.model.ts create mode 100644 src/model/token.model.ts create mode 100644 src/model/user.model.ts create mode 100644 src/router/router.ts create mode 100644 src/static/endpoints.ts create mode 100644 src/static/index.ts create mode 100644 src/static/types.ts create mode 100644 src/static/vocab/dbVocab.ts create mode 100644 src/static/vocab/httpVocab.ts create mode 100644 src/utils/jwt.ts create mode 100644 src/validation/index.ts create mode 100644 src/validation/validateRequest.ts diff --git a/.env b/.env new file mode 100644 index 00000000..fb1f960e --- /dev/null +++ b/.env @@ -0,0 +1,6 @@ +PORT=5700, +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_USER=postgres +POSTGRES_PASSWORD=123 +POSTGRES_DB=postgres \ No newline at end of file diff --git a/.github/workflows/test.yml-template b/.github/workflows/test.yml-template new file mode 100644 index 00000000..bb13dfc4 --- /dev/null +++ b/.github/workflows/test.yml-template @@ -0,0 +1,23 @@ +name: Test + +on: + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm test diff --git a/.gitignore b/.gitignore index ed48a299..bd6a178a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,3 @@ node_modules # MacOS .DS_Store - -# env files -*.env -.env* diff --git a/package-lock.json b/package-lock.json index 288d83dd..1f3c5db3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,15 @@ "version": "1.0.0", "hasInstallScript": true, "license": "GPL-3.0", + "dependencies": { + "dotenv": "^17.2.3", + "pg": "^8.17.2", + "pg-hstore": "^2.3.4", + "sequelize": "^6.37.7" + }, "devDependencies": { "@mate-academy/eslint-config": "latest", - "@mate-academy/scripts": "^1.8.6", + "@mate-academy/scripts": "^2.1.3", "eslint": "^8.57.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-node": "^11.1.0", @@ -59,6 +65,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -1467,10 +1474,11 @@ } }, "node_modules/@mate-academy/scripts": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.8.6.tgz", - "integrity": "sha512-b4om/whj4G9emyi84ORE3FRZzCRwRIesr8tJHXa8EvJdOaAPDpzcJ8A0sFfMsWH9NUOVmOwkBtOXDu5eZZ00Ig==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-2.1.3.tgz", + "integrity": "sha512-a07wHTj/1QUK2Aac5zHad+sGw4rIvcNl5lJmJpAD7OxeSbnCdyI6RXUHwXhjF5MaVo9YHrJ0xVahyERS2IIyBQ==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/rest": "^17.11.2", "@types/get-port": "^4.2.0", @@ -1487,6 +1495,16 @@ "mate-scripts": "bin/mateScripts.js" } }, + "node_modules/@mate-academy/scripts/node_modules/dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -1536,7 +1554,6 @@ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz", "integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==", "dev": true, - "peer": true, "engines": { "node": ">= 18" } @@ -1565,7 +1582,6 @@ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", "dev": true, - "peer": true, "dependencies": { "@octokit/types": "^13.0.0", "universal-user-agent": "^7.0.2" @@ -1579,7 +1595,6 @@ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz", "integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==", "dev": true, - "peer": true, "dependencies": { "@octokit/request": "^9.0.0", "@octokit/types": "^13.0.0", @@ -1593,8 +1608,7 @@ "version": "22.2.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@octokit/plugin-paginate-rest": { "version": "2.21.3", @@ -1656,7 +1670,6 @@ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", "dev": true, - "peer": true, "dependencies": { "@octokit/endpoint": "^10.0.0", "@octokit/request-error": "^6.0.1", @@ -1672,7 +1685,6 @@ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.4.tgz", "integrity": "sha512-VpAhIUxwhWZQImo/dWAN/NpPqqojR6PSLgLYAituLM6U+ddx9hCioFGwBr5Mi+oi5CLeJkcAs3gJ0PYYzU6wUg==", "dev": true, - "peer": true, "dependencies": { "@octokit/types": "^13.0.0" }, @@ -1860,7 +1872,6 @@ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", "dev": true, - "peer": true, "dependencies": { "@octokit/openapi-types": "^22.2.0" } @@ -1968,6 +1979,15 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/get-port": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@types/get-port/-/get-port-4.2.0.tgz", @@ -2017,11 +2037,16 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.14.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -2032,6 +2057,12 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -2203,6 +2234,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2624,8 +2656,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", - "dev": true, - "peer": true + "dev": true }, "node_modules/brace-expansion": { "version": "1.1.11", @@ -2668,6 +2699,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001640", "electron-to-chromium": "^1.4.820", @@ -3041,7 +3073,6 @@ "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -3175,14 +3206,23 @@ } }, "node_modules/dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", - "dev": true, + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", "engines": { - "node": ">=10" + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, + "node_modules/dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==", + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.0.tgz", @@ -3381,6 +3421,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3436,6 +3477,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -3526,6 +3568,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", "dev": true, + "peer": true, "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -3603,6 +3646,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", "dev": true, + "peer": true, "dependencies": { "eslint-plugin-es": "^3.0.0", "eslint-utils": "^2.0.0", @@ -3653,6 +3697,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.3.1.tgz", "integrity": "sha512-bY2sGqyptzFBDLh/GMbAxfdJC+b0f23ME63FOE4+Jao0oZ3E1LEwFtWJX/1pGMJLiTtrSSern2CRM/g+dfc0eQ==", "dev": true, + "peer": true, "engines": { "node": ">=6" } @@ -3676,6 +3721,7 @@ "url": "https://feross.org/support" } ], + "peer": true, "peerDependencies": { "eslint": ">=5.0.0" } @@ -4570,6 +4616,15 @@ "node": ">=0.8.19" } }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ], + "license": "MIT" + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -5044,6 +5099,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -6740,6 +6796,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -6867,11 +6929,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -7262,6 +7344,108 @@ "node": ">=8" } }, + "node_modules/pg": { + "version": "8.17.2", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.17.2.tgz", + "integrity": "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw==", + "license": "MIT", + "peer": true, + "dependencies": { + "pg-connection-string": "^2.10.1", + "pg-pool": "^3.11.0", + "pg-protocol": "^1.11.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.10.1.tgz", + "integrity": "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw==", + "license": "MIT" + }, + "node_modules/pg-hstore": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/pg-hstore/-/pg-hstore-2.3.4.tgz", + "integrity": "sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA==", + "license": "MIT", + "dependencies": { + "underscore": "^1.13.1" + }, + "engines": { + "node": ">= 0.8.x" + } + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz", + "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", + "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", @@ -7362,6 +7546,45 @@ "node": ">= 0.4" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -7376,6 +7599,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -7593,6 +7817,12 @@ "node": ">=10" } }, + "node_modules/retry-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", + "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==", + "license": "MIT" + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -7686,6 +7916,89 @@ "semver": "bin/semver.js" } }, + "node_modules/sequelize": { + "version": "6.37.7", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz", + "integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/sequelize/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -7855,6 +8168,15 @@ "source-map": "^0.6.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -8096,6 +8418,12 @@ "node": ">=8.0" } }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==", + "license": "MIT" + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -8297,18 +8625,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/universal-user-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==", - "dev": true, - "peer": true + "dev": true }, "node_modules/universalify": { "version": "2.0.1", @@ -8358,6 +8690,15 @@ "punycode": "^2.1.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -8372,6 +8713,15 @@ "node": ">=10.12.0" } }, + "node_modules/validator": { + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -8580,6 +8930,15 @@ "which": "bin/which" } }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -8658,6 +9017,15 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 5e195a15..bf6a86e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "node_auth-app", "version": "1.0.0", + "type": "module", "description": "Auth app", "main": "src/index.js", "scripts": { @@ -17,7 +18,7 @@ "license": "GPL-3.0", "devDependencies": { "@mate-academy/eslint-config": "latest", - "@mate-academy/scripts": "^1.8.6", + "@mate-academy/scripts": "^2.1.3", "eslint": "^8.57.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-node": "^11.1.0", @@ -26,5 +27,11 @@ }, "mateAcademy": { "projectType": "javascript" + }, + "dependencies": { + "dotenv": "^17.2.3", + "pg": "^8.17.2", + "pg-hstore": "^2.3.4", + "sequelize": "^6.37.7" } } diff --git a/src/createServer.ts b/src/createServer.ts new file mode 100644 index 00000000..d5091062 --- /dev/null +++ b/src/createServer.ts @@ -0,0 +1,35 @@ +import http from 'http'; +import { dbSetup } from './db/db'; +import { validateRequest } from './validation'; +import { parseBody } from './middleware'; + +// Options preflight +// query params in req +// Unified api response +// rate limiter mdw +// cookies parser +// JSW tokens + + + +export async function createServer() { + await dbSetup(); + + return http.createServer(async (req, res) => { + try { + const { auth, schema, controller } = validateRequest(req); + + if (auth) { + const authHeader = req.headers.authorization; + validateAuth(authHeader) + } + + if (schema) { + const body = await parseBody(req); + validateBody(body, schema) + } + + const result = controller(body); + } catch {} + }); +} diff --git a/src/db/db.ts b/src/db/db.ts new file mode 100644 index 00000000..e6719bc5 --- /dev/null +++ b/src/db/db.ts @@ -0,0 +1,31 @@ +'use strict'; +import { Sequelize } from 'sequelize'; + +const { + POSTGRES_HOST, + POSTGRES_PORT, + POSTGRES_USER, + POSTGRES_PASSWORD, + POSTGRES_DB, +} = process.env; + +const client = new Sequelize({ + database: POSTGRES_DB || 'postgres', + username: POSTGRES_USER || 'postgres', + host: POSTGRES_HOST || 'localhost', + dialect: 'postgres', + port: Number(POSTGRES_PORT) || 5432, + password: POSTGRES_PASSWORD || '123', +}); + +const dbSetup = async () => { + const { User, Token, SocAcc } = await import('../model'); + + await User.sync({ alter: true }); + await Token.sync({ alter: true }); + await SocAcc.sync({ alter: true }); + + console.log('DB UP'); +} + +export { client, dbSetup } \ No newline at end of file diff --git a/src/errors/errors.ts b/src/errors/errors.ts new file mode 100644 index 00000000..ac67dcc2 --- /dev/null +++ b/src/errors/errors.ts @@ -0,0 +1,80 @@ +import { httpStatus } from '../static'; +import { type HTTPStatus } from '../static/types'; + +enum StaticErrorsMsgs { + nf = 'Not found', + na = 'Method not allowed', + ms = 'Exceeded body size limits', + br = 'Bad request', + bnj = 'Expected body to be JSON', + rc = 'Request cancelled', +} + +class ValidationError extends Error { + statusCode: HTTPStatus; + + constructor(message: string, statusCode: HTTPStatus) { + super(message); + this.statusCode = statusCode; + this.name = this.constructor.name; + } +} + +class NotFoundError extends Error { + statusCode: HTTPStatus; + + constructor() { + super(StaticErrorsMsgs.nf); + this.statusCode = httpStatus.nf; + this.name = this.constructor.name; + } +} + +class NotAllowedError extends Error { + statusCode: HTTPStatus; + + constructor() { + super(StaticErrorsMsgs.na); + this.statusCode = httpStatus.na; + this.name = this.constructor.name; + } +} + +class MaxSizeError extends Error { + statusCode: HTTPStatus; + + constructor() { + super(StaticErrorsMsgs.ms); + this.statusCode = httpStatus.br; + this.name = this.constructor.name; + } +} + +class BodyNotJSONError extends Error { + statusCode: HTTPStatus; + + constructor() { + super(StaticErrorsMsgs.bnj); + this.statusCode = httpStatus.br; + this.name = this.constructor.name; + } +} + +class RequestCancelledError extends Error { + statusCode: HTTPStatus; + + constructor() { + super(StaticErrorsMsgs.rc); + this.statusCode = httpStatus.br; + this.name = this.constructor.name; + } +} + +export { + ValidationError, + NotFoundError, + RequestCancelledError, + NotAllowedError, + MaxSizeError, + BodyNotJSONError +}; diff --git a/src/index.js b/src/index.js index ad9a93a7..9a261c20 100644 --- a/src/index.js +++ b/src/index.js @@ -1 +1,11 @@ +/* eslint-disable no-console */ 'use strict'; +import { createServer } from './createServer.ts'; + +const PORT = process.env.PORT || 5700; + +const server = await createServer(); + +server.listen(PORT, () => { + console.log(`Server is running on: ${PORT}`); +}); diff --git a/src/middleware/bodyParser.ts b/src/middleware/bodyParser.ts new file mode 100644 index 00000000..2832be59 --- /dev/null +++ b/src/middleware/bodyParser.ts @@ -0,0 +1,64 @@ +import http from 'http'; +import { BodyNotJSONError, MaxSizeError, RequestCancelledError } from '../errors/errors'; +const maxSizeLimit = 1048576; + +async function parseBody(req: http.IncomingMessage, maxSize = maxSizeLimit) { + return new Promise((resolve, reject) => { + let data = ''; + let size = 0; + let settled = false; + + req.on('error', (err) => { + if (!settled) { + settled = true; + reject(err); + } + }); + + req.on('data', (chunk) => { + if (settled) { + return; + } + + size += chunk.length; + + if (size > maxSize && !settled) { + settled = true; + req.destroy(); + reject(new MaxSizeError()); + + return; + } + + data += chunk; + }); + + req.on('end', () => { + if (settled) { + return; + } + + try { + settled = true; + + const res = JSON.parse(data); + + resolve(res); + } catch { + if (!settled) { + settled = true; + reject(new BodyNotJSONError()); + } + } + }); + + req.on('close', () => { + if (!settled) { + settled = true; + reject(new RequestCancelledError()); + } + }); + }); +} + +export { parseBody }; diff --git a/src/middleware/index.ts b/src/middleware/index.ts new file mode 100644 index 00000000..3ca14fef --- /dev/null +++ b/src/middleware/index.ts @@ -0,0 +1,3 @@ +import { parseBody } from './bodyParser'; + +export { parseBody } \ No newline at end of file diff --git a/src/model/index.ts b/src/model/index.ts new file mode 100644 index 00000000..e781bcbf --- /dev/null +++ b/src/model/index.ts @@ -0,0 +1,5 @@ +import User from './user.model'; +import SocAcc from './socialAccounts.model'; +import Token from './token.model'; + +export { User, SocAcc, Token }; diff --git a/src/model/socialAccounts.model.ts b/src/model/socialAccounts.model.ts new file mode 100644 index 00000000..fb605d8c --- /dev/null +++ b/src/model/socialAccounts.model.ts @@ -0,0 +1,44 @@ +import { client } from '../db/db'; +import { DataTypes } from 'sequelize'; +import { tnames, fnames, ent } from '../static' + +const nms = fnames[ent.soc]; + +const SocAcc = client.define( + 'SocialAccount', + { + [nms.id]: { + type: DataTypes.UUIDV4, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + }, + [nms.usr]: { + type: DataTypes.UUIDV4, + allowNull: false, + references: { + model: tnames[ent.usr], + key: fnames[ent.usr].id, + }, + onDelete: 'CASCADE', + }, + [nms.ggl]: { + type: DataTypes.STRING, + allowNull: true, + }, + [nms.gh]: { + type: DataTypes.STRING, + allowNull: true, + }, + [nms.fb]: { + type: DataTypes.STRING, + allowNull: true, + }, + }, + { + tableName: tnames[ent.soc], + createdAt: false, + updatedAt: false, + }, +); + +export default SocAcc; diff --git a/src/model/token.model.ts b/src/model/token.model.ts new file mode 100644 index 00000000..ca084bd0 --- /dev/null +++ b/src/model/token.model.ts @@ -0,0 +1,45 @@ +import { client } from '../db/db'; +import { DataTypes } from 'sequelize'; +import { tnames, fnames, ent } from '../static'; + +const nms = fnames[ent.tkn]; + +const Token = client.define( + 'Token', + { + [nms.id]: { + type: DataTypes.UUIDV4, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + }, + [nms.usr]: { + type: DataTypes.UUIDV4, + allowNull: false, + references: { + model: tnames[ent.usr], + key: fnames[ent.usr].id, + }, + onDelete: 'CASCADE', + }, + [nms.token]: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + [nms.type]: { + type: DataTypes.ENUM('activation', 'password_reset', 'refresh'), + allowNull: false, + }, + [nms.exp]: { + type: DataTypes.DATE, + allowNull: false, + }, + }, + { + tableName: tnames[ent.tkn], + createdAt: true, + updatedAt: false, + }, +); + +export default Token; diff --git a/src/model/user.model.ts b/src/model/user.model.ts new file mode 100644 index 00000000..26cf9440 --- /dev/null +++ b/src/model/user.model.ts @@ -0,0 +1,52 @@ +import { client } from '../db/db'; +import { DataTypes } from 'sequelize'; +import { tnames, fnames, ent } from '../static'; + +const nms = fnames[ent.usr]; + +const User = client.define( + 'User', + { + [nms.id]: { + type: DataTypes.UUIDV4, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + }, + [nms.name]: { + type: DataTypes.STRING, + allowNull: false, + }, + [nms.email]: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + [nms.pwd]: { + type: DataTypes.STRING, + allowNull: false, + }, + [nms.act]: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + createdAt: { + type: DataTypes.DATE, + field: 'created_at', + allowNull: false, + defaultValue: DataTypes.NOW, + }, + updatedAt: { + type: DataTypes.DATE, + field: 'updated_at', + allowNull: false, + defaultValue: DataTypes.NOW, + }, + }, + { + tableName: tnames[ent.usr], + timestamps: true, + }, +); + +export default User; diff --git a/src/router/router.ts b/src/router/router.ts new file mode 100644 index 00000000..6908eebd --- /dev/null +++ b/src/router/router.ts @@ -0,0 +1,91 @@ +import { NotAllowedError } from '../errors/errors'; +import { Endpoint, endpt } from '../static/endpoints'; +import { Method, mthd } from '../static/vocab/httpVocab'; + +type Entr = { + auth: boolean; + schema: false | Object; + controller: () => void; +}; + +const routeMap = { + [endpt.idx]: { + [mthd.get]: { auth: false, schema: false, + controller: () => true, + }, + }, + [endpt.auth]: { + [mthd.post]: { auth: false, schema: schema.auth, controller: () => true }, + }, + [endpt.snauth]: { + [mthd.post]: { auth: false, schema: schema.authSN, controller: () => true }, + }, + [endpt.lgout]: { + [mthd.patch]: { auth: true, schema: schema.lgout, controller: () => true }, + }, + [endpt.reg]: { + [mthd.post]: { + auth: false, + schema: schema.reg, + controller: () => true, + }, + }, + [endpt.act]: { + [mthd.get]: { + auth: false, + schema: schema.act, + controller: () => true, + }, + }, + [endpt.acc]: { + [mthd.get]: { + auth: true, + schema: false, // I can parse userId from JWT + controller: () => true, + }, + [mthd.del]: { + auth: true, + schema: false, + controller: () => true, + }, + [mthd.patch]: { + auth: true, + schema: schema.profUpd, + controller: () => true, + }, + }, + [endpt.pwd]: { + [mthd.patch]: { + auth: true, + schema: schema.pwdPtch, + controller: () => true, + }, + }, + [endpt.pwdReq]: { + [mthd.post]: { + auth: false, + schema: schema.pwdRes, + controller: () => true, + }, + }, + [endpt.pwdRes]: { + [mthd.post]: { + auth: false, + schema: schema.newPwd, + controller: () => true, + }, + }, +} as const; + +const getRouteConfig = (ep: Endpoint, mthd: Method): Entr => { + const epRoutes = routeMap[ep]; + const conf = epRoutes[mthd as keyof typeof epRoutes]; + + if (!conf) { + throw new NotAllowedError() + }; + + return conf; +} + +export { routeMap, getRouteConfig }; diff --git a/src/static/endpoints.ts b/src/static/endpoints.ts new file mode 100644 index 00000000..22647f0b --- /dev/null +++ b/src/static/endpoints.ts @@ -0,0 +1,16 @@ +const endpt = { + idx: '/', // get page + auth: '/auth', // post user + snauth: '/auth/social', // post user + lgout: '/auth/logout', // + reg: '/register', // techical endpoint '' '/' '/index.html' + act: '/register/activate', // technical endpoint '/login/hash/' + acc: '/profile', // profile, update profile, delete profile + pwd: '/profile/password', + pwdReq: '/password/reset-request', // res password + pwdRes: '/password/reset', +} as const; + +type Endpoint = typeof endpt[keyof typeof endpt] + +export { endpt, type Endpoint }; \ No newline at end of file diff --git a/src/static/index.ts b/src/static/index.ts new file mode 100644 index 00000000..e78bd5a8 --- /dev/null +++ b/src/static/index.ts @@ -0,0 +1,5 @@ +import { tnames, fnames, ent } from './vocab/dbVocab'; +import { pages, mthd, httpStatus } from './vocab/httpVocab'; +import { endpt } from './endpoints'; + +export { tnames, fnames, ent, pages, mthd, httpStatus, endpt }; diff --git a/src/static/types.ts b/src/static/types.ts new file mode 100644 index 00000000..fff79445 --- /dev/null +++ b/src/static/types.ts @@ -0,0 +1,4 @@ +import { type Endpoint } from "./endpoints"; +import { type Method, type HTTPStatus } from "./vocab/httpVocab"; + +export { type Endpoint, type Method, type HTTPStatus }; \ No newline at end of file diff --git a/src/static/vocab/dbVocab.ts b/src/static/vocab/dbVocab.ts new file mode 100644 index 00000000..a7bbc059 --- /dev/null +++ b/src/static/vocab/dbVocab.ts @@ -0,0 +1,37 @@ +const ent = { + usr: 'user', + soc: 'soc', + tkn: 'token', +} as const; + +const tnames = { + [ent.usr]: 'users', + [ent.soc]: 'social_accounts', + [ent.tkn]: 'tokens', +} as const; + +const fnames = { + [ent.usr]: { + id: 'id', + name: 'name', + email: 'email', + pwd: 'password', + act: 'activated', + }, + [ent.soc]: { + id: 'id', + usr: 'userId', + ggl: 'google', + gh: 'github', + fb: 'facebook', + }, + [ent.tkn]: { + id: 'id', + usr: 'userId', + token: 'token', + type: 'type', + exp: 'expiresAt', + }, +} as const; + +export { tnames, fnames, ent }; diff --git a/src/static/vocab/httpVocab.ts b/src/static/vocab/httpVocab.ts new file mode 100644 index 00000000..545a3e44 --- /dev/null +++ b/src/static/vocab/httpVocab.ts @@ -0,0 +1,29 @@ +const pages = { + idx: '/', + lgin: '/login', + prof: '/profile', + act: '/activate', +} as const; + + +const mthd = { + get: 'GET', + post: 'POST', + del: 'DELETE', + patch: 'PATCH', +} as const; + +const httpStatus = { + ok: 200, + cr: 201, + nc: 204, + br: 400, + nf: 404, + na: 405, + se: 500, +} as const; + +type Method = typeof mthd[keyof typeof mthd]; +type HTTPStatus = typeof httpStatus[keyof typeof httpStatus]; + +export { pages, mthd, httpStatus, type Method, type HTTPStatus }; \ No newline at end of file diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/validation/index.ts b/src/validation/index.ts new file mode 100644 index 00000000..3e41a6c1 --- /dev/null +++ b/src/validation/index.ts @@ -0,0 +1,3 @@ +import { validateRequest } from "./validateRequest"; + +export { validateRequest }; \ No newline at end of file diff --git a/src/validation/validateRequest.ts b/src/validation/validateRequest.ts new file mode 100644 index 00000000..39d9d7eb --- /dev/null +++ b/src/validation/validateRequest.ts @@ -0,0 +1,40 @@ +import http from 'http'; +import { ValidationError, NotFoundError } from '../errors/errors'; +import { httpStatus, mthd, endpt } from '../static'; +import { type Method, type Endpoint } from '../static/types'; +import { getRouteConfig } from '../router/router'; + +const validatePath = (path: string): path is Endpoint => { + return Object.values(endpt).some((el) => el === path); +}; + +const validateMethod = (method: string | undefined): method is Method => { + return Object.values(mthd).some((el) => el === method); +}; + +function validateRequest(req: http.IncomingMessage) { + // check if URL + if (!req.url) { + throw new ValidationError('Expected request URL', httpStatus.br); + } + const url = new URL(req.url, 'http://localhost'); + // validate endpoint + const endpoint = url.pathname; + if (!validatePath(endpoint)) { + throw new NotFoundError(); + } + + // validate method + const method = req.method; + if (!validateMethod(method)) { + throw new ValidationError(`Unknown method: ${method}`, httpStatus.br); + } + + // validate enpoint support method && get conf + + const config = getRouteConfig(endpoint, method); + + return config; +} + +export { validateRequest }; From 812bb0059b7475e8be2db64cadf37138ec4ea2fd Mon Sep 17 00:00:00 2001 From: Bespalov Tymofii Date: Fri, 30 Jan 2026 14:47:40 +0100 Subject: [PATCH 02/14] refactor enums, vocab, errors. Add JWT utils --- .env | 3 +- package-lock.json | 144 ++++++++++++++++++++++++++++++ package.json | 2 + src/createServer.ts | 15 ++-- src/db/db.ts | 19 ++-- src/errors/errors.ts | 69 +++----------- src/errors/index.ts | 3 + src/middleware/bodyParser.ts | 9 +- src/model/index.ts | 11 ++- src/model/socialAccounts.model.ts | 14 +-- src/model/token.model.ts | 12 +-- src/model/user.model.ts | 6 +- src/router/router.ts | 73 +++++---------- src/services/general.service.ts | 0 src/services/user.service.ts | 0 src/static/endpoints.ts | 16 ++-- src/static/index.ts | 4 +- src/static/vocab/dbVocab.ts | 31 +++---- src/utils/jwt.ts | 95 ++++++++++++++++++++ src/utils/types.ts | 0 src/validation/validateRequest.ts | 15 ++-- 21 files changed, 361 insertions(+), 180 deletions(-) create mode 100644 src/errors/index.ts create mode 100644 src/services/general.service.ts create mode 100644 src/services/user.service.ts create mode 100644 src/utils/types.ts diff --git a/.env b/.env index fb1f960e..74ea5c20 100644 --- a/.env +++ b/.env @@ -3,4 +3,5 @@ POSTGRES_HOST=localhost POSTGRES_PORT=5432 POSTGRES_USER=postgres POSTGRES_PASSWORD=123 -POSTGRES_DB=postgres \ No newline at end of file +POSTGRES_DB=postgres +JWT_SECRET_KEY=itlv&80f8 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1f3c5db3..f8e13748 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,9 @@ "hasInstallScript": true, "license": "GPL-3.0", "dependencies": { + "@types/jsonwebtoken": "^9.0.10", "dotenv": "^17.2.3", + "jsonwebtoken": "^9.0.3", "pg": "^8.17.2", "pg-hstore": "^2.3.4", "sequelize": "^6.37.7" @@ -2037,6 +2039,16 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -2722,6 +2734,12 @@ "node-int64": "^0.4.0" } }, + "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-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3223,6 +3241,15 @@ "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==", "license": "MIT" }, + "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/electron-to-chromium": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.0.tgz", @@ -6729,12 +6756,67 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "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/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/just-extend": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "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": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -6808,12 +6890,54 @@ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "dev": true }, + "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", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "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/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -7890,6 +8014,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-regex-test": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", diff --git a/package.json b/package.json index bf6a86e9..78ae1a3d 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,9 @@ "projectType": "javascript" }, "dependencies": { + "@types/jsonwebtoken": "^9.0.10", "dotenv": "^17.2.3", + "jsonwebtoken": "^9.0.3", "pg": "^8.17.2", "pg-hstore": "^2.3.4", "sequelize": "^6.37.7" diff --git a/src/createServer.ts b/src/createServer.ts index d5091062..a3452e86 100644 --- a/src/createServer.ts +++ b/src/createServer.ts @@ -2,6 +2,7 @@ import http from 'http'; import { dbSetup } from './db/db'; import { validateRequest } from './validation'; import { parseBody } from './middleware'; +import getRouteConfig from './router/router'; // Options preflight // query params in req @@ -10,26 +11,28 @@ import { parseBody } from './middleware'; // cookies parser // JSW tokens - - export async function createServer() { await dbSetup(); return http.createServer(async (req, res) => { try { - const { auth, schema, controller } = validateRequest(req); + const { endpoint, method } = validateRequest(req); + + const { auth, schema, controller } = getRouteConfig(endpoint, method); if (auth) { const authHeader = req.headers.authorization; - validateAuth(authHeader) + validateAuth(authHeader); } if (schema) { const body = await parseBody(req); - validateBody(body, schema) + validateBody(body, schema); } const result = controller(body); - } catch {} + } catch (error) { + + } }); } diff --git a/src/db/db.ts b/src/db/db.ts index e6719bc5..cd182cf8 100644 --- a/src/db/db.ts +++ b/src/db/db.ts @@ -1,5 +1,6 @@ 'use strict'; import { Sequelize } from 'sequelize'; +import { DBError } from '../errors'; const { POSTGRES_HOST, @@ -19,13 +20,15 @@ const client = new Sequelize({ }); const dbSetup = async () => { - const { User, Token, SocAcc } = await import('../model'); + try { + const { default: DB } = await import('../model'); - await User.sync({ alter: true }); - await Token.sync({ alter: true }); - await SocAcc.sync({ alter: true }); + await Promise.all( + Object.values(DB).map((model) => model.sync({ alter: true })), + ); + } catch (error) { + throw new DBError(`Database setup failed: ${error}`) + } +}; - console.log('DB UP'); -} - -export { client, dbSetup } \ No newline at end of file +export { client, dbSetup }; diff --git a/src/errors/errors.ts b/src/errors/errors.ts index ac67dcc2..2fb842ea 100644 --- a/src/errors/errors.ts +++ b/src/errors/errors.ts @@ -1,80 +1,37 @@ import { httpStatus } from '../static'; import { type HTTPStatus } from '../static/types'; -enum StaticErrorsMsgs { - nf = 'Not found', - na = 'Method not allowed', - ms = 'Exceeded body size limits', - br = 'Bad request', - bnj = 'Expected body to be JSON', - rc = 'Request cancelled', -} - -class ValidationError extends Error { +class RequestError extends Error { statusCode: HTTPStatus; constructor(message: string, statusCode: HTTPStatus) { - super(message); + super(); this.statusCode = statusCode; this.name = this.constructor.name; + this.message = message; } } -class NotFoundError extends Error { +class DBError extends Error { statusCode: HTTPStatus; - constructor() { - super(StaticErrorsMsgs.nf); - this.statusCode = httpStatus.nf; + constructor(message: string) { + super(); + this.statusCode = httpStatus.se; this.name = this.constructor.name; + this.message = message; } } -class NotAllowedError extends Error { +class AuthError extends Error { statusCode: HTTPStatus; - constructor() { - super(StaticErrorsMsgs.na); + constructor(message: string) { + super(); this.statusCode = httpStatus.na; this.name = this.constructor.name; + this.message = message; } } -class MaxSizeError extends Error { - statusCode: HTTPStatus; - - constructor() { - super(StaticErrorsMsgs.ms); - this.statusCode = httpStatus.br; - this.name = this.constructor.name; - } -} - -class BodyNotJSONError extends Error { - statusCode: HTTPStatus; - - constructor() { - super(StaticErrorsMsgs.bnj); - this.statusCode = httpStatus.br; - this.name = this.constructor.name; - } -} - -class RequestCancelledError extends Error { - statusCode: HTTPStatus; - - constructor() { - super(StaticErrorsMsgs.rc); - this.statusCode = httpStatus.br; - this.name = this.constructor.name; - } -} - -export { - ValidationError, - NotFoundError, - RequestCancelledError, - NotAllowedError, - MaxSizeError, - BodyNotJSONError -}; +export { RequestError, DBError, AuthError }; diff --git a/src/errors/index.ts b/src/errors/index.ts new file mode 100644 index 00000000..cacdab32 --- /dev/null +++ b/src/errors/index.ts @@ -0,0 +1,3 @@ +import { RequestError, DBError, AuthError } from './errors'; + +export { RequestError, DBError, AuthError }; diff --git a/src/middleware/bodyParser.ts b/src/middleware/bodyParser.ts index 2832be59..289cd905 100644 --- a/src/middleware/bodyParser.ts +++ b/src/middleware/bodyParser.ts @@ -1,5 +1,6 @@ import http from 'http'; -import { BodyNotJSONError, MaxSizeError, RequestCancelledError } from '../errors/errors'; +import { RequestError } from '../errors'; +import { httpStatus } from '../static'; const maxSizeLimit = 1048576; async function parseBody(req: http.IncomingMessage, maxSize = maxSizeLimit) { @@ -25,7 +26,7 @@ async function parseBody(req: http.IncomingMessage, maxSize = maxSizeLimit) { if (size > maxSize && !settled) { settled = true; req.destroy(); - reject(new MaxSizeError()); + reject(new RequestError('Body max size exceeded', httpStatus.br)); return; } @@ -47,7 +48,7 @@ async function parseBody(req: http.IncomingMessage, maxSize = maxSizeLimit) { } catch { if (!settled) { settled = true; - reject(new BodyNotJSONError()); + reject(new RequestError('Expected JSON', httpStatus.br)); } } }); @@ -55,7 +56,7 @@ async function parseBody(req: http.IncomingMessage, maxSize = maxSizeLimit) { req.on('close', () => { if (!settled) { settled = true; - reject(new RequestCancelledError()); + reject(new RequestError('Request cancelled', httpStatus.br)); } }); }); diff --git a/src/model/index.ts b/src/model/index.ts index e781bcbf..7443b5e4 100644 --- a/src/model/index.ts +++ b/src/model/index.ts @@ -1,5 +1,12 @@ import User from './user.model'; -import SocAcc from './socialAccounts.model'; +import SocialAccount from './socialAccounts.model'; import Token from './token.model'; +import { TNAMES } from '../static'; -export { User, SocAcc, Token }; +const DB = { + [TNAMES.USR]: User, + [TNAMES.SCN]: SocialAccount, + [TNAMES.TKN]: Token, +} + +export default DB; diff --git a/src/model/socialAccounts.model.ts b/src/model/socialAccounts.model.ts index fb605d8c..5a636319 100644 --- a/src/model/socialAccounts.model.ts +++ b/src/model/socialAccounts.model.ts @@ -1,10 +1,10 @@ import { client } from '../db/db'; import { DataTypes } from 'sequelize'; -import { tnames, fnames, ent } from '../static' +import { TNAMES, fnames } from '../static' -const nms = fnames[ent.soc]; +const nms = fnames[TNAMES.SCN] -const SocAcc = client.define( +const SocialAccount = client.define( 'SocialAccount', { [nms.id]: { @@ -16,8 +16,8 @@ const SocAcc = client.define( type: DataTypes.UUIDV4, allowNull: false, references: { - model: tnames[ent.usr], - key: fnames[ent.usr].id, + model: TNAMES.USR, + key: fnames[TNAMES.USR].id, }, onDelete: 'CASCADE', }, @@ -35,10 +35,10 @@ const SocAcc = client.define( }, }, { - tableName: tnames[ent.soc], + tableName: TNAMES.SCN, createdAt: false, updatedAt: false, }, ); -export default SocAcc; +export default SocialAccount; diff --git a/src/model/token.model.ts b/src/model/token.model.ts index ca084bd0..10e83351 100644 --- a/src/model/token.model.ts +++ b/src/model/token.model.ts @@ -1,8 +1,8 @@ import { client } from '../db/db'; import { DataTypes } from 'sequelize'; -import { tnames, fnames, ent } from '../static'; +import { TNAMES, fnames, TKN } from '../static'; -const nms = fnames[ent.tkn]; +const nms = fnames[TNAMES.TKN]; const Token = client.define( 'Token', @@ -16,8 +16,8 @@ const Token = client.define( type: DataTypes.UUIDV4, allowNull: false, references: { - model: tnames[ent.usr], - key: fnames[ent.usr].id, + model: TNAMES.USR, + key: fnames[TNAMES.USR].id, }, onDelete: 'CASCADE', }, @@ -27,7 +27,7 @@ const Token = client.define( unique: true, }, [nms.type]: { - type: DataTypes.ENUM('activation', 'password_reset', 'refresh'), + type: DataTypes.ENUM(TKN.ACT, TKN.PWR, TKN.RFR), allowNull: false, }, [nms.exp]: { @@ -36,7 +36,7 @@ const Token = client.define( }, }, { - tableName: tnames[ent.tkn], + tableName: TNAMES.TKN, createdAt: true, updatedAt: false, }, diff --git a/src/model/user.model.ts b/src/model/user.model.ts index 26cf9440..1c2ad5ce 100644 --- a/src/model/user.model.ts +++ b/src/model/user.model.ts @@ -1,8 +1,8 @@ import { client } from '../db/db'; import { DataTypes } from 'sequelize'; -import { tnames, fnames, ent } from '../static'; +import { TNAMES, fnames } from '../static'; -const nms = fnames[ent.usr]; +const nms = fnames[TNAMES.USR]; const User = client.define( 'User', @@ -44,7 +44,7 @@ const User = client.define( }, }, { - tableName: tnames[ent.usr], + tableName: TNAMES.USR, timestamps: true, }, ); diff --git a/src/router/router.ts b/src/router/router.ts index 6908eebd..4f01be99 100644 --- a/src/router/router.ts +++ b/src/router/router.ts @@ -1,6 +1,6 @@ -import { NotAllowedError } from '../errors/errors'; +import { RequestError } from '../errors'; import { Endpoint, endpt } from '../static/endpoints'; -import { Method, mthd } from '../static/vocab/httpVocab'; +import { httpStatus, Method, mthd } from '../static/vocab/httpVocab'; type Entr = { auth: boolean; @@ -10,70 +10,36 @@ type Entr = { const routeMap = { [endpt.idx]: { - [mthd.get]: { auth: false, schema: false, - controller: () => true, - }, + [mthd.get]: { auth: false, schema: false, controller: () => true }, }, [endpt.auth]: { - [mthd.post]: { auth: false, schema: schema.auth, controller: () => true }, + [mthd.post]: { auth: false, schema: sch.auth, controller: () => true }, }, [endpt.snauth]: { - [mthd.post]: { auth: false, schema: schema.authSN, controller: () => true }, + [mthd.post]: { auth: false, schema: sch.authSN, controller: () => true }, }, [endpt.lgout]: { - [mthd.patch]: { auth: true, schema: schema.lgout, controller: () => true }, + [mthd.patch]: { auth: true, schema: sch.lgout, controller: () => true }, }, [endpt.reg]: { - [mthd.post]: { - auth: false, - schema: schema.reg, - controller: () => true, - }, + [mthd.post]: { auth: false, schema: sch.reg, controller: () => true }, }, [endpt.act]: { - [mthd.get]: { - auth: false, - schema: schema.act, - controller: () => true, - }, + [mthd.get]: { auth: false, schema: sch.act, controller: () => true }, }, [endpt.acc]: { - [mthd.get]: { - auth: true, - schema: false, // I can parse userId from JWT - controller: () => true, - }, - [mthd.del]: { - auth: true, - schema: false, - controller: () => true, - }, - [mthd.patch]: { - auth: true, - schema: schema.profUpd, - controller: () => true, - }, + [mthd.get]: { auth: true, schema: false, controller: () => true }, + [mthd.del]: { auth: true, schema: false, controller: () => true }, + [mthd.patch]: { auth: true, schema: sch.profUpd, controller: () => true }, }, [endpt.pwd]: { - [mthd.patch]: { - auth: true, - schema: schema.pwdPtch, - controller: () => true, - }, + [mthd.patch]: { auth: true, schema: sch.pwdPtch, controller: () => true }, }, [endpt.pwdReq]: { - [mthd.post]: { - auth: false, - schema: schema.pwdRes, - controller: () => true, - }, + [mthd.post]: { auth: false, schema: sch.pwdRes, controller: () => true }, }, [endpt.pwdRes]: { - [mthd.post]: { - auth: false, - schema: schema.newPwd, - controller: () => true, - }, + [mthd.post]: { auth: false, schema: sch.newPwd, controller: () => true }, }, } as const; @@ -82,10 +48,13 @@ const getRouteConfig = (ep: Endpoint, mthd: Method): Entr => { const conf = epRoutes[mthd as keyof typeof epRoutes]; if (!conf) { - throw new NotAllowedError() - }; + throw new RequestError( + `Method ${mthd} is not supported for ${ep} endpoint`, + httpStatus.na, + ); + } return conf; -} +}; -export { routeMap, getRouteConfig }; +export default getRouteConfig; diff --git a/src/services/general.service.ts b/src/services/general.service.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/services/user.service.ts b/src/services/user.service.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/static/endpoints.ts b/src/static/endpoints.ts index 22647f0b..63538c38 100644 --- a/src/static/endpoints.ts +++ b/src/static/endpoints.ts @@ -1,13 +1,13 @@ const endpt = { - idx: '/', // get page - auth: '/auth', // post user - snauth: '/auth/social', // post user - lgout: '/auth/logout', // - reg: '/register', // techical endpoint '' '/' '/index.html' - act: '/register/activate', // technical endpoint '/login/hash/' - acc: '/profile', // profile, update profile, delete profile + idx: '/', + auth: '/auth', + snauth: '/auth/social', + lgout: '/auth/logout', + reg: '/register', + act: '/register/activate', + acc: '/profile', pwd: '/profile/password', - pwdReq: '/password/reset-request', // res password + pwdReq: '/password/reset-request', pwdRes: '/password/reset', } as const; diff --git a/src/static/index.ts b/src/static/index.ts index e78bd5a8..c0513a19 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -1,5 +1,5 @@ -import { tnames, fnames, ent } from './vocab/dbVocab'; +import { TNAMES, fnames, TKN } from './vocab/dbVocab'; import { pages, mthd, httpStatus } from './vocab/httpVocab'; import { endpt } from './endpoints'; -export { tnames, fnames, ent, pages, mthd, httpStatus, endpt }; +export { TNAMES, TKN, fnames, pages, mthd, httpStatus, endpt }; diff --git a/src/static/vocab/dbVocab.ts b/src/static/vocab/dbVocab.ts index a7bbc059..5f4405bd 100644 --- a/src/static/vocab/dbVocab.ts +++ b/src/static/vocab/dbVocab.ts @@ -1,31 +1,25 @@ -const ent = { - usr: 'user', - soc: 'soc', - tkn: 'token', -} as const; - -const tnames = { - [ent.usr]: 'users', - [ent.soc]: 'social_accounts', - [ent.tkn]: 'tokens', -} as const; +enum TNAMES { + USR = 'users', + SCN = 'social_accounts', + TKN = 'tokens', +}; const fnames = { - [ent.usr]: { + [TNAMES.USR]: { id: 'id', name: 'name', email: 'email', pwd: 'password', act: 'activated', }, - [ent.soc]: { + [TNAMES.SCN]: { id: 'id', usr: 'userId', ggl: 'google', gh: 'github', fb: 'facebook', }, - [ent.tkn]: { + [TNAMES.TKN]: { id: 'id', usr: 'userId', token: 'token', @@ -34,4 +28,11 @@ const fnames = { }, } as const; -export { tnames, fnames, ent }; +enum TKN { + ACT = 'activation', + RFR = 'refresh', + PWR = 'password_reset', + ACC = 'access', +} + +export { TNAMES, fnames, TKN }; diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts index e69de29b..f73a3b67 100644 --- a/src/utils/jwt.ts +++ b/src/utils/jwt.ts @@ -0,0 +1,95 @@ +import jwt from 'jsonwebtoken'; +import { RequestError } from '../errors'; +import { fnames, httpStatus, TKN, TNAMES } from '../static'; + +const SECRET_KEY = process.env.JWT_SECRET || 'your-secret-key'; + +const TOKEN_EXPIRY: Record = { + [TKN.ACC]: '15m', // access token - 15 минут + [TKN.RFR]: '7d', // refresh token - 7 дней + [TKN.ACT]: '24h', // activation token - 24 часа + [TKN.PWR]: '1h', // password reset token - 1 час +}; + +const nms = fnames[TNAMES.USR]; + +type JWTPayload = { + [nms.id]: string; + [nms.name]: string; + [nms.email]: string; +}; + +function signToken(payload: JWTPayload, type: TKN): string { + const expiry = TOKEN_EXPIRY[type]; + + return jwt.sign( + { + ...payload, + type, + }, + SECRET_KEY, + { expiresIn: expiry } as jwt.SignOptions, + ); +} + +/** + * Проверяет и декодирует JWT токен + */ +function verifyToken(token: string): JWTPayload & { type: TKN } { + try { + const decoded = jwt.verify(token, SECRET_KEY) as JWTPayload & { + type: TKN; + }; + + return decoded; + } catch (error) { + if (error instanceof jwt.TokenExpiredError) { + throw new RequestError('Token expired', httpStatus.br); + } + + if (error instanceof jwt.JsonWebTokenError) { + throw new RequestError('Invalid token', httpStatus.br); + } + + throw new RequestError('Token verification failed', httpStatus.br); + } +} + +/** + * Создает access токен для пользователя + * TODO: Заглушка - будет получать данные из БД + */ +async function createAccessToken(userId: string): Promise { + // TODO: Получить данные пользователя из БД + const user = { + [nms.id]: userId, + [nms.name]: 'Mock User', + [nms.email]: 'mock@example.com', + }; + + return signToken(user, TKN.ACC); +} + +async function createRefreshToken(userId: string): Promise { + // TODO: Получить данные пользователя из БД + const user = { + userId, + name: 'Mock User', + email: 'mock@example.com', + }; + + const token = signToken(user, TKN.RFR); + + // TODO: Сохранить refresh токен в БД (модель Token) + // await Token.create({ + // userId, + // token, + // type: 'refresh', + // expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + // }); + + return token; +} + +export { signToken, verifyToken, createAccessToken, createRefreshToken }; +export type { JWTPayload }; diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/validation/validateRequest.ts b/src/validation/validateRequest.ts index 39d9d7eb..8c1215de 100644 --- a/src/validation/validateRequest.ts +++ b/src/validation/validateRequest.ts @@ -1,8 +1,7 @@ import http from 'http'; -import { ValidationError, NotFoundError } from '../errors/errors'; +import { RequestError } from '../errors'; import { httpStatus, mthd, endpt } from '../static'; import { type Method, type Endpoint } from '../static/types'; -import { getRouteConfig } from '../router/router'; const validatePath = (path: string): path is Endpoint => { return Object.values(endpt).some((el) => el === path); @@ -15,26 +14,22 @@ const validateMethod = (method: string | undefined): method is Method => { function validateRequest(req: http.IncomingMessage) { // check if URL if (!req.url) { - throw new ValidationError('Expected request URL', httpStatus.br); + throw new RequestError('Expected request URL', httpStatus.br); } const url = new URL(req.url, 'http://localhost'); // validate endpoint const endpoint = url.pathname; if (!validatePath(endpoint)) { - throw new NotFoundError(); + throw new RequestError('Not found', httpStatus.nf); } // validate method const method = req.method; if (!validateMethod(method)) { - throw new ValidationError(`Unknown method: ${method}`, httpStatus.br); + throw new RequestError(`Unknown method: ${method}`, httpStatus.br); } - // validate enpoint support method && get conf - - const config = getRouteConfig(endpoint, method); - - return config; + return { endpoint, method }; } export { validateRequest }; From 01a75e52eba23bb279197ab637825b570de65367 Mon Sep 17 00:00:00 2001 From: Bespalov Tymofii Date: Sat, 31 Jan 2026 13:39:48 +0100 Subject: [PATCH 03/14] add body schemas, general services --- src/createServer.ts | 2 +- src/middleware/errorHandler.ts | 0 src/model/token.model.ts | 7 +++++- src/model/types.ts | 18 +++++++++++++ src/router/router.ts | 9 ++++--- src/services/general.service.ts | 42 +++++++++++++++++++++++++++++++ src/services/index.ts | 32 +++++++++++++++++++++++ src/services/user.service.ts | 5 ++++ src/validation/schemas.ts | 38 ++++++++++++++++++++++++++++ src/validation/validateRequest.ts | 15 ++++++++++- 10 files changed, 161 insertions(+), 7 deletions(-) create mode 100644 src/middleware/errorHandler.ts create mode 100644 src/model/types.ts create mode 100644 src/services/index.ts create mode 100644 src/validation/schemas.ts diff --git a/src/createServer.ts b/src/createServer.ts index a3452e86..112b3c7d 100644 --- a/src/createServer.ts +++ b/src/createServer.ts @@ -16,7 +16,7 @@ export async function createServer() { return http.createServer(async (req, res) => { try { - const { endpoint, method } = validateRequest(req); + const { endpoint, method, param } = validateRequest(req); const { auth, schema, controller } = getRouteConfig(endpoint, method); diff --git a/src/middleware/errorHandler.ts b/src/middleware/errorHandler.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/model/token.model.ts b/src/model/token.model.ts index 10e83351..a9ecbcd5 100644 --- a/src/model/token.model.ts +++ b/src/model/token.model.ts @@ -34,10 +34,15 @@ const Token = client.define( type: DataTypes.DATE, allowNull: false, }, + createdAt: { + type: DataTypes.DATE, + field: 'created_at', + allowNull: false, + defaultValue: DataTypes.NOW, + }, }, { tableName: TNAMES.TKN, - createdAt: true, updatedAt: false, }, ); diff --git a/src/model/types.ts b/src/model/types.ts new file mode 100644 index 00000000..3623ab89 --- /dev/null +++ b/src/model/types.ts @@ -0,0 +1,18 @@ +import { fnames, TNAMES } from '../static'; + +const usrNames = fnames[TNAMES.USR]; +const scnNames = fnames[TNAMES.SCN]; +const tknNames = fnames[TNAMES.TKN]; + +interface NormUser { + [usrNames.id]: string; + [usrNames.name]: string; + [usrNames.email]: string; + [usrNames.pwd]: string; +} + +interface DBUser extends NormUser { + [usrNames.act]: boolean; + createdAt: string; + updatedAt: string; +} diff --git a/src/router/router.ts b/src/router/router.ts index 4f01be99..71b8830e 100644 --- a/src/router/router.ts +++ b/src/router/router.ts @@ -1,10 +1,11 @@ import { RequestError } from '../errors'; import { Endpoint, endpt } from '../static/endpoints'; import { httpStatus, Method, mthd } from '../static/vocab/httpVocab'; +import sch from '../validation/schemas'; type Entr = { auth: boolean; - schema: false | Object; + schema: false | typeof sch[]; controller: () => void; }; @@ -19,13 +20,13 @@ const routeMap = { [mthd.post]: { auth: false, schema: sch.authSN, controller: () => true }, }, [endpt.lgout]: { - [mthd.patch]: { auth: true, schema: sch.lgout, controller: () => true }, + [mthd.patch]: { auth: true, schema: false, controller: () => true }, }, [endpt.reg]: { [mthd.post]: { auth: false, schema: sch.reg, controller: () => true }, }, [endpt.act]: { - [mthd.get]: { auth: false, schema: sch.act, controller: () => true }, + [mthd.get]: { auth: false, schema: false, controller: () => true }, }, [endpt.acc]: { [mthd.get]: { auth: true, schema: false, controller: () => true }, @@ -36,7 +37,7 @@ const routeMap = { [mthd.patch]: { auth: true, schema: sch.pwdPtch, controller: () => true }, }, [endpt.pwdReq]: { - [mthd.post]: { auth: false, schema: sch.pwdRes, controller: () => true }, + [mthd.post]: { auth: false, schema: sch.pwdReq, controller: () => true }, }, [endpt.pwdRes]: { [mthd.post]: { auth: false, schema: sch.newPwd, controller: () => true }, diff --git a/src/services/general.service.ts b/src/services/general.service.ts index e69de29b..86a57744 100644 --- a/src/services/general.service.ts +++ b/src/services/general.service.ts @@ -0,0 +1,42 @@ +import { DBError, RequestError } from '../errors'; +import DB from '../model'; +import { fnames, httpStatus, TNAMES } from '../static'; + +const get = async (table: TNAMES, key: string) => { + const item = await DB[table].findByPk(key); + + if (!item) { + throw new RequestError(`Id not found: ${key}`, httpStatus.nf); + } + + return item; +}; + +const del = async (table: TNAMES, key: string) => { + const item = await get(table, key); + + item.destroy(); +}; + +const getByParam = async ( + table: T, + field: (typeof fnames)[T][keyof (typeof fnames)[T]], + query: string, +) => { + const item = await DB[table].findOne({ where: { [field as string]: query } }); + + if (!item) { + throw new RequestError( + `${table} with ${field}=${query} not found`, + httpStatus.nf, + ); + } + + return item; +}; + +const create = async (table: TNAMES, obj) => { + +} + +export { get, del, getByParam }; diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 00000000..7067da3c --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1,32 @@ +import { DBError } from '../errors'; +import { fnames, TNAMES } from '../static'; +import { del, get, getByParam } from './general.service'; + +function asyncDBHandler( + fn: (...args: TArgs) => Promise, +): (...args: TArgs) => Promise { + return async (...args: TArgs): Promise => { + try { + return await fn(...args); + } catch (error) { + throw new DBError(`Database operation failed: ${error}`); + } + }; +} + +const services = { + [TNAMES.USR]: { + getById: asyncDBHandler((id: string) => get(TNAMES.USR, id)), + getByEmail: asyncDBHandler((email: string) => getByParam(TNAMES.USR, fnames[TNAMES.USR].email, email)), + delete: asyncDBHandler((id: string) => del(TNAMES.USR, id)), + create: asyncDBHandler + }, + [TNAMES.SCN]: { + delete: asyncDBHandler((id: string) => del(TNAMES.SCN, id)), + }, + [TNAMES.TKN]: { + delete: asyncDBHandler((id: string) => del(TNAMES.TKN, id)), + }, +}; + +export default services; \ No newline at end of file diff --git a/src/services/user.service.ts b/src/services/user.service.ts index e69de29b..4643aa41 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -0,0 +1,5 @@ +import { RequestError } from "../errors"; +import DB from "../model" +import { fnames, httpStatus, TNAMES } from "../static" + + diff --git a/src/validation/schemas.ts b/src/validation/schemas.ts new file mode 100644 index 00000000..416ce1e9 --- /dev/null +++ b/src/validation/schemas.ts @@ -0,0 +1,38 @@ +import { endpt, fnames, TNAMES } from '../static'; + +const usrFNames = fnames[TNAMES.USR]; +const tkFNames = fnames[TNAMES.TKN]; + +const sch = { + auth: { + [usrFNames.email]: 'string', + [usrFNames.pwd]: 'string', + }, + authSN: {}, + reg: { + [usrFNames.name]: 'string', + [usrFNames.email]: 'string', + [usrFNames.pwd]: 'string', + }, + profUpd: { + [usrFNames.name]: 'string', + [usrFNames.email]: 'string', + [usrFNames.pwd]: 'string', + confirmation: 'string', + }, + pwdPtch: { + oldPwd: 'string', + newPwd: 'string', + confirmation: 'string', + }, + pwdReq: { + [usrFNames.email]: 'string', + }, + newPwd: { + [tkFNames.token]: 'string', + newPassword: 'string', + confirmation: 'string', + }, +} as const; + +export default sch; \ No newline at end of file diff --git a/src/validation/validateRequest.ts b/src/validation/validateRequest.ts index 8c1215de..749b8e87 100644 --- a/src/validation/validateRequest.ts +++ b/src/validation/validateRequest.ts @@ -3,6 +3,10 @@ import { RequestError } from '../errors'; import { httpStatus, mthd, endpt } from '../static'; import { type Method, type Endpoint } from '../static/types'; +const REQUIRED_PARAMS: Partial> = { + [endpt.act]: 'token', +}; + const validatePath = (path: string): path is Endpoint => { return Object.values(endpt).some((el) => el === path); }; @@ -29,7 +33,16 @@ function validateRequest(req: http.IncomingMessage) { throw new RequestError(`Unknown method: ${method}`, httpStatus.br); } - return { endpoint, method }; + if (!(endpoint in REQUIRED_PARAMS)) { + return { endpoint, method, param: null }; + } + const param = url.searchParams.get(REQUIRED_PARAMS[endpoint] as string); + + if (!param) { + throw new RequestError('Missing required token parameter', httpStatus.br); + } + + return { endpoint, method, param }; } export { validateRequest }; From 50157c64efa951f6f25379a94f7f50ade666f343 Mon Sep 17 00:00:00 2001 From: Bespalov Tymofii Date: Tue, 3 Feb 2026 11:45:03 +0100 Subject: [PATCH 04/14] processing controllers --- package-lock.json | 45 +++++++++++++++++++++++++ package.json | 2 ++ src/controllers/auth.ctrlr.ts | 35 +++++++++++++++++++ src/controllers/idx.ctrlr.ts | 12 +++++++ src/controllers/index.ts | 3 ++ src/createServer.ts | 13 ++++---- src/middleware/index.ts | 3 +- src/model/types.ts | 18 ---------- src/router/router.ts | 6 +++- src/services/general.service.ts | 24 ++++++++------ src/services/index.ts | 35 ++----------------- src/services/token.service.ts | 35 +++++++++++++++++++ src/services/types.ts | 36 ++++++++++++++++++++ src/services/user.service.ts | 14 ++++++-- src/static/endpoints.ts | 1 + src/static/vocab/httpVocab.ts | 10 +----- src/utils/dbHandler.ts | 15 +++++++++ src/utils/index.ts | 3 ++ src/utils/jwt.ts | 59 +++++++++++---------------------- src/validation/schemas.ts | 26 +++++++++------ src/validation/validateAuth.ts | 33 ++++++++++++++++++ 21 files changed, 296 insertions(+), 132 deletions(-) create mode 100644 src/controllers/auth.ctrlr.ts create mode 100644 src/controllers/idx.ctrlr.ts create mode 100644 src/controllers/index.ts delete mode 100644 src/model/types.ts create mode 100644 src/services/token.service.ts create mode 100644 src/services/types.ts create mode 100644 src/utils/dbHandler.ts create mode 100644 src/utils/index.ts create mode 100644 src/validation/validateAuth.ts diff --git a/package-lock.json b/package-lock.json index f8e13748..c00822b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,9 @@ "hasInstallScript": true, "license": "GPL-3.0", "dependencies": { + "@types/bcrypt": "^6.0.0", "@types/jsonwebtoken": "^9.0.10", + "bcrypt": "^6.0.0", "dotenv": "^17.2.3", "jsonwebtoken": "^9.0.3", "pg": "^8.17.2", @@ -1981,6 +1983,15 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -2664,6 +2675,20 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/before-after-hook": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", @@ -7122,6 +7147,15 @@ "@sinonjs/commons": "^1.7.0" } }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -7142,6 +7176,17 @@ } } }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", diff --git a/package.json b/package.json index 78ae1a3d..c46621bc 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,9 @@ "projectType": "javascript" }, "dependencies": { + "@types/bcrypt": "^6.0.0", "@types/jsonwebtoken": "^9.0.10", + "bcrypt": "^6.0.0", "dotenv": "^17.2.3", "jsonwebtoken": "^9.0.3", "pg": "^8.17.2", diff --git a/src/controllers/auth.ctrlr.ts b/src/controllers/auth.ctrlr.ts new file mode 100644 index 00000000..25bbc65e --- /dev/null +++ b/src/controllers/auth.ctrlr.ts @@ -0,0 +1,35 @@ +import http from 'http'; +import bcrypt from 'bcrypt'; +import { RequestError } from '../errors'; +import { tknServices, usrServices } from '../services'; +import { httpStatus, TNAMES, fnames, TKN } from '../static'; +import sch from '../validation/schemas'; +import { createAccessToken } from '../utils/jwt'; + +const auth = async (res: http.ServerResponse, body: typeof sch.auth) => { + const { email, password } = body; + const nms = fnames[TNAMES.USR]; + + const usr = await usrServices.getByEmail(email); + + if (!usr) { + throw new RequestError(`User with email ${email} not found`, httpStatus.nf); + } + + const isValid = await bcrypt.compare(password, usr.password); + + if (!isValid) { + throw new RequestError(`Wrong password`, httpStatus.na); + } + const { id, name } = usr; + + const payload = { id, name, email }; + + const token = await createAccessToken(payload); + const ref = await createAccessToken(payload); + + tknServices.create(id, ref.token, TKN.RFR, ref.expiry); + + + // setCors(); +}; diff --git a/src/controllers/idx.ctrlr.ts b/src/controllers/idx.ctrlr.ts new file mode 100644 index 00000000..5e28bb79 --- /dev/null +++ b/src/controllers/idx.ctrlr.ts @@ -0,0 +1,12 @@ +import http from 'http'; + +const index = '

some-html

'; + +const idx = (res: http.ServerResponse) => { + // setCors(); + res.statusCode = 200; + res.setHeader('Content-Type', 'text/html'); + res.end(index); +}; + +export default idx; \ No newline at end of file diff --git a/src/controllers/index.ts b/src/controllers/index.ts new file mode 100644 index 00000000..28a7960c --- /dev/null +++ b/src/controllers/index.ts @@ -0,0 +1,3 @@ +import idx from './idx.ctrlr'; + +export { idx }; \ No newline at end of file diff --git a/src/createServer.ts b/src/createServer.ts index 112b3c7d..9fad1caa 100644 --- a/src/createServer.ts +++ b/src/createServer.ts @@ -1,11 +1,10 @@ import http from 'http'; import { dbSetup } from './db/db'; import { validateRequest } from './validation'; -import { parseBody } from './middleware'; +import { parseBody, validateAuth } from './middleware'; import getRouteConfig from './router/router'; // Options preflight -// query params in req // Unified api response // rate limiter mdw // cookies parser @@ -20,19 +19,19 @@ export async function createServer() { const { auth, schema, controller } = getRouteConfig(endpoint, method); + let body = null; if (auth) { const authHeader = req.headers.authorization; - validateAuth(authHeader); + + body = validateAuth(authHeader); } if (schema) { - const body = await parseBody(req); + body = await parseBody(req); validateBody(body, schema); } const result = controller(body); - } catch (error) { - - } + } catch (error) {} }); } diff --git a/src/middleware/index.ts b/src/middleware/index.ts index 3ca14fef..ec17521a 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -1,3 +1,4 @@ import { parseBody } from './bodyParser'; +import { validateAuth, authMiddleware } from './auth'; -export { parseBody } \ No newline at end of file +export { parseBody, validateAuth, authMiddleware }; \ No newline at end of file diff --git a/src/model/types.ts b/src/model/types.ts deleted file mode 100644 index 3623ab89..00000000 --- a/src/model/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { fnames, TNAMES } from '../static'; - -const usrNames = fnames[TNAMES.USR]; -const scnNames = fnames[TNAMES.SCN]; -const tknNames = fnames[TNAMES.TKN]; - -interface NormUser { - [usrNames.id]: string; - [usrNames.name]: string; - [usrNames.email]: string; - [usrNames.pwd]: string; -} - -interface DBUser extends NormUser { - [usrNames.act]: boolean; - createdAt: string; - updatedAt: string; -} diff --git a/src/router/router.ts b/src/router/router.ts index 71b8830e..a259cf6c 100644 --- a/src/router/router.ts +++ b/src/router/router.ts @@ -1,3 +1,4 @@ +import { idx } from '../controllers'; import { RequestError } from '../errors'; import { Endpoint, endpt } from '../static/endpoints'; import { httpStatus, Method, mthd } from '../static/vocab/httpVocab'; @@ -11,11 +12,14 @@ type Entr = { const routeMap = { [endpt.idx]: { - [mthd.get]: { auth: false, schema: false, controller: () => true }, + [mthd.get]: { auth: false, schema: false, controller: idx }, }, [endpt.auth]: { [mthd.post]: { auth: false, schema: sch.auth, controller: () => true }, }, + [endpt.refr]: { + [mthd.post]: { auth: false, schema: sch.refr, controller: () => true }, + }, [endpt.snauth]: { [mthd.post]: { auth: false, schema: sch.authSN, controller: () => true }, }, diff --git a/src/services/general.service.ts b/src/services/general.service.ts index 86a57744..aa4d96ad 100644 --- a/src/services/general.service.ts +++ b/src/services/general.service.ts @@ -1,28 +1,34 @@ import { DBError, RequestError } from '../errors'; import DB from '../model'; import { fnames, httpStatus, TNAMES } from '../static'; +import { ObjectMapType } from './types'; -const get = async (table: TNAMES, key: string) => { +const get = async (table: T, key: string): Promise => { const item = await DB[table].findByPk(key); if (!item) { throw new RequestError(`Id not found: ${key}`, httpStatus.nf); } - return item; + return item.toJSON(); }; -const del = async (table: TNAMES, key: string) => { - const item = await get(table, key); +const del = async (table: TNAMES, key: string): Promise => { + // Нужен Model instance для destroy, не plain object + const item = await DB[table].findByPk(key); - item.destroy(); + if (!item) { + throw new RequestError(`Id not found: ${key}`, httpStatus.nf); + } + + await item.destroy(); }; const getByParam = async ( table: T, field: (typeof fnames)[T][keyof (typeof fnames)[T]], query: string, -) => { +): Promise => { const item = await DB[table].findOne({ where: { [field as string]: query } }); if (!item) { @@ -32,11 +38,7 @@ const getByParam = async ( ); } - return item; + return item.toJSON(); }; -const create = async (table: TNAMES, obj) => { - -} - export { get, del, getByParam }; diff --git a/src/services/index.ts b/src/services/index.ts index 7067da3c..10812c72 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,32 +1,3 @@ -import { DBError } from '../errors'; -import { fnames, TNAMES } from '../static'; -import { del, get, getByParam } from './general.service'; - -function asyncDBHandler( - fn: (...args: TArgs) => Promise, -): (...args: TArgs) => Promise { - return async (...args: TArgs): Promise => { - try { - return await fn(...args); - } catch (error) { - throw new DBError(`Database operation failed: ${error}`); - } - }; -} - -const services = { - [TNAMES.USR]: { - getById: asyncDBHandler((id: string) => get(TNAMES.USR, id)), - getByEmail: asyncDBHandler((email: string) => getByParam(TNAMES.USR, fnames[TNAMES.USR].email, email)), - delete: asyncDBHandler((id: string) => del(TNAMES.USR, id)), - create: asyncDBHandler - }, - [TNAMES.SCN]: { - delete: asyncDBHandler((id: string) => del(TNAMES.SCN, id)), - }, - [TNAMES.TKN]: { - delete: asyncDBHandler((id: string) => del(TNAMES.TKN, id)), - }, -}; - -export default services; \ No newline at end of file +import usrServices from './user.service'; +import tknServices from './token.service' +export { usrServices, tknServices }; diff --git a/src/services/token.service.ts b/src/services/token.service.ts new file mode 100644 index 00000000..8a1faadc --- /dev/null +++ b/src/services/token.service.ts @@ -0,0 +1,35 @@ +import DB from '../model'; +import { TKN, TNAMES, fnames } from '../static'; +import { asyncDBHandler } from '../utils'; +import { ObjectMapType } from './types'; + +const tkFNames = fnames[TNAMES.TKN]; + +const createToken = async ( + userId: string, + token: string, + type: Exclude, + expiresAt: string, +): Promise => { + const newToken = await DB[TNAMES.TKN].create({ + [tkFNames.usr]: userId, + [tkFNames.token]: token, + [tkFNames.type]: type, + [tkFNames.exp]: expiresAt, + }); + + return newToken.toJSON(); +}; + +const tknServices = { + create: asyncDBHandler( + ( + userId: string, + token: string, + type: Exclude, + expiresAt: string, + ) => createToken(userId, token, type, expiresAt), + ), +}; + +export default tknServices; diff --git a/src/services/types.ts b/src/services/types.ts new file mode 100644 index 00000000..0f77199e --- /dev/null +++ b/src/services/types.ts @@ -0,0 +1,36 @@ +import { TKN, TNAMES } from "../static"; + +interface UserAttributes { + id: string; + name: string; + email: string; + password: string; + activated: boolean; + createdAt: Date; + updatedAt: Date; +} + +interface TokenAttr { + id: string, + userId: string, + token: string, + type: TKN, + expiresAt: Date, + createdAt: Date, +} + +interface SocialNetworkAttr { + id: string, + userId: string, + google: string | null, + github: string | null, + facebook: string | null, +} + +type ObjectMapType = { + [TNAMES.USR]: UserAttributes; + [TNAMES.SCN]: SocialNetworkAttr; + [TNAMES.TKN]: TokenAttr; +} + +export { type ObjectMapType }; \ No newline at end of file diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 4643aa41..1c4ef1ba 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -1,5 +1,13 @@ -import { RequestError } from "../errors"; -import DB from "../model" -import { fnames, httpStatus, TNAMES } from "../static" +import { fnames, TNAMES } from '../static'; +import { asyncDBHandler } from '../utils'; +import { del, get, getByParam } from './general.service'; +const usrServices = { + getById: asyncDBHandler((id: string) => get(TNAMES.USR, id)), + getByEmail: asyncDBHandler((email: string) => + getByParam(TNAMES.USR, fnames[TNAMES.USR].email, email), + ), + delete: asyncDBHandler((id: string) => del(TNAMES.USR, id)), +}; +export default usrServices; diff --git a/src/static/endpoints.ts b/src/static/endpoints.ts index 63538c38..64c614bb 100644 --- a/src/static/endpoints.ts +++ b/src/static/endpoints.ts @@ -1,6 +1,7 @@ const endpt = { idx: '/', auth: '/auth', + refr: '/auth/resresh', snauth: '/auth/social', lgout: '/auth/logout', reg: '/register', diff --git a/src/static/vocab/httpVocab.ts b/src/static/vocab/httpVocab.ts index 545a3e44..e558e970 100644 --- a/src/static/vocab/httpVocab.ts +++ b/src/static/vocab/httpVocab.ts @@ -1,11 +1,3 @@ -const pages = { - idx: '/', - lgin: '/login', - prof: '/profile', - act: '/activate', -} as const; - - const mthd = { get: 'GET', post: 'POST', @@ -26,4 +18,4 @@ const httpStatus = { type Method = typeof mthd[keyof typeof mthd]; type HTTPStatus = typeof httpStatus[keyof typeof httpStatus]; -export { pages, mthd, httpStatus, type Method, type HTTPStatus }; \ No newline at end of file +export { mthd, httpStatus, type Method, type HTTPStatus }; \ No newline at end of file diff --git a/src/utils/dbHandler.ts b/src/utils/dbHandler.ts new file mode 100644 index 00000000..b59b3ade --- /dev/null +++ b/src/utils/dbHandler.ts @@ -0,0 +1,15 @@ +import { DBError } from "../errors"; + +function asyncDBHandler( + fn: (...args: TArgs) => Promise, +): (...args: TArgs) => Promise { + return async (...args: TArgs): Promise => { + try { + return await fn(...args); + } catch (error) { + throw new DBError(`Database operation failed: ${error}`); + } + }; +} + +export default asyncDBHandler; \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 00000000..47bb8e9e --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,3 @@ +import asyncDBHandler from './dbHandler'; + +export { asyncDBHandler }; \ No newline at end of file diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts index f73a3b67..2f3ca76a 100644 --- a/src/utils/jwt.ts +++ b/src/utils/jwt.ts @@ -5,10 +5,10 @@ import { fnames, httpStatus, TKN, TNAMES } from '../static'; const SECRET_KEY = process.env.JWT_SECRET || 'your-secret-key'; const TOKEN_EXPIRY: Record = { - [TKN.ACC]: '15m', // access token - 15 минут - [TKN.RFR]: '7d', // refresh token - 7 дней - [TKN.ACT]: '24h', // activation token - 24 часа - [TKN.PWR]: '1h', // password reset token - 1 час + [TKN.ACC]: '15m', // access token - 15 минут + [TKN.RFR]: '7d', // refresh token - 7 дней + [TKN.ACT]: '24h', // activation token - 24 часа + [TKN.PWR]: '1h', // password reset token - 1 час }; const nms = fnames[TNAMES.USR]; @@ -19,10 +19,13 @@ type JWTPayload = { [nms.email]: string; }; -function signToken(payload: JWTPayload, type: TKN): string { +function signToken( + payload: JWTPayload, + type: TKN, +): { expiry: string; token: string } { const expiry = TOKEN_EXPIRY[type]; - return jwt.sign( + const token = jwt.sign( { ...payload, type, @@ -30,6 +33,8 @@ function signToken(payload: JWTPayload, type: TKN): string { SECRET_KEY, { expiresIn: expiry } as jwt.SignOptions, ); + + return { expiry, token }; } /** @@ -44,7 +49,7 @@ function verifyToken(token: string): JWTPayload & { type: TKN } { return decoded; } catch (error) { if (error instanceof jwt.TokenExpiredError) { - throw new RequestError('Token expired', httpStatus.br); + throw new RequestError('Token has expired', httpStatus.na); } if (error instanceof jwt.JsonWebTokenError) { @@ -55,40 +60,16 @@ function verifyToken(token: string): JWTPayload & { type: TKN } { } } -/** - * Создает access токен для пользователя - * TODO: Заглушка - будет получать данные из БД - */ -async function createAccessToken(userId: string): Promise { - // TODO: Получить данные пользователя из БД - const user = { - [nms.id]: userId, - [nms.name]: 'Mock User', - [nms.email]: 'mock@example.com', - }; - - return signToken(user, TKN.ACC); +async function createAccessToken( + payload: JWTPayload, +): Promise<{ expiry: string; token: string }> { + return signToken(payload, TKN.ACC); } -async function createRefreshToken(userId: string): Promise { - // TODO: Получить данные пользователя из БД - const user = { - userId, - name: 'Mock User', - email: 'mock@example.com', - }; - - const token = signToken(user, TKN.RFR); - - // TODO: Сохранить refresh токен в БД (модель Token) - // await Token.create({ - // userId, - // token, - // type: 'refresh', - // expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), - // }); - - return token; +async function createRefreshToken( + payload: JWTPayload, +): Promise<{ expiry: string; token: string }> { + return signToken(payload, TKN.RFR); } export { signToken, verifyToken, createAccessToken, createRefreshToken }; diff --git a/src/validation/schemas.ts b/src/validation/schemas.ts index 416ce1e9..de23be50 100644 --- a/src/validation/schemas.ts +++ b/src/validation/schemas.ts @@ -3,17 +3,21 @@ import { endpt, fnames, TNAMES } from '../static'; const usrFNames = fnames[TNAMES.USR]; const tkFNames = fnames[TNAMES.TKN]; +const authSch = { + [usrFNames.email]: 'string', + [usrFNames.pwd]: 'string', +}; + +const regSch = { + [usrFNames.name]: 'string', + [usrFNames.email]: 'string', + [usrFNames.pwd]: 'string', +} as const; const sch = { - auth: { - [usrFNames.email]: 'string', - [usrFNames.pwd]: 'string', - }, + auth: authSch, authSN: {}, - reg: { - [usrFNames.name]: 'string', - [usrFNames.email]: 'string', - [usrFNames.pwd]: 'string', - }, + refr: {}, + reg: regSch, profUpd: { [usrFNames.name]: 'string', [usrFNames.email]: 'string', @@ -33,6 +37,6 @@ const sch = { newPassword: 'string', confirmation: 'string', }, -} as const; +}; -export default sch; \ No newline at end of file +export default sch; diff --git a/src/validation/validateAuth.ts b/src/validation/validateAuth.ts new file mode 100644 index 00000000..a443e2ae --- /dev/null +++ b/src/validation/validateAuth.ts @@ -0,0 +1,33 @@ +import { AuthError } from "../errors"; +import { TKN } from "../static"; +import { JWTPayload, verifyToken } from "../utils/jwt"; + +function validateAuth(authHeader: string | undefined): JWTPayload { + if (!authHeader) { + throw new AuthError('Authorization header is required'); + } + + const parts = authHeader.split(' '); + + if (parts.length !== 2 || parts[0] !== 'Bearer') { + throw new AuthError( + 'Invalid authorization header format. Expected: Bearer ', + ); + } + + const token = parts[1]; + + if (!token) { + throw new AuthError('Token is missing'); + } + + const decoded = verifyToken(token); + + if (decoded.type !== TKN.ACC) { + throw new AuthError('Invalid token type. Access token required'); + } + + return decoded; +} + +export default validateAuth; \ No newline at end of file From f3ff47e6bdcd7b0482b27dceec92f4f78aecc79f Mon Sep 17 00:00:00 2001 From: Bespalov Tymofii Date: Wed, 4 Feb 2026 16:27:27 +0100 Subject: [PATCH 05/14] add controllers, add body validation via zod, fixed type issues --- package-lock.json | 12 ++- package.json | 3 +- src/controllers/auth.ctrlr.ts | 123 ++++++++++++++++++++++++++---- src/controllers/index.ts | 3 +- src/createServer.ts | 29 ++++--- src/errors/errors.ts | 33 ++++---- src/errors/index.ts | 4 +- src/middleware/errorHandler.ts | 17 +++++ src/middleware/index.ts | 4 +- src/router/router.ts | 41 +++++----- src/services/general.service.ts | 10 +-- src/services/index.ts | 4 +- src/services/token.service.ts | 40 +++++----- src/services/types.ts | 16 ++-- src/static/constants.ts | 10 +++ src/static/endpoints.ts | 2 +- src/static/index.ts | 5 +- src/static/types.ts | 14 +++- src/static/vocab/httpVocab.ts | 1 + src/utils/cookies.ts | 19 +++++ src/utils/index.ts | 6 +- src/utils/jwt.ts | 63 +++++---------- src/utils/types.ts | 11 +++ src/validation/index.ts | 7 +- src/validation/schemas.ts | 87 +++++++++++++-------- src/validation/validateAuth.ts | 33 -------- src/validation/validateBody.ts | 19 +++++ src/validation/validateRequest.ts | 2 +- 28 files changed, 393 insertions(+), 225 deletions(-) create mode 100644 src/static/constants.ts create mode 100644 src/utils/cookies.ts delete mode 100644 src/validation/validateAuth.ts create mode 100644 src/validation/validateBody.ts diff --git a/package-lock.json b/package-lock.json index c00822b6..ea485179 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,8 @@ "jsonwebtoken": "^9.0.3", "pg": "^8.17.2", "pg-hstore": "^2.3.4", - "sequelize": "^6.37.7" + "sequelize": "^6.37.7", + "zod": "^4.3.6" }, "devDependencies": { "@mate-academy/eslint-config": "latest", @@ -9268,6 +9269,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index c46621bc..ed1875bd 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "jsonwebtoken": "^9.0.3", "pg": "^8.17.2", "pg-hstore": "^2.3.4", - "sequelize": "^6.37.7" + "sequelize": "^6.37.7", + "zod": "^4.3.6" } } diff --git a/src/controllers/auth.ctrlr.ts b/src/controllers/auth.ctrlr.ts index 25bbc65e..0cff08f9 100644 --- a/src/controllers/auth.ctrlr.ts +++ b/src/controllers/auth.ctrlr.ts @@ -1,35 +1,130 @@ import http from 'http'; import bcrypt from 'bcrypt'; import { RequestError } from '../errors'; -import { tknServices, usrServices } from '../services'; -import { httpStatus, TNAMES, fnames, TKN } from '../static'; +import { tkn, usrServices } from '../services'; +import { httpStatus, TKN, TOKEN_EXPIRY } from '../static'; import sch from '../validation/schemas'; -import { createAccessToken } from '../utils/jwt'; +import { parseCookies, jwtAct } from '../utils'; +import { Ctx } from '../static/types'; -const auth = async (res: http.ServerResponse, body: typeof sch.auth) => { - const { email, password } = body; - const nms = fnames[TNAMES.USR]; +const ckNms = { + [TKN.ACC]: 'access_token', + [TKN.RFR]: 'refresh_token', +}; + +const cks = (t: TKN.ACC | TKN.RFR, s: string, age: string) => + `${ckNms[t]}=${s}; HttpOnly; Path=/; Max-Age=${age}; SameSite=Strict`; +async function authMan(ctx: Ctx): Promise { + const { res, body } = ctx; + const { email, password } = body; + // check usr const usr = await usrServices.getByEmail(email); if (!usr) { throw new RequestError(`User with email ${email} not found`, httpStatus.nf); } - + // compare pwd const isValid = await bcrypt.compare(password, usr.password); if (!isValid) { throw new RequestError(`Wrong password`, httpStatus.na); } + // create tkns const { id, name } = usr; - const payload = { id, name, email }; + const accTkn = jwtAct.create[TKN.ACC](payload); + const refTkn = jwtAct.create[TKN.RFR](payload); - const token = await createAccessToken(payload); - const ref = await createAccessToken(payload); + //add refresh tkn to DB + tkn.create(id, refTkn.token, TKN.RFR, refTkn.expiry); - tknServices.create(id, ref.token, TKN.RFR, ref.expiry); + // set cookies + res.setHeader('Set-Cookie', [ + cks(TKN.ACC, accTkn.token, TOKEN_EXPIRY.access[1]), + cks(TKN.RFR, refTkn.token, TOKEN_EXPIRY.refresh[1]), + ]); - - // setCors(); -}; + res.statusCode = httpStatus.ok; + res.end(JSON.stringify({ message: 'Authorized', user: { id, name, email } })); +} + +function authTkn(req: http.IncomingMessage) { + const cookies = parseCookies(req); + const token = cookies[ckNms[TKN.ACC]]; + + if (!token) { + throw new RequestError('No token provided', httpStatus.ua); + } + + const { type, ...pl } = jwtAct.ver(token); + + if (type !== TKN.ACC) { + throw new RequestError('Invalid token type', httpStatus.ua); + } + + return pl; +} + +async function refresh(ctx: Ctx) { + const { req, res } = ctx; + const cookies = parseCookies(req); + const token = cookies[ckNms[TKN.RFR]]; + + if (!token) { + throw new RequestError('No token provided', httpStatus.ua); + } + + const { type, ...pl } = jwtAct.ver(token); + + if (type !== TKN.RFR) { + throw new RequestError('Invalid token type', httpStatus.ua); + } + + const dbTok = await tkn.getByTkn(token); + + if (!dbTok) { + throw new RequestError(`Token ${token} doesn't exist`, httpStatus.ua); + } + + // delete token + tkn.del(dbTok.id); + + //create new Tokens + const accTkn = jwtAct.create[TKN.ACC](pl); + const refTkn = jwtAct.create[TKN.RFR](pl); + + // add refr token to DB + + tkn.create(pl.id, refTkn.token, TKN.RFR, refTkn.expiry); + + // set cookies + res.setHeader('Set-Cookie', [ + cks(TKN.ACC, accTkn.token, TOKEN_EXPIRY.access[1]), + cks(TKN.RFR, refTkn.token, TOKEN_EXPIRY.refresh[1]), + ]); + + // end res + res.statusCode = httpStatus.ok; + res.end(JSON.stringify({ message: 'Authorized', user: { ...pl } })); +} + +async function logout(ctx: Ctx) { + const { req, res, usr } = ctx; + const cookies = parseCookies(req); + + const refToken = cookies[ckNms[TKN.RFR]]; + + if (refToken) { + const dbTok = await tkn.getByTkn(refToken); + if (dbTok) { + tkn.del(dbTok.id); + } + } + res.setHeader('Set-Cookie', [cks(TKN.ACC, '', '0'), cks(TKN.RFR, '', '0')]); + + res.statusCode = httpStatus.ok; + res.end(JSON.stringify({ message: 'Logged out' })); +} + +export { authMan, authTkn, refresh, logout }; diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 28a7960c..027cc53a 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,3 +1,4 @@ import idx from './idx.ctrlr'; +import { authMan, authTkn, refresh, logout } from './auth.ctrlr'; -export { idx }; \ No newline at end of file +export { idx, authMan, authTkn, refresh, logout }; \ No newline at end of file diff --git a/src/createServer.ts b/src/createServer.ts index 9fad1caa..0fe2b66a 100644 --- a/src/createServer.ts +++ b/src/createServer.ts @@ -1,14 +1,15 @@ import http from 'http'; +import { z } from 'zod'; import { dbSetup } from './db/db'; -import { validateRequest } from './validation'; -import { parseBody, validateAuth } from './middleware'; +import { validateBody, validateRequest } from './validation'; +import { errorHandler, parseBody } from './middleware'; import getRouteConfig from './router/router'; +import { authTkn } from './controllers'; +import { Ctx } from './static/types'; // Options preflight // Unified api response // rate limiter mdw -// cookies parser -// JSW tokens export async function createServer() { await dbSetup(); @@ -17,21 +18,19 @@ export async function createServer() { try { const { endpoint, method, param } = validateRequest(req); - const { auth, schema, controller } = getRouteConfig(endpoint, method); + const { auth, schema, ctr } = getRouteConfig(endpoint, method); + let usr = null; - let body = null; if (auth) { - const authHeader = req.headers.authorization; - - body = validateAuth(authHeader); + usr = authTkn(req); } - if (schema) { - body = await parseBody(req); - validateBody(body, schema); - } + const body = schema ? validateBody(await parseBody(req), schema) : false; - const result = controller(body); - } catch (error) {} + const ctx: Ctx = { req, res, body, usr, param }; + ctr(ctx); + } catch (e) { + errorHandler(res, e); + } }); } diff --git a/src/errors/errors.ts b/src/errors/errors.ts index 2fb842ea..71c7f927 100644 --- a/src/errors/errors.ts +++ b/src/errors/errors.ts @@ -1,37 +1,32 @@ import { httpStatus } from '../static'; import { type HTTPStatus } from '../static/types'; -class RequestError extends Error { +class CustomError extends Error { statusCode: HTTPStatus; - - constructor(message: string, statusCode: HTTPStatus) { - super(); + + constructor(message: string, statusCode: HTTPStatus = httpStatus.br) { + super(message); this.statusCode = statusCode; this.name = this.constructor.name; - this.message = message; } } -class DBError extends Error { - statusCode: HTTPStatus; +class RequestError extends CustomError { + constructor(message: string, statusCode: HTTPStatus) { + super(message, statusCode); + } +} +class DBError extends CustomError { constructor(message: string) { - super(); - this.statusCode = httpStatus.se; - this.name = this.constructor.name; - this.message = message; + super(message, httpStatus.se); } } -class AuthError extends Error { - statusCode: HTTPStatus; - +class AuthError extends CustomError { constructor(message: string) { - super(); - this.statusCode = httpStatus.na; - this.name = this.constructor.name; - this.message = message; + super(message, httpStatus.na); } } -export { RequestError, DBError, AuthError }; +export { RequestError, DBError, AuthError, CustomError }; diff --git a/src/errors/index.ts b/src/errors/index.ts index cacdab32..4403840b 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -1,3 +1,3 @@ -import { RequestError, DBError, AuthError } from './errors'; +import { RequestError, DBError, AuthError, CustomError } from './errors'; -export { RequestError, DBError, AuthError }; +export { RequestError, DBError, AuthError, CustomError }; diff --git a/src/middleware/errorHandler.ts b/src/middleware/errorHandler.ts index e69de29b..dd28ba73 100644 --- a/src/middleware/errorHandler.ts +++ b/src/middleware/errorHandler.ts @@ -0,0 +1,17 @@ +import http from 'http'; +import { httpStatus } from '../static'; +import { CustomError } from '../errors'; + +function errorHandler(res: http.ServerResponse, e: unknown) { + if (e instanceof CustomError) { + res.statusCode = e.statusCode; + res.end(e.message); + } + + res.statusCode = httpStatus.se; + res.end('Unexpected server error'); + + return; +} + +export default errorHandler; \ No newline at end of file diff --git a/src/middleware/index.ts b/src/middleware/index.ts index ec17521a..0b824736 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -1,4 +1,4 @@ import { parseBody } from './bodyParser'; -import { validateAuth, authMiddleware } from './auth'; +import errorHandler from './errorHandler'; -export { parseBody, validateAuth, authMiddleware }; \ No newline at end of file +export { parseBody, errorHandler }; diff --git a/src/router/router.ts b/src/router/router.ts index a259cf6c..08e506a5 100644 --- a/src/router/router.ts +++ b/src/router/router.ts @@ -1,54 +1,59 @@ -import { idx } from '../controllers'; +import { idx, authMan, refresh, logout } from '../controllers'; import { RequestError } from '../errors'; import { Endpoint, endpt } from '../static/endpoints'; import { httpStatus, Method, mthd } from '../static/vocab/httpVocab'; import sch from '../validation/schemas'; +import { Ctx } from '../static/types'; -type Entr = { + +type Entr = { auth: boolean; - schema: false | typeof sch[]; - controller: () => void; + schema: S; + ctr: (args: Ctx) => void | Promise; }; const routeMap = { [endpt.idx]: { - [mthd.get]: { auth: false, schema: false, controller: idx }, + [mthd.get]: { auth: false, schema: false, ctr: idx }, }, [endpt.auth]: { - [mthd.post]: { auth: false, schema: sch.auth, controller: () => true }, + [mthd.post]: { auth: false, schema: sch.auth, ctr: authMan }, }, [endpt.refr]: { - [mthd.post]: { auth: false, schema: sch.refr, controller: () => true }, + [mthd.post]: { auth: false, schema: false, ctr: refresh }, }, [endpt.snauth]: { - [mthd.post]: { auth: false, schema: sch.authSN, controller: () => true }, + [mthd.post]: { auth: false, schema: false, ctr: () => true }, }, [endpt.lgout]: { - [mthd.patch]: { auth: true, schema: false, controller: () => true }, + [mthd.patch]: { auth: true, schema: false, ctr: logout }, }, [endpt.reg]: { - [mthd.post]: { auth: false, schema: sch.reg, controller: () => true }, + [mthd.post]: { auth: false, schema: sch.reg, ctr: () => true }, }, [endpt.act]: { - [mthd.get]: { auth: false, schema: false, controller: () => true }, + [mthd.get]: { auth: false, schema: false, ctr: () => true }, }, [endpt.acc]: { - [mthd.get]: { auth: true, schema: false, controller: () => true }, - [mthd.del]: { auth: true, schema: false, controller: () => true }, - [mthd.patch]: { auth: true, schema: sch.profUpd, controller: () => true }, + [mthd.get]: { auth: true, schema: false, ctr: () => true }, + [mthd.del]: { auth: true, schema: false, ctr: () => true }, + [mthd.patch]: { auth: true, schema: sch.profUpd, ctr: () => true }, }, [endpt.pwd]: { - [mthd.patch]: { auth: true, schema: sch.pwdPtch, controller: () => true }, + [mthd.patch]: { auth: true, schema: sch.pwdPtch, ctr: () => true }, }, [endpt.pwdReq]: { - [mthd.post]: { auth: false, schema: sch.pwdReq, controller: () => true }, + [mthd.post]: { auth: false, schema: sch.pwdReq, ctr: () => true }, }, [endpt.pwdRes]: { - [mthd.post]: { auth: false, schema: sch.newPwd, controller: () => true }, + [mthd.post]: { auth: false, schema: sch.newPwd, ctr: () => true }, }, } as const; -const getRouteConfig = (ep: Endpoint, mthd: Method): Entr => { +const getRouteConfig = ( + ep: Endpoint, + mthd: Method, +): Entr => { const epRoutes = routeMap[ep]; const conf = epRoutes[mthd as keyof typeof epRoutes]; diff --git a/src/services/general.service.ts b/src/services/general.service.ts index aa4d96ad..6d1472a9 100644 --- a/src/services/general.service.ts +++ b/src/services/general.service.ts @@ -1,9 +1,9 @@ import { DBError, RequestError } from '../errors'; import DB from '../model'; import { fnames, httpStatus, TNAMES } from '../static'; -import { ObjectMapType } from './types'; +import { DBRes } from './types'; -const get = async (table: T, key: string): Promise => { +const get = async (table: T, key: string): Promise => { const item = await DB[table].findByPk(key); if (!item) { @@ -14,7 +14,7 @@ const get = async (table: T, key: string): Promise => { - // Нужен Model instance для destroy, не plain object + const item = await DB[table].findByPk(key); if (!item) { @@ -27,8 +27,8 @@ const del = async (table: TNAMES, key: string): Promise => { const getByParam = async ( table: T, field: (typeof fnames)[T][keyof (typeof fnames)[T]], - query: string, -): Promise => { + query: string | boolean, +): Promise => { const item = await DB[table].findOne({ where: { [field as string]: query } }); if (!item) { diff --git a/src/services/index.ts b/src/services/index.ts index 10812c72..e2d655f2 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,3 +1,3 @@ import usrServices from './user.service'; -import tknServices from './token.service' -export { usrServices, tknServices }; +import tkn from './token.service' +export { usrServices, tkn }; diff --git a/src/services/token.service.ts b/src/services/token.service.ts index 8a1faadc..0495e475 100644 --- a/src/services/token.service.ts +++ b/src/services/token.service.ts @@ -1,35 +1,39 @@ import DB from '../model'; import { TKN, TNAMES, fnames } from '../static'; import { asyncDBHandler } from '../utils'; -import { ObjectMapType } from './types'; +import { del, getByParam } from './general.service'; +import { DBResTkn } from './types'; -const tkFNames = fnames[TNAMES.TKN]; +const tkNames = fnames[TNAMES.TKN]; -const createToken = async ( +async function createToken ( userId: string, token: string, type: Exclude, expiresAt: string, -): Promise => { +): Promise { const newToken = await DB[TNAMES.TKN].create({ - [tkFNames.usr]: userId, - [tkFNames.token]: token, - [tkFNames.type]: type, - [tkFNames.exp]: expiresAt, + [tkNames.usr]: userId, + [tkNames.token]: token, + [tkNames.type]: type, + [tkNames.exp]: expiresAt, }); return newToken.toJSON(); }; -const tknServices = { - create: asyncDBHandler( - ( - userId: string, - token: string, - type: Exclude, - expiresAt: string, - ) => createToken(userId, token, type, expiresAt), - ), +async function getByTkn(tkn: string): Promise { + const token = await getByParam(TNAMES.TKN, fnames[TNAMES.TKN].token, tkn); + + return token; +} + + +const tkn = { + create: asyncDBHandler((userId, token, type, expiresAt) => + createToken(userId, token, type, expiresAt)), + getByTkn: asyncDBHandler((tkn: string) => getByTkn(tkn)), + del: asyncDBHandler((id: string) => del(TNAMES.TKN, id)), }; -export default tknServices; +export default tkn; diff --git a/src/services/types.ts b/src/services/types.ts index 0f77199e..98d3a1cd 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -1,6 +1,6 @@ import { TKN, TNAMES } from "../static"; -interface UserAttributes { +interface DBResUsr { id: string; name: string; email: string; @@ -10,7 +10,7 @@ interface UserAttributes { updatedAt: Date; } -interface TokenAttr { +interface DBResTkn { id: string, userId: string, token: string, @@ -19,7 +19,7 @@ interface TokenAttr { createdAt: Date, } -interface SocialNetworkAttr { +interface DBResSN { id: string, userId: string, google: string | null, @@ -27,10 +27,10 @@ interface SocialNetworkAttr { facebook: string | null, } -type ObjectMapType = { - [TNAMES.USR]: UserAttributes; - [TNAMES.SCN]: SocialNetworkAttr; - [TNAMES.TKN]: TokenAttr; +type DBRes = { + [TNAMES.USR]: DBResUsr; + [TNAMES.SCN]: DBResSN; + [TNAMES.TKN]: DBResTkn; } -export { type ObjectMapType }; \ No newline at end of file +export type { DBRes, DBResTkn }; \ No newline at end of file diff --git a/src/static/constants.ts b/src/static/constants.ts new file mode 100644 index 00000000..3967e492 --- /dev/null +++ b/src/static/constants.ts @@ -0,0 +1,10 @@ +import { TKN } from '.'; + +const TOKEN_EXPIRY: Record = { + [TKN.ACC]: ['15m', '900'], + [TKN.RFR]: ['7d', '604800'], + [TKN.ACT]: ['24h', '86400'], + [TKN.PWR]: ['1h', '3600'], +}; + +export { TOKEN_EXPIRY }; diff --git a/src/static/endpoints.ts b/src/static/endpoints.ts index 64c614bb..fb151443 100644 --- a/src/static/endpoints.ts +++ b/src/static/endpoints.ts @@ -1,7 +1,7 @@ const endpt = { idx: '/', auth: '/auth', - refr: '/auth/resresh', + refr: '/auth/refresh', snauth: '/auth/social', lgout: '/auth/logout', reg: '/register', diff --git a/src/static/index.ts b/src/static/index.ts index c0513a19..b04471a2 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -1,5 +1,6 @@ import { TNAMES, fnames, TKN } from './vocab/dbVocab'; -import { pages, mthd, httpStatus } from './vocab/httpVocab'; +import { mthd, httpStatus } from './vocab/httpVocab'; import { endpt } from './endpoints'; +import { TOKEN_EXPIRY } from './constants' -export { TNAMES, TKN, fnames, pages, mthd, httpStatus, endpt }; +export { TNAMES, TKN, fnames, TOKEN_EXPIRY, mthd, httpStatus, endpt }; diff --git a/src/static/types.ts b/src/static/types.ts index fff79445..7b041a2a 100644 --- a/src/static/types.ts +++ b/src/static/types.ts @@ -1,4 +1,16 @@ +import http from 'http'; +import { z } from 'zod' + import { type Endpoint } from "./endpoints"; import { type Method, type HTTPStatus } from "./vocab/httpVocab"; +import { JWTPayload } from '../utils'; + +type Ctx = { + req: http.IncomingMessage; + res: http.ServerResponse; + body: S extends z.ZodSchema ? z.infer : false; + usr: JWTPayload | null; + param: string | null; +}; -export { type Endpoint, type Method, type HTTPStatus }; \ No newline at end of file +export type { Endpoint, Method, HTTPStatus, Ctx }; \ No newline at end of file diff --git a/src/static/vocab/httpVocab.ts b/src/static/vocab/httpVocab.ts index e558e970..9e0b098f 100644 --- a/src/static/vocab/httpVocab.ts +++ b/src/static/vocab/httpVocab.ts @@ -10,6 +10,7 @@ const httpStatus = { cr: 201, nc: 204, br: 400, + ua: 401, nf: 404, na: 405, se: 500, diff --git a/src/utils/cookies.ts b/src/utils/cookies.ts new file mode 100644 index 00000000..6670af7d --- /dev/null +++ b/src/utils/cookies.ts @@ -0,0 +1,19 @@ +import http from 'http'; + +type Cookies = Record; + +function parseCookies(req: http.IncomingMessage): Cookies { + const cookieHeader = req.headers.cookie || ''; + + return Object.fromEntries( + cookieHeader + .split(';') + .filter(Boolean) + .map((c) => { + const [key, ...val] = c.trim().split('='); + return [key, val.join('=')]; + }), + ); +} + +export { parseCookies }; diff --git a/src/utils/index.ts b/src/utils/index.ts index 47bb8e9e..7ea5a1fe 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,7 @@ import asyncDBHandler from './dbHandler'; +import { parseCookies } from './cookies'; +import { type JWTPayload } from './types'; +import jwtAct from './jwt'; -export { asyncDBHandler }; \ No newline at end of file +export { asyncDBHandler, parseCookies, jwtAct }; +export type { JWTPayload }; diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts index 2f3ca76a..838425d6 100644 --- a/src/utils/jwt.ts +++ b/src/utils/jwt.ts @@ -1,45 +1,22 @@ import jwt from 'jsonwebtoken'; import { RequestError } from '../errors'; -import { fnames, httpStatus, TKN, TNAMES } from '../static'; +import { httpStatus, TKN, TOKEN_EXPIRY } from '../static'; +import { JWTPayload } from './types'; -const SECRET_KEY = process.env.JWT_SECRET || 'your-secret-key'; +const SECRET_KEY = process.env.JWT_SECRET || '7>?~!id(#;fd13/^^$fdkq124<'; -const TOKEN_EXPIRY: Record = { - [TKN.ACC]: '15m', // access token - 15 минут - [TKN.RFR]: '7d', // refresh token - 7 дней - [TKN.ACT]: '24h', // activation token - 24 часа - [TKN.PWR]: '1h', // password reset token - 1 час -}; - -const nms = fnames[TNAMES.USR]; +type Signed = { expiry: string; token: string }; -type JWTPayload = { - [nms.id]: string; - [nms.name]: string; - [nms.email]: string; -}; +function signToken(payload: JWTPayload, type: TKN): Signed { + const expiry = TOKEN_EXPIRY[type][0]; -function signToken( - payload: JWTPayload, - type: TKN, -): { expiry: string; token: string } { - const expiry = TOKEN_EXPIRY[type]; - - const token = jwt.sign( - { - ...payload, - type, - }, - SECRET_KEY, - { expiresIn: expiry } as jwt.SignOptions, - ); + const token = jwt.sign({ ...payload, type }, SECRET_KEY, { + expiresIn: expiry, + } as jwt.SignOptions); return { expiry, token }; } -/** - * Проверяет и декодирует JWT токен - */ function verifyToken(token: string): JWTPayload & { type: TKN } { try { const decoded = jwt.verify(token, SECRET_KEY) as JWTPayload & { @@ -59,18 +36,14 @@ function verifyToken(token: string): JWTPayload & { type: TKN } { throw new RequestError('Token verification failed', httpStatus.br); } } +const jwtAct = { + sign: (pl: JWTPayload, tp: TKN) => signToken(pl, tp), + ver: (tk: string) => verifyToken(tk), + create: { + [TKN.ACC]: (payload: JWTPayload): Signed => signToken(payload, TKN.ACC), + [TKN.RFR]: (payload: JWTPayload): Signed => signToken(payload, TKN.RFR), + }, +}; -async function createAccessToken( - payload: JWTPayload, -): Promise<{ expiry: string; token: string }> { - return signToken(payload, TKN.ACC); -} - -async function createRefreshToken( - payload: JWTPayload, -): Promise<{ expiry: string; token: string }> { - return signToken(payload, TKN.RFR); -} - -export { signToken, verifyToken, createAccessToken, createRefreshToken }; +export default jwtAct; export type { JWTPayload }; diff --git a/src/utils/types.ts b/src/utils/types.ts index e69de29b..630f50ac 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -0,0 +1,11 @@ +import { fnames, TNAMES } from "../static"; + +const nms = fnames[TNAMES.USR]; + +type JWTPayload = { + [nms.id]: string; + [nms.name]: string; + [nms.email]: string; +}; + +export type { JWTPayload }; \ No newline at end of file diff --git a/src/validation/index.ts b/src/validation/index.ts index 3e41a6c1..a5b579f1 100644 --- a/src/validation/index.ts +++ b/src/validation/index.ts @@ -1,3 +1,4 @@ -import { validateRequest } from "./validateRequest"; - -export { validateRequest }; \ No newline at end of file +import validateRequest from './validateRequest'; +import validateBody from './validateBody'; +import sch from './schemas'; +export { validateRequest, validateBody, sch }; diff --git a/src/validation/schemas.ts b/src/validation/schemas.ts index de23be50..2d0511fa 100644 --- a/src/validation/schemas.ts +++ b/src/validation/schemas.ts @@ -1,42 +1,65 @@ -import { endpt, fnames, TNAMES } from '../static'; +import { z } from 'zod'; +import { fnames, TNAMES } from '../static'; const usrFNames = fnames[TNAMES.USR]; const tkFNames = fnames[TNAMES.TKN]; -const authSch = { - [usrFNames.email]: 'string', - [usrFNames.pwd]: 'string', -}; +const authSch = z.object({ + [usrFNames.email]: z + .string() + .refine((val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val), { + message: 'Invalid email', + }), + [usrFNames.pwd]: z.string().min(6), +}); + +const regUpdSch = z.object({ + [usrFNames.name]: z.string().min(3), + [usrFNames.email]: z + .string() + .refine((val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val), { + message: 'Invalid email', + }), + [usrFNames.pwd]: z.string().min(6), +}); + +const pwdUpd = z + .object({ + oldPwd: z.string(), + newPwd: z.string().min(6), + confirmation: z.string().min(6), + }) + .refine((data) => data.confirmation === data.newPwd, { + message: "Passwords don't match", + path: ['confirmation'], + }); + +const pwdReq = z.object({ + [usrFNames.email]: z + .string() + .refine((val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val), { + message: 'Invalid email', + }), +}); + +const pwdRes = z + .object({ + [tkFNames.token]: z.string(), + newPwd: z.string().min(6), + confirmation: z.string(), + }) + .refine((data) => data.confirmation === data.newPwd, { + message: "Passwords don't match", + path: ['confirmation'], + }); -const regSch = { - [usrFNames.name]: 'string', - [usrFNames.email]: 'string', - [usrFNames.pwd]: 'string', -} as const; const sch = { auth: authSch, - authSN: {}, - refr: {}, - reg: regSch, - profUpd: { - [usrFNames.name]: 'string', - [usrFNames.email]: 'string', - [usrFNames.pwd]: 'string', - confirmation: 'string', - }, - pwdPtch: { - oldPwd: 'string', - newPwd: 'string', - confirmation: 'string', - }, - pwdReq: { - [usrFNames.email]: 'string', - }, - newPwd: { - [tkFNames.token]: 'string', - newPassword: 'string', - confirmation: 'string', - }, + reg: regUpdSch, + profUpd: regUpdSch, + pwdPtch: pwdUpd, + pwdReq: pwdReq, + newPwd: pwdRes, }; export default sch; diff --git a/src/validation/validateAuth.ts b/src/validation/validateAuth.ts deleted file mode 100644 index a443e2ae..00000000 --- a/src/validation/validateAuth.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { AuthError } from "../errors"; -import { TKN } from "../static"; -import { JWTPayload, verifyToken } from "../utils/jwt"; - -function validateAuth(authHeader: string | undefined): JWTPayload { - if (!authHeader) { - throw new AuthError('Authorization header is required'); - } - - const parts = authHeader.split(' '); - - if (parts.length !== 2 || parts[0] !== 'Bearer') { - throw new AuthError( - 'Invalid authorization header format. Expected: Bearer ', - ); - } - - const token = parts[1]; - - if (!token) { - throw new AuthError('Token is missing'); - } - - const decoded = verifyToken(token); - - if (decoded.type !== TKN.ACC) { - throw new AuthError('Invalid token type. Access token required'); - } - - return decoded; -} - -export default validateAuth; \ No newline at end of file diff --git a/src/validation/validateBody.ts b/src/validation/validateBody.ts new file mode 100644 index 00000000..a95f76bb --- /dev/null +++ b/src/validation/validateBody.ts @@ -0,0 +1,19 @@ +import { z } from 'zod'; +import { RequestError } from '../errors'; +import { httpStatus } from '../static'; + +function validateBody( + body: unknown, + schema: T, +): z.infer { + try { + return schema.parse(body); + } catch (e) { + if (e instanceof z.ZodError) { + throw new RequestError(e.issues[0].message, httpStatus.br); + } + throw e; + } +} + +export default validateBody; \ No newline at end of file diff --git a/src/validation/validateRequest.ts b/src/validation/validateRequest.ts index 749b8e87..21fa3dd4 100644 --- a/src/validation/validateRequest.ts +++ b/src/validation/validateRequest.ts @@ -45,4 +45,4 @@ function validateRequest(req: http.IncomingMessage) { return { endpoint, method, param }; } -export { validateRequest }; +export default validateRequest; From f37450349e68a746dedc0b15372b39d97aebd257 Mon Sep 17 00:00:00 2001 From: Bespalov Tymofii Date: Wed, 4 Feb 2026 16:33:00 +0100 Subject: [PATCH 06/14] test --- .eslintrc.cjs | 14 +++ .eslintrc.js | 10 --- package-lock.json | 207 ++++++++++++++++++++++++++++++++++---------- package.json | 2 + src/createServer.ts | 11 ++- 5 files changed, 185 insertions(+), 59 deletions(-) create mode 100644 .eslintrc.cjs delete mode 100644 .eslintrc.js diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 00000000..4d21aacc --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,14 @@ +module.exports = { + extends: '@mate-academy/eslint-config', + parser: '@typescript-eslint/parser', + parserOptions: { + project: './tsconfig.json', + }, + env: { + jest: true, + }, + rules: { + 'no-proto': 0, + }, + plugins: ['jest', '@typescript-eslint'], +}; diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index f44c7a1d..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - extends: '@mate-academy/eslint-config', - env: { - jest: true - }, - rules: { - 'no-proto': 0 - }, - plugins: ['jest'] -}; diff --git a/package-lock.json b/package-lock.json index ea485179..d1392d7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,8 @@ "devDependencies": { "@mate-academy/eslint-config": "latest", "@mate-academy/scripts": "^2.1.3", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", "eslint": "^8.57.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-node": "^11.1.0", @@ -575,10 +577,11 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", - "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -2102,14 +2105,100 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.17.0.tgz", - "integrity": "sha512-0P2jTTqyxWp9HiKLu/Vemr2Rg1Xb5B7uHItdVZ6iAenXmPo4SZ86yOPCJwMqpCyaMiEHTNqizHfsbmCFT1x9SA==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.17.0", - "@typescript-eslint/visitor-keys": "7.17.0" + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2117,13 +2206,22 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.17.0.tgz", - "integrity": "sha512-a29Ir0EbyKTKHnZWbNsrc/gqfIBqYPwj3F2M+jWE/9bqfEHg0AMtXzkbUkOG6QgEScxh2+Pz9OXe11jHDnHR7A==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || >=20.0.0" }, @@ -2133,13 +2231,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.17.0.tgz", - "integrity": "sha512-72I3TGq93t2GoSBWI093wmKo0n6/b7O4j9o8U+f65TVD0FS6bI2180X5eGEr8MA8PhKMvYe9myZJquUT2JkCZw==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.17.0", - "@typescript-eslint/visitor-keys": "7.17.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2161,10 +2260,11 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -2174,6 +2274,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -2185,10 +2286,11 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -2197,15 +2299,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.17.0.tgz", - "integrity": "sha512-r+JFlm5NdB+JXc7aWWZ3fKSm1gn0pkswEwIYsrGPdsT2GjsRATAKXiNtp3vgAAO1xZhX8alIOEQnNMl3kbTgJw==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.17.0", - "@typescript-eslint/types": "7.17.0", - "@typescript-eslint/typescript-estree": "7.17.0" + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2219,12 +2322,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.17.0.tgz", - "integrity": "sha512-RVGC9UhPOCsfCdI9pU++K4nD7to+jTcMIbXTSOcrLqUEW6gF2pU1UUbYJKc9cvcRSK1UDeMJ7pdMxf4bhMpV/A==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/types": "7.18.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -2240,6 +2344,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -2387,6 +2492,7 @@ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3114,11 +3220,12 @@ } }, "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -3230,6 +3337,7 @@ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, + "license": "MIT", "dependencies": { "path-type": "^4.0.0" }, @@ -4134,16 +4242,17 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -4154,6 +4263,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -4481,6 +4591,7 @@ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, + "license": "MIT", "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -7032,15 +7143,17 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -7101,9 +7214,10 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "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/natural-compare": { "version": "1.4.0", @@ -7510,6 +7624,7 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } diff --git a/package.json b/package.json index ed1875bd..c012005d 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "devDependencies": { "@mate-academy/eslint-config": "latest", "@mate-academy/scripts": "^2.1.3", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", "eslint": "^8.57.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-node": "^11.1.0", diff --git a/src/createServer.ts b/src/createServer.ts index 0fe2b66a..3ae31a27 100644 --- a/src/createServer.ts +++ b/src/createServer.ts @@ -1,5 +1,4 @@ import http from 'http'; -import { z } from 'zod'; import { dbSetup } from './db/db'; import { validateBody, validateRequest } from './validation'; import { errorHandler, parseBody } from './middleware'; @@ -9,7 +8,6 @@ import { Ctx } from './static/types'; // Options preflight // Unified api response -// rate limiter mdw export async function createServer() { await dbSetup(); @@ -27,7 +25,14 @@ export async function createServer() { const body = schema ? validateBody(await parseBody(req), schema) : false; - const ctx: Ctx = { req, res, body, usr, param }; + const ctx: Ctx = { + req, + res, + body, + usr, + param, + }; + ctr(ctx); } catch (e) { errorHandler(res, e); From 33195c8ae9f474c9b80459f44a83021f306acab4 Mon Sep 17 00:00:00 2001 From: Bespalov Tymofii Date: Wed, 4 Feb 2026 17:21:00 +0100 Subject: [PATCH 07/14] fix file exports, tsconfig, DB issues --- src/controllers/auth.ctrlr.ts | 29 ++++++++++++++++++----------- src/controllers/idx.ctrlr.ts | 2 +- src/controllers/index.ts | 6 +++--- src/createServer.ts | 12 ++++++------ src/db/db.ts | 12 ++++++------ src/errors/errors.ts | 7 ++++--- src/errors/index.ts | 2 +- src/middleware/bodyParser.ts | 5 +++-- src/middleware/errorHandler.ts | 10 +++++----- src/middleware/index.ts | 4 ++-- src/model/index.ts | 10 +++++----- src/model/socialAccounts.model.ts | 10 +++++----- src/model/token.model.ts | 8 ++++---- src/model/user.model.ts | 6 +++--- src/router/router.ts | 19 +++++++++---------- src/services/general.service.ts | 18 ++++++++++-------- src/services/index.ts | 4 ++-- src/services/token.service.ts | 26 ++++++++++++-------------- src/services/types.ts | 28 ++++++++++++++-------------- src/services/user.service.ts | 17 +++++++++-------- src/static/constants.ts | 4 ++-- src/static/endpoints.ts | 4 ++-- src/static/index.ts | 10 ++++++---- src/static/types.ts | 10 +++++----- src/static/vocab/dbVocab.ts | 27 ++++++++++++++++----------- src/static/vocab/httpVocab.ts | 6 +++--- src/utils/cookies.ts | 1 + src/utils/dbHandler.ts | 8 +++++--- src/utils/index.ts | 8 ++++---- src/utils/jwt.ts | 15 ++++++++------- src/utils/types.ts | 4 ++-- src/validation/index.ts | 6 +++--- src/validation/schemas.ts | 2 +- src/validation/validateBody.ts | 6 +++--- src/validation/validateRequest.ts | 10 +++++++--- tsconfig.json | 18 ++++++++++++++++++ 36 files changed, 208 insertions(+), 166 deletions(-) create mode 100644 tsconfig.json diff --git a/src/controllers/auth.ctrlr.ts b/src/controllers/auth.ctrlr.ts index 0cff08f9..d1011e77 100644 --- a/src/controllers/auth.ctrlr.ts +++ b/src/controllers/auth.ctrlr.ts @@ -1,18 +1,18 @@ import http from 'http'; import bcrypt from 'bcrypt'; -import { RequestError } from '../errors'; -import { tkn, usrServices } from '../services'; -import { httpStatus, TKN, TOKEN_EXPIRY } from '../static'; -import sch from '../validation/schemas'; -import { parseCookies, jwtAct } from '../utils'; -import { Ctx } from '../static/types'; +import { RequestError } from '../errors/index.ts'; +import { tkn, usrServices } from '../services/index.ts'; +import { httpStatus, TKN, TOKEN_EXPIRY } from '../static/index.ts'; +import sch from '../validation/schemas.ts'; +import { parseCookies, jwtAct } from '../utils/index.ts'; +import { type Ctx } from '../static/types.ts'; const ckNms = { [TKN.ACC]: 'access_token', [TKN.RFR]: 'refresh_token', }; -const cks = (t: TKN.ACC | TKN.RFR, s: string, age: string) => +const cks = (t: typeof TKN.ACC | typeof TKN.RFR, s: string, age: string) => `${ckNms[t]}=${s}; HttpOnly; Path=/; Max-Age=${age}; SameSite=Strict`; async function authMan(ctx: Ctx): Promise { @@ -24,19 +24,24 @@ async function authMan(ctx: Ctx): Promise { if (!usr) { throw new RequestError(`User with email ${email} not found`, httpStatus.nf); } - // compare pwd + + // compare pw + const isValid = await bcrypt.compare(password, usr.password); if (!isValid) { throw new RequestError(`Wrong password`, httpStatus.na); } + // create tkns + const { id, name } = usr; const payload = { id, name, email }; const accTkn = jwtAct.create[TKN.ACC](payload); const refTkn = jwtAct.create[TKN.RFR](payload); - //add refresh tkn to DB + // add refresh tkn to DB + tkn.create(id, refTkn.token, TKN.RFR, refTkn.expiry); // set cookies @@ -90,7 +95,8 @@ async function refresh(ctx: Ctx) { // delete token tkn.del(dbTok.id); - //create new Tokens + // create new Tokens + const accTkn = jwtAct.create[TKN.ACC](pl); const refTkn = jwtAct.create[TKN.RFR](pl); @@ -110,13 +116,14 @@ async function refresh(ctx: Ctx) { } async function logout(ctx: Ctx) { - const { req, res, usr } = ctx; + const { req, res } = ctx; const cookies = parseCookies(req); const refToken = cookies[ckNms[TKN.RFR]]; if (refToken) { const dbTok = await tkn.getByTkn(refToken); + if (dbTok) { tkn.del(dbTok.id); } diff --git a/src/controllers/idx.ctrlr.ts b/src/controllers/idx.ctrlr.ts index 5e28bb79..74896ef8 100644 --- a/src/controllers/idx.ctrlr.ts +++ b/src/controllers/idx.ctrlr.ts @@ -9,4 +9,4 @@ const idx = (res: http.ServerResponse) => { res.end(index); }; -export default idx; \ No newline at end of file +export default idx; diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 027cc53a..07ff354c 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,4 +1,4 @@ -import idx from './idx.ctrlr'; -import { authMan, authTkn, refresh, logout } from './auth.ctrlr'; +import idx from './idx.ctrlr.ts'; +import { authMan, authTkn, refresh, logout } from './auth.ctrlr.ts'; -export { idx, authMan, authTkn, refresh, logout }; \ No newline at end of file +export { idx, authMan, authTkn, refresh, logout }; diff --git a/src/createServer.ts b/src/createServer.ts index 3ae31a27..1a9dbea2 100644 --- a/src/createServer.ts +++ b/src/createServer.ts @@ -1,10 +1,10 @@ import http from 'http'; -import { dbSetup } from './db/db'; -import { validateBody, validateRequest } from './validation'; -import { errorHandler, parseBody } from './middleware'; -import getRouteConfig from './router/router'; -import { authTkn } from './controllers'; -import { Ctx } from './static/types'; +import { dbSetup } from './db/db.ts'; +import { validateBody, validateRequest } from './validation/index.ts'; +import { errorHandler, parseBody } from './middleware/index.ts'; +import getRouteConfig from './router/router.ts'; +import { authTkn } from './controllers/index.ts'; +import { type Ctx } from './static/types.ts'; // Options preflight // Unified api response diff --git a/src/db/db.ts b/src/db/db.ts index cd182cf8..56c95eeb 100644 --- a/src/db/db.ts +++ b/src/db/db.ts @@ -1,6 +1,6 @@ 'use strict'; import { Sequelize } from 'sequelize'; -import { DBError } from '../errors'; +import { DBError } from '../errors/index.ts'; const { POSTGRES_HOST, @@ -21,13 +21,13 @@ const client = new Sequelize({ const dbSetup = async () => { try { - const { default: DB } = await import('../model'); + const { default: DB } = await import('../model/index.ts'); - await Promise.all( - Object.values(DB).map((model) => model.sync({ alter: true })), - ); + for (const model of Object.values(DB)) { + await model.sync({ force: true }); + } } catch (error) { - throw new DBError(`Database setup failed: ${error}`) + throw new DBError(`Database setup failed: ${error}`); } }; diff --git a/src/errors/errors.ts b/src/errors/errors.ts index 71c7f927..f99d30a4 100644 --- a/src/errors/errors.ts +++ b/src/errors/errors.ts @@ -1,9 +1,9 @@ -import { httpStatus } from '../static'; -import { type HTTPStatus } from '../static/types'; +import { httpStatus } from '../static/index.ts'; +import { type HTTPStatus } from '../static/types.ts'; class CustomError extends Error { statusCode: HTTPStatus; - + constructor(message: string, statusCode: HTTPStatus = httpStatus.br) { super(message); this.statusCode = statusCode; @@ -12,6 +12,7 @@ class CustomError extends Error { } class RequestError extends CustomError { + // eslint-disable-next-line no-useless-constructor constructor(message: string, statusCode: HTTPStatus) { super(message, statusCode); } diff --git a/src/errors/index.ts b/src/errors/index.ts index 4403840b..c957c4e5 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -1,3 +1,3 @@ -import { RequestError, DBError, AuthError, CustomError } from './errors'; +import { RequestError, DBError, AuthError, CustomError } from './errors.ts'; export { RequestError, DBError, AuthError, CustomError }; diff --git a/src/middleware/bodyParser.ts b/src/middleware/bodyParser.ts index 289cd905..c0419583 100644 --- a/src/middleware/bodyParser.ts +++ b/src/middleware/bodyParser.ts @@ -1,6 +1,7 @@ import http from 'http'; -import { RequestError } from '../errors'; -import { httpStatus } from '../static'; +import { RequestError } from '../errors/index.ts'; +import { httpStatus } from '../static/index.ts'; + const maxSizeLimit = 1048576; async function parseBody(req: http.IncomingMessage, maxSize = maxSizeLimit) { diff --git a/src/middleware/errorHandler.ts b/src/middleware/errorHandler.ts index dd28ba73..7caefbf8 100644 --- a/src/middleware/errorHandler.ts +++ b/src/middleware/errorHandler.ts @@ -1,17 +1,17 @@ import http from 'http'; -import { httpStatus } from '../static'; -import { CustomError } from '../errors'; +import { httpStatus } from '../static/index.ts'; +import { CustomError } from '../errors/index.ts'; function errorHandler(res: http.ServerResponse, e: unknown) { if (e instanceof CustomError) { res.statusCode = e.statusCode; res.end(e.message); + + return; } res.statusCode = httpStatus.se; res.end('Unexpected server error'); - - return; } -export default errorHandler; \ No newline at end of file +export default errorHandler; diff --git a/src/middleware/index.ts b/src/middleware/index.ts index 0b824736..dd54ad09 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -1,4 +1,4 @@ -import { parseBody } from './bodyParser'; -import errorHandler from './errorHandler'; +import { parseBody } from './bodyParser.ts'; +import errorHandler from './errorHandler.ts'; export { parseBody, errorHandler }; diff --git a/src/model/index.ts b/src/model/index.ts index 7443b5e4..b00b0ef7 100644 --- a/src/model/index.ts +++ b/src/model/index.ts @@ -1,12 +1,12 @@ -import User from './user.model'; -import SocialAccount from './socialAccounts.model'; -import Token from './token.model'; -import { TNAMES } from '../static'; +import User from './user.model.ts'; +import SocialAccount from './socialAccounts.model.ts'; +import Token from './token.model.ts'; +import { TNAMES } from '../static/index.ts'; const DB = { [TNAMES.USR]: User, [TNAMES.SCN]: SocialAccount, [TNAMES.TKN]: Token, -} +}; export default DB; diff --git a/src/model/socialAccounts.model.ts b/src/model/socialAccounts.model.ts index 5a636319..8b45ba7c 100644 --- a/src/model/socialAccounts.model.ts +++ b/src/model/socialAccounts.model.ts @@ -1,19 +1,19 @@ -import { client } from '../db/db'; +import { client } from '../db/db.ts'; import { DataTypes } from 'sequelize'; -import { TNAMES, fnames } from '../static' +import { TNAMES, fnames } from '../static/index.ts'; -const nms = fnames[TNAMES.SCN] +const nms = fnames[TNAMES.SCN]; const SocialAccount = client.define( 'SocialAccount', { [nms.id]: { - type: DataTypes.UUIDV4, + type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4, }, [nms.usr]: { - type: DataTypes.UUIDV4, + type: DataTypes.UUID, allowNull: false, references: { model: TNAMES.USR, diff --git a/src/model/token.model.ts b/src/model/token.model.ts index a9ecbcd5..2075d7e8 100644 --- a/src/model/token.model.ts +++ b/src/model/token.model.ts @@ -1,6 +1,6 @@ -import { client } from '../db/db'; +import { client } from '../db/db.ts'; import { DataTypes } from 'sequelize'; -import { TNAMES, fnames, TKN } from '../static'; +import { TNAMES, fnames, TKN } from '../static/index.ts'; const nms = fnames[TNAMES.TKN]; @@ -8,12 +8,12 @@ const Token = client.define( 'Token', { [nms.id]: { - type: DataTypes.UUIDV4, + type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4, }, [nms.usr]: { - type: DataTypes.UUIDV4, + type: DataTypes.UUID, allowNull: false, references: { model: TNAMES.USR, diff --git a/src/model/user.model.ts b/src/model/user.model.ts index 1c2ad5ce..1584880d 100644 --- a/src/model/user.model.ts +++ b/src/model/user.model.ts @@ -1,6 +1,6 @@ -import { client } from '../db/db'; +import { client } from '../db/db.ts'; import { DataTypes } from 'sequelize'; -import { TNAMES, fnames } from '../static'; +import { TNAMES, fnames } from '../static/index.ts'; const nms = fnames[TNAMES.USR]; @@ -8,7 +8,7 @@ const User = client.define( 'User', { [nms.id]: { - type: DataTypes.UUIDV4, + type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4, }, diff --git a/src/router/router.ts b/src/router/router.ts index 08e506a5..f722f213 100644 --- a/src/router/router.ts +++ b/src/router/router.ts @@ -1,10 +1,9 @@ -import { idx, authMan, refresh, logout } from '../controllers'; -import { RequestError } from '../errors'; -import { Endpoint, endpt } from '../static/endpoints'; -import { httpStatus, Method, mthd } from '../static/vocab/httpVocab'; -import sch from '../validation/schemas'; -import { Ctx } from '../static/types'; - +import { idx, authMan, refresh, logout } from '../controllers/index.ts'; +import { RequestError } from '../errors/index.ts'; +import { type Endpoint, endpt } from '../static/endpoints.ts'; +import { httpStatus, type Method, mthd } from '../static/vocab/httpVocab.ts'; +import { sch } from '../validation/index.ts'; +import { type Ctx } from '../static/types.ts'; type Entr = { auth: boolean; @@ -52,14 +51,14 @@ const routeMap = { const getRouteConfig = ( ep: Endpoint, - mthd: Method, + m: Method, ): Entr => { const epRoutes = routeMap[ep]; - const conf = epRoutes[mthd as keyof typeof epRoutes]; + const conf = epRoutes[m as keyof typeof epRoutes]; if (!conf) { throw new RequestError( - `Method ${mthd} is not supported for ${ep} endpoint`, + `Method ${m} is not supported for ${ep} endpoint`, httpStatus.na, ); } diff --git a/src/services/general.service.ts b/src/services/general.service.ts index 6d1472a9..8f374214 100644 --- a/src/services/general.service.ts +++ b/src/services/general.service.ts @@ -1,9 +1,12 @@ -import { DBError, RequestError } from '../errors'; -import DB from '../model'; -import { fnames, httpStatus, TNAMES } from '../static'; -import { DBRes } from './types'; +import { RequestError } from '../errors/index.ts'; +import DB from '../model/index.ts'; +import { fnames, httpStatus, type Tnames } from '../static/index.ts'; +import { type DBRes } from './types.ts'; -const get = async (table: T, key: string): Promise => { +const get = async ( + table: T, + key: string, +): Promise => { const item = await DB[table].findByPk(key); if (!item) { @@ -13,8 +16,7 @@ const get = async (table: T, key: string): Promise = return item.toJSON(); }; -const del = async (table: TNAMES, key: string): Promise => { - +const del = async (table: Tnames, key: string): Promise => { const item = await DB[table].findByPk(key); if (!item) { @@ -24,7 +26,7 @@ const del = async (table: TNAMES, key: string): Promise => { await item.destroy(); }; -const getByParam = async ( +const getByParam = async ( table: T, field: (typeof fnames)[T][keyof (typeof fnames)[T]], query: string | boolean, diff --git a/src/services/index.ts b/src/services/index.ts index e2d655f2..0bc44e2a 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,3 +1,3 @@ -import usrServices from './user.service'; -import tkn from './token.service' +import usrServices from './user.service.ts'; +import tkn from './token.service.ts'; export { usrServices, tkn }; diff --git a/src/services/token.service.ts b/src/services/token.service.ts index 0495e475..92a6d765 100644 --- a/src/services/token.service.ts +++ b/src/services/token.service.ts @@ -1,15 +1,15 @@ -import DB from '../model'; -import { TKN, TNAMES, fnames } from '../static'; -import { asyncDBHandler } from '../utils'; -import { del, getByParam } from './general.service'; -import { DBResTkn } from './types'; +import DB from '../model/index.ts'; +import { TKN, TNAMES, type Tokens, fnames } from '../static/index.ts'; +import { asyncDBHandler } from '../utils/index.ts'; +import { del, getByParam } from './general.service.ts'; +import { type DBResTkn } from './types.ts'; const tkNames = fnames[TNAMES.TKN]; -async function createToken ( +async function createToken( userId: string, token: string, - type: Exclude, + type: Exclude, expiresAt: string, ): Promise { const newToken = await DB[TNAMES.TKN].create({ @@ -20,19 +20,17 @@ async function createToken ( }); return newToken.toJSON(); -}; +} -async function getByTkn(tkn: string): Promise { - const token = await getByParam(TNAMES.TKN, fnames[TNAMES.TKN].token, tkn); +async function getByTkn(t: string): Promise { + const token = await getByParam(TNAMES.TKN, fnames[TNAMES.TKN].token, t); return token; } - const tkn = { - create: asyncDBHandler((userId, token, type, expiresAt) => - createToken(userId, token, type, expiresAt)), - getByTkn: asyncDBHandler((tkn: string) => getByTkn(tkn)), + create: asyncDBHandler((id, t, tp, exp) => createToken(id, t, tp, exp)), + getByTkn: asyncDBHandler((t: string) => getByTkn(t)), del: asyncDBHandler((id: string) => del(TNAMES.TKN, id)), }; diff --git a/src/services/types.ts b/src/services/types.ts index 98d3a1cd..5cd25eae 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -1,4 +1,4 @@ -import { TKN, TNAMES } from "../static"; +import { TNAMES, type Tokens } from '../static/index.ts'; interface DBResUsr { id: string; @@ -11,26 +11,26 @@ interface DBResUsr { } interface DBResTkn { - id: string, - userId: string, - token: string, - type: TKN, - expiresAt: Date, - createdAt: Date, + id: string; + userId: string; + token: string; + type: Tokens; + expiresAt: Date; + createdAt: Date; } interface DBResSN { - id: string, - userId: string, - google: string | null, - github: string | null, - facebook: string | null, + id: string; + userId: string; + google: string | null; + github: string | null; + facebook: string | null; } type DBRes = { [TNAMES.USR]: DBResUsr; [TNAMES.SCN]: DBResSN; [TNAMES.TKN]: DBResTkn; -} +}; -export type { DBRes, DBResTkn }; \ No newline at end of file +export type { DBRes, DBResTkn }; diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 1c4ef1ba..e984e454 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -1,13 +1,14 @@ -import { fnames, TNAMES } from '../static'; -import { asyncDBHandler } from '../utils'; -import { del, get, getByParam } from './general.service'; +import { fnames, TNAMES } from '../static/index.ts'; +import { asyncDBHandler } from '../utils/index.ts'; +import { del, get, getByParam } from './general.service.ts'; + +const a = asyncDBHandler; const usrServices = { - getById: asyncDBHandler((id: string) => get(TNAMES.USR, id)), - getByEmail: asyncDBHandler((email: string) => - getByParam(TNAMES.USR, fnames[TNAMES.USR].email, email), - ), - delete: asyncDBHandler((id: string) => del(TNAMES.USR, id)), + getById: a((id: string) => get(TNAMES.USR, id)), + // eslint-disable-next-line max-len, prettier/prettier + getByEmail: a((e: string) => getByParam(TNAMES.USR, fnames[TNAMES.USR].email, e)), + delete: a((id: string) => del(TNAMES.USR, id)), }; export default usrServices; diff --git a/src/static/constants.ts b/src/static/constants.ts index 3967e492..e849575d 100644 --- a/src/static/constants.ts +++ b/src/static/constants.ts @@ -1,6 +1,6 @@ -import { TKN } from '.'; +import { TKN, type Tokens } from './index.ts'; -const TOKEN_EXPIRY: Record = { +const TOKEN_EXPIRY: Record = { [TKN.ACC]: ['15m', '900'], [TKN.RFR]: ['7d', '604800'], [TKN.ACT]: ['24h', '86400'], diff --git a/src/static/endpoints.ts b/src/static/endpoints.ts index fb151443..43d6ae27 100644 --- a/src/static/endpoints.ts +++ b/src/static/endpoints.ts @@ -12,6 +12,6 @@ const endpt = { pwdRes: '/password/reset', } as const; -type Endpoint = typeof endpt[keyof typeof endpt] +type Endpoint = (typeof endpt)[keyof typeof endpt]; -export { endpt, type Endpoint }; \ No newline at end of file +export { endpt, type Endpoint }; diff --git a/src/static/index.ts b/src/static/index.ts index b04471a2..60361047 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -1,6 +1,8 @@ -import { TNAMES, fnames, TKN } from './vocab/dbVocab'; -import { mthd, httpStatus } from './vocab/httpVocab'; -import { endpt } from './endpoints'; -import { TOKEN_EXPIRY } from './constants' +import { TNAMES, fnames, TKN } from './vocab/dbVocab.ts'; +import { mthd, httpStatus } from './vocab/httpVocab.ts'; +import { endpt } from './endpoints.ts'; +import { TOKEN_EXPIRY } from './constants.ts'; +import type { Tokens, Tnames } from './vocab/dbVocab.ts'; export { TNAMES, TKN, fnames, TOKEN_EXPIRY, mthd, httpStatus, endpt }; +export type { Tokens, Tnames }; diff --git a/src/static/types.ts b/src/static/types.ts index 7b041a2a..96005e47 100644 --- a/src/static/types.ts +++ b/src/static/types.ts @@ -1,9 +1,9 @@ import http from 'http'; -import { z } from 'zod' +import { z } from 'zod'; -import { type Endpoint } from "./endpoints"; -import { type Method, type HTTPStatus } from "./vocab/httpVocab"; -import { JWTPayload } from '../utils'; +import { type Endpoint } from './endpoints.ts'; +import { type Method, type HTTPStatus } from './vocab/httpVocab.ts'; +import { type JWTPayload } from '../utils/index.ts'; type Ctx = { req: http.IncomingMessage; @@ -13,4 +13,4 @@ type Ctx = { param: string | null; }; -export type { Endpoint, Method, HTTPStatus, Ctx }; \ No newline at end of file +export type { Endpoint, Method, HTTPStatus, Ctx }; diff --git a/src/static/vocab/dbVocab.ts b/src/static/vocab/dbVocab.ts index 5f4405bd..86a359ff 100644 --- a/src/static/vocab/dbVocab.ts +++ b/src/static/vocab/dbVocab.ts @@ -1,8 +1,10 @@ -enum TNAMES { - USR = 'users', - SCN = 'social_accounts', - TKN = 'tokens', -}; +const TNAMES = { + USR: 'users', + SCN: 'social_accounts', + TKN: 'tokens', +} as const; + +type Tnames = (typeof TNAMES)[keyof typeof TNAMES]; const fnames = { [TNAMES.USR]: { @@ -28,11 +30,14 @@ const fnames = { }, } as const; -enum TKN { - ACT = 'activation', - RFR = 'refresh', - PWR = 'password_reset', - ACC = 'access', -} +const TKN = { + ACT: 'activation', + RFR: 'refresh', + PWR: 'password_reset', + ACC: 'access', +} as const; + +type Tokens = (typeof TKN)[keyof typeof TKN]; export { TNAMES, fnames, TKN }; +export type { Tokens, Tnames }; diff --git a/src/static/vocab/httpVocab.ts b/src/static/vocab/httpVocab.ts index 9e0b098f..b70f17e5 100644 --- a/src/static/vocab/httpVocab.ts +++ b/src/static/vocab/httpVocab.ts @@ -16,7 +16,7 @@ const httpStatus = { se: 500, } as const; -type Method = typeof mthd[keyof typeof mthd]; -type HTTPStatus = typeof httpStatus[keyof typeof httpStatus]; +type Method = (typeof mthd)[keyof typeof mthd]; +type HTTPStatus = (typeof httpStatus)[keyof typeof httpStatus]; -export { mthd, httpStatus, type Method, type HTTPStatus }; \ No newline at end of file +export { mthd, httpStatus, type Method, type HTTPStatus }; diff --git a/src/utils/cookies.ts b/src/utils/cookies.ts index 6670af7d..7e5aea3b 100644 --- a/src/utils/cookies.ts +++ b/src/utils/cookies.ts @@ -11,6 +11,7 @@ function parseCookies(req: http.IncomingMessage): Cookies { .filter(Boolean) .map((c) => { const [key, ...val] = c.trim().split('='); + return [key, val.join('=')]; }), ); diff --git a/src/utils/dbHandler.ts b/src/utils/dbHandler.ts index b59b3ade..e25d385b 100644 --- a/src/utils/dbHandler.ts +++ b/src/utils/dbHandler.ts @@ -1,4 +1,4 @@ -import { DBError } from "../errors"; +import { DBError } from '../errors/index.ts'; function asyncDBHandler( fn: (...args: TArgs) => Promise, @@ -7,9 +7,11 @@ function asyncDBHandler( try { return await fn(...args); } catch (error) { - throw new DBError(`Database operation failed: ${error}`); + throw new DBError( + `Database operation failed: ${error instanceof Error ? error.message : error}`, + ); } }; } -export default asyncDBHandler; \ No newline at end of file +export default asyncDBHandler; diff --git a/src/utils/index.ts b/src/utils/index.ts index 7ea5a1fe..c0a9ade3 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,7 +1,7 @@ -import asyncDBHandler from './dbHandler'; -import { parseCookies } from './cookies'; -import { type JWTPayload } from './types'; -import jwtAct from './jwt'; +import asyncDBHandler from './dbHandler.ts'; +import { parseCookies } from './cookies.ts'; +import { type JWTPayload } from './types.ts'; +import jwtAct from './jwt.ts'; export { asyncDBHandler, parseCookies, jwtAct }; export type { JWTPayload }; diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts index 838425d6..6e37a878 100644 --- a/src/utils/jwt.ts +++ b/src/utils/jwt.ts @@ -1,13 +1,13 @@ import jwt from 'jsonwebtoken'; -import { RequestError } from '../errors'; -import { httpStatus, TKN, TOKEN_EXPIRY } from '../static'; -import { JWTPayload } from './types'; +import { RequestError } from '../errors/index.ts'; +import { httpStatus, TKN, TOKEN_EXPIRY, type Tokens } from '../static/index.ts'; +import { type JWTPayload } from './types.ts'; const SECRET_KEY = process.env.JWT_SECRET || '7>?~!id(#;fd13/^^$fdkq124<'; type Signed = { expiry: string; token: string }; -function signToken(payload: JWTPayload, type: TKN): Signed { +function signToken(payload: JWTPayload, type: Tokens): Signed { const expiry = TOKEN_EXPIRY[type][0]; const token = jwt.sign({ ...payload, type }, SECRET_KEY, { @@ -17,10 +17,10 @@ function signToken(payload: JWTPayload, type: TKN): Signed { return { expiry, token }; } -function verifyToken(token: string): JWTPayload & { type: TKN } { +function verifyToken(token: string): JWTPayload & { type: Tokens } { try { const decoded = jwt.verify(token, SECRET_KEY) as JWTPayload & { - type: TKN; + type: Tokens; }; return decoded; @@ -36,8 +36,9 @@ function verifyToken(token: string): JWTPayload & { type: TKN } { throw new RequestError('Token verification failed', httpStatus.br); } } + const jwtAct = { - sign: (pl: JWTPayload, tp: TKN) => signToken(pl, tp), + sign: (pl: JWTPayload, tp: Tokens) => signToken(pl, tp), ver: (tk: string) => verifyToken(tk), create: { [TKN.ACC]: (payload: JWTPayload): Signed => signToken(payload, TKN.ACC), diff --git a/src/utils/types.ts b/src/utils/types.ts index 630f50ac..c99cbab4 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -1,4 +1,4 @@ -import { fnames, TNAMES } from "../static"; +import { fnames, TNAMES } from '../static/index.ts'; const nms = fnames[TNAMES.USR]; @@ -8,4 +8,4 @@ type JWTPayload = { [nms.email]: string; }; -export type { JWTPayload }; \ No newline at end of file +export type { JWTPayload }; diff --git a/src/validation/index.ts b/src/validation/index.ts index a5b579f1..67ee9da5 100644 --- a/src/validation/index.ts +++ b/src/validation/index.ts @@ -1,4 +1,4 @@ -import validateRequest from './validateRequest'; -import validateBody from './validateBody'; -import sch from './schemas'; +import validateRequest from './validateRequest.ts'; +import validateBody from './validateBody.ts'; +import sch from './schemas.ts'; export { validateRequest, validateBody, sch }; diff --git a/src/validation/schemas.ts b/src/validation/schemas.ts index 2d0511fa..49f86bdb 100644 --- a/src/validation/schemas.ts +++ b/src/validation/schemas.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { fnames, TNAMES } from '../static'; +import { fnames, TNAMES } from '../static/index.ts'; const usrFNames = fnames[TNAMES.USR]; const tkFNames = fnames[TNAMES.TKN]; diff --git a/src/validation/validateBody.ts b/src/validation/validateBody.ts index a95f76bb..a8a65e4c 100644 --- a/src/validation/validateBody.ts +++ b/src/validation/validateBody.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { RequestError } from '../errors'; -import { httpStatus } from '../static'; +import { RequestError } from '../errors/index.ts'; +import { httpStatus } from '../static/index.ts'; function validateBody( body: unknown, @@ -16,4 +16,4 @@ function validateBody( } } -export default validateBody; \ No newline at end of file +export default validateBody; diff --git a/src/validation/validateRequest.ts b/src/validation/validateRequest.ts index 21fa3dd4..fbae52b9 100644 --- a/src/validation/validateRequest.ts +++ b/src/validation/validateRequest.ts @@ -1,7 +1,7 @@ import http from 'http'; -import { RequestError } from '../errors'; -import { httpStatus, mthd, endpt } from '../static'; -import { type Method, type Endpoint } from '../static/types'; +import { RequestError } from '../errors/index.ts'; +import { httpStatus, mthd, endpt } from '../static/index.ts'; +import { type Endpoint, type Method } from '../static/types.ts'; const REQUIRED_PARAMS: Partial> = { [endpt.act]: 'token', @@ -20,15 +20,18 @@ function validateRequest(req: http.IncomingMessage) { if (!req.url) { throw new RequestError('Expected request URL', httpStatus.br); } + const url = new URL(req.url, 'http://localhost'); // validate endpoint const endpoint = url.pathname; + if (!validatePath(endpoint)) { throw new RequestError('Not found', httpStatus.nf); } // validate method const method = req.method; + if (!validateMethod(method)) { throw new RequestError(`Unknown method: ${method}`, httpStatus.br); } @@ -36,6 +39,7 @@ function validateRequest(req: http.IncomingMessage) { if (!(endpoint in REQUIRED_PARAMS)) { return { endpoint, method, param: null }; } + const param = url.searchParams.get(REQUIRED_PARAMS[endpoint] as string); if (!param) { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..38b45a34 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./src", + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + "noEmit": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} From 395238b36511f13b9d133d96677ee2e5e814aeba Mon Sep 17 00:00:00 2001 From: Bespalov Tymofii Date: Thu, 5 Feb 2026 16:13:07 +0100 Subject: [PATCH 08/14] done obl services --- .env | 10 +- package-lock.json | 21 ++++ package.json | 2 + src/controllers/acc.ctr.ts | 76 +++++++++++++++ .../{auth.ctrlr.ts => auth.ctr.ts} | 96 +++++++++---------- src/controllers/helpers/helpers.ts | 75 +++++++++++++++ src/controllers/{idx.ctrlr.ts => idx.ctr.ts} | 0 src/controllers/index.ts | 17 +++- src/controllers/reg.ctr.ts | 80 ++++++++++++++++ src/controllers/res.ctr.ts | 78 +++++++++++++++ src/createServer.ts | 49 ++++++++-- src/dto/dto.ts | 9 ++ src/dto/index.ts | 7 ++ src/errors/errors.ts | 15 +-- src/errors/index.ts | 4 +- src/middleware/errorHandler.ts | 4 +- src/middleware/index.ts | 9 +- src/middleware/tokenAuth.ts | 24 +++++ src/model/token.model.ts | 2 +- src/router/router.ts | 26 ++--- src/services/email.service.ts | 63 ++++++++++++ src/services/index.ts | 12 ++- .../{general.service.ts => repo.service.ts} | 39 +++++++- src/services/token.service.ts | 44 +++------ src/services/types.ts | 38 +++----- src/services/user.service.ts | 38 ++++++-- src/static/types/index.ts | 3 + src/static/types/token.types.ts | 18 ++++ src/static/types/user.types.ts | 20 ++++ src/utils/dbHandler.ts | 4 +- src/utils/index.ts | 4 +- src/utils/jwt.ts | 11 ++- src/utils/types.ts | 10 +- 33 files changed, 736 insertions(+), 172 deletions(-) create mode 100644 src/controllers/acc.ctr.ts rename src/controllers/{auth.ctrlr.ts => auth.ctr.ts} (53%) create mode 100644 src/controllers/helpers/helpers.ts rename src/controllers/{idx.ctrlr.ts => idx.ctr.ts} (100%) create mode 100644 src/controllers/reg.ctr.ts create mode 100644 src/controllers/res.ctr.ts create mode 100644 src/dto/dto.ts create mode 100644 src/dto/index.ts create mode 100644 src/middleware/tokenAuth.ts create mode 100644 src/services/email.service.ts rename src/services/{general.service.ts => repo.service.ts} (52%) create mode 100644 src/static/types/index.ts create mode 100644 src/static/types/token.types.ts create mode 100644 src/static/types/user.types.ts diff --git a/.env b/.env index 74ea5c20..0b6a1af8 100644 --- a/.env +++ b/.env @@ -4,4 +4,12 @@ POSTGRES_PORT=5432 POSTGRES_USER=postgres POSTGRES_PASSWORD=123 POSTGRES_DB=postgres -JWT_SECRET_KEY=itlv&80f8 \ No newline at end of file +JWT_SECRET_KEY=itlv&80f8 + +SMTP_HOST=smtp.example.com +SMTP_PORT=587 +SMTP_SECURE=false +SMTP_USER=your@email.com +SMTP_PASS=password +SMTP_FROM=noreply@yourapp.com +BASE_URL=http://localhost:5700 diff --git a/package-lock.json b/package-lock.json index d1392d7f..33753a42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "bcrypt": "^6.0.0", "dotenv": "^17.2.3", "jsonwebtoken": "^9.0.3", + "nodemailer": "^8.0.0", "pg": "^8.17.2", "pg-hstore": "^2.3.4", "sequelize": "^6.37.7", @@ -23,6 +24,7 @@ "devDependencies": { "@mate-academy/eslint-config": "latest", "@mate-academy/scripts": "^2.1.3", + "@types/nodemailer": "^7.0.9", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", "eslint": "^8.57.0", @@ -2078,6 +2080,16 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/nodemailer": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.9.tgz", + "integrity": "sha512-vI8oF1M+8JvQhsId0Pc38BdUP2evenIIys7c7p+9OZXSPOH5c1dyINP1jT8xQ2xPuBUXmIC87s+91IZMDjH8Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -7314,6 +7326,15 @@ "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true }, + "node_modules/nodemailer": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.0.tgz", + "integrity": "sha512-xvVJf/f0bzmNpnRIbhCp/IKxaHgJ6QynvUbLXzzMRPG3LDQr5oXkYuw4uDFyFYs8cge8agwwrJAXZsd4hhMquw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", diff --git a/package.json b/package.json index c012005d..da71256a 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "devDependencies": { "@mate-academy/eslint-config": "latest", "@mate-academy/scripts": "^2.1.3", + "@types/nodemailer": "^7.0.9", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", "eslint": "^8.57.0", @@ -36,6 +37,7 @@ "bcrypt": "^6.0.0", "dotenv": "^17.2.3", "jsonwebtoken": "^9.0.3", + "nodemailer": "^8.0.0", "pg": "^8.17.2", "pg-hstore": "^2.3.4", "sequelize": "^6.37.7", diff --git a/src/controllers/acc.ctr.ts b/src/controllers/acc.ctr.ts new file mode 100644 index 00000000..60f4bccd --- /dev/null +++ b/src/controllers/acc.ctr.ts @@ -0,0 +1,76 @@ +import dto from '../dto/index.ts'; +import srv from '../services/index.ts'; +import { httpStatus, TKN } from '../static/index.ts'; +import type { Ctx } from '../static/types.ts'; +import type { JWTPayload } from '../utils/types.ts'; +import { sch } from '../validation/index.ts'; +import { hashPwd, reactivate, setTkn, verifyPwd } from './helpers/helpers.ts'; + +async function getAccData(ctx: Ctx) { + const { res, usr } = ctx; + + const dbres = await srv.usr.getById((usr as JWTPayload).id); + + const data = JSON.stringify(dto.usr(dbres)); + + res.statusCode = httpStatus.ok; + res.end(data); +} + +async function deleteAcc(ctx: Ctx) { + const { res, usr } = ctx; + + await srv.usr.delete((usr as JWTPayload).id); + + res.setHeader('Set-Cookie', [ + setTkn(TKN.ACC, '', '0'), + setTkn(TKN.RFR, '', '0'), + ]); + + res.statusCode = httpStatus.nc; + res.end(); +} + +async function patchAcc(ctx: Ctx) { + const { res, body, usr } = ctx; + const { password, email, name } = body; + + const usrFromDB = await verifyPwd((usr as JWTPayload).id, password); + + let payload; + + if (email === usrFromDB.email) { + payload = { name }; + + const newUsr = await srv.usr.patch(usrFromDB.id, payload); + + res.statusCode = httpStatus.ok; + res.end(JSON.stringify(dto.usr(newUsr))); + } else { + payload = { name, email }; + + const newUsr = await srv.usr.patch(usrFromDB.id, payload); + + await reactivate(newUsr, res); + } +} + +async function resetPass(ctx: Ctx) { + const { res, body, usr } = ctx; + const { oldPwd, newPwd } = body; + + const usrFromDB = await verifyPwd((usr as JWTPayload).id, oldPwd); + const hashedPwd = await hashPwd(newPwd); + const newUsr = await srv.usr.patch(usrFromDB.id, { password: hashedPwd }); + + await reactivate(newUsr, res); +} + +const acc = { + get: getAccData, + del: deleteAcc, + patch: patchAcc, + res: resetPass, +}; + +export default acc; diff --git a/src/controllers/auth.ctrlr.ts b/src/controllers/auth.ctr.ts similarity index 53% rename from src/controllers/auth.ctrlr.ts rename to src/controllers/auth.ctr.ts index d1011e77..544c711b 100644 --- a/src/controllers/auth.ctrlr.ts +++ b/src/controllers/auth.ctr.ts @@ -1,76 +1,60 @@ -import http from 'http'; import bcrypt from 'bcrypt'; import { RequestError } from '../errors/index.ts'; -import { tkn, usrServices } from '../services/index.ts'; +import srv from '../services/index.ts'; import { httpStatus, TKN, TOKEN_EXPIRY } from '../static/index.ts'; import sch from '../validation/schemas.ts'; import { parseCookies, jwtAct } from '../utils/index.ts'; import { type Ctx } from '../static/types.ts'; +import { ckNms, setTkn } from './helpers/helpers.ts'; -const ckNms = { - [TKN.ACC]: 'access_token', - [TKN.RFR]: 'refresh_token', -}; - -const cks = (t: typeof TKN.ACC | typeof TKN.RFR, s: string, age: string) => - `${ckNms[t]}=${s}; HttpOnly; Path=/; Max-Age=${age}; SameSite=Strict`; - -async function authMan(ctx: Ctx): Promise { +async function manual(ctx: Ctx): Promise { const { res, body } = ctx; const { email, password } = body; - // check usr - const usr = await usrServices.getByEmail(email); - - if (!usr) { - throw new RequestError(`User with email ${email} not found`, httpStatus.nf); + // check usr activation + const user = await srv.usr.getByEmail(email); + + if (!user.activated) { + throw new RequestError( + `User with email ${email} is not activated`, + httpStatus.ua, + ); } // compare pw - const isValid = await bcrypt.compare(password, usr.password); + const isValid = await bcrypt.compare(password, user.password); if (!isValid) { - throw new RequestError(`Wrong password`, httpStatus.na); + throw new RequestError(`Wrong password`, httpStatus.ua); } // create tkns - const { id, name } = usr; + const { id, name } = user; const payload = { id, name, email }; const accTkn = jwtAct.create[TKN.ACC](payload); const refTkn = jwtAct.create[TKN.RFR](payload); // add refresh tkn to DB + const createPayload = { + userId: id, + token: refTkn.token, + type: TKN.RFR, + expiresAt: refTkn.expiresAt, + }; - tkn.create(id, refTkn.token, TKN.RFR, refTkn.expiry); + await srv.tkn.create(createPayload); // set cookies res.setHeader('Set-Cookie', [ - cks(TKN.ACC, accTkn.token, TOKEN_EXPIRY.access[1]), - cks(TKN.RFR, refTkn.token, TOKEN_EXPIRY.refresh[1]), + setTkn(TKN.ACC, accTkn.token, TOKEN_EXPIRY.access[1]), + setTkn(TKN.RFR, refTkn.token, TOKEN_EXPIRY.refresh[1]), ]); res.statusCode = httpStatus.ok; res.end(JSON.stringify({ message: 'Authorized', user: { id, name, email } })); } -function authTkn(req: http.IncomingMessage) { - const cookies = parseCookies(req); - const token = cookies[ckNms[TKN.ACC]]; - - if (!token) { - throw new RequestError('No token provided', httpStatus.ua); - } - - const { type, ...pl } = jwtAct.ver(token); - - if (type !== TKN.ACC) { - throw new RequestError('Invalid token type', httpStatus.ua); - } - - return pl; -} - async function refresh(ctx: Ctx) { const { req, res } = ctx; const cookies = parseCookies(req); @@ -86,14 +70,14 @@ async function refresh(ctx: Ctx) { throw new RequestError('Invalid token type', httpStatus.ua); } - const dbTok = await tkn.getByTkn(token); + const dbTok = await srv.tkn.getByTkn(token); if (!dbTok) { throw new RequestError(`Token ${token} doesn't exist`, httpStatus.ua); } // delete token - tkn.del(dbTok.id); + srv.tkn.del(dbTok.id); // create new Tokens @@ -101,13 +85,19 @@ async function refresh(ctx: Ctx) { const refTkn = jwtAct.create[TKN.RFR](pl); // add refr token to DB + const createPayload = { + userId: pl.id, + token: refTkn.token, + type: TKN.RFR, + expiresAt: refTkn.expiresAt, + }; - tkn.create(pl.id, refTkn.token, TKN.RFR, refTkn.expiry); + await srv.tkn.create(createPayload); // set cookies res.setHeader('Set-Cookie', [ - cks(TKN.ACC, accTkn.token, TOKEN_EXPIRY.access[1]), - cks(TKN.RFR, refTkn.token, TOKEN_EXPIRY.refresh[1]), + setTkn(TKN.ACC, accTkn.token, TOKEN_EXPIRY.access[1]), + setTkn(TKN.RFR, refTkn.token, TOKEN_EXPIRY.refresh[1]), ]); // end res @@ -122,16 +112,26 @@ async function logout(ctx: Ctx) { const refToken = cookies[ckNms[TKN.RFR]]; if (refToken) { - const dbTok = await tkn.getByTkn(refToken); + const dbTok = await srv.tkn.getByTkn(refToken); if (dbTok) { - tkn.del(dbTok.id); + srv.tkn.del(dbTok.id); } } - res.setHeader('Set-Cookie', [cks(TKN.ACC, '', '0'), cks(TKN.RFR, '', '0')]); + + res.setHeader('Set-Cookie', [ + setTkn(TKN.ACC, '', '0'), + setTkn(TKN.RFR, '', '0'), + ]); res.statusCode = httpStatus.ok; res.end(JSON.stringify({ message: 'Logged out' })); } -export { authMan, authTkn, refresh, logout }; +const auth = { + man: manual, + rfr: refresh, + lgt: logout, +}; + +export default auth; diff --git a/src/controllers/helpers/helpers.ts b/src/controllers/helpers/helpers.ts new file mode 100644 index 00000000..2e36c34d --- /dev/null +++ b/src/controllers/helpers/helpers.ts @@ -0,0 +1,75 @@ +import http from 'http'; +import bcrypt from 'bcrypt'; +import srv from '../../services/index.ts'; +import { httpStatus, TKN } from '../../static/index.ts'; +import type { DBUser } from '../../static/types/user.types.ts'; +import { RequestError } from '../../errors/errors.ts'; +import jwtAct from '../../utils/jwt.ts'; + +const SALT_ROUNDS = 10; + +const ckNms = { + [TKN.ACC]: 'access_token', + [TKN.RFR]: 'refresh_token', +}; + +const setTkn = (t: typeof TKN.ACC | typeof TKN.RFR, s: string, age: string) => + `${ckNms[t]}=${s}; HttpOnly; Path=/; Max-Age=${age}; SameSite=Strict`; + +const verifyPwd = async (usrId: string, pwd: string): Promise => { + const usr = await srv.usr.getById(usrId); + + const isValid = await bcrypt.compare(pwd, usr.password); + + if (!isValid) { + throw new RequestError(`Wrong password`, httpStatus.ua); + } + + return usr; +}; + +const hashPwd = async (pwd: string) => { + return bcrypt.hash(pwd, SALT_ROUNDS); +}; + +async function reactivate(user: DBUser, res: http.ServerResponse) { + await srv.tkn.delByUserId(user.id, TKN.RFR); + + await srv.usr.patch(user.id, { activated: false }); + + const jwtPayload = { id: user.id, name: user.name, email: user.email }; + + const actToken = jwtAct.sign(jwtPayload, TKN.ACT); + + const createPl = { + userId: user.id, + token: actToken.token, + type: TKN.ACT, + expiresAt: actToken.expiresAt, + }; + + await srv.tkn.create(createPl); + + srv.email + .sendActivation(jwtPayload.email, actToken.token) + .catch((err: Error) => { + // eslint-disable-next-line no-console + console.error('Failed to send activation email:', err.message); + }); + + res.setHeader('Set-Cookie', [ + setTkn(TKN.ACC, '', '0'), + setTkn(TKN.RFR, '', '0'), + ]); + + res.statusCode = httpStatus.ok; + + res.end( + JSON.stringify({ + message: + 'Email or password changed. Check your inbox for activation link.', + }), + ); +} + +export { ckNms, setTkn, verifyPwd, reactivate, hashPwd }; diff --git a/src/controllers/idx.ctrlr.ts b/src/controllers/idx.ctr.ts similarity index 100% rename from src/controllers/idx.ctrlr.ts rename to src/controllers/idx.ctr.ts diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 07ff354c..34633125 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,4 +1,15 @@ -import idx from './idx.ctrlr.ts'; -import { authMan, authTkn, refresh, logout } from './auth.ctrlr.ts'; +import idx from './idx.ctr.ts'; +import auth from './auth.ctr.ts'; +import reg from './reg.ctr.ts'; +import acc from './acc.ctr.ts'; +import rst from './res.ctr.ts'; -export { idx, authMan, authTkn, refresh, logout }; +const ctr = { + idx: idx, + auth: auth, + reg: reg, + acc: acc, + res: rst, +}; + +export default ctr; diff --git a/src/controllers/reg.ctr.ts b/src/controllers/reg.ctr.ts new file mode 100644 index 00000000..ee20e14f --- /dev/null +++ b/src/controllers/reg.ctr.ts @@ -0,0 +1,80 @@ +import { RequestError } from '../errors/index.ts'; +import srv from '../services/index.ts'; +import { httpStatus, TKN } from '../static/index.ts'; +import { type Ctx } from '../static/types.ts'; +import { jwtAct } from '../utils/index.ts'; +import sch from '../validation/schemas.ts'; +import { hashPwd } from './helpers/helpers.ts'; + +async function register(ctx: Ctx): Promise { + const { res, body } = ctx; + const { name, email, password } = body; + + // check if user exists + + const exists = await srv.usr.existsByEmail(email); + + if (exists) { + throw new RequestError( + `User with email ${email} already exists`, + httpStatus.br, + ); + } + + // create user + const hashedPwd = await hashPwd(password); + + const newUsr = await srv.usr.create({ + name, + email, + password: hashedPwd, + }); + + // create activationToken; + const payload = { id: newUsr.id, name, email }; + const actToken = jwtAct.sign(payload, TKN.ACT); + + const createPL = { + userId: newUsr.id, + token: actToken.token, + type: TKN.ACT, + expiresAt: actToken.expiresAt, + }; + + await srv.tkn.create(createPL); + + // send activation email (non-blocking, log errors) + srv.email.sendActivation(email, actToken.token).catch((err: Error) => { + // eslint-disable-next-line no-console + console.error('Failed to send activation email:', err.message); + }); + + res.statusCode = httpStatus.cr; + + res.end( + JSON.stringify({ + message: 'User created. Check your email for activation link.', + user: { id: newUsr.id, name, email }, + }), + ); +} + +async function activate(ctx: Ctx) { + const { res, param } = ctx; + + const token = await srv.tkn.getByTkn(param as string); + + await srv.usr.patch(token.userId, { activated: true }); + + await srv.tkn.del(token.id); + + res.statusCode = httpStatus.ok; + res.end(JSON.stringify({ message: `Succesfully activated` })); +} + +const reg = { + reg: register, + act: activate, +}; + +export default reg; diff --git a/src/controllers/res.ctr.ts b/src/controllers/res.ctr.ts new file mode 100644 index 00000000..92d8f757 --- /dev/null +++ b/src/controllers/res.ctr.ts @@ -0,0 +1,78 @@ +import { RequestError } from '../errors/index.ts'; +import srv from '../services/index.ts'; +import { httpStatus, TKN } from '../static/index.ts'; +import { type Ctx } from '../static/types.ts'; +import jwtAct from '../utils/jwt.ts'; +import { sch } from '../validation/index.ts'; +import { hashPwd } from './helpers/helpers.ts'; + +async function requestReset(ctx: Ctx) { + const { res, body } = ctx; + + const user = await srv.usr.getByEmail(body.email); + + await srv.tkn.delByUserId(user.id, TKN.PWR); + + const jwtPayload = { id: user.id, name: user.name, email: user.email }; + const token = jwtAct.sign(jwtPayload, TKN.PWR); + + const createPl = { + userId: user.id, + token: token.token, + type: TKN.PWR, + expiresAt: token.expiresAt, + }; + + await srv.tkn.create(createPl); + + srv.email.sendReset(user.email, token.token).catch((err: Error) => { + // eslint-disable-next-line no-console + console.error('Failed to send reset email:', err.message); + }); + + res.statusCode = httpStatus.ok; + + res.end( + JSON.stringify({ + message: 'Check your inbox for password reset link.', + }), + ); +} + +async function processReset(ctx: Ctx) { + const { res, body } = ctx; + const { token, newPwd } = body; + + const { type, ...payload } = jwtAct.ver(token); + + if (type !== TKN.PWR) { + throw new RequestError('Invalid token type', httpStatus.br); + } + + const dbToken = await srv.tkn.getByTkn(token); + + if (dbToken.userId !== payload.id) { + throw new RequestError('Token mismatch', httpStatus.br); + } + + const hashedPwd = await hashPwd(newPwd); + + await srv.usr.patch(payload.id, { password: hashedPwd }); + + await srv.tkn.del(dbToken.id); + + res.statusCode = httpStatus.ok; + + res.end( + JSON.stringify({ + message: 'Password reset successfully. You can now login.', + }), + ); +} + +const rst = { + req: requestReset, + prc: processReset, +}; + +export default rst; diff --git a/src/createServer.ts b/src/createServer.ts index 1a9dbea2..b432d72d 100644 --- a/src/createServer.ts +++ b/src/createServer.ts @@ -1,10 +1,12 @@ import http from 'http'; +import bcrypt from 'bcrypt'; import { dbSetup } from './db/db.ts'; import { validateBody, validateRequest } from './validation/index.ts'; -import { errorHandler, parseBody } from './middleware/index.ts'; +import mw from './middleware/index.ts'; import getRouteConfig from './router/router.ts'; -import { authTkn } from './controllers/index.ts'; import { type Ctx } from './static/types.ts'; +import { TNAMES } from './static/index.ts'; +import DB from './model/index.ts'; // Options preflight // Unified api response @@ -12,6 +14,41 @@ import { type Ctx } from './static/types.ts'; export async function createServer() { await dbSetup(); + const createUsr = async (usr: { + name: string; + email: string; + password: string; + activated: boolean; + }) => { + await DB[TNAMES.USR].create({ + ...usr, + }); + }; + + const pwds = ['jdfu70sdf', 'jdddu70sdf', 'jdgfdfd70sdf']; + const testUsrs = [ + { + name: 'JohnDoe', + email: 'gtlafuk@iksf.fsd', + password: await bcrypt.hash(pwds[0], 10), + activated: true, + }, + { + name: 'Yurgen', + email: 'gtlasdk@iksf.fsd', + password: await bcrypt.hash(pwds[1], 10), + activated: true, + }, + { + name: 'JosdnDoe', + email: 'gtsdgasfuk@iksf.fsd', + password: await bcrypt.hash(pwds[2], 10), + activated: true, + }, + ]; + + await testUsrs.map((el) => createUsr(el)); + return http.createServer(async (req, res) => { try { const { endpoint, method, param } = validateRequest(req); @@ -20,10 +57,10 @@ export async function createServer() { let usr = null; if (auth) { - usr = authTkn(req); + usr = mw.tokenAuth(req); } - const body = schema ? validateBody(await parseBody(req), schema) : false; + const body = schema ? validateBody(await mw.parseB(req), schema) : false; const ctx: Ctx = { req, @@ -33,9 +70,9 @@ export async function createServer() { param, }; - ctr(ctx); + await ctr(ctx); } catch (e) { - errorHandler(res, e); + mw.error(res, e); } }); } diff --git a/src/dto/dto.ts b/src/dto/dto.ts new file mode 100644 index 00000000..c7120dec --- /dev/null +++ b/src/dto/dto.ts @@ -0,0 +1,9 @@ +import type { DBUser, DTOUser } from '../static/types/user.types.ts'; + +const usrDto = (usr: DBUser): DTOUser => { + const { password, activated, createdAt, updatedAt, ...user } = usr; + + return user; +}; + +export { usrDto }; diff --git a/src/dto/index.ts b/src/dto/index.ts new file mode 100644 index 00000000..8c765f4c --- /dev/null +++ b/src/dto/index.ts @@ -0,0 +1,7 @@ +import { usrDto } from './dto.ts'; + +const dto = { + usr: usrDto, +}; + +export default dto; diff --git a/src/errors/errors.ts b/src/errors/errors.ts index f99d30a4..88c0c4ee 100644 --- a/src/errors/errors.ts +++ b/src/errors/errors.ts @@ -1,7 +1,7 @@ import { httpStatus } from '../static/index.ts'; import { type HTTPStatus } from '../static/types.ts'; -class CustomError extends Error { +class RequestError extends Error { statusCode: HTTPStatus; constructor(message: string, statusCode: HTTPStatus = httpStatus.br) { @@ -11,23 +11,16 @@ class CustomError extends Error { } } -class RequestError extends CustomError { - // eslint-disable-next-line no-useless-constructor - constructor(message: string, statusCode: HTTPStatus) { - super(message, statusCode); - } -} - -class DBError extends CustomError { +class DBError extends RequestError { constructor(message: string) { super(message, httpStatus.se); } } -class AuthError extends CustomError { +class AuthError extends RequestError { constructor(message: string) { super(message, httpStatus.na); } } -export { RequestError, DBError, AuthError, CustomError }; +export { RequestError, DBError, AuthError }; diff --git a/src/errors/index.ts b/src/errors/index.ts index c957c4e5..0392d583 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -1,3 +1 @@ -import { RequestError, DBError, AuthError, CustomError } from './errors.ts'; - -export { RequestError, DBError, AuthError, CustomError }; +export { RequestError, DBError, AuthError } from './errors.ts'; diff --git a/src/middleware/errorHandler.ts b/src/middleware/errorHandler.ts index 7caefbf8..36e8524d 100644 --- a/src/middleware/errorHandler.ts +++ b/src/middleware/errorHandler.ts @@ -1,9 +1,9 @@ import http from 'http'; import { httpStatus } from '../static/index.ts'; -import { CustomError } from '../errors/index.ts'; +import { RequestError } from '../errors/index.ts'; function errorHandler(res: http.ServerResponse, e: unknown) { - if (e instanceof CustomError) { + if (e instanceof RequestError) { res.statusCode = e.statusCode; res.end(e.message); diff --git a/src/middleware/index.ts b/src/middleware/index.ts index dd54ad09..bfb1649a 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -1,4 +1,11 @@ import { parseBody } from './bodyParser.ts'; import errorHandler from './errorHandler.ts'; +import tokenAuth from './tokenAuth.ts'; -export { parseBody, errorHandler }; +const mw = { + parseB: parseBody, + error: errorHandler, + tokenAuth: tokenAuth, +}; + +export default mw; diff --git a/src/middleware/tokenAuth.ts b/src/middleware/tokenAuth.ts new file mode 100644 index 00000000..a36930aa --- /dev/null +++ b/src/middleware/tokenAuth.ts @@ -0,0 +1,24 @@ +import http from 'http'; +import { jwtAct, parseCookies } from '../utils/index.ts'; +import { ckNms } from '../controllers/helpers/helpers.ts'; +import { httpStatus, TKN } from '../static/index.ts'; +import { RequestError } from '../errors/errors.ts'; + +function tokenAuth(req: http.IncomingMessage) { + const cookies = parseCookies(req); + const token = cookies[ckNms[TKN.ACC]]; + + if (!token) { + throw new RequestError('No token provided', httpStatus.ua); + } + + const { type, ...pl } = jwtAct.ver(token); + + if (type !== TKN.ACC) { + throw new RequestError('Invalid token type', httpStatus.ua); + } + + return pl; +} + +export default tokenAuth; diff --git a/src/model/token.model.ts b/src/model/token.model.ts index 2075d7e8..3de789cf 100644 --- a/src/model/token.model.ts +++ b/src/model/token.model.ts @@ -22,7 +22,7 @@ const Token = client.define( onDelete: 'CASCADE', }, [nms.token]: { - type: DataTypes.STRING, + type: DataTypes.TEXT, allowNull: false, unique: true, }, diff --git a/src/router/router.ts b/src/router/router.ts index f722f213..23c0236e 100644 --- a/src/router/router.ts +++ b/src/router/router.ts @@ -1,4 +1,4 @@ -import { idx, authMan, refresh, logout } from '../controllers/index.ts'; +import ctr from '../controllers/index.ts'; import { RequestError } from '../errors/index.ts'; import { type Endpoint, endpt } from '../static/endpoints.ts'; import { httpStatus, type Method, mthd } from '../static/vocab/httpVocab.ts'; @@ -13,39 +13,39 @@ type Entr = { const routeMap = { [endpt.idx]: { - [mthd.get]: { auth: false, schema: false, ctr: idx }, + [mthd.get]: { auth: false, schema: false, ctr: ctr.idx }, }, [endpt.auth]: { - [mthd.post]: { auth: false, schema: sch.auth, ctr: authMan }, + [mthd.post]: { auth: false, schema: sch.auth, ctr: ctr.auth.man }, }, [endpt.refr]: { - [mthd.post]: { auth: false, schema: false, ctr: refresh }, + [mthd.post]: { auth: false, schema: false, ctr: ctr.auth.rfr }, }, [endpt.snauth]: { [mthd.post]: { auth: false, schema: false, ctr: () => true }, }, [endpt.lgout]: { - [mthd.patch]: { auth: true, schema: false, ctr: logout }, + [mthd.patch]: { auth: true, schema: false, ctr: ctr.auth.lgt }, }, [endpt.reg]: { - [mthd.post]: { auth: false, schema: sch.reg, ctr: () => true }, + [mthd.post]: { auth: false, schema: sch.reg, ctr: ctr.reg.reg }, }, [endpt.act]: { - [mthd.get]: { auth: false, schema: false, ctr: () => true }, + [mthd.get]: { auth: false, schema: false, ctr: ctr.reg.act }, }, [endpt.acc]: { - [mthd.get]: { auth: true, schema: false, ctr: () => true }, - [mthd.del]: { auth: true, schema: false, ctr: () => true }, - [mthd.patch]: { auth: true, schema: sch.profUpd, ctr: () => true }, + [mthd.get]: { auth: true, schema: false, ctr: ctr.acc.get }, + [mthd.del]: { auth: true, schema: false, ctr: ctr.acc.del }, + [mthd.patch]: { auth: true, schema: sch.profUpd, ctr: ctr.acc.patch }, }, [endpt.pwd]: { - [mthd.patch]: { auth: true, schema: sch.pwdPtch, ctr: () => true }, + [mthd.patch]: { auth: true, schema: sch.pwdPtch, ctr: ctr.acc.res }, }, [endpt.pwdReq]: { - [mthd.post]: { auth: false, schema: sch.pwdReq, ctr: () => true }, + [mthd.post]: { auth: false, schema: sch.pwdReq, ctr: ctr.res.req }, }, [endpt.pwdRes]: { - [mthd.post]: { auth: false, schema: sch.newPwd, ctr: () => true }, + [mthd.post]: { auth: false, schema: sch.newPwd, ctr: ctr.res.prc }, }, } as const; diff --git a/src/services/email.service.ts b/src/services/email.service.ts new file mode 100644 index 00000000..790aa5ee --- /dev/null +++ b/src/services/email.service.ts @@ -0,0 +1,63 @@ +import nodemailer from 'nodemailer'; + +const transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST, + port: Number(process.env.SMTP_PORT) || 587, + secure: process.env.SMTP_SECURE === 'true', + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASS, + }, +}); + +type MailOptions = { + to: string; + subject: string; + html: string; +}; + +async function sendMail(options: MailOptions): Promise { + await transporter.sendMail({ + from: process.env.SMTP_FROM || 'noreply@example.com', + ...options, + }); +} + +async function sendActivationEmail( + email: string, + token: string, +): Promise { + const link = `${process.env.BASE_URL}/activate?token=${token}`; + + await sendMail({ + to: email, + subject: 'Account activation', + html: ` +

Hello!

+

Click the link below to activate your account:

+ ${link} + `, + }); +} + +async function sendPwdRes(email: string, token: string): Promise { + const link = `${process.env.BASE_URL}/password/reset?token=${token}`; + + await sendMail({ + to: email, + subject: 'Password reset', + html: ` +

Hello!

+

Click the link below to reset your password:

+ ${link} + `, + }); +} + +const emailService = { + send: sendMail, + sendActivation: sendActivationEmail, + sendReset: sendPwdRes, +}; + +export default emailService; diff --git a/src/services/index.ts b/src/services/index.ts index 0bc44e2a..e38dbc99 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,3 +1,11 @@ -import usrServices from './user.service.ts'; +import usr from './user.service.ts'; import tkn from './token.service.ts'; -export { usrServices, tkn }; +import emailService from './email.service.ts'; + +const srv = { + usr: usr, + tkn: tkn, + email: emailService, +}; + +export default srv; diff --git a/src/services/general.service.ts b/src/services/repo.service.ts similarity index 52% rename from src/services/general.service.ts rename to src/services/repo.service.ts index 8f374214..1f084a65 100644 --- a/src/services/general.service.ts +++ b/src/services/repo.service.ts @@ -1,7 +1,8 @@ import { RequestError } from '../errors/index.ts'; import DB from '../model/index.ts'; import { fnames, httpStatus, type Tnames } from '../static/index.ts'; -import { type DBRes } from './types.ts'; +import { aDBH } from '../utils/index.ts'; +import { type Create, type DBRes } from './types.ts'; const get = async ( table: T, @@ -10,7 +11,10 @@ const get = async ( const item = await DB[table].findByPk(key); if (!item) { - throw new RequestError(`Id not found: ${key}`, httpStatus.nf); + throw new RequestError( + `${table.slice(-1).toUpperCase()} not found: ${key}`, + httpStatus.nf, + ); } return item.toJSON(); @@ -43,4 +47,33 @@ const getByParam = async ( return item.toJSON(); }; -export { get, del, getByParam }; +const create = async >( + table: T, + data: Create[T], +): Promise => { + const newObj = await DB[table].create({ + ...data, + }); + + return newObj.toJSON(); +}; + +// aDBH = asyncDBHandler, try/catch cover; + +const base = { + get: aDBH((t: T, k: string) => get(t, k)), + del: aDBH((t: Tnames, k: string) => del(t, k)), + getByPrm: aDBH( + ( + t: T, + f: (typeof fnames)[T][keyof (typeof fnames)[T]], + q: string, + ) => getByParam(t, f, q), + ), + crt: aDBH( + >(t: T, d: Create[T]) => + create(t, d), + ), +}; + +export { base }; diff --git a/src/services/token.service.ts b/src/services/token.service.ts index 92a6d765..ab1b876a 100644 --- a/src/services/token.service.ts +++ b/src/services/token.service.ts @@ -1,37 +1,21 @@ import DB from '../model/index.ts'; -import { TKN, TNAMES, type Tokens, fnames } from '../static/index.ts'; -import { asyncDBHandler } from '../utils/index.ts'; -import { del, getByParam } from './general.service.ts'; -import { type DBResTkn } from './types.ts'; +import { TNAMES, type Tokens, fnames } from '../static/index.ts'; +import { base } from './repo.service.ts'; +import type { Create } from './types.ts'; -const tkNames = fnames[TNAMES.TKN]; - -async function createToken( - userId: string, - token: string, - type: Exclude, - expiresAt: string, -): Promise { - const newToken = await DB[TNAMES.TKN].create({ - [tkNames.usr]: userId, - [tkNames.token]: token, - [tkNames.type]: type, - [tkNames.exp]: expiresAt, - }); - - return newToken.toJSON(); -} - -async function getByTkn(t: string): Promise { - const token = await getByParam(TNAMES.TKN, fnames[TNAMES.TKN].token, t); - - return token; -} +const nms = fnames[TNAMES.TKN]; const tkn = { - create: asyncDBHandler((id, t, tp, exp) => createToken(id, t, tp, exp)), - getByTkn: asyncDBHandler((t: string) => getByTkn(t)), - del: asyncDBHandler((id: string) => del(TNAMES.TKN, id)), + create: (d: Create[typeof TNAMES.TKN]) => base.crt(TNAMES.TKN, d), + getByTkn: (q: string) => + base.getByPrm(TNAMES.TKN, fnames[TNAMES.TKN].token, q), + del: (id: string) => base.del(TNAMES.TKN, id), + delByUserId: (userId: string, type?: Tokens) => + DB[TNAMES.TKN].destroy({ + where: type + ? { [nms.usr]: userId, [nms.type]: type } + : { [nms.usr]: userId }, + }), }; export default tkn; diff --git a/src/services/types.ts b/src/services/types.ts index 5cd25eae..b4de352d 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -1,23 +1,10 @@ -import { TNAMES, type Tokens } from '../static/index.ts'; - -interface DBResUsr { - id: string; - name: string; - email: string; - password: string; - activated: boolean; - createdAt: Date; - updatedAt: Date; -} - -interface DBResTkn { - id: string; - userId: string; - token: string; - type: Tokens; - expiresAt: Date; - createdAt: Date; -} +import type { + CreateTKN, + CreateUser, + DBToken, + DBUser, +} from '../static/types/index.ts'; +import { TNAMES } from '../static/index.ts'; interface DBResSN { id: string; @@ -28,9 +15,14 @@ interface DBResSN { } type DBRes = { - [TNAMES.USR]: DBResUsr; + [TNAMES.USR]: DBUser; [TNAMES.SCN]: DBResSN; - [TNAMES.TKN]: DBResTkn; + [TNAMES.TKN]: DBToken; +}; + +type Create = { + [TNAMES.USR]: CreateUser; + [TNAMES.TKN]: CreateTKN; }; -export type { DBRes, DBResTkn }; +export type { DBRes, Create }; diff --git a/src/services/user.service.ts b/src/services/user.service.ts index e984e454..014c7d71 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -1,14 +1,34 @@ +import DB from '../model/index.ts'; import { fnames, TNAMES } from '../static/index.ts'; -import { asyncDBHandler } from '../utils/index.ts'; -import { del, get, getByParam } from './general.service.ts'; +import type { PatchUser } from '../static/types/index.ts'; +import aDBH from '../utils/dbHandler.ts'; +import { base } from './repo.service.ts'; +import type { Create } from './types.ts'; -const a = asyncDBHandler; +async function patchUser(id: string, payload: Partial) { + const user = await base.get(TNAMES.USR, id); -const usrServices = { - getById: a((id: string) => get(TNAMES.USR, id)), - // eslint-disable-next-line max-len, prettier/prettier - getByEmail: a((e: string) => getByParam(TNAMES.USR, fnames[TNAMES.USR].email, e)), - delete: a((id: string) => del(TNAMES.USR, id)), + await DB[TNAMES.USR].update(payload, { where: { id } }); + + return { ...user, ...payload }; +} + +const usr = { + getById: (id: string) => base.get(TNAMES.USR, id), + getByEmail: (e: string) => + base.getByPrm(TNAMES.USR, fnames[TNAMES.USR].email, e), + delete: (id: string) => base.del(TNAMES.USR, id), + create: (data: Create[typeof TNAMES.USR]) => base.crt(TNAMES.USR, data), + existsByEmail: async (email: string): Promise => { + try { + await base.getByPrm(TNAMES.USR, fnames[TNAMES.USR].email, email); + + return true; + } catch { + return false; + } + }, + patch: aDBH((id: string, pl: Partial) => patchUser(id, pl)), }; -export default usrServices; +export default usr; diff --git a/src/static/types/index.ts b/src/static/types/index.ts new file mode 100644 index 00000000..ab5e8508 --- /dev/null +++ b/src/static/types/index.ts @@ -0,0 +1,3 @@ +import type { DBUser, DTOUser, PatchUser, CreateUser } from './user.types.ts'; +import type { DBToken, CreateTKN } from './token.types.ts'; +export type { DBUser, DTOUser, PatchUser, CreateUser, DBToken, CreateTKN }; diff --git a/src/static/types/token.types.ts b/src/static/types/token.types.ts new file mode 100644 index 00000000..ac5fb0c3 --- /dev/null +++ b/src/static/types/token.types.ts @@ -0,0 +1,18 @@ +import { TKN, type Tokens } from '../index.ts'; + +interface DBToken { + id: string; + userId: string; + token: string; + type: Tokens; + expiresAt: Date; + createdAt: Date; +} + +interface CreateTKN + extends Omit { + type: Exclude; + expiresAt: string; +} + +export type { DBToken, CreateTKN }; diff --git a/src/static/types/user.types.ts b/src/static/types/user.types.ts new file mode 100644 index 00000000..5eb1b180 --- /dev/null +++ b/src/static/types/user.types.ts @@ -0,0 +1,20 @@ +interface DBUser { + id: string; + name: string; + email: string; + password: string; + activated: boolean; + createdAt: Date; + updatedAt: Date; +} + +type DTOUser = Omit< + DBUser, + 'password' | 'activated' | 'createdAt' | 'updatedAt' +>; + +type PatchUser = Omit; + +type CreateUser = Omit; + +export type { DBUser, DTOUser, PatchUser, CreateUser }; diff --git a/src/utils/dbHandler.ts b/src/utils/dbHandler.ts index e25d385b..09fc4479 100644 --- a/src/utils/dbHandler.ts +++ b/src/utils/dbHandler.ts @@ -1,6 +1,6 @@ import { DBError } from '../errors/index.ts'; -function asyncDBHandler( +function aDBH( fn: (...args: TArgs) => Promise, ): (...args: TArgs) => Promise { return async (...args: TArgs): Promise => { @@ -14,4 +14,4 @@ function asyncDBHandler( }; } -export default asyncDBHandler; +export default aDBH; diff --git a/src/utils/index.ts b/src/utils/index.ts index c0a9ade3..58b30d31 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,7 +1,7 @@ -import asyncDBHandler from './dbHandler.ts'; +import aDBH from './dbHandler.ts'; import { parseCookies } from './cookies.ts'; import { type JWTPayload } from './types.ts'; import jwtAct from './jwt.ts'; -export { asyncDBHandler, parseCookies, jwtAct }; +export { aDBH, parseCookies, jwtAct }; export type { JWTPayload }; diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts index 6e37a878..3b5e93c8 100644 --- a/src/utils/jwt.ts +++ b/src/utils/jwt.ts @@ -5,16 +5,19 @@ import { type JWTPayload } from './types.ts'; const SECRET_KEY = process.env.JWT_SECRET || '7>?~!id(#;fd13/^^$fdkq124<'; -type Signed = { expiry: string; token: string }; +type Signed = { expiresAt: string; token: string }; function signToken(payload: JWTPayload, type: Tokens): Signed { - const expiry = TOKEN_EXPIRY[type][0]; + const expiryDuration = TOKEN_EXPIRY[type][0]; + const expirySeconds = Number(TOKEN_EXPIRY[type][1]); const token = jwt.sign({ ...payload, type }, SECRET_KEY, { - expiresIn: expiry, + expiresIn: expiryDuration, } as jwt.SignOptions); - return { expiry, token }; + const expiresAt = new Date(Date.now() + expirySeconds * 1000).toISOString(); + + return { expiresAt, token }; } function verifyToken(token: string): JWTPayload & { type: Tokens } { diff --git a/src/utils/types.ts b/src/utils/types.ts index c99cbab4..562cf4bd 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -1,11 +1,5 @@ -import { fnames, TNAMES } from '../static/index.ts'; +import type { DTOUser } from '../static/types/user.types.ts'; -const nms = fnames[TNAMES.USR]; - -type JWTPayload = { - [nms.id]: string; - [nms.name]: string; - [nms.email]: string; -}; +type JWTPayload = DTOUser; export type { JWTPayload }; From b754f34c80699a3d9604391b13c127436595269d Mon Sep 17 00:00:00 2001 From: Bespalov Tymofii Date: Fri, 6 Feb 2026 18:00:06 +0100 Subject: [PATCH 09/14] done --- src/controllers/acc.ctr.ts | 76 +++++++++++++++--------- src/controllers/auth.ctr.ts | 82 +++++++------------------- src/controllers/helpers/helpers.ts | 64 ++++++--------------- src/controllers/index.ts | 2 +- src/controllers/reg.ctr.ts | 51 +++++++++-------- src/controllers/res.ctr.ts | 78 ------------------------- src/controllers/rst.ctr.ts | 89 +++++++++++++++++++++++++++++ src/createServer.ts | 68 +++++++--------------- src/db/index.ts | 1 + src/errors/errors.ts | 2 +- src/middleware/errorHandler.ts | 2 + src/middleware/tokenAuth.ts | 6 +- src/model/index.ts | 2 - src/model/socialAccounts.model.ts | 44 -------------- src/router/index.ts | 2 + src/router/router.ts | 58 +++++++++---------- src/services/email.service.ts | 63 -------------------- src/services/email/email.const.ts | 39 +++++++++++++ src/services/email/email.service.ts | 41 +++++++++++++ src/services/email/transporter.ts | 13 +++++ src/services/index.ts | 4 +- src/services/repo.service.ts | 57 ++++++++++-------- src/services/token.service.ts | 18 +++--- src/services/types.ts | 28 --------- src/services/user.service.ts | 29 +++++----- src/static/bodySchemas.ts | 82 ++++++++++++++++++++++++++ src/static/constants.ts | 10 ---- src/static/endpoints.ts | 18 +++--- src/static/index.ts | 17 ++++-- src/static/types.ts | 16 ------ src/static/types/index.ts | 36 +++++++++++- src/static/types/maps.type.ts | 15 +++++ src/static/types/token.types.ts | 6 +- src/static/types/user.types.ts | 7 +-- src/static/vocab/dbVocab.ts | 11 +--- src/test.ts | 42 ++++++++++++++ src/utils/cookies.ts | 2 +- src/utils/cors.ts | 16 ++++++ src/utils/dbHandler.ts | 4 +- src/utils/index.ts | 18 ++++-- src/utils/jwt.ts | 21 ++++--- src/utils/types.ts | 5 -- src/validation/index.ts | 9 ++- src/validation/schemas.ts | 65 --------------------- src/validation/validateRequest.ts | 9 +-- 45 files changed, 668 insertions(+), 660 deletions(-) delete mode 100644 src/controllers/res.ctr.ts create mode 100644 src/controllers/rst.ctr.ts create mode 100644 src/db/index.ts delete mode 100644 src/model/socialAccounts.model.ts create mode 100644 src/router/index.ts delete mode 100644 src/services/email.service.ts create mode 100644 src/services/email/email.const.ts create mode 100644 src/services/email/email.service.ts create mode 100644 src/services/email/transporter.ts delete mode 100644 src/services/types.ts create mode 100644 src/static/bodySchemas.ts delete mode 100644 src/static/constants.ts delete mode 100644 src/static/types.ts create mode 100644 src/static/types/maps.type.ts create mode 100644 src/test.ts create mode 100644 src/utils/cors.ts delete mode 100644 src/utils/types.ts delete mode 100644 src/validation/schemas.ts diff --git a/src/controllers/acc.ctr.ts b/src/controllers/acc.ctr.ts index 60f4bccd..b2712773 100644 --- a/src/controllers/acc.ctr.ts +++ b/src/controllers/acc.ctr.ts @@ -1,26 +1,24 @@ import dto from '../dto/index.ts'; +import { RequestError } from '../errors/index.ts'; import srv from '../services/index.ts'; -import { httpStatus, TKN } from '../static/index.ts'; -import type { Ctx } from '../static/types.ts'; -import type { JWTPayload } from '../utils/types.ts'; -import { sch } from '../validation/index.ts'; -import { hashPwd, reactivate, setTkn, verifyPwd } from './helpers/helpers.ts'; +import { httpStatus, TKN, sch } from '../static/index.ts'; +import type { Ctx, DTOUser } from '../static/types/index.ts'; +import { hashPwd, setTkn, verifyPwd } from './helpers/helpers.ts'; async function getAccData(ctx: Ctx) { const { res, usr } = ctx; - const dbres = await srv.usr.getById((usr as JWTPayload).id); - - const data = JSON.stringify(dto.usr(dbres)); + const dbres = await srv.usr.gbId((usr as DTOUser).id); res.statusCode = httpStatus.ok; - res.end(data); + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(dto.usr(dbres))); } async function deleteAcc(ctx: Ctx) { const { res, usr } = ctx; - await srv.usr.delete((usr as JWTPayload).id); + await srv.usr.dlt((usr as DTOUser).id); res.setHeader('Set-Cookie', [ setTkn(TKN.ACC, '', '0'), @@ -31,46 +29,68 @@ async function deleteAcc(ctx: Ctx) { res.end(); } -async function patchAcc(ctx: Ctx) { +async function patchAcc(ctx: Ctx) { const { res, body, usr } = ctx; const { password, email, name } = body; - const usrFromDB = await verifyPwd((usr as JWTPayload).id, password); - - let payload; + const user = await srv.usr.gbId((usr as DTOUser).id); - if (email === usrFromDB.email) { - payload = { name }; + const isValid = await verifyPwd(user.password, password); - const newUsr = await srv.usr.patch(usrFromDB.id, payload); + if (!isValid) { + throw new RequestError('Invalid credentials', httpStatus.ua); + } - res.statusCode = httpStatus.ok; - res.end(JSON.stringify(dto.usr(newUsr))); - } else { - payload = { name, email }; + const payload: { name?: string; email?: string } = {}; - const newUsr = await srv.usr.patch(usrFromDB.id, payload); + if (name) { + payload.name = name; + } - await reactivate(newUsr, res); + if (email) { + payload.email = email; } + + const newUsr = await srv.usr.ptch(user.id, payload); + + res.statusCode = httpStatus.ok; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(dto.usr(newUsr))); } -async function resetPass(ctx: Ctx) { +async function changePass(ctx: Ctx) { const { res, body, usr } = ctx; const { oldPwd, newPwd } = body; - const usrFromDB = await verifyPwd((usr as JWTPayload).id, oldPwd); + const user = await srv.usr.gbId((usr as DTOUser).id); + + const isValid = await verifyPwd(user.password, oldPwd); + + if (!isValid) { + throw new RequestError('Invalid credentials', httpStatus.ua); + } + const hashedPwd = await hashPwd(newPwd); - const newUsr = await srv.usr.patch(usrFromDB.id, { password: hashedPwd }); - await reactivate(newUsr, res); + await srv.usr.ptch(user.id, { password: hashedPwd }); + + res.setHeader('Set-Cookie', [ + setTkn(TKN.ACC, '', '0'), + setTkn(TKN.RFR, '', '0'), + ]); + + res.setHeader('Content-Type', 'application/json'); + + res.statusCode = httpStatus.ok; + + res.end(JSON.stringify({ message: 'Password changed successfully' })); } const acc = { get: getAccData, del: deleteAcc, patch: patchAcc, - res: resetPass, + pwd: changePass, }; export default acc; diff --git a/src/controllers/auth.ctr.ts b/src/controllers/auth.ctr.ts index 544c711b..a5afcb87 100644 --- a/src/controllers/auth.ctr.ts +++ b/src/controllers/auth.ctr.ts @@ -1,23 +1,19 @@ import bcrypt from 'bcrypt'; import { RequestError } from '../errors/index.ts'; import srv from '../services/index.ts'; -import { httpStatus, TKN, TOKEN_EXPIRY } from '../static/index.ts'; -import sch from '../validation/schemas.ts'; -import { parseCookies, jwtAct } from '../utils/index.ts'; -import { type Ctx } from '../static/types.ts'; -import { ckNms, setTkn } from './helpers/helpers.ts'; +import { httpStatus, TKN, sch } from '../static/index.ts'; +import utl from '../utils/index.ts'; +import type { Ctx } from '../static/types/index.ts'; +import { ckNms, handleTokens, setTkn } from './helpers/helpers.ts'; async function manual(ctx: Ctx): Promise { const { res, body } = ctx; const { email, password } = body; // check usr activation - const user = await srv.usr.getByEmail(email); + const user = await srv.usr.gbEm(email); if (!user.activated) { - throw new RequestError( - `User with email ${email} is not activated`, - httpStatus.ua, - ); + throw new RequestError('Invalid credentials', httpStatus.ua); } // compare pw @@ -25,98 +21,61 @@ async function manual(ctx: Ctx): Promise { const isValid = await bcrypt.compare(password, user.password); if (!isValid) { - throw new RequestError(`Wrong password`, httpStatus.ua); + throw new RequestError('Invalid credentials', httpStatus.ua); } // create tkns const { id, name } = user; const payload = { id, name, email }; - const accTkn = jwtAct.create[TKN.ACC](payload); - const refTkn = jwtAct.create[TKN.RFR](payload); - // add refresh tkn to DB - const createPayload = { - userId: id, - token: refTkn.token, - type: TKN.RFR, - expiresAt: refTkn.expiresAt, - }; - - await srv.tkn.create(createPayload); - - // set cookies - res.setHeader('Set-Cookie', [ - setTkn(TKN.ACC, accTkn.token, TOKEN_EXPIRY.access[1]), - setTkn(TKN.RFR, refTkn.token, TOKEN_EXPIRY.refresh[1]), - ]); + await handleTokens(res, payload); res.statusCode = httpStatus.ok; + res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ message: 'Authorized', user: { id, name, email } })); } async function refresh(ctx: Ctx) { const { req, res } = ctx; - const cookies = parseCookies(req); + const cookies = utl.prsCks(req); const token = cookies[ckNms[TKN.RFR]]; if (!token) { throw new RequestError('No token provided', httpStatus.ua); } - const { type, ...pl } = jwtAct.ver(token); + const { type, ...pl } = utl.jwt.ver(token); if (type !== TKN.RFR) { throw new RequestError('Invalid token type', httpStatus.ua); } - const dbTok = await srv.tkn.getByTkn(token); - - if (!dbTok) { - throw new RequestError(`Token ${token} doesn't exist`, httpStatus.ua); - } + const dbTok = await srv.tkn.gBTkn(token); // delete token - srv.tkn.del(dbTok.id); + await srv.tkn.dlt(dbTok.id); - // create new Tokens - - const accTkn = jwtAct.create[TKN.ACC](pl); - const refTkn = jwtAct.create[TKN.RFR](pl); - - // add refr token to DB - const createPayload = { - userId: pl.id, - token: refTkn.token, - type: TKN.RFR, - expiresAt: refTkn.expiresAt, - }; - - await srv.tkn.create(createPayload); - - // set cookies - res.setHeader('Set-Cookie', [ - setTkn(TKN.ACC, accTkn.token, TOKEN_EXPIRY.access[1]), - setTkn(TKN.RFR, refTkn.token, TOKEN_EXPIRY.refresh[1]), - ]); + await handleTokens(res, pl); // end res res.statusCode = httpStatus.ok; + res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ message: 'Authorized', user: { ...pl } })); } async function logout(ctx: Ctx) { const { req, res } = ctx; - const cookies = parseCookies(req); + const cookies = utl.prsCks(req); const refToken = cookies[ckNms[TKN.RFR]]; if (refToken) { - const dbTok = await srv.tkn.getByTkn(refToken); + try { + const dbTok = await srv.tkn.gBTkn(refToken); - if (dbTok) { - srv.tkn.del(dbTok.id); - } + await srv.tkn.dlt(dbTok.id); + } catch {} } res.setHeader('Set-Cookie', [ @@ -125,6 +84,7 @@ async function logout(ctx: Ctx) { ]); res.statusCode = httpStatus.ok; + res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ message: 'Logged out' })); } diff --git a/src/controllers/helpers/helpers.ts b/src/controllers/helpers/helpers.ts index 2e36c34d..40c478e6 100644 --- a/src/controllers/helpers/helpers.ts +++ b/src/controllers/helpers/helpers.ts @@ -1,10 +1,9 @@ import http from 'http'; import bcrypt from 'bcrypt'; import srv from '../../services/index.ts'; -import { httpStatus, TKN } from '../../static/index.ts'; -import type { DBUser } from '../../static/types/user.types.ts'; -import { RequestError } from '../../errors/errors.ts'; -import jwtAct from '../../utils/jwt.ts'; +import { TKN, TOKEN_EXPIRY } from '../../static/index.ts'; +import type { DTOUser } from '../../static/types/index.ts'; +import utl from '../../utils/index.ts'; const SALT_ROUNDS = 10; @@ -16,60 +15,31 @@ const ckNms = { const setTkn = (t: typeof TKN.ACC | typeof TKN.RFR, s: string, age: string) => `${ckNms[t]}=${s}; HttpOnly; Path=/; Max-Age=${age}; SameSite=Strict`; -const verifyPwd = async (usrId: string, pwd: string): Promise => { - const usr = await srv.usr.getById(usrId); - - const isValid = await bcrypt.compare(pwd, usr.password); - - if (!isValid) { - throw new RequestError(`Wrong password`, httpStatus.ua); - } - - return usr; +const verifyPwd = async (pwdhash: string, pwd: string): Promise => { + return bcrypt.compare(pwdhash, pwd); }; const hashPwd = async (pwd: string) => { return bcrypt.hash(pwd, SALT_ROUNDS); }; -async function reactivate(user: DBUser, res: http.ServerResponse) { - await srv.tkn.delByUserId(user.id, TKN.RFR); - - await srv.usr.patch(user.id, { activated: false }); - - const jwtPayload = { id: user.id, name: user.name, email: user.email }; +async function handleTokens(res: http.ServerResponse, pl: DTOUser) { + const accTkn = utl.jwt.create[TKN.ACC](pl); + const refTkn = utl.jwt.create[TKN.RFR](pl); - const actToken = jwtAct.sign(jwtPayload, TKN.ACT); - - const createPl = { - userId: user.id, - token: actToken.token, - type: TKN.ACT, - expiresAt: actToken.expiresAt, + const createPayload = { + userId: pl.id, + token: refTkn.token, + type: TKN.RFR, + expiresAt: refTkn.expiresAt, }; - await srv.tkn.create(createPl); - - srv.email - .sendActivation(jwtPayload.email, actToken.token) - .catch((err: Error) => { - // eslint-disable-next-line no-console - console.error('Failed to send activation email:', err.message); - }); + await srv.tkn.crt(createPayload); res.setHeader('Set-Cookie', [ - setTkn(TKN.ACC, '', '0'), - setTkn(TKN.RFR, '', '0'), + setTkn(TKN.ACC, accTkn.token, TOKEN_EXPIRY.access[1]), + setTkn(TKN.RFR, refTkn.token, TOKEN_EXPIRY.refresh[1]), ]); - - res.statusCode = httpStatus.ok; - - res.end( - JSON.stringify({ - message: - 'Email or password changed. Check your inbox for activation link.', - }), - ); } -export { ckNms, setTkn, verifyPwd, reactivate, hashPwd }; +export { ckNms, setTkn, verifyPwd, hashPwd, handleTokens }; diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 34633125..4ae96542 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -2,7 +2,7 @@ import idx from './idx.ctr.ts'; import auth from './auth.ctr.ts'; import reg from './reg.ctr.ts'; import acc from './acc.ctr.ts'; -import rst from './res.ctr.ts'; +import rst from './rst.ctr.ts'; const ctr = { idx: idx, diff --git a/src/controllers/reg.ctr.ts b/src/controllers/reg.ctr.ts index ee20e14f..1cf08385 100644 --- a/src/controllers/reg.ctr.ts +++ b/src/controllers/reg.ctr.ts @@ -1,18 +1,18 @@ +import dto from '../dto/index.ts'; import { RequestError } from '../errors/index.ts'; +import { mailTemplate } from '../services/email/email.const.ts'; import srv from '../services/index.ts'; -import { httpStatus, TKN } from '../static/index.ts'; -import { type Ctx } from '../static/types.ts'; -import { jwtAct } from '../utils/index.ts'; -import sch from '../validation/schemas.ts'; +import { httpStatus, sch, TKN } from '../static/index.ts'; +import type { Ctx } from '../static/types/index.ts'; +import utl from '../utils/index.ts'; + import { hashPwd } from './helpers/helpers.ts'; async function register(ctx: Ctx): Promise { const { res, body } = ctx; const { name, email, password } = body; - // check if user exists - - const exists = await srv.usr.existsByEmail(email); + const exists = await srv.usr.exBEm(email); if (exists) { throw new RequestError( @@ -21,35 +21,31 @@ async function register(ctx: Ctx): Promise { ); } - // create user const hashedPwd = await hashPwd(password); - const newUsr = await srv.usr.create({ + const newUsr = await srv.usr.crt({ name, email, password: hashedPwd, }); - // create activationToken; - const payload = { id: newUsr.id, name, email }; - const actToken = jwtAct.sign(payload, TKN.ACT); + const { token, expiresAt } = utl.jwt.sign(dto.usr(newUsr), TKN.ACT); const createPL = { userId: newUsr.id, - token: actToken.token, + token: token, type: TKN.ACT, - expiresAt: actToken.expiresAt, + expiresAt: expiresAt, }; - await srv.tkn.create(createPL); + const sent = await srv.eml.sdTM(email, token, mailTemplate.act); - // send activation email (non-blocking, log errors) - srv.email.sendActivation(email, actToken.token).catch((err: Error) => { - // eslint-disable-next-line no-console - console.error('Failed to send activation email:', err.message); - }); + if (sent) { + await srv.tkn.crt(createPL); + } res.statusCode = httpStatus.cr; + res.setHeader('Content-Type', 'application/json'); res.end( JSON.stringify({ @@ -62,13 +58,22 @@ async function register(ctx: Ctx): Promise { async function activate(ctx: Ctx) { const { res, param } = ctx; - const token = await srv.tkn.getByTkn(param as string); + const token = await srv.tkn.gBTkn(param as string); + + if (token.type !== TKN.ACT) { + throw new RequestError('Invalid token type', httpStatus.br); + } + + if (new Date(token.expiresAt) < new Date()) { + throw new RequestError('Token expired', httpStatus.br); + } - await srv.usr.patch(token.userId, { activated: true }); + await srv.usr.ptch(token.userId, { activated: true }); - await srv.tkn.del(token.id); + await srv.tkn.dlt(token.id); res.statusCode = httpStatus.ok; + res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ message: `Succesfully activated` })); } diff --git a/src/controllers/res.ctr.ts b/src/controllers/res.ctr.ts deleted file mode 100644 index 92d8f757..00000000 --- a/src/controllers/res.ctr.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { RequestError } from '../errors/index.ts'; -import srv from '../services/index.ts'; -import { httpStatus, TKN } from '../static/index.ts'; -import { type Ctx } from '../static/types.ts'; -import jwtAct from '../utils/jwt.ts'; -import { sch } from '../validation/index.ts'; -import { hashPwd } from './helpers/helpers.ts'; - -async function requestReset(ctx: Ctx) { - const { res, body } = ctx; - - const user = await srv.usr.getByEmail(body.email); - - await srv.tkn.delByUserId(user.id, TKN.PWR); - - const jwtPayload = { id: user.id, name: user.name, email: user.email }; - const token = jwtAct.sign(jwtPayload, TKN.PWR); - - const createPl = { - userId: user.id, - token: token.token, - type: TKN.PWR, - expiresAt: token.expiresAt, - }; - - await srv.tkn.create(createPl); - - srv.email.sendReset(user.email, token.token).catch((err: Error) => { - // eslint-disable-next-line no-console - console.error('Failed to send reset email:', err.message); - }); - - res.statusCode = httpStatus.ok; - - res.end( - JSON.stringify({ - message: 'Check your inbox for password reset link.', - }), - ); -} - -async function processReset(ctx: Ctx) { - const { res, body } = ctx; - const { token, newPwd } = body; - - const { type, ...payload } = jwtAct.ver(token); - - if (type !== TKN.PWR) { - throw new RequestError('Invalid token type', httpStatus.br); - } - - const dbToken = await srv.tkn.getByTkn(token); - - if (dbToken.userId !== payload.id) { - throw new RequestError('Token mismatch', httpStatus.br); - } - - const hashedPwd = await hashPwd(newPwd); - - await srv.usr.patch(payload.id, { password: hashedPwd }); - - await srv.tkn.del(dbToken.id); - - res.statusCode = httpStatus.ok; - - res.end( - JSON.stringify({ - message: 'Password reset successfully. You can now login.', - }), - ); -} - -const rst = { - req: requestReset, - prc: processReset, -}; - -export default rst; diff --git a/src/controllers/rst.ctr.ts b/src/controllers/rst.ctr.ts new file mode 100644 index 00000000..b7d48787 --- /dev/null +++ b/src/controllers/rst.ctr.ts @@ -0,0 +1,89 @@ +import dto from '../dto/index.ts'; +import { RequestError } from '../errors/index.ts'; +import { mailTemplate } from '../services/email/email.const.ts'; +import srv from '../services/index.ts'; +import { httpStatus, TKN, sch } from '../static/index.ts'; +import { type Ctx } from '../static/types/index.ts'; +import utl from '../utils/index.ts'; +import { hashPwd } from './helpers/helpers.ts'; + +async function requestReset(ctx: Ctx) { + const { res, body } = ctx; + const { email } = body; + + res.statusCode = httpStatus.ok; + res.setHeader('Content-Type', 'application/json'); + + res.end( + JSON.stringify({ + message: 'Check your inbox for password reset link.', + }), + ); + + try { + const user = await srv.usr.gbEm(email); + + await srv.tkn.dltBUID(user.id, TKN.PWR); + + const { token, expiresAt } = utl.jwt.sign(dto.usr(user), TKN.PWR); + + const createPl = { + userId: user.id, + token: token, + type: TKN.PWR, + expiresAt: expiresAt, + }; + + const mail = await srv.eml.sdTM(user.email, token, mailTemplate.res); + + if (mail) { + await srv.tkn.crt(createPl); + } + } catch (e) { + if (e instanceof RequestError && e.statusCode === httpStatus.nf) { + // eslint-disable-next-line no-console + console.warn('UID was not found'); + } else { + throw e; + } + } +} + +async function processReset(ctx: Ctx) { + const { res, body } = ctx; + const { token, newPwd } = body; + + const { type, ...payload } = utl.jwt.ver(token); + + if (type !== TKN.PWR) { + throw new RequestError('Invalid token type', httpStatus.br); + } + + const dbToken = await srv.tkn.gBTkn(token); + + if (dbToken.userId !== payload.id) { + throw new RequestError('Token mismatch', httpStatus.br); + } + + const hashedPwd = await hashPwd(newPwd); + + await srv.usr.ptch(payload.id, { password: hashedPwd }); + + await srv.tkn.dlt(dbToken.id); + + res.statusCode = httpStatus.ok; + res.setHeader('Content-Type', 'application/json'); + + res.end( + JSON.stringify({ + message: 'Password reset successfully. You can now login.', + }), + ); +} + +const rst = { + req: requestReset, + prc: processReset, +}; + +export default rst; diff --git a/src/createServer.ts b/src/createServer.ts index b432d72d..b89df1b3 100644 --- a/src/createServer.ts +++ b/src/createServer.ts @@ -1,67 +1,42 @@ import http from 'http'; -import bcrypt from 'bcrypt'; -import { dbSetup } from './db/db.ts'; -import { validateBody, validateRequest } from './validation/index.ts'; +import { dbSetup } from './db/index.ts'; +import val from './validation/index.ts'; import mw from './middleware/index.ts'; -import getRouteConfig from './router/router.ts'; -import { type Ctx } from './static/types.ts'; -import { TNAMES } from './static/index.ts'; -import DB from './model/index.ts'; - -// Options preflight -// Unified api response +import grc from './router/index.ts'; +import type { Ctx } from './static/types/index.ts'; +import utl from './utils/index.ts'; export async function createServer() { await dbSetup(); - const createUsr = async (usr: { - name: string; - email: string; - password: string; - activated: boolean; - }) => { - await DB[TNAMES.USR].create({ - ...usr, - }); - }; + return http.createServer(async (req, res) => { + utl.setCors(res); - const pwds = ['jdfu70sdf', 'jdddu70sdf', 'jdgfdfd70sdf']; - const testUsrs = [ - { - name: 'JohnDoe', - email: 'gtlafuk@iksf.fsd', - password: await bcrypt.hash(pwds[0], 10), - activated: true, - }, - { - name: 'Yurgen', - email: 'gtlasdk@iksf.fsd', - password: await bcrypt.hash(pwds[1], 10), - activated: true, - }, - { - name: 'JosdnDoe', - email: 'gtsdgasfuk@iksf.fsd', - password: await bcrypt.hash(pwds[2], 10), - activated: true, - }, - ]; + // Handle preflight OPTIONS + if (req.method === 'OPTIONS') { + res.statusCode = 204; + res.end(); - await testUsrs.map((el) => createUsr(el)); + return; + } - return http.createServer(async (req, res) => { try { - const { endpoint, method, param } = validateRequest(req); + // validate request + const { endpoint, method, param } = val.req(req); - const { auth, schema, ctr } = getRouteConfig(endpoint, method); + // get route config from router + const { auth, schema, ctr } = grc(endpoint, method); let usr = null; + // check auth token if router auth === true if (auth) { usr = mw.tokenAuth(req); } - const body = schema ? validateBody(await mw.parseB(req), schema) : false; + // validating body by comparing to schema if router schema !== null + const body = schema ? val.bd(await mw.parseB(req), schema) : null; + // creating ctx for controller const ctx: Ctx = { req, res, @@ -70,6 +45,7 @@ export async function createServer() { param, }; + // executing controller fn with ctx payload await ctr(ctx); } catch (e) { mw.error(res, e); diff --git a/src/db/index.ts b/src/db/index.ts new file mode 100644 index 00000000..b7744eef --- /dev/null +++ b/src/db/index.ts @@ -0,0 +1 @@ +export { client, dbSetup } from './db.ts'; diff --git a/src/errors/errors.ts b/src/errors/errors.ts index 88c0c4ee..b52ac3c2 100644 --- a/src/errors/errors.ts +++ b/src/errors/errors.ts @@ -1,5 +1,5 @@ import { httpStatus } from '../static/index.ts'; -import { type HTTPStatus } from '../static/types.ts'; +import type { HTTPStatus } from '../static/types/index.ts'; class RequestError extends Error { statusCode: HTTPStatus; diff --git a/src/middleware/errorHandler.ts b/src/middleware/errorHandler.ts index 36e8524d..04592a61 100644 --- a/src/middleware/errorHandler.ts +++ b/src/middleware/errorHandler.ts @@ -3,6 +3,8 @@ import { httpStatus } from '../static/index.ts'; import { RequestError } from '../errors/index.ts'; function errorHandler(res: http.ServerResponse, e: unknown) { + res.setHeader('Content-Type', 'text/plain'); + if (e instanceof RequestError) { res.statusCode = e.statusCode; res.end(e.message); diff --git a/src/middleware/tokenAuth.ts b/src/middleware/tokenAuth.ts index a36930aa..05141c59 100644 --- a/src/middleware/tokenAuth.ts +++ b/src/middleware/tokenAuth.ts @@ -1,18 +1,18 @@ import http from 'http'; -import { jwtAct, parseCookies } from '../utils/index.ts'; +import utl from '../utils/index.ts'; import { ckNms } from '../controllers/helpers/helpers.ts'; import { httpStatus, TKN } from '../static/index.ts'; import { RequestError } from '../errors/errors.ts'; function tokenAuth(req: http.IncomingMessage) { - const cookies = parseCookies(req); + const cookies = utl.prsCks(req); const token = cookies[ckNms[TKN.ACC]]; if (!token) { throw new RequestError('No token provided', httpStatus.ua); } - const { type, ...pl } = jwtAct.ver(token); + const { type, ...pl } = utl.jwt.ver(token); if (type !== TKN.ACC) { throw new RequestError('Invalid token type', httpStatus.ua); diff --git a/src/model/index.ts b/src/model/index.ts index b00b0ef7..810b4540 100644 --- a/src/model/index.ts +++ b/src/model/index.ts @@ -1,11 +1,9 @@ import User from './user.model.ts'; -import SocialAccount from './socialAccounts.model.ts'; import Token from './token.model.ts'; import { TNAMES } from '../static/index.ts'; const DB = { [TNAMES.USR]: User, - [TNAMES.SCN]: SocialAccount, [TNAMES.TKN]: Token, }; diff --git a/src/model/socialAccounts.model.ts b/src/model/socialAccounts.model.ts deleted file mode 100644 index 8b45ba7c..00000000 --- a/src/model/socialAccounts.model.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { client } from '../db/db.ts'; -import { DataTypes } from 'sequelize'; -import { TNAMES, fnames } from '../static/index.ts'; - -const nms = fnames[TNAMES.SCN]; - -const SocialAccount = client.define( - 'SocialAccount', - { - [nms.id]: { - type: DataTypes.UUID, - primaryKey: true, - defaultValue: DataTypes.UUIDV4, - }, - [nms.usr]: { - type: DataTypes.UUID, - allowNull: false, - references: { - model: TNAMES.USR, - key: fnames[TNAMES.USR].id, - }, - onDelete: 'CASCADE', - }, - [nms.ggl]: { - type: DataTypes.STRING, - allowNull: true, - }, - [nms.gh]: { - type: DataTypes.STRING, - allowNull: true, - }, - [nms.fb]: { - type: DataTypes.STRING, - allowNull: true, - }, - }, - { - tableName: TNAMES.SCN, - createdAt: false, - updatedAt: false, - }, -); - -export default SocialAccount; diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 00000000..0c634083 --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,2 @@ +import getRouteConfig from './router.ts'; +export default getRouteConfig; diff --git a/src/router/router.ts b/src/router/router.ts index 23c0236e..bbeccf56 100644 --- a/src/router/router.ts +++ b/src/router/router.ts @@ -1,64 +1,62 @@ import ctr from '../controllers/index.ts'; import { RequestError } from '../errors/index.ts'; -import { type Endpoint, endpt } from '../static/endpoints.ts'; -import { httpStatus, type Method, mthd } from '../static/vocab/httpVocab.ts'; -import { sch } from '../validation/index.ts'; -import { type Ctx } from '../static/types.ts'; +import { ep, httpStatus, mthd, sch } from '../static/index.ts'; +import type { Schema, Ctx, Endpoint, Method } from '../static/types/index.ts'; -type Entr = { +type Entr = { auth: boolean; schema: S; ctr: (args: Ctx) => void | Promise; }; const routeMap = { - [endpt.idx]: { - [mthd.get]: { auth: false, schema: false, ctr: ctr.idx }, + [ep.idx]: { + [mthd.get]: { auth: false, schema: null, ctr: ctr.idx }, }, - [endpt.auth]: { + [ep.auth]: { [mthd.post]: { auth: false, schema: sch.auth, ctr: ctr.auth.man }, }, - [endpt.refr]: { - [mthd.post]: { auth: false, schema: false, ctr: ctr.auth.rfr }, + [ep.refr]: { + [mthd.post]: { auth: false, schema: null, ctr: ctr.auth.rfr }, }, - [endpt.snauth]: { - [mthd.post]: { auth: false, schema: false, ctr: () => true }, + [ep.authsn]: { + [mthd.post]: { auth: false, schema: null, ctr: () => true }, }, - [endpt.lgout]: { - [mthd.patch]: { auth: true, schema: false, ctr: ctr.auth.lgt }, + [ep.lgt]: { + [mthd.patch]: { auth: true, schema: null, ctr: ctr.auth.lgt }, }, - [endpt.reg]: { + [ep.reg]: { [mthd.post]: { auth: false, schema: sch.reg, ctr: ctr.reg.reg }, }, - [endpt.act]: { - [mthd.get]: { auth: false, schema: false, ctr: ctr.reg.act }, + [ep.act]: { + [mthd.get]: { auth: false, schema: null, ctr: ctr.reg.act }, }, - [endpt.acc]: { - [mthd.get]: { auth: true, schema: false, ctr: ctr.acc.get }, - [mthd.del]: { auth: true, schema: false, ctr: ctr.acc.del }, - [mthd.patch]: { auth: true, schema: sch.profUpd, ctr: ctr.acc.patch }, + [ep.prf]: { + [mthd.get]: { auth: true, schema: null, ctr: ctr.acc.get }, + [mthd.del]: { auth: true, schema: null, ctr: ctr.acc.del }, + [mthd.patch]: { auth: true, schema: sch.upd, ctr: ctr.acc.patch }, }, - [endpt.pwd]: { - [mthd.patch]: { auth: true, schema: sch.pwdPtch, ctr: ctr.acc.res }, + [ep.pwc]: { + [mthd.patch]: { auth: true, schema: sch.pwdUpd, ctr: ctr.acc.pwd }, }, - [endpt.pwdReq]: { + [ep.pwrr]: { [mthd.post]: { auth: false, schema: sch.pwdReq, ctr: ctr.res.req }, }, - [endpt.pwdRes]: { - [mthd.post]: { auth: false, schema: sch.newPwd, ctr: ctr.res.prc }, + [ep.pwrc]: { + [mthd.post]: { auth: false, schema: sch.pwdRes, ctr: ctr.res.prc }, }, } as const; -const getRouteConfig = ( - ep: Endpoint, +const getRouteConfig = ( + e: Endpoint, m: Method, ): Entr => { - const epRoutes = routeMap[ep]; + const epRoutes = routeMap[e]; const conf = epRoutes[m as keyof typeof epRoutes]; if (!conf) { throw new RequestError( - `Method ${m} is not supported for ${ep} endpoint`, + `Method ${m} is not supported for ${e} endpoint`, httpStatus.na, ); } diff --git a/src/services/email.service.ts b/src/services/email.service.ts deleted file mode 100644 index 790aa5ee..00000000 --- a/src/services/email.service.ts +++ /dev/null @@ -1,63 +0,0 @@ -import nodemailer from 'nodemailer'; - -const transporter = nodemailer.createTransport({ - host: process.env.SMTP_HOST, - port: Number(process.env.SMTP_PORT) || 587, - secure: process.env.SMTP_SECURE === 'true', - auth: { - user: process.env.SMTP_USER, - pass: process.env.SMTP_PASS, - }, -}); - -type MailOptions = { - to: string; - subject: string; - html: string; -}; - -async function sendMail(options: MailOptions): Promise { - await transporter.sendMail({ - from: process.env.SMTP_FROM || 'noreply@example.com', - ...options, - }); -} - -async function sendActivationEmail( - email: string, - token: string, -): Promise { - const link = `${process.env.BASE_URL}/activate?token=${token}`; - - await sendMail({ - to: email, - subject: 'Account activation', - html: ` -

Hello!

-

Click the link below to activate your account:

- ${link} - `, - }); -} - -async function sendPwdRes(email: string, token: string): Promise { - const link = `${process.env.BASE_URL}/password/reset?token=${token}`; - - await sendMail({ - to: email, - subject: 'Password reset', - html: ` -

Hello!

-

Click the link below to reset your password:

- ${link} - `, - }); -} - -const emailService = { - send: sendMail, - sendActivation: sendActivationEmail, - sendReset: sendPwdRes, -}; - -export default emailService; diff --git a/src/services/email/email.const.ts b/src/services/email/email.const.ts new file mode 100644 index 00000000..a397b4f4 --- /dev/null +++ b/src/services/email/email.const.ts @@ -0,0 +1,39 @@ +import { ep } from '../../static/index.ts'; + +const mailTemplate = { + act: 'activation', + res: 'pwd reset', +} as const; + +const activate = (token: string) => { + const link = `${process.env.BASE_URL}${ep.act}?token=${token}`; + + return { + subject: 'Account activation', + html: ` +

Hello!

+

Click the link below to activate your account:

+ ${link} + `, + }; +}; + +const reset = (token: string) => { + const link = `${process.env.BASE_URL}${ep.pwrc}?token=${token}`; + + return { + subject: 'Password reset', + html: ` +

Hello!

+

Click the link below to reset your password:

+ ${link} + `, + }; +}; + +const getTemplate = { + [mailTemplate.act]: activate, + [mailTemplate.res]: reset, +}; + +export { mailTemplate, getTemplate }; diff --git a/src/services/email/email.service.ts b/src/services/email/email.service.ts new file mode 100644 index 00000000..bcfec08d --- /dev/null +++ b/src/services/email/email.service.ts @@ -0,0 +1,41 @@ +import { getTemplate, mailTemplate } from './email.const.ts'; +import transporter from './transporter.ts'; + +async function sendMail( + to: string, + subject: string, + html: string, +): Promise { + await transporter.sendMail({ + from: process.env.SMTP_FROM || 'noreply@example.com', + to: to, + subject: subject, + html: html, + }); +} + +async function sendTemplateMail( + email: string, + token: string, + type: (typeof mailTemplate)[keyof typeof mailTemplate], +): Promise { + const data = getTemplate[type](token); + + try { + await sendMail(email, data.subject, data.html); + + return true; + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + + return false; + } +} + +const eml = { + sdM: sendMail, + sdTM: sendTemplateMail, +}; + +export default eml; diff --git a/src/services/email/transporter.ts b/src/services/email/transporter.ts new file mode 100644 index 00000000..3822b047 --- /dev/null +++ b/src/services/email/transporter.ts @@ -0,0 +1,13 @@ +import nodemailer from 'nodemailer'; + +const transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST, + port: Number(process.env.SMTP_PORT) || 587, + secure: process.env.SMTP_SECURE === 'true', + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASS, + }, +}); + +export default transporter; diff --git a/src/services/index.ts b/src/services/index.ts index e38dbc99..7d4c36a8 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,11 +1,11 @@ import usr from './user.service.ts'; import tkn from './token.service.ts'; -import emailService from './email.service.ts'; +import eml from './email/email.service.ts'; const srv = { usr: usr, tkn: tkn, - email: emailService, + eml: eml, }; export default srv; diff --git a/src/services/repo.service.ts b/src/services/repo.service.ts index 1f084a65..47dfcaf2 100644 --- a/src/services/repo.service.ts +++ b/src/services/repo.service.ts @@ -1,8 +1,24 @@ -import { RequestError } from '../errors/index.ts'; +import { DBError, RequestError } from '../errors/index.ts'; import DB from '../model/index.ts'; -import { fnames, httpStatus, type Tnames } from '../static/index.ts'; -import { aDBH } from '../utils/index.ts'; -import { type Create, type DBRes } from './types.ts'; +import { fnames, httpStatus } from '../static/index.ts'; +import type { Create, DBRes, Tnames, Fnames } from '../static/types/index.ts'; + +function dbHandler( + fn: (...args: TArgs) => Promise, +): (...args: TArgs) => Promise { + return async (...args: TArgs): Promise => { + try { + return await fn(...args); + } catch (e) { + if (e instanceof RequestError) { + throw e; + } + throw new DBError( + `Database operation failed: ${e instanceof Error ? e.message : e}`, + ); + } + }; +} const get = async ( table: T, @@ -20,19 +36,19 @@ const get = async ( return item.toJSON(); }; -const del = async (table: Tnames, key: string): Promise => { - const item = await DB[table].findByPk(key); +const del = async (table: Tnames, id: string): Promise => { + const deleted = await DB[table].destroy({ + where: { [fnames[table].id]: id }, + }); - if (!item) { - throw new RequestError(`Id not found: ${key}`, httpStatus.nf); + if (deleted === 0) { + throw new RequestError(`Id not found: ${id}`, httpStatus.nf); } - - await item.destroy(); }; const getByParam = async ( table: T, - field: (typeof fnames)[T][keyof (typeof fnames)[T]], + field: Fnames, query: string | boolean, ): Promise => { const item = await DB[table].findOne({ where: { [field as string]: query } }); @@ -61,19 +77,10 @@ const create = async >( // aDBH = asyncDBHandler, try/catch cover; const base = { - get: aDBH((t: T, k: string) => get(t, k)), - del: aDBH((t: Tnames, k: string) => del(t, k)), - getByPrm: aDBH( - ( - t: T, - f: (typeof fnames)[T][keyof (typeof fnames)[T]], - q: string, - ) => getByParam(t, f, q), - ), - crt: aDBH( - >(t: T, d: Create[T]) => - create(t, d), - ), + get: dbHandler(get), + del: dbHandler(del), + gBPrm: dbHandler(getByParam), + crt: dbHandler(create), }; -export { base }; +export { base, dbHandler }; diff --git a/src/services/token.service.ts b/src/services/token.service.ts index ab1b876a..00975086 100644 --- a/src/services/token.service.ts +++ b/src/services/token.service.ts @@ -1,21 +1,21 @@ import DB from '../model/index.ts'; -import { TNAMES, type Tokens, fnames } from '../static/index.ts'; +import { TNAMES, fnames } from '../static/index.ts'; import { base } from './repo.service.ts'; -import type { Create } from './types.ts'; +import type { Create, Tokens } from '../static/types/index.ts'; const nms = fnames[TNAMES.TKN]; const tkn = { - create: (d: Create[typeof TNAMES.TKN]) => base.crt(TNAMES.TKN, d), - getByTkn: (q: string) => - base.getByPrm(TNAMES.TKN, fnames[TNAMES.TKN].token, q), - del: (id: string) => base.del(TNAMES.TKN, id), - delByUserId: (userId: string, type?: Tokens) => - DB[TNAMES.TKN].destroy({ + crt: (d: Create[typeof TNAMES.TKN]) => base.crt(TNAMES.TKN, d), + gBTkn: (q: string) => base.gBPrm(TNAMES.TKN, fnames[TNAMES.TKN].token, q), + dlt: (id: string) => base.del(TNAMES.TKN, id), + dltBUID: async (userId: string, type?: Tokens) => { + return DB[TNAMES.TKN].destroy({ where: type ? { [nms.usr]: userId, [nms.type]: type } : { [nms.usr]: userId }, - }), + }); + }, }; export default tkn; diff --git a/src/services/types.ts b/src/services/types.ts deleted file mode 100644 index b4de352d..00000000 --- a/src/services/types.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { - CreateTKN, - CreateUser, - DBToken, - DBUser, -} from '../static/types/index.ts'; -import { TNAMES } from '../static/index.ts'; - -interface DBResSN { - id: string; - userId: string; - google: string | null; - github: string | null; - facebook: string | null; -} - -type DBRes = { - [TNAMES.USR]: DBUser; - [TNAMES.SCN]: DBResSN; - [TNAMES.TKN]: DBToken; -}; - -type Create = { - [TNAMES.USR]: CreateUser; - [TNAMES.TKN]: CreateTKN; -}; - -export type { DBRes, Create }; diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 014c7d71..9c2bb44d 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -1,9 +1,8 @@ import DB from '../model/index.ts'; import { fnames, TNAMES } from '../static/index.ts'; -import type { PatchUser } from '../static/types/index.ts'; -import aDBH from '../utils/dbHandler.ts'; -import { base } from './repo.service.ts'; -import type { Create } from './types.ts'; +import { base, dbHandler } from './repo.service.ts'; +import type { Create, PatchUser } from '../static/types/index.ts'; +import { RequestError } from '../errors/index.ts'; async function patchUser(id: string, payload: Partial) { const user = await base.get(TNAMES.USR, id); @@ -14,21 +13,23 @@ async function patchUser(id: string, payload: Partial) { } const usr = { - getById: (id: string) => base.get(TNAMES.USR, id), - getByEmail: (e: string) => - base.getByPrm(TNAMES.USR, fnames[TNAMES.USR].email, e), - delete: (id: string) => base.del(TNAMES.USR, id), - create: (data: Create[typeof TNAMES.USR]) => base.crt(TNAMES.USR, data), - existsByEmail: async (email: string): Promise => { + gbId: (id: string) => base.get(TNAMES.USR, id), + gbEm: (e: string) => base.gBPrm(TNAMES.USR, fnames[TNAMES.USR].email, e), + dlt: (id: string) => base.del(TNAMES.USR, id), + crt: (data: Create[typeof TNAMES.USR]) => base.crt(TNAMES.USR, data), + exBEm: async (email: string): Promise => { try { - await base.getByPrm(TNAMES.USR, fnames[TNAMES.USR].email, email); + await base.gBPrm(TNAMES.USR, fnames[TNAMES.USR].email, email); return true; - } catch { - return false; + } catch (e) { + if (e instanceof RequestError) { + return false; + } + throw e; } }, - patch: aDBH((id: string, pl: Partial) => patchUser(id, pl)), + ptch: dbHandler((id: string, pl: Partial) => patchUser(id, pl)), }; export default usr; diff --git a/src/static/bodySchemas.ts b/src/static/bodySchemas.ts new file mode 100644 index 00000000..0da61dec --- /dev/null +++ b/src/static/bodySchemas.ts @@ -0,0 +1,82 @@ +import { z } from 'zod'; +import { fnames, TNAMES } from './vocab/dbVocab.ts'; + +const MIN_PWD = 6; +const MIN_STR = 3; + +const authorize = z.object({ + [fnames[TNAMES.USR].email]: z + .string() + .refine((val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val), { + message: 'Invalid email', + }), + [fnames[TNAMES.USR].pwd]: z.string().min(MIN_PWD), +}); + +const registration = z.object({ + [fnames[TNAMES.USR].name]: z.string().min(MIN_STR), + [fnames[TNAMES.USR].email]: z + .string() + .refine((val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val), { + message: 'Invalid email', + }), + [fnames[TNAMES.USR].pwd]: z.string().min(MIN_PWD), +}); + +const profileUpdate = z + .object({ + [fnames[TNAMES.USR].pwd]: z.string().min(MIN_PWD), + [fnames[TNAMES.USR].name]: z.string().min(MIN_STR).optional(), + [fnames[TNAMES.USR].email]: z + .string() + .refine((val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val), { + message: 'Invalid email', + }) + .optional(), + }) + .refine((data) => data.name || data.email, { + message: 'At least one field (name or email) is required', + }); + +const passwordUpdate = z + .object({ + oldPwd: z.string(), + newPwd: z.string().min(MIN_PWD), + confirmation: z.string(), + }) + .refine((data) => data.confirmation === data.newPwd, { + message: "Passwords don't match", + path: ['confirmation'], + }); + +const requestPwdUpdate = z.object({ + [fnames[TNAMES.USR].email]: z + .string() + .refine((val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val), { + message: 'Invalid email', + }), +}); + +const resolvePwdUpdate = z + .object({ + [fnames[TNAMES.TKN].token]: z.string(), + newPwd: z.string().min(MIN_PWD), + confirmation: z.string(), + }) + .refine((data) => data.confirmation === data.newPwd, { + message: "Passwords don't match", + path: ['confirmation'], + }); + +const sch = { + auth: authorize, + reg: registration, + upd: profileUpdate, + pwdUpd: passwordUpdate, + pwdReq: requestPwdUpdate, + pwdRes: resolvePwdUpdate, +}; + +type Schema = (typeof sch)[keyof typeof sch]; + +export { sch, type Schema }; diff --git a/src/static/constants.ts b/src/static/constants.ts deleted file mode 100644 index e849575d..00000000 --- a/src/static/constants.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { TKN, type Tokens } from './index.ts'; - -const TOKEN_EXPIRY: Record = { - [TKN.ACC]: ['15m', '900'], - [TKN.RFR]: ['7d', '604800'], - [TKN.ACT]: ['24h', '86400'], - [TKN.PWR]: ['1h', '3600'], -}; - -export { TOKEN_EXPIRY }; diff --git a/src/static/endpoints.ts b/src/static/endpoints.ts index 43d6ae27..49488b4d 100644 --- a/src/static/endpoints.ts +++ b/src/static/endpoints.ts @@ -1,17 +1,17 @@ -const endpt = { +const ep = { idx: '/', auth: '/auth', refr: '/auth/refresh', - snauth: '/auth/social', - lgout: '/auth/logout', + authsn: '/auth/social', + lgt: '/auth/logout', reg: '/register', act: '/register/activate', - acc: '/profile', - pwd: '/profile/password', - pwdReq: '/password/reset-request', - pwdRes: '/password/reset', + prf: '/profile', + pwc: '/profile/password', + pwrr: '/password/reset-request', + pwrc: '/password/reset', } as const; -type Endpoint = (typeof endpt)[keyof typeof endpt]; +type Endpoint = (typeof ep)[keyof typeof ep]; -export { endpt, type Endpoint }; +export { ep, type Endpoint }; diff --git a/src/static/index.ts b/src/static/index.ts index 60361047..399ee2b4 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -1,8 +1,13 @@ -import { TNAMES, fnames, TKN } from './vocab/dbVocab.ts'; +import { ep } from './endpoints.ts'; +import { TNAMES, fnames, TKN, Tokens } from './vocab/dbVocab.ts'; import { mthd, httpStatus } from './vocab/httpVocab.ts'; -import { endpt } from './endpoints.ts'; -import { TOKEN_EXPIRY } from './constants.ts'; -import type { Tokens, Tnames } from './vocab/dbVocab.ts'; +import { sch } from './bodySchemas.ts'; -export { TNAMES, TKN, fnames, TOKEN_EXPIRY, mthd, httpStatus, endpt }; -export type { Tokens, Tnames }; +const TOKEN_EXPIRY: Record = { + [TKN.ACC]: ['15m', '900'], + [TKN.RFR]: ['7d', '604800'], + [TKN.ACT]: ['24h', '86400'], + [TKN.PWR]: ['1h', '3600'], +}; + +export { TNAMES, TKN, fnames, TOKEN_EXPIRY, mthd, httpStatus, ep, sch }; diff --git a/src/static/types.ts b/src/static/types.ts deleted file mode 100644 index 96005e47..00000000 --- a/src/static/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -import http from 'http'; -import { z } from 'zod'; - -import { type Endpoint } from './endpoints.ts'; -import { type Method, type HTTPStatus } from './vocab/httpVocab.ts'; -import { type JWTPayload } from '../utils/index.ts'; - -type Ctx = { - req: http.IncomingMessage; - res: http.ServerResponse; - body: S extends z.ZodSchema ? z.infer : false; - usr: JWTPayload | null; - param: string | null; -}; - -export type { Endpoint, Method, HTTPStatus, Ctx }; diff --git a/src/static/types/index.ts b/src/static/types/index.ts index ab5e8508..7a9d634c 100644 --- a/src/static/types/index.ts +++ b/src/static/types/index.ts @@ -1,3 +1,37 @@ +import http from 'http'; +import { z } from 'zod'; + import type { DBUser, DTOUser, PatchUser, CreateUser } from './user.types.ts'; import type { DBToken, CreateTKN } from './token.types.ts'; -export type { DBUser, DTOUser, PatchUser, CreateUser, DBToken, CreateTKN }; +import type { Tokens, Tnames, Fnames } from '../vocab/dbVocab.ts'; +import type { Method, HTTPStatus } from '../vocab/httpVocab.ts'; +import type { Schema } from '../bodySchemas.ts'; +import type { Endpoint } from '../endpoints.ts'; +import type { DBRes, Create } from './maps.type.ts'; + +type Ctx = { + req: http.IncomingMessage; + res: http.ServerResponse; + body: S extends z.ZodSchema ? z.infer : null; + usr: DTOUser | null; + param: string | null; +}; + +export type { + DBUser, + DTOUser, + PatchUser, + CreateUser, + DBToken, + CreateTKN, + Ctx, + Tokens, + Tnames, + Method, + HTTPStatus, + Schema, + Endpoint, + DBRes, + Create, + Fnames, +}; diff --git a/src/static/types/maps.type.ts b/src/static/types/maps.type.ts new file mode 100644 index 00000000..9a0ca47d --- /dev/null +++ b/src/static/types/maps.type.ts @@ -0,0 +1,15 @@ +import { TNAMES } from '../vocab/dbVocab.ts'; +import { CreateTKN, DBToken } from './token.types.ts'; +import { CreateUser, DBUser } from './user.types.ts'; + +type DBRes = { + [TNAMES.USR]: DBUser; + [TNAMES.TKN]: DBToken; +}; + +type Create = { + [TNAMES.USR]: CreateUser; + [TNAMES.TKN]: CreateTKN; +}; + +export type { DBRes, Create }; diff --git a/src/static/types/token.types.ts b/src/static/types/token.types.ts index ac5fb0c3..0af8085e 100644 --- a/src/static/types/token.types.ts +++ b/src/static/types/token.types.ts @@ -1,4 +1,5 @@ -import { TKN, type Tokens } from '../index.ts'; +import { TKN } from '../index.ts'; +import type { Tokens } from '../vocab/dbVocab.ts'; interface DBToken { id: string; @@ -9,8 +10,7 @@ interface DBToken { createdAt: Date; } -interface CreateTKN - extends Omit { +interface CreateTKN extends Pick { type: Exclude; expiresAt: string; } diff --git a/src/static/types/user.types.ts b/src/static/types/user.types.ts index 5eb1b180..44f10194 100644 --- a/src/static/types/user.types.ts +++ b/src/static/types/user.types.ts @@ -8,13 +8,10 @@ interface DBUser { updatedAt: Date; } -type DTOUser = Omit< - DBUser, - 'password' | 'activated' | 'createdAt' | 'updatedAt' ->; +type DTOUser = Pick; type PatchUser = Omit; -type CreateUser = Omit; +type CreateUser = Pick; export type { DBUser, DTOUser, PatchUser, CreateUser }; diff --git a/src/static/vocab/dbVocab.ts b/src/static/vocab/dbVocab.ts index 86a359ff..d9b0a04f 100644 --- a/src/static/vocab/dbVocab.ts +++ b/src/static/vocab/dbVocab.ts @@ -1,6 +1,5 @@ const TNAMES = { USR: 'users', - SCN: 'social_accounts', TKN: 'tokens', } as const; @@ -14,13 +13,6 @@ const fnames = { pwd: 'password', act: 'activated', }, - [TNAMES.SCN]: { - id: 'id', - usr: 'userId', - ggl: 'google', - gh: 'github', - fb: 'facebook', - }, [TNAMES.TKN]: { id: 'id', usr: 'userId', @@ -37,7 +29,8 @@ const TKN = { ACC: 'access', } as const; +type Fnames = (typeof fnames)[T][keyof (typeof fnames)[T]]; type Tokens = (typeof TKN)[keyof typeof TKN]; export { TNAMES, fnames, TKN }; -export type { Tokens, Tnames }; +export type { Tokens, Tnames, Fnames }; diff --git a/src/test.ts b/src/test.ts new file mode 100644 index 00000000..fb7add2a --- /dev/null +++ b/src/test.ts @@ -0,0 +1,42 @@ +import DB from './model/index.ts'; +import { TNAMES } from './static/index.ts'; +import bcrypt from 'bcrypt'; + +async function getTestUsr() { + const createUsr = async (usr: { + name: string; + email: string; + password: string; + activated: boolean; + }) => { + await DB[TNAMES.USR].create({ + ...usr, + }); + }; + + const pwds = ['jdfu70sdf', 'jdddu70sdf', 'jdgfdfd70sdf']; + const testUsrs = [ + { + name: 'JohnDoe', + email: 'gtlafuk@iksf.fsd', + password: await bcrypt.hash(pwds[0], 10), + activated: true, + }, + { + name: 'Yurgen', + email: 'gtlasdk@iksf.fsd', + password: await bcrypt.hash(pwds[1], 10), + activated: true, + }, + { + name: 'JosdnDoe', + email: 'gtsdgasfuk@iksf.fsd', + password: await bcrypt.hash(pwds[2], 10), + activated: true, + }, + ]; + + await testUsrs.map((el) => createUsr(el)); +} + +export default getTestUsr; diff --git a/src/utils/cookies.ts b/src/utils/cookies.ts index 7e5aea3b..2a0b103e 100644 --- a/src/utils/cookies.ts +++ b/src/utils/cookies.ts @@ -17,4 +17,4 @@ function parseCookies(req: http.IncomingMessage): Cookies { ); } -export { parseCookies }; +export default parseCookies; diff --git a/src/utils/cors.ts b/src/utils/cors.ts new file mode 100644 index 00000000..a491de0a --- /dev/null +++ b/src/utils/cors.ts @@ -0,0 +1,16 @@ +import http from 'http'; + +const CORS_ORIGIN = process.env.CORS_ORIGIN || '*'; + +function setCorsHeaders(res: http.ServerResponse) { + res.setHeader('Access-Control-Allow-Origin', CORS_ORIGIN); + + res.setHeader( + 'Access-Control-Allow-Methods', + 'GET, POST, PATCH, DELETE, OPTIONS', + ); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + res.setHeader('Access-Control-Allow-Credentials', 'true'); +} + +export default setCorsHeaders; diff --git a/src/utils/dbHandler.ts b/src/utils/dbHandler.ts index 09fc4479..4d919c26 100644 --- a/src/utils/dbHandler.ts +++ b/src/utils/dbHandler.ts @@ -1,6 +1,6 @@ import { DBError } from '../errors/index.ts'; -function aDBH( +function dbHandler( fn: (...args: TArgs) => Promise, ): (...args: TArgs) => Promise { return async (...args: TArgs): Promise => { @@ -14,4 +14,4 @@ function aDBH( }; } -export default aDBH; +export default dbHandler; diff --git a/src/utils/index.ts b/src/utils/index.ts index 58b30d31..cd232bce 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,7 +1,13 @@ -import aDBH from './dbHandler.ts'; -import { parseCookies } from './cookies.ts'; -import { type JWTPayload } from './types.ts'; -import jwtAct from './jwt.ts'; +import dbHandler from './dbHandler.ts'; +import parseCookies from './cookies.ts'; +import jwtAction from './jwt.ts'; +import setCorsHeaders from './cors.ts'; -export { aDBH, parseCookies, jwtAct }; -export type { JWTPayload }; +const utl = { + dbh: dbHandler, + jwt: jwtAction, + prsCks: parseCookies, + setCors: setCorsHeaders, +}; + +export default utl; diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts index 3b5e93c8..f22216d0 100644 --- a/src/utils/jwt.ts +++ b/src/utils/jwt.ts @@ -1,13 +1,13 @@ import jwt from 'jsonwebtoken'; import { RequestError } from '../errors/index.ts'; -import { httpStatus, TKN, TOKEN_EXPIRY, type Tokens } from '../static/index.ts'; -import { type JWTPayload } from './types.ts'; +import { httpStatus, TKN, TOKEN_EXPIRY } from '../static/index.ts'; +import type { DTOUser, Tokens } from '../static/types/index.ts'; const SECRET_KEY = process.env.JWT_SECRET || '7>?~!id(#;fd13/^^$fdkq124<'; type Signed = { expiresAt: string; token: string }; -function signToken(payload: JWTPayload, type: Tokens): Signed { +function signToken(payload: DTOUser, type: Tokens): Signed { const expiryDuration = TOKEN_EXPIRY[type][0]; const expirySeconds = Number(TOKEN_EXPIRY[type][1]); @@ -20,9 +20,9 @@ function signToken(payload: JWTPayload, type: Tokens): Signed { return { expiresAt, token }; } -function verifyToken(token: string): JWTPayload & { type: Tokens } { +function verifyToken(token: string): DTOUser & { type: Tokens } { try { - const decoded = jwt.verify(token, SECRET_KEY) as JWTPayload & { + const decoded = jwt.verify(token, SECRET_KEY) as DTOUser & { type: Tokens; }; @@ -40,14 +40,13 @@ function verifyToken(token: string): JWTPayload & { type: Tokens } { } } -const jwtAct = { - sign: (pl: JWTPayload, tp: Tokens) => signToken(pl, tp), +const jwtAction = { + sign: (pl: DTOUser, tp: Tokens) => signToken(pl, tp), ver: (tk: string) => verifyToken(tk), create: { - [TKN.ACC]: (payload: JWTPayload): Signed => signToken(payload, TKN.ACC), - [TKN.RFR]: (payload: JWTPayload): Signed => signToken(payload, TKN.RFR), + [TKN.ACC]: (payload: DTOUser): Signed => signToken(payload, TKN.ACC), + [TKN.RFR]: (payload: DTOUser): Signed => signToken(payload, TKN.RFR), }, }; -export default jwtAct; -export type { JWTPayload }; +export default jwtAction; diff --git a/src/utils/types.ts b/src/utils/types.ts deleted file mode 100644 index 562cf4bd..00000000 --- a/src/utils/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { DTOUser } from '../static/types/user.types.ts'; - -type JWTPayload = DTOUser; - -export type { JWTPayload }; diff --git a/src/validation/index.ts b/src/validation/index.ts index 67ee9da5..5b20b449 100644 --- a/src/validation/index.ts +++ b/src/validation/index.ts @@ -1,4 +1,9 @@ import validateRequest from './validateRequest.ts'; import validateBody from './validateBody.ts'; -import sch from './schemas.ts'; -export { validateRequest, validateBody, sch }; + +const val = { + req: validateRequest, + bd: validateBody, +}; + +export default val; diff --git a/src/validation/schemas.ts b/src/validation/schemas.ts deleted file mode 100644 index 49f86bdb..00000000 --- a/src/validation/schemas.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { z } from 'zod'; -import { fnames, TNAMES } from '../static/index.ts'; - -const usrFNames = fnames[TNAMES.USR]; -const tkFNames = fnames[TNAMES.TKN]; - -const authSch = z.object({ - [usrFNames.email]: z - .string() - .refine((val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val), { - message: 'Invalid email', - }), - [usrFNames.pwd]: z.string().min(6), -}); - -const regUpdSch = z.object({ - [usrFNames.name]: z.string().min(3), - [usrFNames.email]: z - .string() - .refine((val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val), { - message: 'Invalid email', - }), - [usrFNames.pwd]: z.string().min(6), -}); - -const pwdUpd = z - .object({ - oldPwd: z.string(), - newPwd: z.string().min(6), - confirmation: z.string().min(6), - }) - .refine((data) => data.confirmation === data.newPwd, { - message: "Passwords don't match", - path: ['confirmation'], - }); - -const pwdReq = z.object({ - [usrFNames.email]: z - .string() - .refine((val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val), { - message: 'Invalid email', - }), -}); - -const pwdRes = z - .object({ - [tkFNames.token]: z.string(), - newPwd: z.string().min(6), - confirmation: z.string(), - }) - .refine((data) => data.confirmation === data.newPwd, { - message: "Passwords don't match", - path: ['confirmation'], - }); - -const sch = { - auth: authSch, - reg: regUpdSch, - profUpd: regUpdSch, - pwdPtch: pwdUpd, - pwdReq: pwdReq, - newPwd: pwdRes, -}; - -export default sch; diff --git a/src/validation/validateRequest.ts b/src/validation/validateRequest.ts index fbae52b9..77cdf23b 100644 --- a/src/validation/validateRequest.ts +++ b/src/validation/validateRequest.ts @@ -1,14 +1,14 @@ import http from 'http'; import { RequestError } from '../errors/index.ts'; -import { httpStatus, mthd, endpt } from '../static/index.ts'; -import { type Endpoint, type Method } from '../static/types.ts'; +import { httpStatus, mthd, ep } from '../static/index.ts'; +import type { Endpoint, Method } from '../static/types/index.ts'; const REQUIRED_PARAMS: Partial> = { - [endpt.act]: 'token', + [ep.act]: 'token', }; const validatePath = (path: string): path is Endpoint => { - return Object.values(endpt).some((el) => el === path); + return Object.values(ep).some((el) => el === path); }; const validateMethod = (method: string | undefined): method is Method => { @@ -36,6 +36,7 @@ function validateRequest(req: http.IncomingMessage) { throw new RequestError(`Unknown method: ${method}`, httpStatus.br); } + // check if ep requires searchparam if (!(endpoint in REQUIRED_PARAMS)) { return { endpoint, method, param: null }; } From 1437780ed63b13c18efcd34db840545d89e815a1 Mon Sep 17 00:00:00 2001 From: Bespalov Tymofii Date: Fri, 6 Feb 2026 18:01:12 +0100 Subject: [PATCH 10/14] mvp --- src/test.ts | 42 ------------------------------------------ 1 file changed, 42 deletions(-) delete mode 100644 src/test.ts diff --git a/src/test.ts b/src/test.ts deleted file mode 100644 index fb7add2a..00000000 --- a/src/test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import DB from './model/index.ts'; -import { TNAMES } from './static/index.ts'; -import bcrypt from 'bcrypt'; - -async function getTestUsr() { - const createUsr = async (usr: { - name: string; - email: string; - password: string; - activated: boolean; - }) => { - await DB[TNAMES.USR].create({ - ...usr, - }); - }; - - const pwds = ['jdfu70sdf', 'jdddu70sdf', 'jdgfdfd70sdf']; - const testUsrs = [ - { - name: 'JohnDoe', - email: 'gtlafuk@iksf.fsd', - password: await bcrypt.hash(pwds[0], 10), - activated: true, - }, - { - name: 'Yurgen', - email: 'gtlasdk@iksf.fsd', - password: await bcrypt.hash(pwds[1], 10), - activated: true, - }, - { - name: 'JosdnDoe', - email: 'gtsdgasfuk@iksf.fsd', - password: await bcrypt.hash(pwds[2], 10), - activated: true, - }, - ]; - - await testUsrs.map((el) => createUsr(el)); -} - -export default getTestUsr; From 7e107aa1db2b1ab4788d00b4514945fbc3fd8933 Mon Sep 17 00:00:00 2001 From: Bespalov Tymofii Date: Fri, 6 Feb 2026 18:08:46 +0100 Subject: [PATCH 11/14] fixes --- .env | 7 ------- src/router/router.ts | 3 --- src/services/email/transporter.ts | 9 ++++----- src/static/endpoints.ts | 1 - 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/.env b/.env index 0b6a1af8..99fe8083 100644 --- a/.env +++ b/.env @@ -6,10 +6,3 @@ POSTGRES_PASSWORD=123 POSTGRES_DB=postgres JWT_SECRET_KEY=itlv&80f8 -SMTP_HOST=smtp.example.com -SMTP_PORT=587 -SMTP_SECURE=false -SMTP_USER=your@email.com -SMTP_PASS=password -SMTP_FROM=noreply@yourapp.com -BASE_URL=http://localhost:5700 diff --git a/src/router/router.ts b/src/router/router.ts index bbeccf56..4540844b 100644 --- a/src/router/router.ts +++ b/src/router/router.ts @@ -19,9 +19,6 @@ const routeMap = { [ep.refr]: { [mthd.post]: { auth: false, schema: null, ctr: ctr.auth.rfr }, }, - [ep.authsn]: { - [mthd.post]: { auth: false, schema: null, ctr: () => true }, - }, [ep.lgt]: { [mthd.patch]: { auth: true, schema: null, ctr: ctr.auth.lgt }, }, diff --git a/src/services/email/transporter.ts b/src/services/email/transporter.ts index 3822b047..47cd8734 100644 --- a/src/services/email/transporter.ts +++ b/src/services/email/transporter.ts @@ -1,12 +1,11 @@ import nodemailer from 'nodemailer'; const transporter = nodemailer.createTransport({ - host: process.env.SMTP_HOST, - port: Number(process.env.SMTP_PORT) || 587, - secure: process.env.SMTP_SECURE === 'true', + host: 'smtp.ethereal.email', + port: 587, auth: { - user: process.env.SMTP_USER, - pass: process.env.SMTP_PASS, + user: 'palma.spencer@ethereal.email', + pass: 'YhYHdzdxqn1Vcs98GW', }, }); diff --git a/src/static/endpoints.ts b/src/static/endpoints.ts index 49488b4d..4b432cdb 100644 --- a/src/static/endpoints.ts +++ b/src/static/endpoints.ts @@ -2,7 +2,6 @@ const ep = { idx: '/', auth: '/auth', refr: '/auth/refresh', - authsn: '/auth/social', lgt: '/auth/logout', reg: '/register', act: '/register/activate', From 71213a0cbd9e7d36dae21e0bd8fd0d210bafec4e Mon Sep 17 00:00:00 2001 From: Bespalov Tymofii Date: Fri, 6 Feb 2026 18:11:11 +0100 Subject: [PATCH 12/14] fixes --- src/static/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/static/index.ts b/src/static/index.ts index 399ee2b4..d4ed0397 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -1,7 +1,8 @@ import { ep } from './endpoints.ts'; -import { TNAMES, fnames, TKN, Tokens } from './vocab/dbVocab.ts'; +import { TNAMES, fnames, TKN } from './vocab/dbVocab.ts'; import { mthd, httpStatus } from './vocab/httpVocab.ts'; import { sch } from './bodySchemas.ts'; +import type { Tokens } from './types/index.ts'; const TOKEN_EXPIRY: Record = { [TKN.ACC]: ['15m', '900'], From ac29e46070ddce232da740d08168aadd6804c618 Mon Sep 17 00:00:00 2001 From: Bespalov Tymofii Date: Fri, 6 Feb 2026 18:35:41 +0100 Subject: [PATCH 13/14] fixes --- package-lock.json | 531 +++++++++++++++++++++++++++++- package.json | 8 +- src/controllers/acc.ctr.ts | 5 + src/controllers/auth.ctr.ts | 2 +- src/controllers/idx.ctr.ts | 3 +- src/controllers/reg.ctr.ts | 10 +- src/{index.js => index.ts} | 0 src/services/email/email.const.ts | 13 + src/services/user.service.ts | 4 +- src/static/bodySchemas.ts | 5 + src/utils/dbHandler.ts | 17 - src/utils/index.ts | 2 - src/utils/jwt.ts | 2 +- 13 files changed, 570 insertions(+), 32 deletions(-) rename src/{index.js => index.ts} (100%) delete mode 100644 src/utils/dbHandler.ts diff --git a/package-lock.json b/package-lock.json index 33753a42..f9f767d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,9 @@ "eslint-plugin-jest": "^28.6.0", "eslint-plugin-node": "^11.1.0", "jest": "^29.7.0", - "prettier": "^3.3.2" + "prettier": "^3.3.2", + "tsx": "^4.19.0", + "typescript": "^5.5.0" } }, "node_modules/@ampproject/remapping": { @@ -551,6 +553,448 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -3571,6 +4015,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -4540,6 +5026,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -8114,6 +8613,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -8816,6 +9325,26 @@ "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "dev": true }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index da71256a..a53ac54f 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,10 @@ "version": "1.0.0", "type": "module", "description": "Auth app", - "main": "src/index.js", + "main": "src/index.ts", "scripts": { "init": "mate-scripts init", - "start": "node src/index.js", + "start": "tsx src/index.ts", "lint": "npm run format && mate-scripts lint", "format": "prettier --ignore-path .prettierignore --write './src/**/*.{js,ts}'", "test:only": "mate-scripts test", @@ -26,7 +26,9 @@ "eslint-plugin-jest": "^28.6.0", "eslint-plugin-node": "^11.1.0", "jest": "^29.7.0", - "prettier": "^3.3.2" + "prettier": "^3.3.2", + "tsx": "^4.19.0", + "typescript": "^5.5.0" }, "mateAcademy": { "projectType": "javascript" diff --git a/src/controllers/acc.ctr.ts b/src/controllers/acc.ctr.ts index b2712773..38685fea 100644 --- a/src/controllers/acc.ctr.ts +++ b/src/controllers/acc.ctr.ts @@ -1,5 +1,6 @@ import dto from '../dto/index.ts'; import { RequestError } from '../errors/index.ts'; +import { mailTemplate } from '../services/email/email.const.ts'; import srv from '../services/index.ts'; import { httpStatus, TKN, sch } from '../static/index.ts'; import type { Ctx, DTOUser } from '../static/types/index.ts'; @@ -53,6 +54,10 @@ async function patchAcc(ctx: Ctx) { const newUsr = await srv.usr.ptch(user.id, payload); + if (email && email !== user.email) { + await srv.eml.sdTM(user.email, email, mailTemplate.emlChg); + } + res.statusCode = httpStatus.ok; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(dto.usr(newUsr))); diff --git a/src/controllers/auth.ctr.ts b/src/controllers/auth.ctr.ts index a5afcb87..7cb818d0 100644 --- a/src/controllers/auth.ctr.ts +++ b/src/controllers/auth.ctr.ts @@ -13,7 +13,7 @@ async function manual(ctx: Ctx): Promise { const user = await srv.usr.gbEm(email); if (!user.activated) { - throw new RequestError('Invalid credentials', httpStatus.ua); + throw new RequestError('Please activate your account before logging in', httpStatus.ua); } // compare pw diff --git a/src/controllers/idx.ctr.ts b/src/controllers/idx.ctr.ts index 74896ef8..a762148b 100644 --- a/src/controllers/idx.ctr.ts +++ b/src/controllers/idx.ctr.ts @@ -1,9 +1,8 @@ import http from 'http'; -const index = '

some-html

'; +const index = '

some-html

'; const idx = (res: http.ServerResponse) => { - // setCors(); res.statusCode = 200; res.setHeader('Content-Type', 'text/html'); res.end(index); diff --git a/src/controllers/reg.ctr.ts b/src/controllers/reg.ctr.ts index 1cf08385..68aab9b3 100644 --- a/src/controllers/reg.ctr.ts +++ b/src/controllers/reg.ctr.ts @@ -6,7 +6,7 @@ import { httpStatus, sch, TKN } from '../static/index.ts'; import type { Ctx } from '../static/types/index.ts'; import utl from '../utils/index.ts'; -import { hashPwd } from './helpers/helpers.ts'; +import { handleTokens, hashPwd } from './helpers/helpers.ts'; async function register(ctx: Ctx): Promise { const { res, body } = ctx; @@ -68,13 +68,17 @@ async function activate(ctx: Ctx) { throw new RequestError('Token expired', httpStatus.br); } - await srv.usr.ptch(token.userId, { activated: true }); + const user = await srv.usr.ptch(token.userId, { activated: true }); await srv.tkn.dlt(token.id); + const payload = dto.usr(user); + + await handleTokens(res, payload); + res.statusCode = httpStatus.ok; res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({ message: `Succesfully activated` })); + res.end(JSON.stringify({ message: 'Activated', user: payload })); } const reg = { diff --git a/src/index.js b/src/index.ts similarity index 100% rename from src/index.js rename to src/index.ts diff --git a/src/services/email/email.const.ts b/src/services/email/email.const.ts index a397b4f4..2a22700e 100644 --- a/src/services/email/email.const.ts +++ b/src/services/email/email.const.ts @@ -3,6 +3,7 @@ import { ep } from '../../static/index.ts'; const mailTemplate = { act: 'activation', res: 'pwd reset', + emlChg: 'email change', } as const; const activate = (token: string) => { @@ -31,9 +32,21 @@ const reset = (token: string) => { }; }; +const emailChange = (newEmail: string) => { + return { + subject: 'Email address changed', + html: ` +

Hello!

+

Your email address has been changed to: ${newEmail}

+

If you did not make this change, please contact support immediately.

+ `, + }; +}; + const getTemplate = { [mailTemplate.act]: activate, [mailTemplate.res]: reset, + [mailTemplate.emlChg]: emailChange, }; export { mailTemplate, getTemplate }; diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 9c2bb44d..ca1ddf9b 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -1,5 +1,5 @@ import DB from '../model/index.ts'; -import { fnames, TNAMES } from '../static/index.ts'; +import { fnames, httpStatus, TNAMES } from '../static/index.ts'; import { base, dbHandler } from './repo.service.ts'; import type { Create, PatchUser } from '../static/types/index.ts'; import { RequestError } from '../errors/index.ts'; @@ -23,7 +23,7 @@ const usr = { return true; } catch (e) { - if (e instanceof RequestError) { + if (e instanceof RequestError && e.statusCode !== httpStatus.se) { return false; } throw e; diff --git a/src/static/bodySchemas.ts b/src/static/bodySchemas.ts index 0da61dec..9af21a4e 100644 --- a/src/static/bodySchemas.ts +++ b/src/static/bodySchemas.ts @@ -33,9 +33,14 @@ const profileUpdate = z message: 'Invalid email', }) .optional(), + confirmEmail: z.string().optional(), }) .refine((data) => data.name || data.email, { message: 'At least one field (name or email) is required', + }) + .refine((data) => !data.email || data.confirmEmail === data.email, { + message: "Emails don't match", + path: ['confirmEmail'], }); const passwordUpdate = z diff --git a/src/utils/dbHandler.ts b/src/utils/dbHandler.ts deleted file mode 100644 index 4d919c26..00000000 --- a/src/utils/dbHandler.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { DBError } from '../errors/index.ts'; - -function dbHandler( - fn: (...args: TArgs) => Promise, -): (...args: TArgs) => Promise { - return async (...args: TArgs): Promise => { - try { - return await fn(...args); - } catch (error) { - throw new DBError( - `Database operation failed: ${error instanceof Error ? error.message : error}`, - ); - } - }; -} - -export default dbHandler; diff --git a/src/utils/index.ts b/src/utils/index.ts index cd232bce..93687e2f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,10 +1,8 @@ -import dbHandler from './dbHandler.ts'; import parseCookies from './cookies.ts'; import jwtAction from './jwt.ts'; import setCorsHeaders from './cors.ts'; const utl = { - dbh: dbHandler, jwt: jwtAction, prsCks: parseCookies, setCors: setCorsHeaders, diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts index f22216d0..4466cd9a 100644 --- a/src/utils/jwt.ts +++ b/src/utils/jwt.ts @@ -29,7 +29,7 @@ function verifyToken(token: string): DTOUser & { type: Tokens } { return decoded; } catch (error) { if (error instanceof jwt.TokenExpiredError) { - throw new RequestError('Token has expired', httpStatus.na); + throw new RequestError('Token has expired', httpStatus.ua); } if (error instanceof jwt.JsonWebTokenError) { From 6ce9c77c25634bc360765ef5dd2a971d2e698966 Mon Sep 17 00:00:00 2001 From: Bespalov Tymofii Date: Fri, 6 Feb 2026 18:42:26 +0100 Subject: [PATCH 14/14] fixes --- src/controllers/auth.ctr.ts | 5 ++++- src/controllers/helpers/helpers.ts | 4 ++-- src/db/db.ts | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/controllers/auth.ctr.ts b/src/controllers/auth.ctr.ts index 7cb818d0..1d4dddea 100644 --- a/src/controllers/auth.ctr.ts +++ b/src/controllers/auth.ctr.ts @@ -13,7 +13,10 @@ async function manual(ctx: Ctx): Promise { const user = await srv.usr.gbEm(email); if (!user.activated) { - throw new RequestError('Please activate your account before logging in', httpStatus.ua); + throw new RequestError( + 'Please activate your account before logging in', + httpStatus.ua, + ); } // compare pw diff --git a/src/controllers/helpers/helpers.ts b/src/controllers/helpers/helpers.ts index 40c478e6..e51244bc 100644 --- a/src/controllers/helpers/helpers.ts +++ b/src/controllers/helpers/helpers.ts @@ -37,8 +37,8 @@ async function handleTokens(res: http.ServerResponse, pl: DTOUser) { await srv.tkn.crt(createPayload); res.setHeader('Set-Cookie', [ - setTkn(TKN.ACC, accTkn.token, TOKEN_EXPIRY.access[1]), - setTkn(TKN.RFR, refTkn.token, TOKEN_EXPIRY.refresh[1]), + setTkn(TKN.ACC, accTkn.token, TOKEN_EXPIRY[TKN.ACC][1]), + setTkn(TKN.RFR, refTkn.token, TOKEN_EXPIRY[TKN.RFR][1]), ]); } diff --git a/src/db/db.ts b/src/db/db.ts index 56c95eeb..57825651 100644 --- a/src/db/db.ts +++ b/src/db/db.ts @@ -24,7 +24,7 @@ const dbSetup = async () => { const { default: DB } = await import('../model/index.ts'); for (const model of Object.values(DB)) { - await model.sync({ force: true }); + await model.sync(); } } catch (error) { throw new DBError(`Database setup failed: ${error}`);