diff --git a/src/common/modules/hashing/providers/bcrypt.provider.ts b/src/common/modules/hashing/providers/bcrypt.provider.ts index 37f3817..b16dfe9 100644 --- a/src/common/modules/hashing/providers/bcrypt.provider.ts +++ b/src/common/modules/hashing/providers/bcrypt.provider.ts @@ -1,6 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ import { Injectable } from '@nestjs/common'; import { HashingProvider } from './hashing.provider'; import * as bcrypt from 'bcrypt'; @@ -8,9 +5,9 @@ import * as bcrypt from 'bcrypt'; @Injectable() export class BcryptProvider implements HashingProvider { public async hash(data: string | Buffer): Promise { - const salt = (await bcrypt.genSalt()) as string; + const salt = await bcrypt.genSalt(); - return (await bcrypt.hash(data, salt)) as string; + return await bcrypt.hash(data, salt); } public async compare( diff --git a/src/i18n/en/auth.json b/src/i18n/en/auth.json index 50883bc..52797da 100644 --- a/src/i18n/en/auth.json +++ b/src/i18n/en/auth.json @@ -1,6 +1,7 @@ { "fields": { "phone": "Phone", - "password": "Password" + "password": "Password", + "otp": "Otp" } } diff --git a/src/i18n/fa/auth.json b/src/i18n/fa/auth.json index 330c929..a6c719f 100644 --- a/src/i18n/fa/auth.json +++ b/src/i18n/fa/auth.json @@ -1,6 +1,7 @@ { "fields": { "phone": "شماره موبایل", - "password": "رمز عبور" + "password": "رمز عبور", + "otp": "کد تایید" } } diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 616abb0..2620c05 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -6,6 +6,8 @@ import { OtpModule } from '../otp/otp.module'; import { HashingModule } from '@/common/modules/hashing/hashing.module'; import { JwtModule } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; +import { LoginProvider } from './providers/login.provider'; +import { GenerateTokenProvider } from './providers/generate-token.provider'; @Module({ imports: [ @@ -25,6 +27,6 @@ import { ConfigService } from '@nestjs/config'; }), ], controllers: [AuthController], - providers: [AuthService], + providers: [AuthService, GenerateTokenProvider, LoginProvider], }) export class AuthModule {} diff --git a/src/modules/auth/dtos/login.dto.ts b/src/modules/auth/dtos/login.dto.ts index 1e1d7b0..07d318d 100644 --- a/src/modules/auth/dtos/login.dto.ts +++ b/src/modules/auth/dtos/login.dto.ts @@ -4,7 +4,7 @@ import { Transform } from 'class-transformer'; import { IsNotEmpty, IsOptional, IsString, Matches } from 'class-validator'; import { i18nValidationMessage as t } from 'nestjs-i18n'; -export class LoginDTO { +export class PhoneDTO { @ApiProperty({ type: 'string', description: `User's Phone Number`, @@ -24,7 +24,9 @@ export class LoginDTO { }) @Transform(({ value }) => Utils.StringUtils.normalizePhone(value as string)) phone: string; +} +export class LoginDTO extends PhoneDTO { @ApiProperty({ type: 'string', description: 'Password for User Account', diff --git a/src/modules/auth/dtos/verify-otp.dto.ts b/src/modules/auth/dtos/verify-otp.dto.ts new file mode 100644 index 0000000..d582fe3 --- /dev/null +++ b/src/modules/auth/dtos/verify-otp.dto.ts @@ -0,0 +1,13 @@ +import { IsNotEmpty, IsString } from 'class-validator'; +import { PhoneDTO } from './login.dto'; +import { i18nValidationMessage as t } from 'nestjs-i18n'; + +export class VerifyOtpDTO extends PhoneDTO { + @IsString() + @IsNotEmpty({ + message: t('validation.requiredField', { + field: '$t(auth.fields.otp)', + }), + }) + otp: string; +} diff --git a/src/modules/auth/http/login.post.endpoints.http b/src/modules/auth/http/login.post.endpoints.http index 6d4f304..6c13784 100644 --- a/src/modules/auth/http/login.post.endpoints.http +++ b/src/modules/auth/http/login.post.endpoints.http @@ -2,6 +2,6 @@ POST http://localhost:3000/users/login Content-Type: application/json { - "phone": "09333026363", + "phone": "09126093033", "password": "rad1.3.5" } \ No newline at end of file diff --git a/src/modules/auth/interfaces/jwt.interface.ts b/src/modules/auth/interfaces/jwt.interface.ts new file mode 100644 index 0000000..463677b --- /dev/null +++ b/src/modules/auth/interfaces/jwt.interface.ts @@ -0,0 +1,9 @@ +export interface AccessTokenPayload { + sub: number; + phone: string; +} + +export interface RefreshTokenPayload { + sub: number; + jti: string; +} diff --git a/src/modules/auth/providers/auth.service.ts b/src/modules/auth/providers/auth.service.ts index 9ab8a2e..24e3b6f 100644 --- a/src/modules/auth/providers/auth.service.ts +++ b/src/modules/auth/providers/auth.service.ts @@ -1,88 +1,17 @@ -import { UsersService } from '@/modules/users/providers/users.service'; -import { Inject, Injectable, UnauthorizedException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { LoginDTO } from '../dtos/login.dto'; -import { OtpService } from '@/modules/otp/providers/otp.service'; -import { HashingProvider } from '@/common/modules/hashing/providers/hashing.provider'; -import { JwtService } from '@nestjs/jwt'; -import type { ConfigType } from '@nestjs/config'; -import jwtConfig from '@/config/jwt.config'; +import { LoginProvider } from './login.provider'; @Injectable() export class AuthService { constructor( /** - * Inject Users Service + * Inject Login Provider */ - private readonly usersService: UsersService, - - /** - * Inject OTP Service - */ - private readonly otpService: OtpService, - - /** - * Inject Hashing Provider - */ - private readonly hashingProvider: HashingProvider, - - /** - * Inject Jwt Service - */ - private readonly jwtService: JwtService, - - /** - * Inject Jwt Config - */ - @Inject(jwtConfig.KEY) - private readonly jwtConfiguration: ConfigType, + private readonly loginProvider: LoginProvider, ) {} public async logIn(loginDto: LoginDTO) { - const { phone, password } = loginDto; - const user = await this.usersService.findOneByPhone(phone); - - if (!user) { - // TODO: Send OTP - const otpCode = await this.otpService.createOTP(phone); - - return { - newUser: true, - message: `Otp sent to ${phone}`, - // Just for development - code: otpCode, - }; - } - - if (!password) { - return { - newUser: false, - message: 'Please send password', - }; - } - - const isPasswordCorrect = await this.hashingProvider.compare( - password, - user.password || '', - ); - - if (isPasswordCorrect) { - const tokenPayload = { - sub: user.id, - phone: user.phone, - }; - - const accessToken = await this.jwtService.signAsync(tokenPayload); - - const refreshToken = await this.jwtService.signAsync(tokenPayload, { - secret: this.jwtConfiguration.refresh.secret, - expiresIn: this.jwtConfiguration.refresh.expiresIn, - }); - - return { access: accessToken, refresh: refreshToken }; - } - - throw new UnauthorizedException( - 'Entered phone number or password is wrong.', - ); + return await this.loginProvider.logIn(loginDto); } } diff --git a/src/modules/auth/providers/generate-token.provider.ts b/src/modules/auth/providers/generate-token.provider.ts new file mode 100644 index 0000000..ca59382 --- /dev/null +++ b/src/modules/auth/providers/generate-token.provider.ts @@ -0,0 +1,67 @@ +import jwtConfig from '@/config/jwt.config'; +import { User } from '@/modules/users/user.entity'; +import { Inject, Injectable } from '@nestjs/common'; +import type { ConfigType } from '@nestjs/config'; +import { JwtService } from '@nestjs/jwt'; +import { + AccessTokenPayload, + RefreshTokenPayload, +} from '../interfaces/jwt.interface'; + +@Injectable() +export class GenerateTokenProvider { + constructor( + /** + * Inject Jwt Service + */ + private readonly jwtService: JwtService, + + /** + * Inject Jwt Config + */ + @Inject(jwtConfig.KEY) + private readonly jwtConfiguration: ConfigType, + ) {} + + public async signToken( + userId: number, + expiresIn: number, + secret?: string, + payload?: T, + ) { + return await this.jwtService.signAsync( + { + sub: userId, + ...payload, + }, + { + audience: this.jwtConfiguration.audience, + issuer: this.jwtConfiguration.issuer, + secret, + expiresIn, + }, + ); + } + + public async generateTokens(user: User, jti: string) { + const [access, refresh] = await Promise.all([ + // Sign Access Token + this.signToken>( + user.id, + this.jwtConfiguration.access.expiresIn, + this.jwtConfiguration.access.secret, + { phone: user.phone }, + ), + + // Sign Refresh Token + this.signToken>( + user.id, + this.jwtConfiguration.refresh.expiresIn, + this.jwtConfiguration.refresh.secret, + { jti }, + ), + ]); + + return { access, refresh }; + } +} diff --git a/src/modules/auth/providers/login.provider.ts b/src/modules/auth/providers/login.provider.ts new file mode 100644 index 0000000..de49d2c --- /dev/null +++ b/src/modules/auth/providers/login.provider.ts @@ -0,0 +1,70 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { LoginDTO } from '../dtos/login.dto'; +import { UsersService } from '@/modules/users/providers/users.service'; +import { OtpService } from '@/modules/otp/providers/otp.service'; +import { HashingProvider } from '@/common/modules/hashing/providers/hashing.provider'; +import { GenerateTokenProvider } from './generate-token.provider'; +import { randomUUID } from 'crypto'; + +@Injectable() +export class LoginProvider { + constructor( + /** + * Inject Users Service + */ + private readonly usersService: UsersService, + + /** + * Inject OTP Service + */ + private readonly otpService: OtpService, + + /** + * Inject Hashing Provider + */ + private readonly hashingProvider: HashingProvider, + + /** + * Inject GenerateToken Provider + */ + private readonly generateTokenProvider: GenerateTokenProvider, + ) {} + + public async logIn(loginDto: LoginDTO) { + const { phone, password } = loginDto; + const user = await this.usersService.findOneByPhone(phone); + + if (!user) { + // TODO: Send OTP + const otpCode = await this.otpService.createOTP(phone); + + return { + newUser: true, + message: `Otp sent to ${phone}`, + // Just for development + code: otpCode, + }; + } + + if (!password) { + return { + newUser: false, + message: 'Please send password', + }; + } + + const isPasswordCorrect = await this.hashingProvider.compare( + password, + user.password || '', + ); + + if (isPasswordCorrect) { + const jti = randomUUID(); + return await this.generateTokenProvider.generateTokens(user, jti); + } + + throw new UnauthorizedException( + 'Entered phone number or password is wrong.', + ); + } +} diff --git a/src/modules/users/providers/users.service.ts b/src/modules/users/providers/users.service.ts index ae236c6..20e0cca 100644 --- a/src/modules/users/providers/users.service.ts +++ b/src/modules/users/providers/users.service.ts @@ -2,7 +2,6 @@ import { Injectable, RequestTimeoutException } from '@nestjs/common'; import { Repository } from 'typeorm'; import { User } from '../user.entity'; import { InjectRepository } from '@nestjs/typeorm'; -import { CreateUserDTO } from '../dto/create-user.dto'; @Injectable() export class UsersService { @@ -25,8 +24,8 @@ export class UsersService { } } - public async createOne(createUserDto: CreateUserDTO) { - const newUser = this.userRepository.create(createUserDto); + public async createOne(phone: string) { + const newUser = this.userRepository.create({ phone }); try { await this.userRepository.save(newUser); diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index 0979ab1..4fdbc99 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -1,12 +1,4 @@ -import { - Body, - Controller, - Get, - Param, - ParseIntPipe, - Post, -} from '@nestjs/common'; -import { CreateUserDTO } from './dto/create-user.dto'; +import { Controller } from '@nestjs/common'; import { UsersService } from './providers/users.service'; @Controller('users') @@ -18,18 +10,18 @@ export class UsersController { private readonly usersService: UsersService, ) {} - @Get() - public getAllUsers() { - return ['user1', 'user2']; - } + // @Get() + // public getAllUsers() { + // return ['user1', 'user2']; + // } - @Get(':id') - public getUserById(@Param('id', ParseIntPipe) id: number) { - return `user ${id}`; - } + // @Get(':id') + // public getUserById(@Param('id', ParseIntPipe) id: number) { + // return `user ${id}`; + // } - @Post() - public async createUser(@Body() createUserDto: CreateUserDTO) { - return await this.usersService.createOne(createUserDto); - } + // @Post() + // public async createUser(@Body() createUserDto: CreateUserDTO) { + // return await this.usersService.createOne(createUserDto); + // } }