From 178126c16c3f75eaa6ddaa2e862c02571416511e Mon Sep 17 00:00:00 2001 From: Zafar Shaikh Date: Mon, 20 Oct 2025 18:01:20 +0530 Subject: [PATCH 1/8] feat(auth): create login user dto --- apps/backend/src/auth/dto/login-user.dto.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 apps/backend/src/auth/dto/login-user.dto.ts diff --git a/apps/backend/src/auth/dto/login-user.dto.ts b/apps/backend/src/auth/dto/login-user.dto.ts new file mode 100644 index 0000000..aca04e5 --- /dev/null +++ b/apps/backend/src/auth/dto/login-user.dto.ts @@ -0,0 +1,11 @@ +import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; + +export class LoginUserDto { + @IsNotEmpty() + @IsEmail() + email: string; + + @IsNotEmpty() + @IsString() + password: string; +} From e22ff9a2d7110a033b89eab77371b7b6ec6b3fd2 Mon Sep 17 00:00:00 2001 From: Zafar Shaikh Date: Mon, 20 Oct 2025 18:50:23 +0530 Subject: [PATCH 2/8] feat(auth): install and configure jwt module --- apps/backend/package.json | 3 + apps/backend/src/auth/auth.module.ts | 20 ++- package-lock.json | 193 +++++++++++++++++++++++++-- 3 files changed, 197 insertions(+), 19 deletions(-) diff --git a/apps/backend/package.json b/apps/backend/package.json index 9ae902c..8ef08f3 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -23,11 +23,14 @@ "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.1", "@nestjs/platform-express": "^11.0.1", "@nestjs/typeorm": "^11.0.0", + "@types/passport-jwt": "^4.0.1", "bcrypt": "^6.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", + "passport-jwt": "^4.0.1", "pg": "^8.16.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", diff --git a/apps/backend/src/auth/auth.module.ts b/apps/backend/src/auth/auth.module.ts index 908e053..a41389b 100644 --- a/apps/backend/src/auth/auth.module.ts +++ b/apps/backend/src/auth/auth.module.ts @@ -1,11 +1,21 @@ import { Module } from '@nestjs/common'; -import { AuthController } from './auth.controller'; -import { AuthService } from './auth.service'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { User } from '@/users/user.entity'; +import { AuthController } from '@/auth/auth.controller'; +import { AuthService } from '@/auth/auth.service'; +import { UsersModule } from '@/users/users.module'; +import { JwtModule } from '@nestjs/jwt'; +import { ConfigService } from '@nestjs/config'; @Module({ - imports: [TypeOrmModule.forFeature([User])], + imports: [ + UsersModule, + JwtModule.registerAsync({ + useFactory: (configService: ConfigService) => ({ + global: true, + secret: configService.get('JWT_SECRET'), + signOptions: { expiresIn: '60m' }, + }), + }), + ], controllers: [AuthController], providers: [AuthService], }) diff --git a/package-lock.json b/package-lock.json index b7077b9..09d252d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,11 +21,14 @@ "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.1", "@nestjs/platform-express": "^11.0.1", "@nestjs/typeorm": "^11.0.0", + "@types/passport-jwt": "^4.0.1", "bcrypt": "^6.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", + "passport-jwt": "^4.0.1", "pg": "^8.16.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -5105,6 +5108,19 @@ } } }, + "node_modules/@nestjs/jwt": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.1.tgz", + "integrity": "sha512-HXSsc7SAnCnjA98TsZqrE7trGtHDnYXWp4Ffy6LwSmck1QvbGYdMzBquXofX5l6tIRpeY4Qidl2Ti2CVG77Pdw==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.10", + "jsonwebtoken": "9.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + } + }, "node_modules/@nestjs/platform-express": { "version": "11.1.6", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.6.tgz", @@ -6921,7 +6937,6 @@ "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -6932,7 +6947,6 @@ "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": "*" @@ -6988,7 +7002,6 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", - "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -7000,7 +7013,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -7013,7 +7025,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, "license": "MIT" }, "node_modules/@types/istanbul-lib-coverage": { @@ -7075,6 +7086,16 @@ "dev": true, "license": "MIT" }, + "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/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -7086,19 +7107,52 @@ "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/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": "22.18.8", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.8.tgz", "integrity": "sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw==", - "devOptional": true, "license": "MIT", "dependencies": { "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-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/pegjs": { "version": "0.10.6", "resolved": "https://registry.npmjs.org/@types/pegjs/-/pegjs-0.10.6.tgz", @@ -7110,21 +7164,18 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "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": "1.2.0", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz", "integrity": "sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -7134,7 +7185,6 @@ "version": "1.15.9", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.9.tgz", "integrity": "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==", - "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -7146,7 +7196,6 @@ "version": "0.17.5", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", - "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -9076,6 +9125,12 @@ "ieee754": "^1.1.13" } }, + "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", @@ -10353,6 +10408,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", @@ -14103,6 +14167,28 @@ ], "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jsox": { "version": "1.2.123", "resolved": "https://registry.npmjs.org/jsox/-/jsox-1.2.123.tgz", @@ -14113,6 +14199,27 @@ "jsox": "lib/cli.js" } }, + "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/karma": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", @@ -14799,6 +14906,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", @@ -14813,6 +14956,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", @@ -16517,6 +16666,24 @@ "node": ">= 0.8" } }, + "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-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", @@ -17647,7 +17814,6 @@ "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -19849,7 +20015,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, "license": "MIT" }, "node_modules/unique-filename": { From 92ad1d800b1fa14123e7c1fff25d30826f29245e Mon Sep 17 00:00:00 2001 From: Zafar Shaikh Date: Mon, 20 Oct 2025 19:52:39 +0530 Subject: [PATCH 3/8] fix(backend): update jwt configuration to import config module and inject config service --- apps/backend/src/auth/auth.module.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/auth/auth.module.ts b/apps/backend/src/auth/auth.module.ts index a41389b..19cb140 100644 --- a/apps/backend/src/auth/auth.module.ts +++ b/apps/backend/src/auth/auth.module.ts @@ -3,17 +3,19 @@ import { AuthController } from '@/auth/auth.controller'; import { AuthService } from '@/auth/auth.service'; import { UsersModule } from '@/users/users.module'; import { JwtModule } from '@nestjs/jwt'; -import { ConfigService } from '@nestjs/config'; +import { ConfigModule, ConfigService } from '@nestjs/config'; @Module({ imports: [ UsersModule, JwtModule.registerAsync({ + imports: [ConfigModule], useFactory: (configService: ConfigService) => ({ global: true, secret: configService.get('JWT_SECRET'), signOptions: { expiresIn: '60m' }, }), + inject: [ConfigService], }), ], controllers: [AuthController], From 9605720748ca95fb1c8f426184a9bb33983f9b75 Mon Sep 17 00:00:00 2001 From: Zafar Shaikh Date: Mon, 20 Oct 2025 19:54:09 +0530 Subject: [PATCH 4/8] feat(auth): implement user login endpoint with JWT --- apps/backend/src/auth/auth.controller.ts | 11 ++++- apps/backend/src/auth/auth.service.ts | 44 ++++++++++++++++++- apps/backend/src/auth/dto/access-token.dto.ts | 3 ++ apps/backend/src/users/users.module.ts | 3 ++ apps/backend/src/users/users.service.ts | 23 +++++++--- 5 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 apps/backend/src/auth/dto/access-token.dto.ts diff --git a/apps/backend/src/auth/auth.controller.ts b/apps/backend/src/auth/auth.controller.ts index 46bd950..0725674 100644 --- a/apps/backend/src/auth/auth.controller.ts +++ b/apps/backend/src/auth/auth.controller.ts @@ -2,13 +2,22 @@ import { Body, Controller, Post } from '@nestjs/common'; import { RegisterUserDto } from '@/auth/dto/register-user.dto'; import { AuthService } from '@/auth/auth.service'; import { UserResponseDto } from '@/users/user-response.dto'; +import { LoginUserDto } from './dto/login-user.dto'; +import { AccessTokenDto } from './dto/access-token.dto'; @Controller('auth') export class AuthController { constructor(private authService: AuthService) {} @Post('register') - register(@Body() registerUserDto: RegisterUserDto): Promise { + async register( + @Body() registerUserDto: RegisterUserDto, + ): Promise { return this.authService.register(registerUserDto); } + + @Post('login') + async login(@Body() loginUserDto: LoginUserDto): Promise { + return this.authService.login(loginUserDto); + } } diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts index ea0d851..01c3cfc 100644 --- a/apps/backend/src/auth/auth.service.ts +++ b/apps/backend/src/auth/auth.service.ts @@ -1,16 +1,26 @@ -import { ConflictException, Injectable } from '@nestjs/common'; +import { + ConflictException, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; import { RegisterUserDto } from '@/auth/dto/register-user.dto'; import * as bcrypt from 'bcrypt'; import { DataSource } from 'typeorm'; import { User } from '@/users/user.entity'; import { UserResponseDto } from '@/users/user-response.dto'; import { ConfigService } from '@nestjs/config'; +import { UsersService } from '@/users/users.service'; +import { JwtService } from '@nestjs/jwt'; +import { LoginUserDto } from '@/auth/dto/login-user.dto'; +import { AccessTokenDto } from './dto/access-token.dto'; @Injectable() export class AuthService { constructor( private dataSource: DataSource, private configService: ConfigService, + private usersService: UsersService, + private jwtService: JwtService, ) {} async register(registerUserDto: RegisterUserDto): Promise { const { name, email, password } = registerUserDto; @@ -62,4 +72,36 @@ export class AuthService { await queryRunner.release(); } } + + async login(loginUserDto: LoginUserDto): Promise { + const user = await this.usersService.findOneByEmail(loginUserDto.email); + + if (!user) { + throw new UnauthorizedException({ + message: 'Invalid credentials.', + }); + } + + const isPasswordCorrect = await bcrypt.compare( + loginUserDto.password, + user.password, + ); + + if (!isPasswordCorrect) { + throw new UnauthorizedException({ + message: 'Invalid credentials.', + }); + } + + const payload = { + sub: user.id, + email: user.email, + }; + + const accessToken = this.jwtService.sign(payload); + + return { + access_token: accessToken, + }; + } } diff --git a/apps/backend/src/auth/dto/access-token.dto.ts b/apps/backend/src/auth/dto/access-token.dto.ts new file mode 100644 index 0000000..3f9d045 --- /dev/null +++ b/apps/backend/src/auth/dto/access-token.dto.ts @@ -0,0 +1,3 @@ +export class AccessTokenDto { + access_token: string; +} diff --git a/apps/backend/src/users/users.module.ts b/apps/backend/src/users/users.module.ts index c75728c..7b13af6 100644 --- a/apps/backend/src/users/users.module.ts +++ b/apps/backend/src/users/users.module.ts @@ -1,7 +1,10 @@ import { Module } from '@nestjs/common'; import { UsersService } from '@/users/users.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { User } from '@/users/user.entity'; @Module({ + imports: [TypeOrmModule.forFeature([User])], providers: [UsersService], exports: [UsersService], }) diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index c105258..993162f 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -1,9 +1,20 @@ import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { User } from '@/users/user.entity'; +import { Repository } from 'typeorm'; -/** - * @Injectable() - * Service responsible for all user-related business logic and data access. - * This service will be used for operations like finding users, updating profiles, etc. - */ @Injectable() -export class UsersService {} +export class UsersService { + constructor( + @InjectRepository(User) + private userRespository: Repository, + ) {} + + async findOneByEmail(email: string): Promise { + return this.userRespository + .createQueryBuilder('user') + .where('user.email = :email', { email }) + .addSelect('user.password') + .getOne(); + } +} From 198bf4bf7a686ab4228332789490c6801b98d8b1 Mon Sep 17 00:00:00 2001 From: Zafar Shaikh Date: Tue, 21 Oct 2025 00:22:02 +0530 Subject: [PATCH 5/8] test(auth): add unit tests for user login with jwt --- apps/backend/src/auth/auth.controller.spec.ts | 25 ++++ apps/backend/src/auth/auth.service.spec.ts | 118 +++++++++++++++++- apps/backend/src/users/users.service.spec.ts | 50 +++++++- 3 files changed, 191 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/auth/auth.controller.spec.ts b/apps/backend/src/auth/auth.controller.spec.ts index 2e29a81..626ff2f 100644 --- a/apps/backend/src/auth/auth.controller.spec.ts +++ b/apps/backend/src/auth/auth.controller.spec.ts @@ -3,9 +3,12 @@ import { AuthController } from '@/auth/auth.controller'; import { AuthService } from '@/auth/auth.service'; import { RegisterUserDto } from '@/auth/dto/register-user.dto'; import { UserResponseDto } from '@/users/user-response.dto'; +import { LoginUserDto } from './dto/login-user.dto'; +import { AccessTokenDto } from './dto/access-token.dto'; const mockAuthService = { register: jest.fn(), + login: jest.fn(), }; describe('AuthController', () => { @@ -59,4 +62,26 @@ describe('AuthController', () => { expect(actualUserResponseDto).toEqual(mockUserResponseDto); }); }); + + describe('login', () => { + it('should call the login method of the Auth Service', async () => { + // Arrange + const mockLoginUserDto: LoginUserDto = { + email: 'test.user@test.com', + password: 'PlainTextPassword', + }; + const expectedResponse: AccessTokenDto = { + access_token: 'SomeReallyLongAccessTokenText', + }; + + (authService.login as jest.Mock).mockResolvedValue(expectedResponse); + + // Act + const actualResponse = await controller.login(mockLoginUserDto); + + // Assert + expect(authService.login).toHaveBeenCalledWith(mockLoginUserDto); + expect(actualResponse).toEqual(expectedResponse); + }); + }); }); diff --git a/apps/backend/src/auth/auth.service.spec.ts b/apps/backend/src/auth/auth.service.spec.ts index d792295..727c89e 100644 --- a/apps/backend/src/auth/auth.service.spec.ts +++ b/apps/backend/src/auth/auth.service.spec.ts @@ -4,11 +4,16 @@ import { DataSource } from 'typeorm'; import { RegisterUserDto } from '@/auth/dto/register-user.dto'; import { User } from '@/users/user.entity'; import * as bcrypt from 'bcrypt'; -import { ConflictException } from '@nestjs/common'; +import { ConflictException, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { UsersService } from '@/users/users.service'; +import { JwtService } from '@nestjs/jwt'; +import { LoginUserDto } from '@/auth/dto/login-user.dto'; +import { AccessTokenDto } from '@/auth/dto/access-token.dto'; jest.mock('bcrypt', () => ({ hash: jest.fn(), + compare: jest.fn(), })); const mockQueryRunner = { @@ -30,10 +35,20 @@ const mockConfigService = { get: jest.fn(), }; +const mockUsersService = { + findOneByEmail: jest.fn(), +}; + +const mockJwtService = { + sign: jest.fn(), +}; + describe('AuthService', () => { let service: AuthService; let dataSource: DataSource; let configService: ConfigService; + let usersService: UsersService; + let jwtService: JwtService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -47,12 +62,22 @@ describe('AuthService', () => { provide: ConfigService, useValue: mockConfigService, }, + { + provide: UsersService, + useValue: mockUsersService, + }, + { + provide: JwtService, + useValue: mockJwtService, + }, ], }).compile(); service = module.get(AuthService); dataSource = module.get(DataSource); configService = module.get(ConfigService); + usersService = module.get(UsersService); + jwtService = module.get(JwtService); jest.clearAllMocks(); }); @@ -257,4 +282,95 @@ describe('AuthService', () => { ); }); }); + + describe('login', () => { + it('should return the access token when credentials are valid', async () => { + // Arrange + const mockLoginUserDto: LoginUserDto = { + email: 'test.user@test.com', + password: 'PlainTextPassword', + }; + + const testUser = new User(); + testUser.id = 1; + testUser.email = 'test.user@test.com'; + testUser.password = 'SomeRandomHashedPassword'; + + const expectedPayload = { + sub: testUser.id, + email: testUser.email, + }; + const expectedAccessToken = 'SomeReallyLongAccessTokenText'; + const expectedResponse: AccessTokenDto = { + access_token: expectedAccessToken, + }; + + (usersService.findOneByEmail as jest.Mock).mockResolvedValue(testUser); + (bcrypt.compare as jest.Mock).mockResolvedValue(true); + (jwtService.sign as jest.Mock).mockReturnValue(expectedAccessToken); + + // Act + const response = await service.login(mockLoginUserDto); + + // Assert + expect(usersService.findOneByEmail).toHaveBeenCalledWith( + mockLoginUserDto.email, + ); + expect(bcrypt.compare).toHaveBeenCalledWith( + mockLoginUserDto.password, + testUser.password, + ); + expect(jwtService.sign).toHaveBeenCalledWith(expectedPayload); + expect(response).toEqual(expectedResponse); + }); + + it('should throw Unauthorized exception when user does not exists', async () => { + // Arrange + const mockLoginUserDto: LoginUserDto = { + email: 'nouser@test.com', + password: 'PlainTextPassword', + }; + + (usersService.findOneByEmail as jest.Mock).mockResolvedValue(null); + + // Act & Assert + await expect(service.login(mockLoginUserDto)).rejects.toBeInstanceOf( + UnauthorizedException, + ); + expect(usersService.findOneByEmail).toHaveBeenCalledWith( + mockLoginUserDto.email, + ); + expect(bcrypt.compare).not.toHaveBeenCalled(); + expect(jwtService.sign).not.toHaveBeenCalled(); + }); + + it('should throw Unauthorized exception when passwords do not match', async () => { + // Arrange + const mockLoginUserDto: LoginUserDto = { + email: 'test.user@test.com', + password: 'PlainTextPassword', + }; + + const testUser = new User(); + testUser.id = 1; + testUser.email = 'test.user@test.com'; + testUser.password = 'SomeRandomHashedPassword'; + + (usersService.findOneByEmail as jest.Mock).mockResolvedValue(testUser); + (bcrypt.compare as jest.Mock).mockResolvedValue(false); + + // Act & Assert + await expect(service.login(mockLoginUserDto)).rejects.toBeInstanceOf( + UnauthorizedException, + ); + expect(usersService.findOneByEmail).toHaveBeenCalledWith( + mockLoginUserDto.email, + ); + expect(bcrypt.compare).toHaveBeenCalledWith( + mockLoginUserDto.password, + testUser.password, + ); + expect(jwtService.sign).not.toHaveBeenCalled(); + }); + }); }); diff --git a/apps/backend/src/users/users.service.spec.ts b/apps/backend/src/users/users.service.spec.ts index 74933c6..ce759f9 100644 --- a/apps/backend/src/users/users.service.spec.ts +++ b/apps/backend/src/users/users.service.spec.ts @@ -1,18 +1,66 @@ import { Test, TestingModule } from '@nestjs/testing'; import { UsersService } from '@/users/users.service'; +import { Repository } from 'typeorm'; +import { User } from '@/users/user.entity'; +import { getRepositoryToken } from '@nestjs/typeorm'; + +const mockQueryBuilder = { + where: jest.fn().mockReturnThis(), + addSelect: jest.fn().mockReturnThis(), + getOne: jest.fn(), +}; + +const mockUserRepository = { + createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder), +}; describe('UsersService', () => { let service: UsersService; + let userRespository: Repository; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [UsersService], + providers: [ + UsersService, + { + provide: getRepositoryToken(User), + useValue: mockUserRepository, + }, + ], }).compile(); service = module.get(UsersService); + userRespository = module.get>(getRepositoryToken(User)); + + jest.clearAllMocks(); }); it('should be defined', () => { expect(service).toBeDefined(); }); + + describe('findOneByEmail', () => { + it('should call the query builder to find a user by email', async () => { + // Arrange + const mockEmail = 'test.user@test.com'; + const expectedUser = new User(); + + ( + userRespository.createQueryBuilder().getOne as jest.Mock + ).mockReturnValue(expectedUser); + + // Act + const user = await service.findOneByEmail(mockEmail); + + // Assert + expect(userRespository.createQueryBuilder).toHaveBeenCalledWith('user'); + expect(mockQueryBuilder.where).toHaveBeenCalledWith( + 'user.email = :email', + { email: mockEmail }, + ); + expect(mockQueryBuilder.addSelect).toHaveBeenCalledWith('user.password'); + expect(mockQueryBuilder.getOne).toHaveBeenCalled(); + expect(user).toEqual(expectedUser); + }); + }); }); From 3bc4e0057cb0c6eb8a9494cd1e462815aa1fe036 Mon Sep 17 00:00:00 2001 From: Zafar Shaikh Date: Sun, 26 Oct 2025 16:12:59 +0530 Subject: [PATCH 6/8] fix(backend): apply all suggestions from automated code review --- apps/backend/package.json | 2 +- apps/backend/src/auth/auth.controller.spec.ts | 60 +++++++++++++++++++ apps/backend/src/auth/auth.controller.ts | 4 +- apps/backend/src/auth/auth.module.ts | 6 +- apps/backend/src/auth/auth.service.spec.ts | 10 ++-- apps/backend/src/auth/auth.service.ts | 7 ++- apps/backend/src/auth/dto/access-token.dto.ts | 2 +- apps/backend/src/auth/dto/login-user.dto.ts | 4 +- apps/backend/src/users/users.service.spec.ts | 12 ++-- apps/backend/src/users/users.service.ts | 4 +- package-lock.json | 16 ++++- 11 files changed, 103 insertions(+), 24 deletions(-) diff --git a/apps/backend/package.json b/apps/backend/package.json index 8ef08f3..f23a312 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -26,7 +26,6 @@ "@nestjs/jwt": "^11.0.1", "@nestjs/platform-express": "^11.0.1", "@nestjs/typeorm": "^11.0.0", - "@types/passport-jwt": "^4.0.1", "bcrypt": "^6.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", @@ -46,6 +45,7 @@ "@types/express": "^5.0.0", "@types/jest": "^30.0.0", "@types/node": "^22.10.7", + "@types/passport-jwt": "^4.0.1", "@types/supertest": "^6.0.2", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", diff --git a/apps/backend/src/auth/auth.controller.spec.ts b/apps/backend/src/auth/auth.controller.spec.ts index 626ff2f..45e340e 100644 --- a/apps/backend/src/auth/auth.controller.spec.ts +++ b/apps/backend/src/auth/auth.controller.spec.ts @@ -5,6 +5,7 @@ import { RegisterUserDto } from '@/auth/dto/register-user.dto'; import { UserResponseDto } from '@/users/user-response.dto'; import { LoginUserDto } from './dto/login-user.dto'; import { AccessTokenDto } from './dto/access-token.dto'; +import { ConflictException, UnauthorizedException } from '@nestjs/common'; const mockAuthService = { register: jest.fn(), @@ -61,6 +62,45 @@ describe('AuthController', () => { expect(authService.register).toHaveBeenCalledWith(mockRegisterUserDto); expect(actualUserResponseDto).toEqual(mockUserResponseDto); }); + + it('should propagate ConflictException from the Auth Service', async () => { + // Arrange + const mockRegisterUserDto: RegisterUserDto = { + name: 'Test User', + email: 'test.user@test.com', + password: 'StrongPassword@123', + }; + + (authService.register as jest.Mock).mockRejectedValue( + new ConflictException('Unable to complete registration at this time.'), + ); + + // Act & Assert + await expect(controller.register(mockRegisterUserDto)).rejects.toThrow( + ConflictException, + ); + await expect(controller.register(mockRegisterUserDto)).rejects.toThrow( + 'Unable to complete registration at this time.', + ); + }); + + it('should propagate any general error from the Auth Service', async () => { + // Arrange + const mockRegisterUserDto: RegisterUserDto = { + name: 'Test User', + email: 'test.user@test.com', + password: 'StrongPassword@123', + }; + + (authService.register as jest.Mock).mockRejectedValue( + new Error('Something went wrong.'), + ); + + // Act & Assert + await expect(controller.register(mockRegisterUserDto)).rejects.toThrow( + 'Something went wrong.', + ); + }); }); describe('login', () => { @@ -83,5 +123,25 @@ describe('AuthController', () => { expect(authService.login).toHaveBeenCalledWith(mockLoginUserDto); expect(actualResponse).toEqual(expectedResponse); }); + + it('should propagate UnauthorizedException from the Auth Service', async () => { + // Arrange + const mockLoginUserDto: LoginUserDto = { + email: 'test.user@test.com', + password: 'PlainTextPassword', + }; + + (authService.login as jest.Mock).mockRejectedValue( + new UnauthorizedException('Invalid credentials.'), + ); + + // Act & Assert + await expect(controller.login(mockLoginUserDto)).rejects.toThrow( + UnauthorizedException, + ); + await expect(controller.login(mockLoginUserDto)).rejects.toThrow( + 'Invalid credentials.', + ); + }); }); }); diff --git a/apps/backend/src/auth/auth.controller.ts b/apps/backend/src/auth/auth.controller.ts index 0725674..7726eda 100644 --- a/apps/backend/src/auth/auth.controller.ts +++ b/apps/backend/src/auth/auth.controller.ts @@ -2,8 +2,8 @@ import { Body, Controller, Post } from '@nestjs/common'; import { RegisterUserDto } from '@/auth/dto/register-user.dto'; import { AuthService } from '@/auth/auth.service'; import { UserResponseDto } from '@/users/user-response.dto'; -import { LoginUserDto } from './dto/login-user.dto'; -import { AccessTokenDto } from './dto/access-token.dto'; +import { LoginUserDto } from '@/auth/dto/login-user.dto'; +import { AccessTokenDto } from '@/auth/dto/access-token.dto'; @Controller('auth') export class AuthController { diff --git a/apps/backend/src/auth/auth.module.ts b/apps/backend/src/auth/auth.module.ts index 19cb140..01788fd 100644 --- a/apps/backend/src/auth/auth.module.ts +++ b/apps/backend/src/auth/auth.module.ts @@ -12,8 +12,10 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; imports: [ConfigModule], useFactory: (configService: ConfigService) => ({ global: true, - secret: configService.get('JWT_SECRET'), - signOptions: { expiresIn: '60m' }, + secret: configService.getOrThrow('JWT_SECRET'), + signOptions: { + expiresIn: '60m', + }, }), inject: [ConfigService], }), diff --git a/apps/backend/src/auth/auth.service.spec.ts b/apps/backend/src/auth/auth.service.spec.ts index 727c89e..8f88677 100644 --- a/apps/backend/src/auth/auth.service.spec.ts +++ b/apps/backend/src/auth/auth.service.spec.ts @@ -40,7 +40,7 @@ const mockUsersService = { }; const mockJwtService = { - sign: jest.fn(), + signAsync: jest.fn(), }; describe('AuthService', () => { @@ -307,7 +307,7 @@ describe('AuthService', () => { (usersService.findOneByEmail as jest.Mock).mockResolvedValue(testUser); (bcrypt.compare as jest.Mock).mockResolvedValue(true); - (jwtService.sign as jest.Mock).mockReturnValue(expectedAccessToken); + (jwtService.signAsync as jest.Mock).mockReturnValue(expectedAccessToken); // Act const response = await service.login(mockLoginUserDto); @@ -320,7 +320,7 @@ describe('AuthService', () => { mockLoginUserDto.password, testUser.password, ); - expect(jwtService.sign).toHaveBeenCalledWith(expectedPayload); + expect(jwtService.signAsync).toHaveBeenCalledWith(expectedPayload); expect(response).toEqual(expectedResponse); }); @@ -341,7 +341,7 @@ describe('AuthService', () => { mockLoginUserDto.email, ); expect(bcrypt.compare).not.toHaveBeenCalled(); - expect(jwtService.sign).not.toHaveBeenCalled(); + expect(jwtService.signAsync).not.toHaveBeenCalled(); }); it('should throw Unauthorized exception when passwords do not match', async () => { @@ -370,7 +370,7 @@ describe('AuthService', () => { mockLoginUserDto.password, testUser.password, ); - expect(jwtService.sign).not.toHaveBeenCalled(); + expect(jwtService.signAsync).not.toHaveBeenCalled(); }); }); }); diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts index 01c3cfc..888e11d 100644 --- a/apps/backend/src/auth/auth.service.ts +++ b/apps/backend/src/auth/auth.service.ts @@ -12,7 +12,7 @@ import { ConfigService } from '@nestjs/config'; import { UsersService } from '@/users/users.service'; import { JwtService } from '@nestjs/jwt'; import { LoginUserDto } from '@/auth/dto/login-user.dto'; -import { AccessTokenDto } from './dto/access-token.dto'; +import { AccessTokenDto } from '@/auth/dto/access-token.dto'; @Injectable() export class AuthService { @@ -74,7 +74,8 @@ export class AuthService { } async login(loginUserDto: LoginUserDto): Promise { - const user = await this.usersService.findOneByEmail(loginUserDto.email); + const lowerCaseEmail = loginUserDto.email.toLowerCase().trim(); + const user = await this.usersService.findOneByEmail(lowerCaseEmail); if (!user) { throw new UnauthorizedException({ @@ -98,7 +99,7 @@ export class AuthService { email: user.email, }; - const accessToken = this.jwtService.sign(payload); + const accessToken = await this.jwtService.signAsync(payload); return { access_token: accessToken, diff --git a/apps/backend/src/auth/dto/access-token.dto.ts b/apps/backend/src/auth/dto/access-token.dto.ts index 3f9d045..3668c60 100644 --- a/apps/backend/src/auth/dto/access-token.dto.ts +++ b/apps/backend/src/auth/dto/access-token.dto.ts @@ -1,3 +1,3 @@ export class AccessTokenDto { - access_token: string; + readonly access_token: string; } diff --git a/apps/backend/src/auth/dto/login-user.dto.ts b/apps/backend/src/auth/dto/login-user.dto.ts index aca04e5..690dcb4 100644 --- a/apps/backend/src/auth/dto/login-user.dto.ts +++ b/apps/backend/src/auth/dto/login-user.dto.ts @@ -1,11 +1,13 @@ -import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; +import { IsEmail, IsNotEmpty, IsString, MaxLength } from 'class-validator'; export class LoginUserDto { @IsNotEmpty() @IsEmail() + @MaxLength(255) email: string; @IsNotEmpty() @IsString() + @MaxLength(100) password: string; } diff --git a/apps/backend/src/users/users.service.spec.ts b/apps/backend/src/users/users.service.spec.ts index ce759f9..df9d9b5 100644 --- a/apps/backend/src/users/users.service.spec.ts +++ b/apps/backend/src/users/users.service.spec.ts @@ -16,7 +16,7 @@ const mockUserRepository = { describe('UsersService', () => { let service: UsersService; - let userRespository: Repository; + let userRepository: Repository; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -30,7 +30,7 @@ describe('UsersService', () => { }).compile(); service = module.get(UsersService); - userRespository = module.get>(getRepositoryToken(User)); + userRepository = module.get>(getRepositoryToken(User)); jest.clearAllMocks(); }); @@ -45,15 +45,15 @@ describe('UsersService', () => { const mockEmail = 'test.user@test.com'; const expectedUser = new User(); - ( - userRespository.createQueryBuilder().getOne as jest.Mock - ).mockReturnValue(expectedUser); + (userRepository.createQueryBuilder().getOne as jest.Mock).mockReturnValue( + expectedUser, + ); // Act const user = await service.findOneByEmail(mockEmail); // Assert - expect(userRespository.createQueryBuilder).toHaveBeenCalledWith('user'); + expect(userRepository.createQueryBuilder).toHaveBeenCalledWith('user'); expect(mockQueryBuilder.where).toHaveBeenCalledWith( 'user.email = :email', { email: mockEmail }, diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index 993162f..2435615 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -7,11 +7,11 @@ import { Repository } from 'typeorm'; export class UsersService { constructor( @InjectRepository(User) - private userRespository: Repository, + private userRepository: Repository, ) {} async findOneByEmail(email: string): Promise { - return this.userRespository + return this.userRepository .createQueryBuilder('user') .where('user.email = :email', { email }) .addSelect('user.password') diff --git a/package-lock.json b/package-lock.json index 09d252d..79089f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,6 @@ "@nestjs/jwt": "^11.0.1", "@nestjs/platform-express": "^11.0.1", "@nestjs/typeorm": "^11.0.0", - "@types/passport-jwt": "^4.0.1", "bcrypt": "^6.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", @@ -44,6 +43,7 @@ "@types/express": "^5.0.0", "@types/jest": "^30.0.0", "@types/node": "^22.10.7", + "@types/passport-jwt": "^4.0.1", "@types/supertest": "^6.0.2", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", @@ -6937,6 +6937,7 @@ "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -6947,6 +6948,7 @@ "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": "*" @@ -7002,6 +7004,7 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -7013,6 +7016,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -7025,6 +7029,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, "license": "MIT" }, "node_modules/@types/istanbul-lib-coverage": { @@ -7107,6 +7112,7 @@ "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/ms": { @@ -7128,6 +7134,7 @@ "version": "1.0.17", "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, "license": "MIT", "dependencies": { "@types/express": "*" @@ -7137,6 +7144,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/jsonwebtoken": "*", @@ -7147,6 +7155,7 @@ "version": "0.2.38", "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, "license": "MIT", "dependencies": { "@types/express": "*", @@ -7164,18 +7173,21 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "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": "1.2.0", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz", "integrity": "sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -7185,6 +7197,7 @@ "version": "1.15.9", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.9.tgz", "integrity": "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==", + "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -7196,6 +7209,7 @@ "version": "0.17.5", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", From 0cb4404cefab3049fcbb6a1e776e5f6dc5f4abe8 Mon Sep 17 00:00:00 2001 From: Zafar Shaikh Date: Sun, 26 Oct 2025 16:30:26 +0530 Subject: [PATCH 7/8] refactor(test): remove redundant promise calls --- apps/backend/src/auth/auth.controller.spec.ts | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/apps/backend/src/auth/auth.controller.spec.ts b/apps/backend/src/auth/auth.controller.spec.ts index 45e340e..7ad38ca 100644 --- a/apps/backend/src/auth/auth.controller.spec.ts +++ b/apps/backend/src/auth/auth.controller.spec.ts @@ -76,12 +76,12 @@ describe('AuthController', () => { ); // Act & Assert - await expect(controller.register(mockRegisterUserDto)).rejects.toThrow( - ConflictException, - ); - await expect(controller.register(mockRegisterUserDto)).rejects.toThrow( - 'Unable to complete registration at this time.', - ); + await expect( + controller.register(mockRegisterUserDto), + ).rejects.toMatchObject({ + name: 'ConflictException', + message: 'Unable to complete registration at this time.', + }); }); it('should propagate any general error from the Auth Service', async () => { @@ -136,12 +136,10 @@ describe('AuthController', () => { ); // Act & Assert - await expect(controller.login(mockLoginUserDto)).rejects.toThrow( - UnauthorizedException, - ); - await expect(controller.login(mockLoginUserDto)).rejects.toThrow( - 'Invalid credentials.', - ); + await expect(controller.login(mockLoginUserDto)).rejects.toMatchObject({ + name: 'UnauthorizedException', + message: 'Invalid credentials.', + }); }); }); }); From 90f42f728ab9665369acfc160057b56b4798693c Mon Sep 17 00:00:00 2001 From: Zafar Shaikh Date: Sun, 26 Oct 2025 16:36:55 +0530 Subject: [PATCH 8/8] refactor(backend): use absolute imports --- apps/backend/src/auth/auth.controller.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/auth/auth.controller.spec.ts b/apps/backend/src/auth/auth.controller.spec.ts index 7ad38ca..45ff2d5 100644 --- a/apps/backend/src/auth/auth.controller.spec.ts +++ b/apps/backend/src/auth/auth.controller.spec.ts @@ -3,8 +3,8 @@ import { AuthController } from '@/auth/auth.controller'; import { AuthService } from '@/auth/auth.service'; import { RegisterUserDto } from '@/auth/dto/register-user.dto'; import { UserResponseDto } from '@/users/user-response.dto'; -import { LoginUserDto } from './dto/login-user.dto'; -import { AccessTokenDto } from './dto/access-token.dto'; +import { LoginUserDto } from '@/auth/dto/login-user.dto'; +import { AccessTokenDto } from '@/auth/dto/access-token.dto'; import { ConflictException, UnauthorizedException } from '@nestjs/common'; const mockAuthService = {