mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-07-17 02:45:08 +00:00
Merge pull request #4 from HamzaSha1/feat/forget-password
feat:forget password
This commit is contained in:
@ -1 +1,3 @@
|
|||||||
export * from './country-code-regex.constant.';
|
export * from './country-code-regex.constant.';
|
||||||
|
export * from './passcode-regext.constant';
|
||||||
|
export * from './password-regex.constant';
|
||||||
|
1
src/auth/constants/passcode-regext.constant.ts
Normal file
1
src/auth/constants/passcode-regext.constant.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const PASSCODE_REGEX = /^\d{6}$/;
|
1
src/auth/constants/password-regex.constant.ts
Normal file
1
src/auth/constants/password-regex.constant.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const PASSWORD_REGEX = /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&#\-_])[A-Za-z\d@$!%*?&#\-_]{8,}$/;
|
@ -8,12 +8,14 @@ import {
|
|||||||
CreateUnverifiedUserRequestDto,
|
CreateUnverifiedUserRequestDto,
|
||||||
DisableBiometricRequestDto,
|
DisableBiometricRequestDto,
|
||||||
EnableBiometricRequestDto,
|
EnableBiometricRequestDto,
|
||||||
|
ForgetPasswordRequestDto,
|
||||||
LoginRequestDto,
|
LoginRequestDto,
|
||||||
|
SendForgetPasswordOtpRequestDto,
|
||||||
SetEmailRequestDto,
|
SetEmailRequestDto,
|
||||||
SetPasscodeRequestDto,
|
SetPasscodeRequestDto,
|
||||||
VerifyUserRequestDto,
|
VerifyUserRequestDto,
|
||||||
} from '../dtos/request';
|
} from '../dtos/request';
|
||||||
import { SendRegisterOtpResponseDto } from '../dtos/response';
|
import { SendForgetPasswordOtpResponseDto, SendRegisterOtpResponseDto } from '../dtos/response';
|
||||||
import { LoginResponseDto } from '../dtos/response/login.response.dto';
|
import { LoginResponseDto } from '../dtos/response/login.response.dto';
|
||||||
import { IJwtPayload } from '../interfaces';
|
import { IJwtPayload } from '../interfaces';
|
||||||
import { AuthService } from '../services';
|
import { AuthService } from '../services';
|
||||||
@ -63,6 +65,18 @@ export class AuthController {
|
|||||||
return this.authService.disableBiometric(sub, disableBiometricDto);
|
return this.authService.disableBiometric(sub, disableBiometricDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('forget-password/otp')
|
||||||
|
async forgetPassword(@Body() sendForgetPasswordOtpDto: SendForgetPasswordOtpRequestDto) {
|
||||||
|
const email = await this.authService.sendForgetPasswordOtp(sendForgetPasswordOtpDto);
|
||||||
|
return ResponseFactory.data(new SendForgetPasswordOtpResponseDto(email));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('forget-password/reset')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
resetPassword(@Body() forgetPasswordDto: ForgetPasswordRequestDto) {
|
||||||
|
return this.authService.verifyForgetPasswordOtp(forgetPasswordDto);
|
||||||
|
}
|
||||||
|
|
||||||
@Post('login')
|
@Post('login')
|
||||||
async login(@Body() loginDto: LoginRequestDto, @Headers(DEVICE_ID_HEADER) deviceId: string) {
|
async login(@Body() loginDto: LoginRequestDto, @Headers(DEVICE_ID_HEADER) deviceId: string) {
|
||||||
const [res, user] = await this.authService.login(loginDto, deviceId);
|
const [res, user] = await this.authService.login(loginDto, deviceId);
|
||||||
|
32
src/auth/dtos/request/forget-password.request.dto.ts
Normal file
32
src/auth/dtos/request/forget-password.request.dto.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsEmail, IsNotEmpty, IsNumberString, IsString, MaxLength, MinLength } from 'class-validator';
|
||||||
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
|
import { DEFAULT_OTP_LENGTH } from '~/common/modules/otp/constants';
|
||||||
|
export class ForgetPasswordRequestDto {
|
||||||
|
@ApiProperty({ example: 'test@test.com' })
|
||||||
|
@IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }) })
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'password' })
|
||||||
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.password' }) })
|
||||||
|
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.password' }) })
|
||||||
|
password!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'password' })
|
||||||
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.confirmPassword' }) })
|
||||||
|
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.confirmPassword' }) })
|
||||||
|
confirmPassword!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '111111' })
|
||||||
|
@IsNumberString(
|
||||||
|
{ no_symbols: true },
|
||||||
|
{ message: i18n('validation.IsNumberString', { path: 'general', property: 'auth.otp' }) },
|
||||||
|
)
|
||||||
|
@MaxLength(DEFAULT_OTP_LENGTH, {
|
||||||
|
message: i18n('validation.MaxLength', { path: 'general', property: 'auth.otp', length: DEFAULT_OTP_LENGTH }),
|
||||||
|
})
|
||||||
|
@MinLength(DEFAULT_OTP_LENGTH, {
|
||||||
|
message: i18n('validation.MinLength', { path: 'general', property: 'auth.otp', length: DEFAULT_OTP_LENGTH }),
|
||||||
|
})
|
||||||
|
otp!: string;
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
export * from './create-unverified-user.request.dto';
|
export * from './create-unverified-user.request.dto';
|
||||||
export * from './disable-biometric.request.dto';
|
export * from './disable-biometric.request.dto';
|
||||||
export * from './enable-biometric.request.dto';
|
export * from './enable-biometric.request.dto';
|
||||||
|
export * from './forget-password.request.dto';
|
||||||
export * from './login.request.dto';
|
export * from './login.request.dto';
|
||||||
|
export * from './send-forget-password-otp.request.dto';
|
||||||
export * from './set-email.request.dto';
|
export * from './set-email.request.dto';
|
||||||
export * from './set-passcode.request.dto';
|
export * from './set-passcode.request.dto';
|
||||||
export * from './verify-user.request.dto';
|
export * from './verify-user.request.dto';
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
import { PickType } from '@nestjs/swagger';
|
||||||
|
import { LoginRequestDto } from './login.request.dto';
|
||||||
|
|
||||||
|
export class SendForgetPasswordOtpRequestDto extends PickType(LoginRequestDto, ['email']) {}
|
@ -1,3 +1,4 @@
|
|||||||
|
export * from './send-forget-password.response.dto';
|
||||||
export * from './send-register-otp.response.dto';
|
export * from './send-register-otp.response.dto';
|
||||||
export * from './user.response.dto';
|
export * from './user.response.dto';
|
||||||
export * from './verify-user.response.dto';
|
export * from './verify-user.response.dto';
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
export class SendForgetPasswordOtpResponseDto {
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
constructor(email: string) {
|
||||||
|
this.email = email;
|
||||||
|
}
|
||||||
|
}
|
@ -4,16 +4,19 @@ import { JwtService } from '@nestjs/jwt';
|
|||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { OtpScope, OtpType } from '~/common/modules/otp/enums';
|
import { OtpScope, OtpType } from '~/common/modules/otp/enums';
|
||||||
import { OtpService } from '~/common/modules/otp/services';
|
import { OtpService } from '~/common/modules/otp/services';
|
||||||
|
import { PASSCODE_REGEX, PASSWORD_REGEX } from '../constants';
|
||||||
import {
|
import {
|
||||||
CreateUnverifiedUserRequestDto,
|
CreateUnverifiedUserRequestDto,
|
||||||
DisableBiometricRequestDto,
|
DisableBiometricRequestDto,
|
||||||
EnableBiometricRequestDto,
|
EnableBiometricRequestDto,
|
||||||
|
ForgetPasswordRequestDto,
|
||||||
LoginRequestDto,
|
LoginRequestDto,
|
||||||
|
SendForgetPasswordOtpRequestDto,
|
||||||
SetEmailRequestDto,
|
SetEmailRequestDto,
|
||||||
} from '../dtos/request';
|
} from '../dtos/request';
|
||||||
import { VerifyUserRequestDto } from '../dtos/request/verify-user.request.dto';
|
import { VerifyUserRequestDto } from '../dtos/request/verify-user.request.dto';
|
||||||
import { User } from '../entities';
|
import { User } from '../entities';
|
||||||
import { GrantType } from '../enums';
|
import { GrantType, Roles } from '../enums';
|
||||||
import { ILoginResponse } from '../interfaces';
|
import { ILoginResponse } from '../interfaces';
|
||||||
import { removePadding, verifySignature } from '../utils';
|
import { removePadding, verifySignature } from '../utils';
|
||||||
import { DeviceService } from './device.service';
|
import { DeviceService } from './device.service';
|
||||||
@ -35,7 +38,7 @@ export class AuthService {
|
|||||||
|
|
||||||
return this.otpService.generateAndSendOtp({
|
return this.otpService.generateAndSendOtp({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
phoneNumber: user.phoneNumber,
|
recipient: user.phoneNumber,
|
||||||
scope: OtpScope.VERIFY_PHONE,
|
scope: OtpScope.VERIFY_PHONE,
|
||||||
otpType: OtpType.SMS,
|
otpType: OtpType.SMS,
|
||||||
});
|
});
|
||||||
@ -126,6 +129,44 @@ export class AuthService {
|
|||||||
return this.deviceService.updateDevice(deviceId, { publicKey: null });
|
return this.deviceService.updateDevice(deviceId, { publicKey: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendForgetPasswordOtp({ email }: SendForgetPasswordOtpRequestDto) {
|
||||||
|
const user = await this.userService.findUserOrThrow({ email });
|
||||||
|
|
||||||
|
if (!user.isProfileCompleted) {
|
||||||
|
throw new BadRequestException('USERS.PROFILE_NOT_COMPLETED');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.otpService.generateAndSendOtp({
|
||||||
|
userId: user.id,
|
||||||
|
recipient: user.email,
|
||||||
|
scope: OtpScope.FORGET_PASSWORD,
|
||||||
|
otpType: OtpType.EMAIL,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyForgetPasswordOtp({ email, otp, password, confirmPassword }: ForgetPasswordRequestDto) {
|
||||||
|
const user = await this.userService.findUserOrThrow({ email });
|
||||||
|
if (!user.isProfileCompleted) {
|
||||||
|
throw new BadRequestException('USERS.PROFILE_NOT_COMPLETED');
|
||||||
|
}
|
||||||
|
const isOtpValid = await this.otpService.verifyOtp({
|
||||||
|
userId: user.id,
|
||||||
|
scope: OtpScope.FORGET_PASSWORD,
|
||||||
|
otpType: OtpType.EMAIL,
|
||||||
|
value: otp,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isOtpValid) {
|
||||||
|
throw new BadRequestException('USERS.INVALID_OTP');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.validatePassword(password, confirmPassword, user);
|
||||||
|
|
||||||
|
const hashedPassword = bcrypt.hashSync(password, user.salt);
|
||||||
|
|
||||||
|
await this.userService.setPasscode(user.id, hashedPassword, user.salt);
|
||||||
|
}
|
||||||
|
|
||||||
async login(loginDto: LoginRequestDto, deviceId: string): Promise<[ILoginResponse, User]> {
|
async login(loginDto: LoginRequestDto, deviceId: string): Promise<[ILoginResponse, User]> {
|
||||||
const user = await this.userService.findUser({ email: loginDto.email });
|
const user = await this.userService.findUser({ email: loginDto.email });
|
||||||
let tokens;
|
let tokens;
|
||||||
@ -205,4 +246,20 @@ export class AuthService {
|
|||||||
|
|
||||||
return { accessToken, refreshToken, expiresAt: new Date(this.jwtService.decode(accessToken).exp * ONE_THOUSAND) };
|
return { accessToken, refreshToken, expiresAt: new Date(this.jwtService.decode(accessToken).exp * ONE_THOUSAND) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private validatePassword(password: string, confirmPassword: string, user: User) {
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
throw new BadRequestException('AUTH.PASSWORD_MISMATCH');
|
||||||
|
}
|
||||||
|
|
||||||
|
const roles = user.roles;
|
||||||
|
|
||||||
|
if (roles.includes(Roles.GUARDIAN) && !PASSCODE_REGEX.test(password)) {
|
||||||
|
throw new BadRequestException('AUTH.INVALID_PASSCODE');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roles.includes(Roles.JUNIOR) && !PASSWORD_REGEX.test(password)) {
|
||||||
|
throw new BadRequestException('AUTH.INVALID_PASSWORD');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
import { IsNotEmpty } from 'class-validator';
|
|
||||||
import { UserLocale } from '~/core/enums';
|
|
||||||
import { OtpScope, OtpType } from '../../enums';
|
|
||||||
|
|
||||||
export class GenerateOtpRequestDto {
|
|
||||||
@IsNotEmpty()
|
|
||||||
userId!: string;
|
|
||||||
|
|
||||||
@IsNotEmpty()
|
|
||||||
phoneNumber!: string;
|
|
||||||
|
|
||||||
@IsNotEmpty()
|
|
||||||
scope!: OtpScope;
|
|
||||||
|
|
||||||
@IsNotEmpty()
|
|
||||||
language?: UserLocale = UserLocale.ENGLISH;
|
|
||||||
|
|
||||||
@IsNotEmpty()
|
|
||||||
otpType!: OtpType;
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
export * from './generate-otp-request.request.dto';
|
|
||||||
export * from './verify-otp-request.dto';
|
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
import { IsNotEmpty } from 'class-validator';
|
|
||||||
import { OtpScope, OtpType } from '../../enums';
|
|
||||||
|
|
||||||
export class VerifyOtpRequestDto {
|
|
||||||
@IsNotEmpty()
|
|
||||||
userId!: string;
|
|
||||||
|
|
||||||
@IsNotEmpty()
|
|
||||||
scope!: OtpScope;
|
|
||||||
|
|
||||||
@IsNotEmpty()
|
|
||||||
otpType!: OtpType;
|
|
||||||
|
|
||||||
@IsNotEmpty()
|
|
||||||
value!: string;
|
|
||||||
}
|
|
@ -1,3 +1,4 @@
|
|||||||
export enum OtpScope {
|
export enum OtpScope {
|
||||||
VERIFY_PHONE = 'VERIFY_PHONE',
|
VERIFY_PHONE = 'VERIFY_PHONE',
|
||||||
|
FORGET_PASSWORD = 'FORGET_PASSWORD',
|
||||||
}
|
}
|
||||||
|
2
src/common/modules/otp/interfaces/index.ts
Normal file
2
src/common/modules/otp/interfaces/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './send-otp.interface';
|
||||||
|
export * from './verify-otp.interface';
|
9
src/common/modules/otp/interfaces/send-otp.interface.ts
Normal file
9
src/common/modules/otp/interfaces/send-otp.interface.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { OtpScope, OtpType } from '../enums';
|
||||||
|
|
||||||
|
export interface ISendOtp {
|
||||||
|
userId: string;
|
||||||
|
scope: OtpScope;
|
||||||
|
language?: string;
|
||||||
|
otpType: OtpType;
|
||||||
|
recipient: string;
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
import { OtpScope, OtpType } from '../enums';
|
||||||
|
|
||||||
|
export interface IVerifyOtp {
|
||||||
|
userId: string;
|
||||||
|
scope: OtpScope;
|
||||||
|
otpType: OtpType;
|
||||||
|
value: string;
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { MoreThan, Repository } from 'typeorm';
|
import { MoreThan, Repository } from 'typeorm';
|
||||||
import { VerifyOtpRequestDto } from '../dtos/request';
|
|
||||||
import { Otp } from '../entities';
|
import { Otp } from '../entities';
|
||||||
|
import { IVerifyOtp } from '../interfaces';
|
||||||
const FIVE = 5;
|
const FIVE = 5;
|
||||||
const SIXTY = 60;
|
const SIXTY = 60;
|
||||||
const ONE_THOUSAND = 1000;
|
const ONE_THOUSAND = 1000;
|
||||||
@ -23,7 +23,7 @@ export class OtpRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
findOtp(otp: VerifyOtpRequestDto) {
|
findOtp(otp: IVerifyOtp) {
|
||||||
return this.otpRepository.findOne({
|
return this.otpRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
userId: otp.userId,
|
userId: otp.userId,
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { DEFAULT_OTP_DIGIT, DEFAULT_OTP_LENGTH } from '../constants';
|
import { DEFAULT_OTP_DIGIT, DEFAULT_OTP_LENGTH } from '../constants';
|
||||||
import { GenerateOtpRequestDto, VerifyOtpRequestDto } from '../dtos/request';
|
import { OtpType } from '../enums';
|
||||||
|
import { ISendOtp, IVerifyOtp } from '../interfaces';
|
||||||
import { OtpRepository } from '../repositories';
|
import { OtpRepository } from '../repositories';
|
||||||
import { generateRandomOtp } from '../utils';
|
import { generateRandomOtp } from '../utils';
|
||||||
|
|
||||||
@ -9,23 +10,25 @@ import { generateRandomOtp } from '../utils';
|
|||||||
export class OtpService {
|
export class OtpService {
|
||||||
constructor(private readonly configService: ConfigService, private readonly otpRepository: OtpRepository) {}
|
constructor(private readonly configService: ConfigService, private readonly otpRepository: OtpRepository) {}
|
||||||
private useMock = this.configService.get<boolean>('USE_MOCK', false);
|
private useMock = this.configService.get<boolean>('USE_MOCK', false);
|
||||||
async generateAndSendOtp(sendotpRequest: GenerateOtpRequestDto) {
|
async generateAndSendOtp(sendotpRequest: ISendOtp): Promise<string> {
|
||||||
const otp = this.useMock ? DEFAULT_OTP_DIGIT.repeat(DEFAULT_OTP_LENGTH) : generateRandomOtp(DEFAULT_OTP_LENGTH);
|
const otp = this.useMock ? DEFAULT_OTP_DIGIT.repeat(DEFAULT_OTP_LENGTH) : generateRandomOtp(DEFAULT_OTP_LENGTH);
|
||||||
|
|
||||||
await this.otpRepository.createOtp({ ...sendotpRequest, value: otp });
|
await this.otpRepository.createOtp({ ...sendotpRequest, value: otp });
|
||||||
|
|
||||||
this.sendOtp(sendotpRequest, otp);
|
this.sendOtp(sendotpRequest, otp);
|
||||||
|
|
||||||
return sendotpRequest.phoneNumber.replace(/.(?=.{4})/g, '*');
|
return sendotpRequest.otpType == OtpType.EMAIL
|
||||||
|
? sendotpRequest.recipient
|
||||||
|
: sendotpRequest.recipient?.replace(/.(?=.{4})/g, '*');
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyOtp(verifyOtpRequest: VerifyOtpRequestDto) {
|
async verifyOtp(verifyOtpRequest: IVerifyOtp) {
|
||||||
const otp = await this.otpRepository.findOtp(verifyOtpRequest);
|
const otp = await this.otpRepository.findOtp(verifyOtpRequest);
|
||||||
|
|
||||||
return !!otp;
|
return !!otp;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendOtp(sendotpRequest: GenerateOtpRequestDto, otp: string) {
|
private sendOtp(sendotpRequest: ISendOtp, otp: string) {
|
||||||
// TODO: send OTP to the user
|
// TODO: send OTP to the user
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user