From 6e9bfa4b82b93fe219e5b6d15c64895b1f49fed4 Mon Sep 17 00:00:00 2001 From: renolation Date: Mon, 12 May 2025 11:28:18 +0700 Subject: [PATCH] auth --- .prettierrc | 11 - eslint.config.mjs | 49 +-- package-lock.json | 402 +++++++++++++++++- package.json | 15 +- src/app.module.ts | 50 ++- .../authentication.controller.ts | 87 ++++ src/authentication/authentication.module.ts | 30 ++ src/authentication/authentication.service.ts | 121 ++++++ src/authentication/dto/login.dto.ts | 13 + src/authentication/dto/register.dto.ts | 39 ++ .../jwt-authentication.guard.ts | 5 + src/authentication/jwt.strategy.ts | 25 ++ src/authentication/local.strategy.ts | 17 + .../localAuthentication.guard.ts | 5 + .../requestWithUser.interface.ts | 8 + src/authentication/tokenPayload.interface.ts | 5 + src/database/postgresErrorCodes.enum.ts | 4 + src/main.ts | 11 +- src/users/user.entity.ts | 29 +- src/users/user.service.ts | 23 + 20 files changed, 863 insertions(+), 86 deletions(-) delete mode 100644 .prettierrc create mode 100644 src/authentication/authentication.controller.ts create mode 100644 src/authentication/authentication.module.ts create mode 100644 src/authentication/authentication.service.ts create mode 100644 src/authentication/dto/login.dto.ts create mode 100644 src/authentication/dto/register.dto.ts create mode 100644 src/authentication/jwt-authentication.guard.ts create mode 100644 src/authentication/jwt.strategy.ts create mode 100644 src/authentication/local.strategy.ts create mode 100644 src/authentication/localAuthentication.guard.ts create mode 100644 src/authentication/requestWithUser.interface.ts create mode 100644 src/authentication/tokenPayload.interface.ts create mode 100644 src/database/postgresErrorCodes.enum.ts diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index b5b9d7c..0000000 --- a/.prettierrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "semi": true, - "singleQuote": true, - "trailingComma": "all", - "printWidth": 100, - "tabWidth": 2, - "useTabs": false, - "bracketSpacing": true, - "arrowParens": "avoid", - "endOfLine": "auto" -} diff --git a/eslint.config.mjs b/eslint.config.mjs index caebf6e..b82ee6b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,30 +5,31 @@ import globals from 'globals'; import tseslint from 'typescript-eslint'; export default tseslint.config( - { - ignores: ['eslint.config.mjs'], - }, - eslint.configs.recommended, - ...tseslint.configs.recommendedTypeChecked, - eslintPluginPrettierRecommended, - { - languageOptions: { - globals: { - ...globals.node, - ...globals.jest, - }, - sourceType: 'commonjs', - parserOptions: { - projectService: true, - tsconfigRootDir: import.meta.dirname, - }, + { + ignores: ['eslint.config.mjs'], }, - }, - { - rules: { - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-floating-promises': 'warn', - '@typescript-eslint/no-unsafe-argument': 'warn' + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + // eslintPluginPrettierRecommended, + { + languageOptions: { + globals: { + ...globals.node, + ...globals.jest, + }, + sourceType: 'commonjs', + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-floating-promises': 'warn', + '@typescript-eslint/no-unsafe-argument': 'warn' + '@typescript-eslint/no-unsafe-return': 'off', + }, }, - }, ); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e2ffad7..1a348fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,12 +14,24 @@ "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.0", + "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", + "@nestjs/swagger": "^11.2.0", "@nestjs/typeorm": "^11.0.0", "@types/bcrypt": "^5.0.2", + "@types/cookie-parser": "^1.4.8", "@types/hapi__joi": "^17.1.15", + "@types/passport-jwt": "^4.0.1", + "@types/passport-local": "^1.0.38", "bcrypt": "^5.1.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", + "cookie-parser": "^1.4.7", "joi": "^17.13.3", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", "pg": "^8.15.6", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" @@ -32,7 +44,7 @@ "@nestjs/testing": "^11.0.1", "@swc/cli": "^0.6.0", "@swc/core": "^1.10.7", - "@types/express": "^5.0.0", + "@types/express": "^5.0.1", "@types/jest": "^29.5.14", "@types/node": "^22.10.7", "@types/supertest": "^6.0.2", @@ -41,7 +53,6 @@ "eslint-plugin-prettier": "^5.4.0", "globals": "^16.0.0", "jest": "^29.7.0", - "prettier": "^3.5.3", "source-map-support": "^0.5.21", "supertest": "^7.0.0", "ts-jest": "^29.2.5", @@ -2011,6 +2022,12 @@ "semver": "bin/semver.js" } }, + "node_modules/@microsoft/tsdoc": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", + "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", + "license": "MIT" + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.1.tgz", @@ -2653,6 +2670,49 @@ } } }, + "node_modules/@nestjs/jwt": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.0.tgz", + "integrity": "sha512-v7YRsW3Xi8HNTsO+jeHSEEqelX37TVWgwt+BcxtkG/OfXJEOs6GZdbdza200d6KqId1pJQZ6UPj1F0M6E+mxaA==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.7", + "jsonwebtoken": "9.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + } + }, + "node_modules/@nestjs/mapped-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz", + "integrity": "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/passport": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz", + "integrity": "sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "passport": "^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, "node_modules/@nestjs/platform-express": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.0.tgz", @@ -2772,6 +2832,39 @@ "tslib": "^2.1.0" } }, + "node_modules/@nestjs/swagger": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.0.tgz", + "integrity": "sha512-5wolt8GmpNcrQv34tIPUtPoV1EeFbCetm40Ij3+M0FNNnf2RJ3FyWfuQvI8SBlcJyfaounYVTKzKHreFXsUyOg==", + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.15.1", + "@nestjs/mapped-types": "2.1.0", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "8.2.0", + "swagger-ui-dist": "5.21.0" + }, + "peerDependencies": { + "@fastify/static": "^8.0.0", + "@nestjs/common": "^11.0.1", + "@nestjs/core": "^11.0.1", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/testing": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.0.tgz", @@ -2914,6 +3007,13 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -3409,7 +3509,6 @@ "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -3420,12 +3519,20 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, + "node_modules/@types/cookie-parser": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz", + "integrity": "sha512-l37JqFrOJ9yQfRQkljb41l0xVphc7kg5JTjjr+pLRZ0IyZ49V4BQ8vbF4Ut2C2e+WH4al3xD3ZwYwIUfnbT4NQ==", + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, "node_modules/@types/cookiejar": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", @@ -3466,7 +3573,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -3478,7 +3584,6 @@ "version": "5.0.6", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -3514,7 +3619,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true, "license": "MIT" }, "node_modules/@types/istanbul-lib-coverage": { @@ -3562,6 +3666,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -3573,7 +3686,6 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, "license": "MIT" }, "node_modules/@types/node": { @@ -3585,6 +3697,46 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-local": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.38.tgz", + "integrity": "sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==", + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, "node_modules/@types/pg": { "version": "8.15.1", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.1.tgz", @@ -3600,21 +3752,18 @@ "version": "6.9.18", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", - "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -3625,7 +3774,6 @@ "version": "1.15.7", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -3664,6 +3812,12 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/validator": { + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.0.tgz", + "integrity": "sha512-nh7nrWhLr6CBq9ldtw0wx+z9wKnnv/uTVLA9g/3/TcOYxbpOSZE+MhKPmWqU+K0NvThjhv12uD8MuqijB0WzEA==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -5013,7 +5167,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/array-timsort": { @@ -5421,6 +5574,12 @@ "node": "*" } }, + "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", @@ -5638,6 +5797,23 @@ "dev": true, "license": "MIT" }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", + "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.11.1", + "validator": "^13.9.0" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -5921,6 +6097,25 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, "node_modules/cookie-signature": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", @@ -6256,6 +6451,15 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "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/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -8831,7 +9035,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -8914,6 +9117,49 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -8968,6 +9214,12 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.12.8", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.8.tgz", + "integrity": "sha512-f1KakiQJa9tdc7w1phC2ST+DyxWimy9c3g3yeF+84QtEanJr2K77wAmBPP22riU05xldniHsvXuflnLZ4oysqA==", + "license": "MIT" + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -9026,6 +9278,42 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -9040,6 +9328,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -9809,6 +10103,53 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", + "dependencies": { + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -9890,6 +10231,11 @@ "node": ">=8" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/peek-readable": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-7.0.0.tgz", @@ -10255,6 +10601,7 @@ "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11462,6 +11809,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-ui-dist": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.21.0.tgz", + "integrity": "sha512-E0K3AB6HvQd8yQNSMR7eE5bk+323AUxjtCz/4ZNKiahOlPhPJxqn3UPIGs00cyY/dhrTDJ61L7C/a8u6zhGrZg==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -12457,6 +12813,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", @@ -12493,6 +12858,15 @@ "node": ">=10.12.0" } }, + "node_modules/validator": { + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz", + "integrity": "sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 910c73d..4afaa7f 100644 --- a/package.json +++ b/package.json @@ -25,12 +25,24 @@ "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.0", + "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", + "@nestjs/swagger": "^11.2.0", "@nestjs/typeorm": "^11.0.0", "@types/bcrypt": "^5.0.2", + "@types/cookie-parser": "^1.4.8", "@types/hapi__joi": "^17.1.15", + "@types/passport-jwt": "^4.0.1", + "@types/passport-local": "^1.0.38", "bcrypt": "^5.1.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", + "cookie-parser": "^1.4.7", "joi": "^17.13.3", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", "pg": "^8.15.6", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" @@ -43,7 +55,7 @@ "@nestjs/testing": "^11.0.1", "@swc/cli": "^0.6.0", "@swc/core": "^1.10.7", - "@types/express": "^5.0.0", + "@types/express": "^5.0.1", "@types/jest": "^29.5.14", "@types/node": "^22.10.7", "@types/supertest": "^6.0.2", @@ -52,7 +64,6 @@ "eslint-plugin-prettier": "^5.4.0", "globals": "^16.0.0", "jest": "^29.7.0", - "prettier": "^3.5.3", "source-map-support": "^0.5.21", "supertest": "^7.0.0", "ts-jest": "^29.2.5", diff --git a/src/app.module.ts b/src/app.module.ts index b280ec3..e5d227d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,28 +3,34 @@ import { AppController } from './app.controller'; import { AppService } from './app.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule, ConfigService } from '@nestjs/config'; +import * as Joi from 'joi'; @Module({ - imports: [ - TypeOrmModule.forRootAsync({ - imports: [ConfigModule], - inject: [ConfigService], - useFactory: (configService: ConfigService) => ({ - type: 'postgres', - host: configService.get('POSTGRES_HOST'), - port: parseInt(configService.get('POSTGRES_PORT', '5432'), 10), - username: configService.get('POSTGRES_USER'), - password: configService.get('POSTGRES_PASSWORD'), - database: configService.get('POSTGRES_DB'), - ssl: { - rejectUnauthorized: false, // Needed for Neon and similar managed DBs - }, - synchronize: true, // Disable in production - }), - }), - ConfigModule.forRoot(), - ], - controllers: [AppController], - providers: [AppService], + imports: [ + TypeOrmModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + type: 'postgres', + host: configService.get('POSTGRES_HOST'), + port: parseInt(configService.get('POSTGRES_PORT', '5432'), 10), + username: configService.get('POSTGRES_USER'), + password: configService.get('POSTGRES_PASSWORD'), + database: configService.get('POSTGRES_DB'), + ssl: { + rejectUnauthorized: false, // Needed for Neon and similar managed DBs + }, + synchronize: true, // Disable in production + }), + }), + ConfigModule.forRoot({ + validationSchema: Joi.object({ + JWT_SECRET: Joi.string().required(), + JWT_EXPIRATION_TIME: Joi.string().required(), + }) + }) + ], + controllers: [AppController], + providers: [AppService], }) -export class AppModule {} +export class AppModule {} \ No newline at end of file diff --git a/src/authentication/authentication.controller.ts b/src/authentication/authentication.controller.ts new file mode 100644 index 0000000..38228da --- /dev/null +++ b/src/authentication/authentication.controller.ts @@ -0,0 +1,87 @@ +import { + Body, + Req, + Controller, + HttpCode, + Post, + UseGuards, + Get, + ClassSerializerInterceptor, + UseInterceptors, +} from '@nestjs/common'; +import {AuthenticationService} from './authentication.service'; +import RegisterDto from './dto/register.dto'; +import RequestWithUser from './requestWithUser.interface'; +import {LocalAuthenticationGuard} from './localAuthentication.guard'; +import JwtAuthenticationGuard from './jwt-authentication.guard'; +// import {EmailConfirmationService} from '../emailConfirmation/emailConfirmation.service'; +import {ApiBody} from '@nestjs/swagger'; +import LogInDto from './dto/logIn.dto'; +import { UsersService } from 'src/users/user.service'; + +@Controller('authentication') +@UseInterceptors(ClassSerializerInterceptor) +export class AuthenticationController { + constructor( + private readonly authenticationService: AuthenticationService, + private readonly usersService: UsersService, + // private readonly emailConfirmationService: EmailConfirmationService, + ) { + } + + @Post('register') + async register(@Body() registrationData: RegisterDto) { + return this.authenticationService.register(registrationData); + } + + + @HttpCode(200) + @UseGuards(LocalAuthenticationGuard) + @Post('log-in') + @ApiBody({type: LogInDto}) + async logIn(@Req() request: RequestWithUser) { + const {user} = request; + const accessTokenCookie = this.authenticationService.getCookieWithJwtAccessToken( + user.id, + ); + const { + cookie: refreshTokenCookie, + token: refreshToken, + } = this.authenticationService.getCookieWithJwtRefreshToken(user.id); + + await this.usersService.setCurrentRefreshToken(refreshToken, user.id); + + request.res.setHeader('Set-Cookie', [ + accessTokenCookie, + refreshTokenCookie, + ]); + + if (user.isTwoFactorAuthenticationEnabled) { + return; + } + + return user; + } + + + @UseGuards(JwtAuthenticationGuard) + @Post('log-out') + @HttpCode(200) + async logOut(@Req() request: RequestWithUser) { + await this.usersService.removeRefreshToken(request.user.id); + request.res.setHeader( + 'Set-Cookie', + this.authenticationService.getCookiesForLogOut(), + ); + } + + + @UseGuards(JwtAuthenticationGuard) + @Get() + authenticate(@Req() request: RequestWithUser) { + const user = request.user; + user.password = undefined; + return user; + } + +} \ No newline at end of file diff --git a/src/authentication/authentication.module.ts b/src/authentication/authentication.module.ts new file mode 100644 index 0000000..0e13d04 --- /dev/null +++ b/src/authentication/authentication.module.ts @@ -0,0 +1,30 @@ +import { Module } from '@nestjs/common'; +import { AuthenticationService } from './authentication.service'; +import { AuthenticationController } from './authentication.controller'; +import { PassportModule } from '@nestjs/passport'; +import { LocalStrategy } from './local.strategy'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { JwtModule } from '@nestjs/jwt'; +import {JwtStrategy} from "./jwt.strategy"; +import {UsersModule} from "../users/user.module"; + +@Module({ + imports: [UsersModule, PassportModule, + + ConfigModule, + JwtModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + secret: configService.get('JWT_SECRET'), + signOptions: { + expiresIn: `${configService.get('JWT_EXPIRATION_TIME')}s`, + }, + }), + }), + + ] as const, + providers: [AuthenticationService, LocalStrategy, JwtStrategy] as const, + controllers: [AuthenticationController] as const, +}) +export class AuthenticationModule {} \ No newline at end of file diff --git a/src/authentication/authentication.service.ts b/src/authentication/authentication.service.ts new file mode 100644 index 0000000..f16a5f5 --- /dev/null +++ b/src/authentication/authentication.service.ts @@ -0,0 +1,121 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import RegisterDto from './dto/register.dto'; +import * as bcrypt from 'bcrypt'; +import { JwtService } from '@nestjs/jwt'; +import { ConfigService } from '@nestjs/config'; +import {UsersService} from "../users/user.service"; +import PostgresErrorCode from 'src/database/postgresErrorCodes.enum'; + +@Injectable() +export class AuthenticationService { + constructor( + private readonly usersService: UsersService, + private readonly jwtService: JwtService, + private readonly configService: ConfigService, + ) {} + + public async register(registrationData: RegisterDto) { + const hashedPassword = await bcrypt.hash(registrationData.password, 10); + try { + const createdUser = await this.usersService.create({ + ...registrationData, + password: hashedPassword, + }); + createdUser.password = undefined; + return createdUser; + } catch (error) { + if (error?.code === PostgresErrorCode.UniqueViolation) { + throw new HttpException( + 'User with that email already exists', + HttpStatus.BAD_REQUEST, + ); + } + throw new HttpException( + 'Something went wrong', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + public getCookieWithJwtAccessToken( + userId: number, + isSecondFactorAuthenticated = false, + ) { + const payload: TokenPayload = { userId, isSecondFactorAuthenticated }; + const token = this.jwtService.sign(payload, { + secret: this.configService.get('JWT_ACCESS_TOKEN_SECRET'), + expiresIn: `${this.configService.get( + 'JWT_ACCESS_TOKEN_EXPIRATION_TIME', + )}s`, + }); + return `Authentication=${token}; HttpOnly; Path=/; Max-Age=${this.configService.get( + 'JWT_ACCESS_TOKEN_EXPIRATION_TIME', + )}`; + } + + public getCookieWithJwtRefreshToken(userId: number) { + const payload: TokenPayload = { userId }; + const token = this.jwtService.sign(payload, { + secret: this.configService.get('JWT_REFRESH_TOKEN_SECRET'), + expiresIn: `${this.configService.get( + 'JWT_REFRESH_TOKEN_EXPIRATION_TIME', + )}s`, + }); + const cookie = `Refresh=${token}; HttpOnly; Path=/; Max-Age=${this.configService.get( + 'JWT_REFRESH_TOKEN_EXPIRATION_TIME', + )}`; + return { + cookie, + token, + }; + } + + public getCookiesForLogOut() { + return [ + 'Authentication=; HttpOnly; Path=/; Max-Age=0', + 'Refresh=; HttpOnly; Path=/; Max-Age=0', + ]; + } + + public async getAuthenticatedUser(email: string, plainTextPassword: string) { + try { + const user = await this.usersService.getByEmail(email); + await this.verifyPassword(plainTextPassword, user.password); + return user; + } catch (error) { + throw new HttpException( + 'Wrong credentials provided', + HttpStatus.BAD_REQUEST, + ); + } + } + + private async verifyPassword( + plainTextPassword: string, + hashedPassword: string, + ) { + const isPasswordMatching = await bcrypt.compare( + plainTextPassword, + hashedPassword, + ); + if (!isPasswordMatching) { + throw new HttpException( + 'Wrong credentials provided', + HttpStatus.BAD_REQUEST, + ); + } + } + + public async getUserFromAuthenticationToken(token: string) { + const payload: TokenPayload = this.jwtService.verify(token, { + secret: this.configService.get('JWT_ACCESS_TOKEN_SECRET'), + }); + if (payload.userId) { + return this.usersService.getById(payload.userId); + } + } + + public getCookieForLogOut() { + return `Authentication=; HttpOnly; Path=/; Max-Age=0`; + } +} \ No newline at end of file diff --git a/src/authentication/dto/login.dto.ts b/src/authentication/dto/login.dto.ts new file mode 100644 index 0000000..b7d026e --- /dev/null +++ b/src/authentication/dto/login.dto.ts @@ -0,0 +1,13 @@ +import { IsEmail, IsString, IsNotEmpty, MinLength } from 'class-validator'; + +export class LogInDto { + @IsEmail() + email: string; + + @IsString() + @IsNotEmpty() + @MinLength(7) + password: string; +} + +export default LogInDto; \ No newline at end of file diff --git a/src/authentication/dto/register.dto.ts b/src/authentication/dto/register.dto.ts new file mode 100644 index 0000000..593fe95 --- /dev/null +++ b/src/authentication/dto/register.dto.ts @@ -0,0 +1,39 @@ +import { + IsEmail, + IsString, + IsNotEmpty, + MinLength, + Matches, +} from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class RegisterDto { + @IsEmail() + email: string; + + @IsString() + @IsNotEmpty() + name: string; + + @ApiProperty({ + deprecated: true, + description: 'Use the name property instead', + }) + fullName: string; + + @IsString() + @IsNotEmpty() + @MinLength(7) + password: string; + + @ApiProperty({ + description: 'Has to match a regular expression: /^\\+[1-9]\\d{1,14}$/', + example: '+123123123123', + }) + @IsString() + @IsNotEmpty() + @Matches(/^\+[1-9]\d{1,14}$/) + phoneNumber: string; +} + +export default RegisterDto; diff --git a/src/authentication/jwt-authentication.guard.ts b/src/authentication/jwt-authentication.guard.ts new file mode 100644 index 0000000..6851cba --- /dev/null +++ b/src/authentication/jwt-authentication.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export default class JwtAuthenticationGuard extends AuthGuard('jwt') {} \ No newline at end of file diff --git a/src/authentication/jwt.strategy.ts b/src/authentication/jwt.strategy.ts new file mode 100644 index 0000000..56be4ec --- /dev/null +++ b/src/authentication/jwt.strategy.ts @@ -0,0 +1,25 @@ +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Request } from 'express'; +import { UsersService } from 'src/users/user.service'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor( + private readonly configService: ConfigService, + private readonly userService: UsersService, + ) { + super({ + jwtFromRequest: ExtractJwt.fromExtractors([(request: Request): string | null => { + return request?.cookies?.Authentication || null; + }]), + secretOrKey: configService.get('JWT_SECRET') + }); + } + + async validate(payload: TokenPayload) { + return this.userService.getById(payload.userId); + } +} \ No newline at end of file diff --git a/src/authentication/local.strategy.ts b/src/authentication/local.strategy.ts new file mode 100644 index 0000000..6ade241 --- /dev/null +++ b/src/authentication/local.strategy.ts @@ -0,0 +1,17 @@ +import { Strategy } from 'passport-local'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable } from '@nestjs/common'; +import { AuthenticationService } from './authentication.service'; +import User from '../users/user.entity'; + +@Injectable() +export class LocalStrategy extends PassportStrategy(Strategy) { + constructor(private authenticationService: AuthenticationService) { + super({ + usernameField: 'email' + }); + } + async validate(email: string, password: string): Promise { + return this.authenticationService.getAuthenticatedUser(email, password); + } +} \ No newline at end of file diff --git a/src/authentication/localAuthentication.guard.ts b/src/authentication/localAuthentication.guard.ts new file mode 100644 index 0000000..e82a06c --- /dev/null +++ b/src/authentication/localAuthentication.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class LocalAuthenticationGuard extends AuthGuard('local') {} \ No newline at end of file diff --git a/src/authentication/requestWithUser.interface.ts b/src/authentication/requestWithUser.interface.ts new file mode 100644 index 0000000..f942e8d --- /dev/null +++ b/src/authentication/requestWithUser.interface.ts @@ -0,0 +1,8 @@ +import { Request } from 'express'; +import User from '../users/user.entity'; + +interface RequestWithUser extends Request { + user: User; +} + +export default RequestWithUser; \ No newline at end of file diff --git a/src/authentication/tokenPayload.interface.ts b/src/authentication/tokenPayload.interface.ts new file mode 100644 index 0000000..a76c614 --- /dev/null +++ b/src/authentication/tokenPayload.interface.ts @@ -0,0 +1,5 @@ +interface TokenPayload { + userId: number; + isSecondFactorAuthenticated?: boolean; + +} diff --git a/src/database/postgresErrorCodes.enum.ts b/src/database/postgresErrorCodes.enum.ts new file mode 100644 index 0000000..3860fc2 --- /dev/null +++ b/src/database/postgresErrorCodes.enum.ts @@ -0,0 +1,4 @@ +enum PostgresErrorCode { + UniqueViolation = '23505', +} +export default PostgresErrorCode; diff --git a/src/main.ts b/src/main.ts index f76bc8d..2a4c51b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,11 @@ -import { NestFactory } from '@nestjs/core'; -import { AppModule } from './app.module'; +import {NestFactory} from '@nestjs/core'; +import {AppModule} from './app.module'; +import * as cookieParser from 'cookie-parser'; async function bootstrap() { - const app = await NestFactory.create(AppModule); - await app.listen(process.env.PORT ?? 3000); + const app = await NestFactory.create(AppModule); + app.use(cookieParser()); + await app.listen(process.env.PORT ?? 3000); } + bootstrap(); diff --git a/src/users/user.entity.ts b/src/users/user.entity.ts index 8f967b5..b99ea5d 100644 --- a/src/users/user.entity.ts +++ b/src/users/user.entity.ts @@ -1,18 +1,29 @@ -import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import {Column, Entity, PrimaryGeneratedColumn} from 'typeorm'; +import {Exclude} from 'class-transformer'; @Entity() class User { - @PrimaryGeneratedColumn() - public id?: number; + @PrimaryGeneratedColumn() + public id?: number; - @Column({ unique: true }) - public email: string; + @Column({unique: true}) + public email: string; - @Column() - public name: string; + @Column() + public name: string; + + @Column() + public password: string; + + @Column({ + nullable: true, + }) + @Exclude() + public currentHashedRefreshToken?: string; + + @Column({default: false}) + public isTwoFactorAuthenticationEnabled: boolean; - @Column() - public password: string; } export default User; diff --git a/src/users/user.service.ts b/src/users/user.service.ts index 5efc07b..580ae4d 100644 --- a/src/users/user.service.ts +++ b/src/users/user.service.ts @@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import User from './user.entity'; import CreateUserDto from './dto/createUser.dto'; +import * as bcrypt from 'bcrypt'; @Injectable() export class UsersService { @@ -22,9 +23,31 @@ export class UsersService { ); } + async getById(id: number) { + const user = await this.usersRepository.findOne({ where: { id } }); + if (user) { + return user; + } + throw new HttpException('User with this id does not exist', HttpStatus.NOT_FOUND); + } + async create(userData: CreateUserDto) { const newUser = this.usersRepository.create(userData); await this.usersRepository.save(newUser); return newUser; } + + async setCurrentRefreshToken(refreshToken: string, userId: number) { + const currentHashedRefreshToken = await bcrypt.hash(refreshToken, 10); + await this.usersRepository.update(userId, { + currentHashedRefreshToken, + }); + } + + async removeRefreshToken(userId: number) { + return this.usersRepository.update(userId, { + currentHashedRefreshToken: null, + }); + } + }