From 71529f7d24eaa4dc43abe5021f492263122bea92 Mon Sep 17 00:00:00 2001 From: realrafi Date: Thu, 23 Jul 2026 20:48:49 +0330 Subject: [PATCH] feat: added some new functions to the auth service --- package.json | 3 + pnpm-lock.yaml | 151 ++++++++++++++++++ src/auth/DTO/login-credential.dto.ts | 13 ++ src/auth/DTO/verify-otp.dto.ts | 17 ++ src/auth/auth.controller.ts | 30 +++- src/auth/auth.module.ts | 10 +- src/auth/auth.service.ts | 87 ++++++++-- src/common/enums/messages.enum.ts | 12 +- .../interceptors/response.interceptor.ts | 2 +- src/config/config.validation.ts | 8 +- src/users/entities/user.entity.ts | 1 - src/users/users.service.ts | 26 ++- 12 files changed, 343 insertions(+), 17 deletions(-) create mode 100644 src/auth/DTO/login-credential.dto.ts create mode 100644 src/auth/DTO/verify-otp.dto.ts diff --git a/package.json b/package.json index 3ef60c8..b494542 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@nestjs/common": "^11.1.28", "@nestjs/config": "^4.0.4", "@nestjs/core": "^11.1.28", + "@nestjs/jwt": "^11.0.2", "@nestjs/platform-express": "^11.1.28", "@nestjs/swagger": "^11.4.6", "@nestjs/typeorm": "^11.0.3", @@ -32,6 +33,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.15.1", "joi": "^18.2.3", + "passport-jwt": "^4.0.1", "pg": "^8.22.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -47,6 +49,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/pnpm-lock.yaml b/pnpm-lock.yaml index f898572..f87cba0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@nestjs/core': specifier: ^11.1.28 version: 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/jwt': + specifier: ^11.0.2 + version: 11.0.2(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/platform-express': specifier: ^11.1.28 version: 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28) @@ -44,6 +47,9 @@ importers: joi: specifier: ^18.2.3 version: 18.2.3 + passport-jwt: + specifier: ^4.0.1 + version: 4.0.1 pg: specifier: ^8.22.0 version: 8.22.0 @@ -84,6 +90,9 @@ importers: '@types/node': specifier: ^22.10.7 version: 22.20.1 + '@types/passport-jwt': + specifier: ^4.0.1 + version: 4.0.1 '@types/supertest': specifier: ^6.0.2 version: 6.0.3 @@ -755,6 +764,11 @@ packages: '@nestjs/websockets': optional: true + '@nestjs/jwt@11.0.2': + resolution: {integrity: sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/mapped-types@2.1.1': resolution: {integrity: sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==} peerDependencies: @@ -934,12 +948,27 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@22.20.1': resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} + '@types/passport-jwt@4.0.1': + resolution: {integrity: sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==} + + '@types/passport-strategy@0.2.38': + resolution: {integrity: sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==} + + '@types/passport@1.0.17': + resolution: {integrity: sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==} + '@types/qs@6.15.1': resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} @@ -1403,6 +1432,9 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -1653,6 +1685,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -2315,6 +2350,16 @@ packages: jsonfile@6.2.1: resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2348,12 +2393,33 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} @@ -2571,6 +2637,13 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + passport-jwt@4.0.1: + resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==} + + passport-strategy@1.0.0: + resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} + engines: {node: '>= 0.4.0'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -4144,6 +4217,12 @@ snapshots: optionalDependencies: '@nestjs/platform-express': 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28) + '@nestjs/jwt@11.0.2(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))': + dependencies: + '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@types/jsonwebtoken': 9.0.10 + jsonwebtoken: 9.0.3 + '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)': dependencies: '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -4337,12 +4416,33 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 22.20.1 + '@types/methods@1.1.4': {} + '@types/ms@2.1.0': {} + '@types/node@22.20.1': dependencies: undici-types: 6.21.0 + '@types/passport-jwt@4.0.1': + dependencies: + '@types/jsonwebtoken': 9.0.10 + '@types/passport-strategy': 0.2.38 + + '@types/passport-strategy@0.2.38': + dependencies: + '@types/express': 5.0.6 + '@types/passport': 1.0.17 + + '@types/passport@1.0.17': + dependencies: + '@types/express': 5.0.6 + '@types/qs@6.15.1': {} '@types/range-parser@1.2.7': {} @@ -4851,6 +4951,8 @@ snapshots: dependencies: node-int64: 0.4.0 + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -5052,6 +5154,10 @@ snapshots: eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} electron-to-chromium@1.5.393: {} @@ -5932,6 +6038,30 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.8.5 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -5959,10 +6089,24 @@ snapshots: dependencies: p-locate: 5.0.0 + 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.memoize@4.1.2: {} lodash.merge@4.6.2: {} + lodash.once@4.1.1: {} + lodash@4.18.1: {} log-symbols@4.1.0: @@ -6149,6 +6293,13 @@ snapshots: parseurl@1.3.3: {} + passport-jwt@4.0.1: + dependencies: + jsonwebtoken: 9.0.3 + passport-strategy: 1.0.0 + + passport-strategy@1.0.0: {} + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} diff --git a/src/auth/DTO/login-credential.dto.ts b/src/auth/DTO/login-credential.dto.ts new file mode 100644 index 0000000..982d89f --- /dev/null +++ b/src/auth/DTO/login-credential.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsString, Matches } from "class-validator"; + +export class LoginCredentialDto { + @ApiProperty({example: '09123456789'}) + @IsString() + @Matches(/^09\d{9}$/) + phoneNumber: string; + + @ApiProperty({example: 'Mystrongpassword@47'}) + @IsString() + password: string; +} \ No newline at end of file diff --git a/src/auth/DTO/verify-otp.dto.ts b/src/auth/DTO/verify-otp.dto.ts new file mode 100644 index 0000000..838f60c --- /dev/null +++ b/src/auth/DTO/verify-otp.dto.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, Matches, Length } from 'class-validator'; + +export class VerifyOtpDto { + @ApiProperty({ + example: '09123456789', + }) + @Matches(/^09\d{9}$/) + phoneNumber: string; + + @ApiProperty({ + example: '123456', + }) + @IsString() + @Length(6, 6) + code: string; +} \ No newline at end of file diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 59b6bdd..c1b7247 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -1,7 +1,9 @@ import { Body, Controller, Post } from "@nestjs/common"; -import { ApiOperation, ApiTags } from "@nestjs/swagger"; +import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; import { AuthService } from "./auth.service"; import { SendOtpDto } from "./DTO/send-otp.dto"; +import { LoginCredentialDto } from "./DTO/login-credential.dto"; +import { VerifyOtpDto } from "./DTO/verify-otp.dto"; @ApiTags('Authentication') @Controller('auth') @@ -14,9 +16,33 @@ export class AuthController { @ApiOperation({ summary: 'Send OTP to a phone number', }) + @ApiResponse({status: 200, description: 'Sent Otp Successfully!'}) + @ApiResponse({status: 400, description: 'Bad Request from User!'}) async sendOtp( - @Body() dto: SendOtpDto, + @Body() dto: SendOtpDto ) { return this.authService.sendOTP(dto.phoneNumber); } + + @Post('verify-otp') + @ApiOperation({ + summary: 'Verify OTP.' + }) + @ApiResponse({status: 200, description: 'Verified Otp Successfully!'}) + @ApiResponse({status: 400, description: 'Bad Request from the user'}) + async verifyOtp( + @Body() dto: VerifyOtpDto + ) { + return this.authService.verifyOTP(dto); + } + + @Post('check-credential') + @ApiOperation({summary: 'Check Login Credentials. If Valid Sends OTP'}) + @ApiResponse({status: 200, description: 'Valid Credentials!'}) + @ApiResponse({status: 400, description: 'Credentials Invalid!'}) + async checkLoginCredential( + @Body() dto: LoginCredentialDto + ) { + return this.authService.checkLoginCredentials(dto); + } } \ No newline at end of file diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index e22f302..a315c23 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -1,4 +1,5 @@ import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { UtilsModule } from 'src/utils/utils.module'; @@ -6,9 +7,16 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from 'src/users/entities/user.entity'; import { OTP } from './entities/otp.entity'; import { OtpRepository } from './repositories/otp.repository'; +import { UsersModule } from 'src/users/users.module'; +import { ConfigService } from '@nestjs/config'; @Module({ - imports: [TypeOrmModule.forFeature([User, OTP]), UtilsModule], + imports: [ + TypeOrmModule.forFeature([User, OTP]), + JwtModule.register({}), + UtilsModule, + UsersModule, + ], controllers: [AuthController], providers: [AuthService, OtpRepository], }) diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 92693a0..bac0539 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -1,10 +1,16 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import type { ISmsConfigs } from 'src/config/sms.config'; import { SMS_CONFIG } from 'src/utils/constants'; import { SmsService } from 'src/utils/providers/sms.service'; import { OtpRepository } from './repositories/otp.repository'; import { OtpStatus } from './enums/otp-status.enum'; +import { VerifyOtpDto } from './DTO/verify-otp.dto'; +import { OTPMessages, UserMessages } from 'src/common/enums/messages.enum'; +import { LoginCredentialDto } from './DTO/login-credential.dto'; +import { UsersService } from 'src/users/users.service'; +import * as bcrypt from 'bcrypt'; +import { JwtService } from '@nestjs/jwt'; @Injectable() export class AuthService { @@ -13,6 +19,8 @@ export class AuthService { private readonly smsService: SmsService, private readonly configService: ConfigService, private readonly OtpRepository: OtpRepository, + private readonly userService: UsersService, + private readonly jwtService: JwtService ) {} async sendOTP(phone: string) { @@ -35,18 +43,18 @@ export class AuthService { { status: OtpStatus.EXPIRED }, ); + await this.OtpRepository.save( this.OtpRepository.create({ - phoneNumber: phone, - code: otp, - expirationDate: Date.now() + 2 * 60 * 1000, - status: OtpStatus.PENDING, - numberOfTries: 0 - }) + phoneNumber: phone, + code: otp, + expirationDate: new Date(Date.now() + 2 * 60 * 1000), + status: OtpStatus.PENDING, + numberOfTries: 0, + }), ); - return {otp}; - + return { otp }; } catch (error) { if (this.configService.get('NODE_ENV') !== 'production') { return { otp }; @@ -56,7 +64,68 @@ export class AuthService { } } + async verifyOTP(dto: VerifyOtpDto) { + const otp = await this.OtpRepository.findOne({ + where: { phoneNumber: dto.phoneNumber, status: OtpStatus.PENDING }, + order: { + createdAt: 'DESC', + }, + }); + + if (!otp) { + throw new BadRequestException(OTPMessages.OTP_NOT_FOUND); + } + + if (otp.expirationDate < new Date()) { + otp.status = OtpStatus.EXPIRED; + + await this.OtpRepository.save(otp); + + throw new BadRequestException(OTPMessages.OTP_EXPIRED); + } + + if (otp.code !== dto.code) { + otp.numberOfTries++; + + if (otp.numberOfTries >= 3) { + otp.status = OtpStatus.EXPIRED; + + await this.OtpRepository.save(otp); + + throw new BadRequestException(OTPMessages.OTP_TOO_MANY_TRIES); + } + + await this.OtpRepository.save(otp); + + throw new BadRequestException(OTPMessages.OTP_INVALID); + } + + otp.status = OtpStatus.VERIFIED; + + await this.OtpRepository.save(otp); + } + private generateOtp(): string { return Math.floor(100000 + Math.random() * 900000).toString(); } + + async checkLoginCredentials(credential: LoginCredentialDto): Promise { + const { phoneNumber, password } = credential; + + const user = await this.userService.findOneByPhoneOrFail(phoneNumber); + + const isPasswordValid = await bcrypt.compare(password, user.password); + + if (!isPasswordValid) { + throw new BadRequestException(UserMessages.USER_PASSWORD_INVALID); + } + + await this.sendOTP(phoneNumber); + } + + // async VerifyOtpAndLogin(dto: VerifyOtpDto) { + // await this.verifyOTP(dto); + + // const user = await this.userService.findOneByPhoneOrFail(dto.phoneNumber); + // } } diff --git a/src/common/enums/messages.enum.ts b/src/common/enums/messages.enum.ts index 6bc428b..ebe97c3 100644 --- a/src/common/enums/messages.enum.ts +++ b/src/common/enums/messages.enum.ts @@ -1,3 +1,13 @@ export enum UserMessages { - USER_ALREADY_EXISTS = 'کاربر مورد نظر در حال حاضر وجود دارد. لطفا وارد حساب شوید!' + USER_ALREADY_EXISTS = 'کاربر مورد نظر با این ایمیل یا شماره همراه در حال حاضر وجود دارد. لطفا وارد حساب شوید!', + USER_NOT_FOUND = 'کاربر مورد نظر پیدا نشد!', + USER_PASSWORD_INVALID = 'رمز وارد شده اشتباده است!', + USER_VALID_CREDENTIALS = 'اطلاعات وارد شده صحیح میباشد!' +} + +export enum OTPMessages { + OTP_NOT_FOUND = 'این کد پیدا نشد! لطفا دوباره درخواست بدهید!', + OTP_EXPIRED = 'این کد منسوخ شده است لطفا دوباره درخواست بدهید!', + OTP_TOO_MANY_TRIES = 'تعداد تلاش ها بیش از حد مجاز است لطفا دوباره درخواست کد بدهید!', + OTP_INVALID = 'کد وارد شده نا معتبر است!' } \ No newline at end of file diff --git a/src/common/interceptors/response.interceptor.ts b/src/common/interceptors/response.interceptor.ts index ea954b6..67ff91c 100644 --- a/src/common/interceptors/response.interceptor.ts +++ b/src/common/interceptors/response.interceptor.ts @@ -23,7 +23,7 @@ export class ResponseInterceptor implements NestInterceptor< map((data: T) => ({ success: true, statusCode: response.statusCode, - message: 'درخواست با موفقیت پاسخ داده شد!', + message: 'درخواست موفقیت آمیز بود!', timestamp: new Date().toISOString(), data })) diff --git a/src/config/config.validation.ts b/src/config/config.validation.ts index 34596e4..f6f1950 100644 --- a/src/config/config.validation.ts +++ b/src/config/config.validation.ts @@ -15,5 +15,11 @@ export const validationSchema = Joi.object({ SMS_API_KEY: Joi.string().required(), SMS_PATTERN_OTP: Joi.string().required(), - NODE_ENV: Joi.string().required() + NODE_ENV: Joi.string().required(), + + // JWT Config + JWT_ACCESS_SECRET: Joi.string().required(), + JWT_REFRESH_SECRET: Joi.string().required(), + JWT_ACCESS_EXPIRES: Joi.string().required(), + JWT_REFRESH_EXPIRES: Joi.string().required() }); \ No newline at end of file diff --git a/src/users/entities/user.entity.ts b/src/users/entities/user.entity.ts index 9e7189d..b1830b4 100644 --- a/src/users/entities/user.entity.ts +++ b/src/users/entities/user.entity.ts @@ -17,7 +17,6 @@ export class User extends BaseEntity { phone: string; @Column({ - select: false }) @Exclude() password: string; diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 950544c..a224b6b 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -1,4 +1,8 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { UserRepository } from './user.repository'; import { User } from './entities/user.entity'; import * as bcrypt from 'bcrypt'; @@ -9,6 +13,26 @@ import { UserMessages } from 'src/common/enums/messages.enum'; export class UsersService { constructor(private readonly userRepository: UserRepository) {} + async findOneOrFail(id: string): Promise { + const user = await this.userRepository.findOne({ where: { id } }); + + if (!user) { + throw new NotFoundException(UserMessages.USER_NOT_FOUND); + } + + return user; + } + + async findOneByPhoneOrFail(phone: string): Promise { + const user = await this.userRepository.findOne({ where: { phone } }); + + if (!user) { + throw new NotFoundException(UserMessages.USER_NOT_FOUND); + } + + return user; + } + async createUser(userDto: CreateUserDto): Promise { const existingUser = await this.userRepository.findOne({ where: [