feat: added smsService and sendOtp

This commit is contained in:
2026-07-23 18:42:12 +03:30
parent 7c4c83e5d8
commit f380b1267d
20 changed files with 369 additions and 14 deletions
+2
View File
@@ -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",
+70
View File
@@ -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
View File
@@ -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 {}
+12
View File
@@ -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;
}
+22
View File
@@ -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);
}
}
+15
View File
@@ -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 {}
+62
View File
@@ -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();
}
}
+23
View File
@@ -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;
}
+5
View File
@@ -0,0 +1,5 @@
export enum OtpStatus {
PENDING = 'PENDING',
VERIFIED = 'VERIFIED',
EXPIRED = 'EXPIRED'
}
+13
View File
@@ -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,
);
}
}
+9 -1
View File
@@ -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()
});
+22
View File
@@ -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);
} }
} }
+3 -3
View File
@@ -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: [
{ {
+1
View File
@@ -0,0 +1 @@
export const SMS_CONFIG = "SMS_CONFIG";
+24
View File
@@ -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;
+62
View File
@@ -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.');
}
}
}
+10
View File
@@ -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 {}