feat: handle new registration flow

This commit is contained in:
Abdalhamid Alhamad
2025-05-19 17:00:32 +03:00
parent 35ab3df7c1
commit 49326e983f
13 changed files with 140 additions and 132 deletions
+15 -16
View File
@@ -18,7 +18,6 @@ import {
setJuniorPasswordRequestDto,
SetPasscodeRequestDto,
VerifyLoginOtpRequestDto,
VerifyOtpRequestDto,
VerifyUserRequestDto,
} from '../dtos/request';
import { SendForgetPasswordOtpResponseDto, SendRegisterOtpResponseDto } from '../dtos/response';
@@ -72,22 +71,22 @@ export class AuthController {
await this.authService.setPasscode(sub, passcode);
}
@Post('register/set-phone/otp')
@UseGuards(AccessTokenGuard)
async setPhoneNumber(
@AuthenticatedUser() { sub }: IJwtPayload,
@Body() setPhoneNumberDto: CreateUnverifiedUserRequestDto,
) {
const phoneNumber = await this.authService.setPhoneNumber(sub, setPhoneNumberDto);
return ResponseFactory.data(new SendRegisterOtpResponseDto(phoneNumber));
}
// @Post('register/set-phone/otp')
// @UseGuards(AccessTokenGuard)
// async setPhoneNumber(
// @AuthenticatedUser() { sub }: IJwtPayload,
// @Body() setPhoneNumberDto: CreateUnverifiedUserRequestDto,
// ) {
// const phoneNumber = await this.authService.setPhoneNumber(sub, setPhoneNumberDto);
// return ResponseFactory.data(new SendRegisterOtpResponseDto(phoneNumber));
// }
@Post('register/set-phone/verify')
@HttpCode(HttpStatus.NO_CONTENT)
@UseGuards(AccessTokenGuard)
async verifyPhoneNumber(@AuthenticatedUser() { sub }: IJwtPayload, @Body() { otp }: VerifyOtpRequestDto) {
await this.authService.verifyPhoneNumber(sub, otp);
}
// @Post('register/set-phone/verify')
// @HttpCode(HttpStatus.NO_CONTENT)
// @UseGuards(AccessTokenGuard)
// async verifyPhoneNumber(@AuthenticatedUser() { sub }: IJwtPayload, @Body() { otp }: VerifyOtpRequestDto) {
// await this.authService.verifyPhoneNumber(sub, otp);
// }
@Post('biometric/enable')
@HttpCode(HttpStatus.NO_CONTENT)
@@ -1,19 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';
import { Matches } from 'class-validator';
import { IsEmail } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { COUNTRY_CODE_REGEX } from '~/auth/constants';
import { IsValidPhoneNumber } from '~/core/decorators/validations';
export class CreateUnverifiedUserRequestDto {
@ApiProperty({ example: '+962' })
@Matches(COUNTRY_CODE_REGEX, {
message: i18n('validation.Matches', { path: 'general', property: 'auth.countryCode' }),
})
countryCode: string = '+966';
@ApiProperty({ example: '787259134' })
@IsValidPhoneNumber({
message: i18n('validation.IsValidPhoneNumber', { path: 'general', property: 'auth.phoneNumber' }),
})
phoneNumber!: string;
@ApiProperty({ example: '[email protected]' })
@IsEmail(
{},
{
message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }),
},
)
email!: string;
}
@@ -1,10 +1,32 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumberString, MaxLength, MinLength } from 'class-validator';
import { ApiProperty, PickType } from '@nestjs/swagger';
import { IsDateString, IsNotEmpty, IsNumberString, IsOptional, IsString, MaxLength, MinLength } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { DEFAULT_OTP_LENGTH } from '~/common/modules/otp/constants';
import { IsAbove18 } from '~/core/decorators/validations';
import { CreateUnverifiedUserRequestDto } from './create-unverified-user.request.dto';
export class VerifyUserRequestDto extends CreateUnverifiedUserRequestDto {
export class VerifyUserRequestDto extends PickType(CreateUnverifiedUserRequestDto, ['email']) {
@ApiProperty({ example: 'John' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.firstName' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.firstName' }) })
firstName!: string;
@ApiProperty({ example: 'Doe' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.lastName' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.lastName' }) })
lastName!: string;
@ApiProperty({ example: '2021-01-01' })
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'customer.dateOfBirth' }) })
@IsAbove18({ message: i18n('validation.IsAbove18', { path: 'general', property: 'customer.dateOfBirth' }) })
dateOfBirth!: Date;
@ApiProperty({ example: 'JO' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.countryOfResidence' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.countryOfResidence' }) })
@IsOptional()
countryOfResidence: string = 'SA';
@ApiProperty({ example: '111111' })
@IsNumberString(
{ no_symbols: true },
@@ -2,9 +2,9 @@ import { ApiProperty } from '@nestjs/swagger';
export class SendRegisterOtpResponseDto {
@ApiProperty()
phoneNumber!: string;
email!: string;
constructor(phoneNumber: string) {
this.phoneNumber = phoneNumber;
constructor(email: string) {
this.email = email;
}
}
+50 -66
View File
@@ -45,62 +45,45 @@ export class AuthService {
private readonly cacheService: CacheService,
private readonly oauth2Service: Oauth2Service,
) {}
async sendRegisterOtp({ phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) {
this.logger.log(`Sending OTP to ${countryCode + phoneNumber}`);
const user = await this.userService.findOrCreateUser({ phoneNumber, countryCode });
async sendRegisterOtp(body: CreateUnverifiedUserRequestDto) {
this.logger.log(`Sending OTP to ${body.email}`);
const user = await this.userService.findOrCreateUser(body);
return this.otpService.generateAndSendOtp({
userId: user.id,
recipient: user.countryCode + user.phoneNumber,
scope: OtpScope.VERIFY_PHONE,
otpType: OtpType.SMS,
recipient: user.email,
scope: OtpScope.VERIFY_EMAIL,
otpType: OtpType.EMAIL,
});
}
async verifyUser(verifyUserDto: VerifyUserRequestDto): Promise<[ILoginResponse, User]> {
this.logger.log(`Verifying user with phone number ${verifyUserDto.countryCode + verifyUserDto.phoneNumber}`);
const user = await this.userService.findUserOrThrow({ phoneNumber: verifyUserDto.phoneNumber });
this.logger.log(`Verifying user with email ${verifyUserDto.email}`);
const user = await this.userService.findUserOrThrow({ email: verifyUserDto.email });
if (user.isProfileCompleted) {
this.logger.error(
`User with phone number ${verifyUserDto.countryCode + verifyUserDto.phoneNumber} already verified`,
);
throw new BadRequestException('USER.PHONE_ALREADY_VERIFIED');
if (user.isEmailVerified) {
this.logger.error(`User with email ${verifyUserDto.email} already verified`);
throw new BadRequestException('USER.EMAIL_ALREADY_VERIFIED');
}
const isOtpValid = await this.otpService.verifyOtp({
userId: user.id,
scope: OtpScope.VERIFY_PHONE,
otpType: OtpType.SMS,
scope: OtpScope.VERIFY_EMAIL,
otpType: OtpType.EMAIL,
value: verifyUserDto.otp,
});
if (!isOtpValid) {
this.logger.error(
`Invalid OTP for user with phone number ${verifyUserDto.countryCode + verifyUserDto.phoneNumber}`,
);
this.logger.error(`Invalid OTP for user with email ${verifyUserDto.email}`);
throw new BadRequestException('OTP.INVALID_OTP');
}
if (user.isPhoneVerified) {
this.logger.log(
`User with phone number ${
verifyUserDto.countryCode + verifyUserDto.phoneNumber
} already verified but did not complete registration process`,
);
const tokens = await this.generateAuthToken(user);
return [tokens, user];
}
await this.userService.verifyPhoneNumber(user.id);
await this.userService.verifyUser(user.id, verifyUserDto);
await user.reload();
const tokens = await this.generateAuthToken(user);
this.logger.log(
`User with phone number ${verifyUserDto.countryCode + verifyUserDto.phoneNumber} verified successfully`,
);
this.logger.log(`User with email ${verifyUserDto.email} verified successfully`);
return [tokens, user];
}
@@ -138,46 +121,46 @@ export class AuthService {
this.logger.log(`Passcode set successfully for user with id ${userId}`);
}
async setPhoneNumber(userId: string, { phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) {
const user = await this.userService.findUserOrThrow({ id: userId });
// async setPhoneNumber(userId: string, { phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) {
// const user = await this.userService.findUserOrThrow({ id: userId });
if (user.phoneNumber || user.countryCode) {
this.logger.error(`Phone number already set for user with id ${userId}`);
throw new BadRequestException('USER.PHONE_NUMBER_ALREADY_SET');
}
// if (user.phoneNumber || user.countryCode) {
// this.logger.error(`Phone number already set for user with id ${userId}`);
// throw new BadRequestException('USER.PHONE_NUMBER_ALREADY_SET');
// }
const existingUser = await this.userService.findUser({ phoneNumber, countryCode });
// const existingUser = await this.userService.findUser({ phoneNumber, countryCode });
if (existingUser) {
this.logger.error(`Phone number ${countryCode + phoneNumber} already taken`);
throw new BadRequestException('USER.PHONE_NUMBER_ALREADY_TAKEN');
}
// if (existingUser) {
// this.logger.error(`Phone number ${countryCode + phoneNumber} already taken`);
// throw new BadRequestException('USER.PHONE_NUMBER_ALREADY_TAKEN');
// }
await this.userService.setPhoneNumber(userId, phoneNumber, countryCode);
// await this.userService.setPhoneNumber(userId, phoneNumber, countryCode);
return this.otpService.generateAndSendOtp({
userId,
recipient: countryCode + phoneNumber,
scope: OtpScope.VERIFY_PHONE,
otpType: OtpType.SMS,
});
}
// return this.otpService.generateAndSendOtp({
// userId,
// recipient: countryCode + phoneNumber,
// scope: OtpScope.VERIFY_PHONE,
// otpType: OtpType.SMS,
// });
// }
async verifyPhoneNumber(userId: string, otp: string) {
const isOtpValid = await this.otpService.verifyOtp({
otpType: OtpType.SMS,
scope: OtpScope.VERIFY_PHONE,
userId,
value: otp,
});
// async verifyPhoneNumber(userId: string, otp: string) {
// const isOtpValid = await this.otpService.verifyOtp({
// otpType: OtpType.SMS,
// scope: OtpScope.VERIFY_PHONE,
// userId,
// value: otp,
// });
if (!isOtpValid) {
this.logger.error(`Invalid OTP for user with id ${userId}`);
throw new BadRequestException('OTP.INVALID_OTP');
}
// if (!isOtpValid) {
// this.logger.error(`Invalid OTP for user with id ${userId}`);
// throw new BadRequestException('OTP.INVALID_OTP');
// }
return this.userService.verifyPhoneNumber(userId);
}
// return this.userService.verifyPhoneNumber(userId);
// }
async enableBiometric(userId: string, { deviceId, publicKey }: EnableBiometricRequestDto) {
this.logger.log(`Enabling biometric for user with id ${userId}`);
@@ -330,7 +313,8 @@ export class AuthService {
}
async sendLoginOtp({ email }: SendLoginOtpRequestDto) {
const user = await this.userService.findOrCreateByEmail(email);
const user = await this.userService.findUserOrThrow({ email });
this.logger.log(`Sending login OTP to ${email}`);
return this.otpService.generateAndSendOtp({
recipient: email,