diff --git a/src/auth/controllers/auth.controller.ts b/src/auth/controllers/auth.controller.ts index c7cc940..3e3909a 100644 --- a/src/auth/controllers/auth.controller.ts +++ b/src/auth/controllers/auth.controller.ts @@ -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) diff --git a/src/auth/dtos/request/create-unverified-user.request.dto.ts b/src/auth/dtos/request/create-unverified-user.request.dto.ts index 9302584..9fdc3bd 100644 --- a/src/auth/dtos/request/create-unverified-user.request.dto.ts +++ b/src/auth/dtos/request/create-unverified-user.request.dto.ts @@ -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: 'test@test.com' }) + @IsEmail( + {}, + { + message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }), + }, + ) + email!: string; } diff --git a/src/auth/dtos/request/verify-user.request.dto.ts b/src/auth/dtos/request/verify-user.request.dto.ts index c90d495..cb5c74f 100644 --- a/src/auth/dtos/request/verify-user.request.dto.ts +++ b/src/auth/dtos/request/verify-user.request.dto.ts @@ -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 }, diff --git a/src/auth/dtos/response/send-register-otp.response.dto.ts b/src/auth/dtos/response/send-register-otp.response.dto.ts index ec6f35a..0597b90 100644 --- a/src/auth/dtos/response/send-register-otp.response.dto.ts +++ b/src/auth/dtos/response/send-register-otp.response.dto.ts @@ -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; } } diff --git a/src/auth/services/auth.service.ts b/src/auth/services/auth.service.ts index ae300c5..d35bfea 100644 --- a/src/auth/services/auth.service.ts +++ b/src/auth/services/auth.service.ts @@ -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, diff --git a/src/common/modules/otp/enums/otp-scope.enum.ts b/src/common/modules/otp/enums/otp-scope.enum.ts index 7ecc49a..5a6e4e8 100644 --- a/src/common/modules/otp/enums/otp-scope.enum.ts +++ b/src/common/modules/otp/enums/otp-scope.enum.ts @@ -1,5 +1,6 @@ export enum OtpScope { VERIFY_PHONE = 'VERIFY_PHONE', + VERIFY_EMAIL = 'VERIFY_EMAIL', FORGET_PASSWORD = 'FORGET_PASSWORD', LOGIN = 'LOGIN', } diff --git a/src/customer/repositories/customer.repository.ts b/src/customer/repositories/customer.repository.ts index ee17a68..100348f 100644 --- a/src/customer/repositories/customer.repository.ts +++ b/src/customer/repositories/customer.repository.ts @@ -29,17 +29,6 @@ export class CustomerRepository { firstName: body.firstName, lastName: body.lastName, dateOfBirth: body.dateOfBirth, - gender: body.gender, - countryOfResidence: body.countryOfResidence, - nationalId: body.nationalId, - nationalIdExpiry: body.nationalIdExpiry, - sourceOfIncome: body.sourceOfIncome, - profession: body.profession, - professionType: body.professionType, - isPep: body.isPep, - profilePictureId: body.profilePictureId, - civilIdFrontId: body.civilIdFrontId, - civilIdBackId: body.civilIdBackId, }), ); } diff --git a/src/customer/services/customer.service.ts b/src/customer/services/customer.service.ts index 9822f4e..85c4d0a 100644 --- a/src/customer/services/customer.service.ts +++ b/src/customer/services/customer.service.ts @@ -89,13 +89,13 @@ export class CustomerService { } @Transactional() - async createGuardianCustomer(userId: string, body: CreateCustomerRequestDto) { + async createGuardianCustomer(userId: string, body: Partial) { this.logger.log(`Creating guardian customer for user ${userId}`); const existingCustomer = await this.customerRepository.findOne({ id: userId }); if (existingCustomer) { this.logger.error(`Customer ${userId} already exists`); - throw new BadRequestException('CUSTOMER.ALRADY_EXISTS'); + throw new BadRequestException('CUSTOMER.ALREADY_EXISTS'); } // await this.validateCivilIdForCustomer(userId, body.civilIdFrontId, body.civilIdBackId); diff --git a/src/i18n/ar/app.json b/src/i18n/ar/app.json index 3da3936..085b78d 100644 --- a/src/i18n/ar/app.json +++ b/src/i18n/ar/app.json @@ -15,6 +15,7 @@ "USER": { "PHONE_ALREADY_VERIFIED": "تم التحقق من رقم الهاتف بالفعل.", + "EMAIL_ALREADY_VERIFIED": "تم التحقق من عنوان البريد الإلكتروني بالفعل.", "EMAIL_ALREADY_SET": "تم تعيين عنوان البريد الإلكتروني بالفعل.", "EMAIL_ALREADY_TAKEN": "عنوان البريد الإلكتروني مستخدم بالفعل. يرجى تجربة عنوان بريد إلكتروني آخر.", "PHONE_NUMBER_ALREADY_SET": "تم تعيين رقم الهاتف بالفعل.", diff --git a/src/i18n/en/app.json b/src/i18n/en/app.json index 77905b0..da4c402 100644 --- a/src/i18n/en/app.json +++ b/src/i18n/en/app.json @@ -15,6 +15,7 @@ "USER": { "PHONE_ALREADY_VERIFIED": "The phone number has already been verified.", + "EMAIL_ALREADY_VERIFIED": "The email address has already been verified.", "EMAIL_ALREADY_SET": "The email address has already been set.", "EMAIL_ALREADY_TAKEN": "The email address is already in use. Please try another email address.", "PHONE_NUMBER_ALREADY_SET": "The phone number has already been set.", diff --git a/src/user/repositories/user.repository.ts b/src/user/repositories/user.repository.ts index a8630a0..79b8489 100644 --- a/src/user/repositories/user.repository.ts +++ b/src/user/repositories/user.repository.ts @@ -11,8 +11,7 @@ export class UserRepository { createUnverifiedUser(data: Partial) { return this.userRepository.save( this.userRepository.create({ - phoneNumber: data.phoneNumber, - countryCode: data.countryCode, + email: data.email, roles: data.roles, }), ); diff --git a/src/user/services/user.service.ts b/src/user/services/user.service.ts index 46bab16..25aba53 100644 --- a/src/user/services/user.service.ts +++ b/src/user/services/user.service.ts @@ -5,7 +5,8 @@ import moment from 'moment'; import { FindOptionsWhere } from 'typeorm'; import { Transactional } from 'typeorm-transactional'; import { NotificationsService } from '~/common/modules/notification/services'; -import { CreateUnverifiedUserRequestDto } from '../../auth/dtos/request'; +import { CustomerService } from '~/customer/services'; +import { CreateUnverifiedUserRequestDto, VerifyUserRequestDto } from '../../auth/dtos/request'; import { Roles } from '../../auth/enums'; import { CreateCheckerRequestDto, @@ -29,6 +30,7 @@ export class UserService { private readonly deviceService: DeviceService, private readonly userTokenService: UserTokenService, private readonly configService: ConfigService, + private customerService: CustomerService, ) {} findUser(where: FindOptionsWhere | FindOptionsWhere[]) { @@ -51,9 +53,18 @@ export class UserService { return this.userRepository.update(userId, { phoneNumber, countryCode }); } - verifyPhoneNumber(userId: string) { - this.logger.log(`Verifying phone number for user ${userId}`); - return this.userRepository.update(userId, { isPhoneVerified: true }); + @Transactional() + async verifyUser(userId: string, body: VerifyUserRequestDto) { + this.logger.log(`Verifying user email with id ${userId}`); + await Promise.all([ + this.customerService.createGuardianCustomer(userId, { + firstName: body.firstName, + lastName: body.lastName, + dateOfBirth: body.dateOfBirth, + countryOfResidence: body.countryOfResidence, + }), + this.userRepository.update(userId, { isEmailVerified: true }), + ]); } findUsers(filters: UserFiltersRequestDto) { @@ -74,26 +85,27 @@ export class UserService { return user; } - async findOrCreateUser({ phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) { - this.logger.log(`Finding or creating user with phone number ${phoneNumber} and country code ${countryCode}`); - const user = await this.userRepository.findOne({ phoneNumber }); + @Transactional() + async findOrCreateUser(body: CreateUnverifiedUserRequestDto) { + this.logger.log(`Finding or creating user with email ${body.email}`); + const user = await this.userRepository.findOne({ email: body.email }); if (!user) { - this.logger.log(`User with phone number ${phoneNumber} not found, creating new user`); - return this.userRepository.createUnverifiedUser({ phoneNumber, countryCode, roles: [Roles.GUARDIAN] }); + this.logger.log(`User with email ${body.email} not found, creating new user`); + return this.userRepository.createUnverifiedUser({ email: body.email, roles: [Roles.GUARDIAN] }); } - if (user && user.roles.includes(Roles.GUARDIAN) && user.isProfileCompleted) { - this.logger.error(`User with phone number ${phoneNumber} already exists`); - throw new BadRequestException('USER.PHONE_NUMBER_ALREADY_EXISTS'); + if (user && user.roles.includes(Roles.GUARDIAN) && user.isEmailVerified) { + this.logger.error(`User with email ${body.email} already exists`); + throw new BadRequestException('USER.EMAIL_ALREADY_TAKEN'); } if (user && user.roles.includes(Roles.JUNIOR)) { - this.logger.error(`User with phone number ${phoneNumber} is an already registered junior`); + this.logger.error(`User with email ${body.email} is an already registered junior`); throw new BadRequestException('USER.JUNIOR_UPGRADE_NOT_SUPPORTED_YET'); //TODO add role Guardian to the existing user and send OTP } - this.logger.log(`User with phone number ${phoneNumber} and country code ${countryCode} found successfully`); + this.logger.log(`User with email ${body.email}`); return user; } diff --git a/src/user/user.module.ts b/src/user/user.module.ts index 1b86380..50c740f 100644 --- a/src/user/user.module.ts +++ b/src/user/user.module.ts @@ -1,13 +1,18 @@ import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { NotificationModule } from '~/common/modules/notification/notification.module'; +import { CustomerModule } from '~/customer/customer.module'; import { AdminUserController, UserController } from './controllers'; import { Device, User, UserRegistrationToken } from './entities'; import { DeviceRepository, UserRepository, UserTokenRepository } from './repositories'; import { DeviceService, UserService, UserTokenService } from './services'; @Module({ - imports: [TypeOrmModule.forFeature([User, Device, UserRegistrationToken]), forwardRef(() => NotificationModule)], + imports: [ + TypeOrmModule.forFeature([User, Device, UserRegistrationToken]), + forwardRef(() => NotificationModule), + forwardRef(() => CustomerModule), + ], providers: [UserService, DeviceService, UserRepository, DeviceRepository, UserTokenRepository, UserTokenService], exports: [UserService, DeviceService, UserTokenService], controllers: [UserController, AdminUserController],