feat: added smsService and sendOtp
This commit is contained in:
+5
-1
@@ -5,6 +5,8 @@ import Joi from 'joi';
|
||||
import { validationSchema } from './config/config.validation';
|
||||
import { UsersModule } from './users/users.module';
|
||||
import { databaseConfig } from './config/typeorm.config';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -13,7 +15,9 @@ import { databaseConfig } from './config/typeorm.config';
|
||||
validationSchema
|
||||
}),
|
||||
TypeOrmModule.forRootAsync(databaseConfig()),
|
||||
UsersModule
|
||||
HttpModule.register({global: true, timeout: 10000, headers: {"Content-Type": "application/json"}}),
|
||||
UsersModule,
|
||||
AuthModule
|
||||
],
|
||||
})
|
||||
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({
|
||||
APP_PORT: Joi.number().required(),
|
||||
|
||||
// DB Config
|
||||
DB_HOST: Joi.string().required(),
|
||||
DB_PORT: Joi.number().required(),
|
||||
DB_USER: Joi.string().required(),
|
||||
DB_PASSWORD: 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 { User } from "../entities/user.entity";
|
||||
import { User } from "./entities/user.entity";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Body, Controller, Get, Post } from '@nestjs/common';
|
||||
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { CreateUserDto } from '../DTO/create-user.dto';
|
||||
import { User } from '../entities/user.entity';
|
||||
import { UsersService } from '../providers/users.service';
|
||||
import { CreateUserDto } from './DTO/create-user.dto';
|
||||
import { User } from './entities/user.entity';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
@@ -16,6 +16,6 @@ export class UsersController {
|
||||
@ApiResponse({status: 201, description: 'User created successfully!'})
|
||||
@ApiResponse({status: 400, description: 'Bad Request from 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 { UsersController } from './controllers/users.controller';
|
||||
import { UsersService } from './providers/users.service';
|
||||
import { UserRepository } from './repositories/user.repository';
|
||||
import { UsersController } from './users.controller';
|
||||
import { UsersService } from './users.service';
|
||||
import { UserRepository } from './user.repository';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { User } from './entities/user.entity';
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { UserRepository } from '../repositories/user.repository';
|
||||
import { User } from '../entities/user.entity';
|
||||
import { UserRepository } from './user.repository';
|
||||
import { User } from './entities/user.entity';
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(private readonly userRepository: UserRepository) {}
|
||||
|
||||
async signupUser(userDto: CreateUserDto): Promise<User> {
|
||||
async createUser(userDto: CreateUserDto): Promise<User> {
|
||||
const existingUser = await this.userRepository.findOne({
|
||||
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