mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-08-25 13:49:40 +00:00
feat: edit forget password flow
This commit is contained in:
@ -11,10 +11,12 @@ import {
|
|||||||
LoginRequestDto,
|
LoginRequestDto,
|
||||||
RefreshTokenRequestDto,
|
RefreshTokenRequestDto,
|
||||||
SendForgetPasswordOtpRequestDto,
|
SendForgetPasswordOtpRequestDto,
|
||||||
|
VerifyForgetPasswordOtpRequestDto,
|
||||||
VerifyUserRequestDto,
|
VerifyUserRequestDto,
|
||||||
} from '../dtos/request';
|
} from '../dtos/request';
|
||||||
import { SendForgetPasswordOtpResponseDto, 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 { VerifyForgetPasswordOtpResponseDto } from '../dtos/response/verify-forget-password-otp.response.dto';
|
||||||
import { AuthService } from '../services';
|
import { AuthService } from '../services';
|
||||||
|
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
@ -47,10 +49,18 @@ export class AuthController {
|
|||||||
return ResponseFactory.data(new SendForgetPasswordOtpResponseDto(maskedNumber));
|
return ResponseFactory.data(new SendForgetPasswordOtpResponseDto(maskedNumber));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('forget-password/verify')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async verifyForgetPasswordOtp(@Body() forgetPasswordDto: VerifyForgetPasswordOtpRequestDto) {
|
||||||
|
const { token, user } = await this.authService.verifyForgetPasswordOtp(forgetPasswordDto);
|
||||||
|
|
||||||
|
return ResponseFactory.data(new VerifyForgetPasswordOtpResponseDto(token, user));
|
||||||
|
}
|
||||||
|
|
||||||
@Post('forget-password/reset')
|
@Post('forget-password/reset')
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
resetPassword(@Body() forgetPasswordDto: ForgetPasswordRequestDto) {
|
resetPassword(@Body() forgetPasswordDto: ForgetPasswordRequestDto) {
|
||||||
return this.authService.verifyForgetPasswordOtp(forgetPasswordDto);
|
return this.authService.resetPassword(forgetPasswordDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('refresh-token')
|
@Post('refresh-token')
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { ApiProperty, PickType } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsEmail, IsNotEmpty, IsNumberString, IsString, Matches, MaxLength, MinLength } from 'class-validator';
|
import { IsString, Matches } from 'class-validator';
|
||||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
import { COUNTRY_CODE_REGEX, PASSWORD_REGEX } from '~/auth/constants';
|
import { COUNTRY_CODE_REGEX, PASSWORD_REGEX } from '~/auth/constants';
|
||||||
import { DEFAULT_OTP_LENGTH } from '~/common/modules/otp/constants';
|
|
||||||
import { IsValidPhoneNumber } from '~/core/decorators/validations';
|
import { IsValidPhoneNumber } from '~/core/decorators/validations';
|
||||||
export class ForgetPasswordRequestDto {
|
export class ForgetPasswordRequestDto {
|
||||||
@ApiProperty({ example: '+962' })
|
@ApiProperty({ example: '+962' })
|
||||||
@ -29,16 +28,7 @@ export class ForgetPasswordRequestDto {
|
|||||||
})
|
})
|
||||||
confirmPassword!: string;
|
confirmPassword!: string;
|
||||||
|
|
||||||
@ApiProperty({ example: '111111' })
|
@ApiProperty({ example: 'reset-token-32423123' })
|
||||||
@IsNumberString(
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.resetPasswordToken' }) })
|
||||||
{ no_symbols: true },
|
resetPasswordToken!: string;
|
||||||
{ 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;
|
|
||||||
}
|
}
|
||||||
|
@ -10,5 +10,6 @@ export * from './send-forget-password-otp.request.dto';
|
|||||||
export * from './set-email.request.dto';
|
export * from './set-email.request.dto';
|
||||||
export * from './set-junior-password.request.dto';
|
export * from './set-junior-password.request.dto';
|
||||||
export * from './set-passcode.request.dto';
|
export * from './set-passcode.request.dto';
|
||||||
|
export * from './verify-forget-password-otp.request.dto';
|
||||||
export * from './verify-otp.request.dto';
|
export * from './verify-otp.request.dto';
|
||||||
export * from './verify-user.request.dto';
|
export * from './verify-user.request.dto';
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
import { ApiProperty, PickType } from '@nestjs/swagger';
|
||||||
|
import { IsNumberString, MaxLength, MinLength } from 'class-validator';
|
||||||
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
|
import { DEFAULT_OTP_LENGTH } from '~/common/modules/otp/constants';
|
||||||
|
import { ForgetPasswordRequestDto } from './forget-password.request.dto';
|
||||||
|
|
||||||
|
export class VerifyForgetPasswordOtpRequestDto extends PickType(ForgetPasswordRequestDto, [
|
||||||
|
'countryCode',
|
||||||
|
'phoneNumber',
|
||||||
|
]) {
|
||||||
|
@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;
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { User } from '~/user/entities';
|
||||||
|
|
||||||
|
export class VerifyForgetPasswordOtpResponseDto {
|
||||||
|
@ApiProperty()
|
||||||
|
phoneNumber!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
countryCode!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
resetPasswordToken!: string;
|
||||||
|
|
||||||
|
constructor(token: string, user: User) {
|
||||||
|
this.phoneNumber = user.phoneNumber;
|
||||||
|
this.countryCode = user.countryCode;
|
||||||
|
this.resetPasswordToken = token;
|
||||||
|
}
|
||||||
|
}
|
@ -3,13 +3,13 @@ import { ConfigService } from '@nestjs/config';
|
|||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
|
import moment from 'moment';
|
||||||
import { CacheService } from '~/common/modules/cache/services';
|
import { CacheService } from '~/common/modules/cache/services';
|
||||||
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 { UserType } from '~/user/enums';
|
import { UserType } from '~/user/enums';
|
||||||
import { DeviceService, UserService, UserTokenService } from '~/user/services';
|
import { DeviceService, UserService, UserTokenService } from '~/user/services';
|
||||||
import { User } from '../../user/entities';
|
import { User } from '../../user/entities';
|
||||||
import { PASSCODE_REGEX } from '../constants';
|
|
||||||
import {
|
import {
|
||||||
CreateUnverifiedUserRequestDto,
|
CreateUnverifiedUserRequestDto,
|
||||||
DisableBiometricRequestDto,
|
DisableBiometricRequestDto,
|
||||||
@ -18,10 +18,11 @@ import {
|
|||||||
LoginRequestDto,
|
LoginRequestDto,
|
||||||
SendForgetPasswordOtpRequestDto,
|
SendForgetPasswordOtpRequestDto,
|
||||||
setJuniorPasswordRequestDto,
|
setJuniorPasswordRequestDto,
|
||||||
|
VerifyForgetPasswordOtpRequestDto,
|
||||||
VerifyUserRequestDto,
|
VerifyUserRequestDto,
|
||||||
} from '../dtos/request';
|
} from '../dtos/request';
|
||||||
|
import { Roles } from '../enums';
|
||||||
import { IJwtPayload, ILoginResponse } from '../interfaces';
|
import { IJwtPayload, ILoginResponse } from '../interfaces';
|
||||||
import { removePadding, verifySignature } from '../utils';
|
|
||||||
import { Oauth2Service } from './oauth2.service';
|
import { Oauth2Service } from './oauth2.service';
|
||||||
|
|
||||||
const ONE_THOUSAND = 1000;
|
const ONE_THOUSAND = 1000;
|
||||||
@ -147,14 +148,7 @@ export class AuthService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyForgetPasswordOtp({
|
async verifyForgetPasswordOtp({ countryCode, phoneNumber, otp }: VerifyForgetPasswordOtpRequestDto) {
|
||||||
countryCode,
|
|
||||||
phoneNumber,
|
|
||||||
otp,
|
|
||||||
password,
|
|
||||||
confirmPassword,
|
|
||||||
}: ForgetPasswordRequestDto) {
|
|
||||||
this.logger.log(`Verifying forget password OTP for ${countryCode + phoneNumber}`);
|
|
||||||
const user = await this.userService.findUserOrThrow({ countryCode, phoneNumber });
|
const user = await this.userService.findUserOrThrow({ countryCode, phoneNumber });
|
||||||
|
|
||||||
const isOtpValid = await this.otpService.verifyOtp({
|
const isOtpValid = await this.otpService.verifyOtp({
|
||||||
@ -169,6 +163,29 @@ export class AuthService {
|
|||||||
throw new BadRequestException('OTP.INVALID_OTP');
|
throw new BadRequestException('OTP.INVALID_OTP');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generate a token for the user to reset password
|
||||||
|
const token = await this.userTokenService.generateToken(
|
||||||
|
user.id,
|
||||||
|
user.roles.includes(Roles.GUARDIAN) ? UserType.GUARDIAN : UserType.JUNIOR,
|
||||||
|
moment().add(5, 'minutes').toDate(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return { token, user };
|
||||||
|
}
|
||||||
|
async resetPassword({
|
||||||
|
countryCode,
|
||||||
|
phoneNumber,
|
||||||
|
resetPasswordToken,
|
||||||
|
password,
|
||||||
|
confirmPassword,
|
||||||
|
}: ForgetPasswordRequestDto) {
|
||||||
|
this.logger.log(`Verifying forget password OTP for ${countryCode + phoneNumber}`);
|
||||||
|
const user = await this.userService.findUserOrThrow({ countryCode, phoneNumber });
|
||||||
|
await this.userTokenService.validateToken(
|
||||||
|
resetPasswordToken,
|
||||||
|
user.roles.includes(Roles.GUARDIAN) ? UserType.GUARDIAN : UserType.JUNIOR,
|
||||||
|
);
|
||||||
|
|
||||||
if (password !== confirmPassword) {
|
if (password !== confirmPassword) {
|
||||||
this.logger.error('Password and confirm password do not match');
|
this.logger.error('Password and confirm password do not match');
|
||||||
throw new BadRequestException('AUTH.PASSWORD_MISMATCH');
|
throw new BadRequestException('AUTH.PASSWORD_MISMATCH');
|
||||||
@ -177,6 +194,7 @@ export class AuthService {
|
|||||||
const hashedPassword = bcrypt.hashSync(password, user.salt);
|
const hashedPassword = bcrypt.hashSync(password, user.salt);
|
||||||
|
|
||||||
await this.userService.setPassword(user.id, hashedPassword, user.salt);
|
await this.userService.setPassword(user.id, hashedPassword, user.salt);
|
||||||
|
await this.userTokenService.invalidateToken(resetPasswordToken);
|
||||||
this.logger.log(`Passcode updated successfully for user with phone number ${user.fullPhoneNumber}`);
|
this.logger.log(`Passcode updated successfully for user with phone number ${user.fullPhoneNumber}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,9 @@
|
|||||||
"PASSWORD_MISMATCH": "كلمات المرور التي أدخلتها غير متطابقة. يرجى إدخال كلمات المرور مرة أخرى.",
|
"PASSWORD_MISMATCH": "كلمات المرور التي أدخلتها غير متطابقة. يرجى إدخال كلمات المرور مرة أخرى.",
|
||||||
"INVALID_PASSCODE": "رمز المرور الذي أدخلته غير صحيح. يرجى المحاولة مرة أخرى.",
|
"INVALID_PASSCODE": "رمز المرور الذي أدخلته غير صحيح. يرجى المحاولة مرة أخرى.",
|
||||||
"PASSCODE_ALREADY_SET": "تم تعيين رمز المرور بالفعل.",
|
"PASSCODE_ALREADY_SET": "تم تعيين رمز المرور بالفعل.",
|
||||||
"APPLE_RE-CONSENT_REQUIRED": "إعادة الموافقة على آبل مطلوبة. يرجى إلغاء تطبيقك من إعدادات معرف آبل الخاصة بك والمحاولة مرة أخرى."
|
"APPLE_RE-CONSENT_REQUIRED": "إعادة الموافقة على آبل مطلوبة. يرجى إلغاء تطبيقك من إعدادات معرف آبل الخاصة بك والمحاولة مرة أخرى.",
|
||||||
|
"TOKEN_INVALID": "رمز المستخدم غير صالح.",
|
||||||
|
"TOKEN_EXPIRED": "رمز المستخدم منتهي الصلاحية."
|
||||||
},
|
},
|
||||||
|
|
||||||
"USER": {
|
"USER": {
|
||||||
|
@ -11,7 +11,9 @@
|
|||||||
"PASSWORD_MISMATCH": "The passwords you entered do not match. Please re-enter the passwords.",
|
"PASSWORD_MISMATCH": "The passwords you entered do not match. Please re-enter the passwords.",
|
||||||
"INVALID_PASSCODE": "The passcode you entered is incorrect. Please try again.",
|
"INVALID_PASSCODE": "The passcode you entered is incorrect. Please try again.",
|
||||||
"PASSCODE_ALREADY_SET": "The pass code has already been set.",
|
"PASSCODE_ALREADY_SET": "The pass code has already been set.",
|
||||||
"APPLE_RE-CONSENT_REQUIRED": "Apple re-consent is required. Please revoke the app from your Apple ID settings and try again."
|
"APPLE_RE-CONSENT_REQUIRED": "Apple re-consent is required. Please revoke the app from your Apple ID settings and try again.",
|
||||||
|
"TOKEN_INVALID": "The user token is invalid.",
|
||||||
|
"TOKEN_EXPIRED": "The user token has expired."
|
||||||
},
|
},
|
||||||
|
|
||||||
"USER": {
|
"USER": {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export enum UserType {
|
export enum UserType {
|
||||||
CHECKER = 'CHECKER',
|
CHECKER = 'CHECKER',
|
||||||
JUNIOR = 'JUNIOR',
|
JUNIOR = 'JUNIOR',
|
||||||
|
GUARDIAN = 'GUARDIAN',
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ export class UserTokenRepository {
|
|||||||
generateToken(userId: string, userType: UserType, expiryDate?: Date) {
|
generateToken(userId: string, userType: UserType, expiryDate?: Date) {
|
||||||
return this.userTokenRepository.save(
|
return this.userTokenRepository.save(
|
||||||
this.userTokenRepository.create({
|
this.userTokenRepository.create({
|
||||||
userId: userType === UserType.CHECKER ? userId : null,
|
userId,
|
||||||
juniorId: userType === UserType.JUNIOR ? userId : null,
|
juniorId: userType === UserType.JUNIOR ? userId : null,
|
||||||
expiryDate: expiryDate ?? moment().add('15', 'minutes').toDate(),
|
expiryDate: expiryDate ?? moment().add('15', 'minutes').toDate(),
|
||||||
userType,
|
userType,
|
||||||
|
@ -20,12 +20,12 @@ export class UserTokenService {
|
|||||||
|
|
||||||
if (!tokenEntity) {
|
if (!tokenEntity) {
|
||||||
this.logger.error(`Token ${token} not found`);
|
this.logger.error(`Token ${token} not found`);
|
||||||
throw new BadRequestException('TOKEN.INVALID');
|
throw new BadRequestException('AUTH.TOKEN_INVALID');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tokenEntity.expiryDate < new Date()) {
|
if (tokenEntity.expiryDate < new Date()) {
|
||||||
this.logger.error(`Token ${token} expired`);
|
this.logger.error(`Token ${token} expired`);
|
||||||
throw new BadRequestException('TOKEN.EXPIRED');
|
throw new BadRequestException('AUTH.TOKEN_EXPIRED');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`Token validated successfully`);
|
this.logger.log(`Token validated successfully`);
|
||||||
|
Reference in New Issue
Block a user