feat: added smsService and sendOtp
This commit is contained in:
@@ -20,12 +20,14 @@
|
|||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nestjs/axios": "^4.0.1",
|
||||||
"@nestjs/common": "^11.1.28",
|
"@nestjs/common": "^11.1.28",
|
||||||
"@nestjs/config": "^4.0.4",
|
"@nestjs/config": "^4.0.4",
|
||||||
"@nestjs/core": "^11.1.28",
|
"@nestjs/core": "^11.1.28",
|
||||||
"@nestjs/platform-express": "^11.1.28",
|
"@nestjs/platform-express": "^11.1.28",
|
||||||
"@nestjs/swagger": "^11.4.6",
|
"@nestjs/swagger": "^11.4.6",
|
||||||
"@nestjs/typeorm": "^11.0.3",
|
"@nestjs/typeorm": "^11.0.3",
|
||||||
|
"axios": "^1.18.1",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.15.1",
|
"class-validator": "^0.15.1",
|
||||||
|
|||||||
Generated
+70
@@ -8,6 +8,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@nestjs/axios':
|
||||||
|
specifier: ^4.0.1
|
||||||
|
version: 4.0.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))(axios@1.18.1)(rxjs@7.8.2)
|
||||||
'@nestjs/common':
|
'@nestjs/common':
|
||||||
specifier: ^11.1.28
|
specifier: ^11.1.28
|
||||||
version: 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
version: 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
@@ -26,6 +29,9 @@ importers:
|
|||||||
'@nestjs/typeorm':
|
'@nestjs/typeorm':
|
||||||
specifier: ^11.0.3
|
specifier: ^11.0.3
|
||||||
version: 11.0.3(@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)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@1.1.0(pg@8.22.0)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.9.3)))
|
version: 11.0.3(@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)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@1.1.0(pg@8.22.0)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.9.3)))
|
||||||
|
axios:
|
||||||
|
specifier: ^1.18.1
|
||||||
|
version: 1.18.1
|
||||||
bcrypt:
|
bcrypt:
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
@@ -692,6 +698,13 @@ packages:
|
|||||||
'@emnapi/core': ^1.7.1
|
'@emnapi/core': ^1.7.1
|
||||||
'@emnapi/runtime': ^1.7.1
|
'@emnapi/runtime': ^1.7.1
|
||||||
|
|
||||||
|
'@nestjs/axios@4.0.1':
|
||||||
|
resolution: {integrity: sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==}
|
||||||
|
peerDependencies:
|
||||||
|
'@nestjs/common': ^10.0.0 || ^11.0.0
|
||||||
|
axios: ^1.3.1
|
||||||
|
rxjs: ^7.0.0
|
||||||
|
|
||||||
'@nestjs/cli@11.0.24':
|
'@nestjs/cli@11.0.24':
|
||||||
resolution: {integrity: sha512-aIHxQLSYtXShifA3zwWIeznEsZnNa3Iz2QRykFj+sl9IcbERBHr5nH87FRgywM+He3NxoF5WazHfR8FsmVeWxw==}
|
resolution: {integrity: sha512-aIHxQLSYtXShifA3zwWIeznEsZnNa3Iz2QRykFj+sl9IcbERBHr5nH87FRgywM+He3NxoF5WazHfR8FsmVeWxw==}
|
||||||
engines: {node: '>= 20.11'}
|
engines: {node: '>= 20.11'}
|
||||||
@@ -1214,6 +1227,10 @@ packages:
|
|||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
agent-base@6.0.2:
|
||||||
|
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
||||||
|
engines: {node: '>= 6.0.0'}
|
||||||
|
|
||||||
ajv-formats@2.1.1:
|
ajv-formats@2.1.1:
|
||||||
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
|
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1310,6 +1327,9 @@ packages:
|
|||||||
asynckit@0.4.0:
|
asynckit@0.4.0:
|
||||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
|
|
||||||
|
axios@1.18.1:
|
||||||
|
resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==}
|
||||||
|
|
||||||
babel-jest@30.4.1:
|
babel-jest@30.4.1:
|
||||||
resolution: {integrity: sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==}
|
resolution: {integrity: sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==}
|
||||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||||
@@ -1857,6 +1877,15 @@ packages:
|
|||||||
flatted@3.4.2:
|
flatted@3.4.2:
|
||||||
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
|
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
|
||||||
|
|
||||||
|
follow-redirects@1.16.0:
|
||||||
|
resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
peerDependencies:
|
||||||
|
debug: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
debug:
|
||||||
|
optional: true
|
||||||
|
|
||||||
foreground-child@3.3.1:
|
foreground-child@3.3.1:
|
||||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -1993,6 +2022,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
https-proxy-agent@5.0.1:
|
||||||
|
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
human-signals@2.1.0:
|
human-signals@2.1.0:
|
||||||
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
||||||
engines: {node: '>=10.17.0'}
|
engines: {node: '>=10.17.0'}
|
||||||
@@ -2663,6 +2696,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
|
proxy-from-env@2.1.0:
|
||||||
|
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
punycode@2.3.1:
|
punycode@2.3.1:
|
||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -4029,6 +4066,12 @@ snapshots:
|
|||||||
'@tybys/wasm-util': 0.10.3
|
'@tybys/wasm-util': 0.10.3
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@nestjs/axios@4.0.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))(axios@1.18.1)(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)
|
||||||
|
axios: 1.18.1
|
||||||
|
rxjs: 7.8.2
|
||||||
|
|
||||||
'@nestjs/cli@11.0.24(@types/node@22.20.1)(prettier@3.9.5)':
|
'@nestjs/cli@11.0.24(@types/node@22.20.1)(prettier@3.9.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@angular-devkit/core': 19.2.27(chokidar@4.0.3)
|
'@angular-devkit/core': 19.2.27(chokidar@4.0.3)
|
||||||
@@ -4597,6 +4640,12 @@ snapshots:
|
|||||||
|
|
||||||
acorn@8.17.0: {}
|
acorn@8.17.0: {}
|
||||||
|
|
||||||
|
agent-base@6.0.2:
|
||||||
|
dependencies:
|
||||||
|
debug: 4.4.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
ajv-formats@2.1.1(ajv@8.20.0):
|
ajv-formats@2.1.1(ajv@8.20.0):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
ajv: 8.20.0
|
ajv: 8.20.0
|
||||||
@@ -4678,6 +4727,16 @@ snapshots:
|
|||||||
|
|
||||||
asynckit@0.4.0: {}
|
asynckit@0.4.0: {}
|
||||||
|
|
||||||
|
axios@1.18.1:
|
||||||
|
dependencies:
|
||||||
|
follow-redirects: 1.16.0
|
||||||
|
form-data: 4.0.6
|
||||||
|
https-proxy-agent: 5.0.1
|
||||||
|
proxy-from-env: 2.1.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- debug
|
||||||
|
- supports-color
|
||||||
|
|
||||||
babel-jest@30.4.1(@babel/core@7.29.7):
|
babel-jest@30.4.1(@babel/core@7.29.7):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7
|
'@babel/core': 7.29.7
|
||||||
@@ -5253,6 +5312,8 @@ snapshots:
|
|||||||
|
|
||||||
flatted@3.4.2: {}
|
flatted@3.4.2: {}
|
||||||
|
|
||||||
|
follow-redirects@1.16.0: {}
|
||||||
|
|
||||||
foreground-child@3.3.1:
|
foreground-child@3.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
@@ -5405,6 +5466,13 @@ snapshots:
|
|||||||
statuses: 2.0.2
|
statuses: 2.0.2
|
||||||
toidentifier: 1.0.1
|
toidentifier: 1.0.1
|
||||||
|
|
||||||
|
https-proxy-agent@5.0.1:
|
||||||
|
dependencies:
|
||||||
|
agent-base: 6.0.2
|
||||||
|
debug: 4.4.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
human-signals@2.1.0: {}
|
human-signals@2.1.0: {}
|
||||||
|
|
||||||
iconv-lite@0.7.3:
|
iconv-lite@0.7.3:
|
||||||
@@ -6182,6 +6250,8 @@ snapshots:
|
|||||||
forwarded: 0.2.0
|
forwarded: 0.2.0
|
||||||
ipaddr.js: 1.9.1
|
ipaddr.js: 1.9.1
|
||||||
|
|
||||||
|
proxy-from-env@2.1.0: {}
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
pure-rand@7.0.1: {}
|
pure-rand@7.0.1: {}
|
||||||
|
|||||||
+5
-1
@@ -5,6 +5,8 @@ import Joi from 'joi';
|
|||||||
import { validationSchema } from './config/config.validation';
|
import { validationSchema } from './config/config.validation';
|
||||||
import { UsersModule } from './users/users.module';
|
import { UsersModule } from './users/users.module';
|
||||||
import { databaseConfig } from './config/typeorm.config';
|
import { databaseConfig } from './config/typeorm.config';
|
||||||
|
import { HttpModule } from '@nestjs/axios';
|
||||||
|
import { AuthModule } from './auth/auth.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -13,7 +15,9 @@ import { databaseConfig } from './config/typeorm.config';
|
|||||||
validationSchema
|
validationSchema
|
||||||
}),
|
}),
|
||||||
TypeOrmModule.forRootAsync(databaseConfig()),
|
TypeOrmModule.forRootAsync(databaseConfig()),
|
||||||
UsersModule
|
HttpModule.register({global: true, timeout: 10000, headers: {"Content-Type": "application/json"}}),
|
||||||
|
UsersModule,
|
||||||
|
AuthModule
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Matches } from 'class-validator';
|
||||||
|
|
||||||
|
export class SendOtpDto {
|
||||||
|
@ApiProperty({
|
||||||
|
example: '09123456789',
|
||||||
|
})
|
||||||
|
@Matches(/^09\d{9}$/, {
|
||||||
|
message: 'Phone number must be a valid Iranian mobile number.',
|
||||||
|
})
|
||||||
|
phoneNumber!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Body, Controller, Post } from "@nestjs/common";
|
||||||
|
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
|
import { AuthService } from "./auth.service";
|
||||||
|
import { SendOtpDto } from "./DTO/send-otp.dto";
|
||||||
|
|
||||||
|
@ApiTags('Authentication')
|
||||||
|
@Controller('auth')
|
||||||
|
export class AuthController {
|
||||||
|
constructor(
|
||||||
|
private readonly authService: AuthService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post('send-otp')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Send OTP to a phone number',
|
||||||
|
})
|
||||||
|
async sendOtp(
|
||||||
|
@Body() dto: SendOtpDto,
|
||||||
|
) {
|
||||||
|
return this.authService.sendOTP(dto.phoneNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AuthController } from './auth.controller';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { UtilsModule } from 'src/utils/utils.module';
|
||||||
|
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';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([User, OTP]), UtilsModule],
|
||||||
|
controllers: [AuthController],
|
||||||
|
providers: [AuthService, OtpRepository],
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { 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';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
constructor(
|
||||||
|
@Inject(SMS_CONFIG) private smsConfig: ISmsConfigs,
|
||||||
|
private readonly smsService: SmsService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly OtpRepository: OtpRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async sendOTP(phone: string) {
|
||||||
|
const otp = this.generateOtp();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.smsService.sendSms({
|
||||||
|
Mobile: phone,
|
||||||
|
TemplateId: this.smsConfig.SMS_PATTERN_OTP,
|
||||||
|
Parameters: [
|
||||||
|
{
|
||||||
|
name: 'code',
|
||||||
|
value: otp,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.OtpRepository.update(
|
||||||
|
{ phoneNumber: phone, status: OtpStatus.PENDING },
|
||||||
|
{ 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
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return {otp};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (this.configService.get('NODE_ENV') !== 'production') {
|
||||||
|
return { otp };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateOtp(): string {
|
||||||
|
return Math.floor(100000 + Math.random() * 900000).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Column, Entity } from 'typeorm';
|
||||||
|
import { BaseEntity } from 'src/common/entities/base.entity';
|
||||||
|
import { OtpStatus } from '../enums/otp-status.enum';
|
||||||
|
|
||||||
|
@Entity('otps')
|
||||||
|
export class OTP extends BaseEntity {
|
||||||
|
@Column({ type: 'varchar', length: 6 })
|
||||||
|
code: string;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamptz' })
|
||||||
|
expirationDate: Date;
|
||||||
|
|
||||||
|
@Column({type: 'enum', enum: OtpStatus, default: OtpStatus.PENDING})
|
||||||
|
status: OtpStatus;
|
||||||
|
|
||||||
|
@Column({ type: 'smallint', default: 0 })
|
||||||
|
numberOfTries: number;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
length: 11,
|
||||||
|
})
|
||||||
|
phoneNumber: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export enum OtpStatus {
|
||||||
|
PENDING = 'PENDING',
|
||||||
|
VERIFIED = 'VERIFIED',
|
||||||
|
EXPIRED = 'EXPIRED'
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { OTP } from '../entities/otp.entity';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
export class OtpRepository extends Repository<OTP> {
|
||||||
|
constructor(@InjectRepository(OTP) otpRepository: Repository<OTP>) {
|
||||||
|
super(
|
||||||
|
otpRepository.target,
|
||||||
|
otpRepository.manager,
|
||||||
|
otpRepository.queryRunner,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,17 @@ import * as Joi from 'joi';
|
|||||||
export const validationSchema = Joi.object({
|
export const validationSchema = Joi.object({
|
||||||
APP_PORT: Joi.number().required(),
|
APP_PORT: Joi.number().required(),
|
||||||
|
|
||||||
|
// DB Config
|
||||||
DB_HOST: Joi.string().required(),
|
DB_HOST: Joi.string().required(),
|
||||||
DB_PORT: Joi.number().required(),
|
DB_PORT: Joi.number().required(),
|
||||||
DB_USER: Joi.string().required(),
|
DB_USER: Joi.string().required(),
|
||||||
DB_PASSWORD: Joi.string().required(),
|
DB_PASSWORD: Joi.string().required(),
|
||||||
DB_NAME: Joi.string().required(),
|
DB_NAME: Joi.string().required(),
|
||||||
});
|
|
||||||
|
// SMS Config
|
||||||
|
SMS_API_URL: Joi.string().required(),
|
||||||
|
SMS_API_KEY: Joi.string().required(),
|
||||||
|
SMS_PATTERN_OTP: Joi.string().required(),
|
||||||
|
|
||||||
|
NODE_ENV: Joi.string().required()
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { FactoryProvider } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
|
||||||
|
import { SMS_CONFIG } from "src/utils/constants";
|
||||||
|
|
||||||
|
export const smsConfigsProvider: FactoryProvider<ISmsConfigs> = {
|
||||||
|
provide: SMS_CONFIG,
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory(configService: ConfigService) {
|
||||||
|
return {
|
||||||
|
API_URL: configService.getOrThrow<string>("SMS_API_URL"),
|
||||||
|
API_KEY: configService.getOrThrow<string>("SMS_API_KEY"),
|
||||||
|
SMS_PATTERN_OTP: configService.getOrThrow<string>("SMS_PATTERN_OTP")
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ISmsConfigs {
|
||||||
|
API_URL: string;
|
||||||
|
API_KEY: string;
|
||||||
|
SMS_PATTERN_OTP: string;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
import { User } from "../entities/user.entity";
|
import { User } from "./entities/user.entity";
|
||||||
import { InjectRepository } from "@nestjs/typeorm";
|
import { InjectRepository } from "@nestjs/typeorm";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Body, Controller, Get, Post } from '@nestjs/common';
|
import { Body, Controller, Get, Post } from '@nestjs/common';
|
||||||
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
|
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||||
import { CreateUserDto } from '../DTO/create-user.dto';
|
import { CreateUserDto } from './DTO/create-user.dto';
|
||||||
import { User } from '../entities/user.entity';
|
import { User } from './entities/user.entity';
|
||||||
import { UsersService } from '../providers/users.service';
|
import { UsersService } from './users.service';
|
||||||
|
|
||||||
@Controller('users')
|
@Controller('users')
|
||||||
export class UsersController {
|
export class UsersController {
|
||||||
@@ -16,6 +16,6 @@ export class UsersController {
|
|||||||
@ApiResponse({status: 201, description: 'User created successfully!'})
|
@ApiResponse({status: 201, description: 'User created successfully!'})
|
||||||
@ApiResponse({status: 400, description: 'Bad Request from User!'})
|
@ApiResponse({status: 400, description: 'Bad Request from User!'})
|
||||||
signupUser(@Body() body: CreateUserDto): Promise<User> {
|
signupUser(@Body() body: CreateUserDto): Promise<User> {
|
||||||
return this.userService.signupUser(body);
|
return this.userService.createUser(body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { UsersController } from './controllers/users.controller';
|
import { UsersController } from './users.controller';
|
||||||
import { UsersService } from './providers/users.service';
|
import { UsersService } from './users.service';
|
||||||
import { UserRepository } from './repositories/user.repository';
|
import { UserRepository } from './user.repository';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { User } from './entities/user.entity';
|
import { User } from './entities/user.entity';
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { UserRepository } from '../repositories/user.repository';
|
import { UserRepository } from './user.repository';
|
||||||
import { User } from '../entities/user.entity';
|
import { User } from './entities/user.entity';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { CreateUserDto } from '../DTO/create-user.dto';
|
import { CreateUserDto } from './DTO/create-user.dto';
|
||||||
import { UserMessages } from 'src/common/enums/messages.enum';
|
import { UserMessages } from 'src/common/enums/messages.enum';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UsersService {
|
export class UsersService {
|
||||||
constructor(private readonly userRepository: UserRepository) {}
|
constructor(private readonly userRepository: UserRepository) {}
|
||||||
|
|
||||||
async signupUser(userDto: CreateUserDto): Promise<User> {
|
async createUser(userDto: CreateUserDto): Promise<User> {
|
||||||
const existingUser = await this.userRepository.findOne({
|
const existingUser = await this.userRepository.findOne({
|
||||||
where: [
|
where: [
|
||||||
{
|
{
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const SMS_CONFIG = "SMS_CONFIG";
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export interface SmsParameter {
|
||||||
|
name: SmsTemplateParameter;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SmsRequest {
|
||||||
|
Mobile: string;
|
||||||
|
TemplateId: string;
|
||||||
|
Parameters: SmsParameter[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SmsResponse {
|
||||||
|
status: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SmsSendResponse extends SmsResponse {
|
||||||
|
data: {
|
||||||
|
MessageId: number;
|
||||||
|
Cost: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SmsTemplateParameter = string;
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import {
|
||||||
|
BadGatewayException,
|
||||||
|
Inject,
|
||||||
|
Injectable,
|
||||||
|
InternalServerErrorException,
|
||||||
|
Logger,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { SMS_CONFIG } from '../constants';
|
||||||
|
import type { ISmsConfigs } from 'src/config/sms.config';
|
||||||
|
import { catchError, firstValueFrom } from 'rxjs';
|
||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { SmsRequest, SmsSendResponse } from '../interfaces/ISms';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SmsService {
|
||||||
|
private readonly logger = new Logger(SmsService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(SMS_CONFIG) private smsConfigs: ISmsConfigs,
|
||||||
|
private readonly httpService: HttpService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async sendSms(body: SmsRequest): Promise<SmsSendResponse> {
|
||||||
|
try {
|
||||||
|
const { data } = await firstValueFrom(
|
||||||
|
this.httpService
|
||||||
|
.post<SmsSendResponse>(
|
||||||
|
`${this.smsConfigs.API_URL}/send/verify`,
|
||||||
|
body,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'X-API-KEY': this.smsConfigs.API_KEY,
|
||||||
|
},
|
||||||
|
timeout: 5000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.pipe(
|
||||||
|
catchError((error: AxiosError) => {
|
||||||
|
this.logger.error(
|
||||||
|
'Error while calling SMS provider.',
|
||||||
|
error.stack,
|
||||||
|
);
|
||||||
|
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
'Failed to communicate with SMS provider.',
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
'Failed to send SMS.',
|
||||||
|
error instanceof Error ? error.stack : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
throw new InternalServerErrorException('Failed to send SMS.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { SmsService } from './providers/sms.service';
|
||||||
|
import { smsConfigsProvider } from 'src/config/sms.config';
|
||||||
|
import { SMS_CONFIG } from './constants';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [SmsService, smsConfigsProvider],
|
||||||
|
exports: [SMS_CONFIG, SmsService],
|
||||||
|
})
|
||||||
|
export class UtilsModule {}
|
||||||
Reference in New Issue
Block a user