feat: add login and forget password and refactor code

This commit is contained in:
Abdalhamid Alhamad
2025-07-30 15:40:40 +03:00
parent 4cb5814cd3
commit a245545811
19 changed files with 198 additions and 694 deletions

View File

@ -3,14 +3,14 @@ import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { JuniorModule } from '~/junior/junior.module';
import { UserModule } from '~/user/user.module';
import { AuthController, AuthV2Controller } from './controllers';
import { AuthController } from './controllers';
import { AuthService, Oauth2Service } from './services';
import { AccessTokenStrategy } from './strategies';
@Module({
imports: [JwtModule.register({}), UserModule, JuniorModule, HttpModule],
providers: [AuthService, AccessTokenStrategy, Oauth2Service],
controllers: [AuthController, AuthV2Controller],
controllers: [AuthController],
exports: [],
})
export class AuthModule {}

View File

@ -1,29 +1,20 @@
import { Body, Controller, HttpCode, HttpStatus, Post, Req, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { Request } from 'express';
import { AuthenticatedUser, Public } from '~/common/decorators';
import { Public } from '~/common/decorators';
import { AccessTokenGuard } from '~/common/guards';
import { ApiDataResponse, ApiLangRequestHeader } from '~/core/decorators';
import { ApiLangRequestHeader } from '~/core/decorators';
import { ResponseFactory } from '~/core/utils';
import {
AppleLoginRequestDto,
CreateUnverifiedUserRequestDto,
DisableBiometricRequestDto,
EnableBiometricRequestDto,
ForgetPasswordRequestDto,
GoogleLoginRequestDto,
LoginRequestDto,
RefreshTokenRequestDto,
SendForgetPasswordOtpRequestDto,
SendLoginOtpRequestDto,
SetEmailRequestDto,
setJuniorPasswordRequestDto,
SetPasscodeRequestDto,
VerifyLoginOtpRequestDto,
VerifyUserRequestDto,
} from '../dtos/request';
import { SendForgetPasswordOtpResponseDto, SendRegisterOtpResponseDto } from '../dtos/response';
import { LoginResponseDto } from '../dtos/response/login.response.dto';
import { IJwtPayload } from '../interfaces';
import { AuthService } from '../services';
@Controller('auth')
@ -44,85 +35,16 @@ export class AuthController {
return ResponseFactory.data(new LoginResponseDto(res, user));
}
@Post('login/otp')
@HttpCode(HttpStatus.NO_CONTENT)
async sendLoginOtp(@Body() data: SendLoginOtpRequestDto) {
return this.authService.sendLoginOtp(data);
}
@Post('login/verify')
@HttpCode(HttpStatus.OK)
@ApiDataResponse(LoginResponseDto)
async verifyLoginOtp(@Body() data: VerifyLoginOtpRequestDto) {
const [token, user] = await this.authService.verifyLoginOtp(data);
return ResponseFactory.data(new LoginResponseDto(token, user));
}
@Post('login/google')
@HttpCode(HttpStatus.OK)
@ApiDataResponse(LoginResponseDto)
async loginWithGoogle(@Body() data: GoogleLoginRequestDto) {
const [token, user] = await this.authService.loginWithGoogle(data);
return ResponseFactory.data(new LoginResponseDto(token, user));
}
@Post('login/apple')
@HttpCode(HttpStatus.OK)
@ApiDataResponse(LoginResponseDto)
async loginWithApple(@Body() data: AppleLoginRequestDto) {
const [token, user] = await this.authService.loginWithApple(data);
return ResponseFactory.data(new LoginResponseDto(token, user));
}
@Post('register/set-email')
@HttpCode(HttpStatus.NO_CONTENT)
@UseGuards(AccessTokenGuard)
async setEmail(@AuthenticatedUser() { sub }: IJwtPayload, @Body() setEmailDto: SetEmailRequestDto) {
await this.authService.setEmail(sub, setEmailDto);
}
@Post('register/set-passcode')
@HttpCode(HttpStatus.NO_CONTENT)
@UseGuards(AccessTokenGuard)
async setPasscode(@AuthenticatedUser() { sub }: IJwtPayload, @Body() { passcode }: SetPasscodeRequestDto) {
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/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)
@UseGuards(AccessTokenGuard)
enableBiometric(@AuthenticatedUser() { sub }: IJwtPayload, @Body() enableBiometricDto: EnableBiometricRequestDto) {
return this.authService.enableBiometric(sub, enableBiometricDto);
}
@Post('biometric/disable')
@HttpCode(HttpStatus.NO_CONTENT)
@UseGuards(AccessTokenGuard)
disableBiometric(@AuthenticatedUser() { sub }: IJwtPayload, @Body() disableBiometricDto: DisableBiometricRequestDto) {
return this.authService.disableBiometric(sub, disableBiometricDto);
@Post('login')
async login(@Body() verifyUserDto: LoginRequestDto) {
const [res, user] = await this.authService.loginWithPassword(verifyUserDto);
return ResponseFactory.data(new LoginResponseDto(res, user));
}
@Post('forget-password/otp')
async forgetPassword(@Body() sendForgetPasswordOtpDto: SendForgetPasswordOtpRequestDto) {
const email = await this.authService.sendForgetPasswordOtp(sendForgetPasswordOtpDto);
return ResponseFactory.data(new SendForgetPasswordOtpResponseDto(email));
const maskedNumber = await this.authService.sendForgetPasswordOtp(sendForgetPasswordOtpDto);
return ResponseFactory.data(new SendForgetPasswordOtpResponseDto(maskedNumber));
}
@Post('forget-password/reset')
@ -131,13 +53,6 @@ export class AuthController {
return this.authService.verifyForgetPasswordOtp(forgetPasswordDto);
}
@Post('junior/set-passcode')
@HttpCode(HttpStatus.NO_CONTENT)
@Public()
setJuniorPasscode(@Body() setPasscodeDto: setJuniorPasswordRequestDto) {
return this.authService.setJuniorPasscode(setPasscodeDto);
}
@Post('refresh-token')
@Public()
async refreshToken(@Body() { refreshToken }: RefreshTokenRequestDto) {
@ -151,4 +66,25 @@ export class AuthController {
async logout(@Req() request: Request) {
await this.authService.logout(request);
}
// @Post('biometric/enable')
// @HttpCode(HttpStatus.NO_CONTENT)
// @UseGuards(AccessTokenGuard)
// enableBiometric(@AuthenticatedUser() { sub }: IJwtPayload, @Body() enableBiometricDto: EnableBiometricRequestDto) {
// return this.authService.enableBiometric(sub, enableBiometricDto);
// }
// @Post('biometric/disable')
// @HttpCode(HttpStatus.NO_CONTENT)
// @UseGuards(AccessTokenGuard)
// disableBiometric(@AuthenticatedUser() { sub }: IJwtPayload, @Body() disableBiometricDto: DisableBiometricRequestDto) {
// return this.authService.disableBiometric(sub, disableBiometricDto);
// }
// @Post('junior/set-passcode')
// @HttpCode(HttpStatus.NO_CONTENT)
// @Public()
// setJuniorPasscode(@Body() setPasscodeDto: setJuniorPasswordRequestDto) {
// return this.authService.setJuniorPasscode(setPasscodeDto);
// }
}

View File

@ -1,27 +0,0 @@
import { Body, Controller, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { ApiDataResponse } from '~/core/decorators';
import { ResponseFactory } from '~/core/utils';
import { CreateUnverifiedUserV2RequestDto, VerifyUserV2RequestDto } from '../dtos/request';
import { LoginResponseDto } from '../dtos/response/login.response.dto';
import { SendRegisterOtpV2ResponseDto } from '../dtos/response/send-register-otp.v2.response.dto';
import { AuthService } from '../services';
@Controller('auth/v2')
@ApiTags('Auth V2')
export class AuthV2Controller {
constructor(private readonly authService: AuthService) {}
@Post('register/otp')
@ApiDataResponse(SendRegisterOtpV2ResponseDto)
async register(@Body() createUnverifiedUserDto: CreateUnverifiedUserV2RequestDto) {
const phoneNumber = await this.authService.sendPhoneRegisterOtp(createUnverifiedUserDto);
return ResponseFactory.data(new SendRegisterOtpV2ResponseDto(phoneNumber));
}
@Post('register/verify')
@ApiDataResponse(LoginResponseDto)
async verifyUser(@Body() verifyUserDto: VerifyUserV2RequestDto) {
const [loginResponse, user] = await this.authService.verifyUserV2(verifyUserDto);
return ResponseFactory.data(new LoginResponseDto(loginResponse, user));
}
}

View File

@ -1,2 +1 @@
export * from './auth.controller';
export * from './auth.v2.controller';

View File

@ -1,14 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { OmitType } from '@nestjs/swagger';
import { VerifyUserRequestDto } from './verify-user.request.dto';
export class CreateUnverifiedUserRequestDto {
@ApiProperty({ example: 'test@test.com' })
@IsEmail(
{},
{
message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }),
},
)
email!: string;
}
export class CreateUnverifiedUserRequestDto extends OmitType(VerifyUserRequestDto, ['otp']) {}

View File

@ -1,4 +0,0 @@
import { OmitType } from '@nestjs/swagger';
import { VerifyUserV2RequestDto } from './verify-user.v2.request.dto';
export class CreateUnverifiedUserV2RequestDto extends OmitType(VerifyUserV2RequestDto, ['otp']) {}

View File

@ -1,20 +1,32 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsNumberString, IsString, MaxLength, MinLength } from 'class-validator';
import { ApiProperty, PickType } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsNumberString, IsString, Matches, MaxLength, MinLength } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { COUNTRY_CODE_REGEX, PASSWORD_REGEX } from '~/auth/constants';
import { DEFAULT_OTP_LENGTH } from '~/common/modules/otp/constants';
import { IsValidPhoneNumber } from '~/core/decorators/validations';
export class ForgetPasswordRequestDto {
@ApiProperty({ example: 'test@test.com' })
@IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }) })
email!: string;
@ApiProperty({ example: '+962' })
@Matches(COUNTRY_CODE_REGEX, {
message: i18n('validation.Matches', { path: 'general', property: 'auth.countryCode' }),
})
countryCode!: string;
@ApiProperty({ example: 'password' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.password' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.password' }) })
@ApiProperty({ example: '787259134' })
@IsValidPhoneNumber({
message: i18n('validation.IsValidPhoneNumber', { path: 'general', property: 'auth.phoneNumber' }),
})
phoneNumber!: string;
@ApiProperty({ example: 'Abcd1234@' })
@Matches(PASSWORD_REGEX, {
message: i18n('validation.Matches', { 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' }) })
@ApiProperty({ example: 'Abcd1234@' })
@Matches(PASSWORD_REGEX, {
message: i18n('validation.Matches', { path: 'general', property: 'auth.confirmPassword' }),
})
confirmPassword!: string;
@ApiProperty({ example: '111111' })

View File

@ -1,6 +1,5 @@
export * from './apple-login.request.dto';
export * from './create-unverified-user.request.dto';
export * from './create-unverified-user.request.v2.dto';
export * from './disable-biometric.request.dto';
export * from './enable-biometric.request.dto';
export * from './forget-password.request.dto';
@ -8,11 +7,8 @@ export * from './google-login.request.dto';
export * from './login.request.dto';
export * from './refresh-token.request.dto';
export * from './send-forget-password-otp.request.dto';
export * from './send-login-otp.request.dto';
export * from './set-email.request.dto';
export * from './set-junior-password.request.dto';
export * from './set-passcode.request.dto';
export * from './verify-login-otp.request.dto';
export * from './verify-otp.request.dto';
export * from './verify-user.request.dto';
export * from './verify-user.v2.request.dto';

View File

@ -1,43 +1,24 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsEnum, IsNotEmpty, IsOptional, IsString, ValidateIf } from 'class-validator';
import { IsEmail, IsEnum, IsNotEmpty, IsOptional, IsString, Matches, ValidateIf } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { COUNTRY_CODE_REGEX } from '~/auth/constants';
import { GrantType } from '~/auth/enums';
import { IsValidPhoneNumber } from '~/core/decorators/validations';
export class LoginRequestDto {
@ApiProperty({ example: GrantType.APPLE })
@IsEnum(GrantType, { message: i18n('validation.IsEnum', { path: 'general', property: 'auth.grantType' }) })
grantType!: GrantType;
@ApiProperty({ example: '+962' })
@Matches(COUNTRY_CODE_REGEX, {
message: i18n('validation.Matches', { path: 'general', property: 'auth.countryCode' }),
})
countryCode!: string;
@ApiProperty({ example: 'test@test.com' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.email' }) })
@IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }) })
@ValidateIf((o) => o.grantType !== GrantType.APPLE && o.grantType !== GrantType.GOOGLE)
email!: string;
@ApiProperty({ example: '787259134' })
@IsValidPhoneNumber({
message: i18n('validation.IsValidPhoneNumber', { path: 'general', property: 'auth.phoneNumber' }),
})
phoneNumber!: string;
@ApiProperty({ example: '123456' })
@ApiProperty({ example: 'Abcd1234@' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.password' }) })
@ValidateIf((o) => o.grantType === GrantType.PASSWORD)
password!: string;
@ApiProperty({ example: 'Login signature' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.signature' }) })
@ValidateIf((o) => o.grantType === GrantType.BIOMETRIC)
signature!: string;
@ApiProperty({ example: 'google_token' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.googleToken' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.googleToken' }) })
@ValidateIf((o) => o.grantType === GrantType.GOOGLE)
googleToken!: string;
@ApiProperty({ example: 'apple_token' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.appleToken' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.appleToken' }) })
@ValidateIf((o) => o.grantType === GrantType.APPLE)
appleToken!: string;
@ApiProperty({ example: 'fcm-device-token' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.fcmToken' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.fcmToken' }) })
@IsOptional()
fcmToken?: string;
}

View File

@ -1,4 +1,4 @@
import { PickType } from '@nestjs/swagger';
import { LoginRequestDto } from './login.request.dto';
export class SendForgetPasswordOtpRequestDto extends PickType(LoginRequestDto, ['email']) {}
export class SendForgetPasswordOtpRequestDto extends PickType(LoginRequestDto, ['countryCode', 'phoneNumber']) {}

View File

@ -1,9 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
export class SendLoginOtpRequestDto {
@ApiProperty({ example: 'test@test.com' })
@IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }) })
email!: string;
}

View File

@ -1,20 +0,0 @@
import { ApiProperty } 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 { SendLoginOtpRequestDto } from './send-login-otp.request.dto';
export class VerifyLoginOtpRequestDto extends SendLoginOtpRequestDto {
@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;
}

View File

@ -1,21 +1,34 @@
import { ApiProperty, PickType } from '@nestjs/swagger';
import { ApiProperty } from '@nestjs/swagger';
import {
IsDateString,
IsEmail,
IsEnum,
IsNotEmpty,
IsNumberString,
IsOptional,
IsString,
Matches,
MaxLength,
MinLength,
} from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { COUNTRY_CODE_REGEX, PASSWORD_REGEX } from '~/auth/constants';
import { CountryIso } from '~/common/enums';
import { DEFAULT_OTP_LENGTH } from '~/common/modules/otp/constants';
import { IsAbove18 } from '~/core/decorators/validations';
import { CreateUnverifiedUserRequestDto } from './create-unverified-user.request.dto';
import { IsAbove18, IsValidPhoneNumber } from '~/core/decorators/validations';
export class VerifyUserRequestDto extends PickType(CreateUnverifiedUserRequestDto, ['email']) {
export class VerifyUserRequestDto {
@ApiProperty({ example: '+962' })
@Matches(COUNTRY_CODE_REGEX, {
message: i18n('validation.Matches', { path: 'general', property: 'auth.countryCode' }),
})
countryCode!: string;
@ApiProperty({ example: '787259134' })
@IsValidPhoneNumber({
message: i18n('validation.IsValidPhoneNumber', { path: 'general', property: 'auth.phoneNumber' }),
})
phoneNumber!: string;
@ApiProperty({ example: 'John' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.firstName' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.firstName' }) })
@ -38,6 +51,23 @@ export class VerifyUserRequestDto extends PickType(CreateUnverifiedUserRequestDt
@IsOptional()
countryOfResidence: CountryIso = CountryIso.SAUDI_ARABIA;
@ApiProperty({ example: 'test@test.com' })
@IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }) })
@IsOptional()
email!: string;
@ApiProperty({ example: 'Abcd1234@' })
@Matches(PASSWORD_REGEX, {
message: i18n('validation.Matches', { path: 'general', property: 'auth.password' }),
})
password!: string;
@ApiProperty({ example: 'Abcd1234@' })
@Matches(PASSWORD_REGEX, {
message: i18n('validation.Matches', { path: 'general', property: 'auth.confirmPassword' }),
})
confirmPassword!: string;
@ApiProperty({ example: '111111' })
@IsNumberString(
{ no_symbols: true },

View File

@ -1,71 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsDateString,
IsEmail,
IsEnum,
IsNotEmpty,
IsNumberString,
IsOptional,
IsString,
Matches,
MaxLength,
MinLength,
} from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { COUNTRY_CODE_REGEX } from '~/auth/constants';
import { CountryIso } from '~/common/enums';
import { DEFAULT_OTP_LENGTH } from '~/common/modules/otp/constants';
import { IsAbove18, IsValidPhoneNumber } from '~/core/decorators/validations';
export class VerifyUserV2RequestDto {
@ApiProperty({ example: '+962' })
@Matches(COUNTRY_CODE_REGEX, {
message: i18n('validation.Matches', { path: 'general', property: 'auth.countryCode' }),
})
countryCode!: string;
@ApiProperty({ example: '787259134' })
@IsValidPhoneNumber({
message: i18n('validation.IsValidPhoneNumber', { path: 'general', property: 'auth.phoneNumber' }),
})
phoneNumber!: string;
@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' })
@IsEnum(CountryIso, {
message: i18n('validation.IsEnum', { path: 'general', property: 'customer.countryOfResidence' }),
})
@IsOptional()
countryOfResidence: CountryIso = CountryIso.SAUDI_ARABIA;
@ApiProperty({ example: 'test@test.com' })
@IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }) })
@IsOptional()
email!: 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;
}

View File

@ -1,7 +1,7 @@
export class SendForgetPasswordOtpResponseDto {
email!: string;
maskedNumber!: string;
constructor(email: string) {
this.email = email;
constructor(maskedNumber: string) {
this.maskedNumber = maskedNumber;
}
}

View File

@ -2,9 +2,9 @@ import { ApiProperty } from '@nestjs/swagger';
export class SendRegisterOtpResponseDto {
@ApiProperty()
email!: string;
maskedNumber!: string;
constructor(email: string) {
this.email = email;
constructor(maskedNumber: string) {
this.maskedNumber = maskedNumber;
}
}

View File

@ -1,6 +1,4 @@
export enum GrantType {
PASSWORD = 'PASSWORD',
BIOMETRIC = 'BIOMETRIC',
GOOGLE = 'GOOGLE',
APPLE = 'APPLE',
}

View File

@ -3,7 +3,6 @@ import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { Request } from 'express';
import { ArrayContains } from 'typeorm';
import { CacheService } from '~/common/modules/cache/services';
import { OtpScope, OtpType } from '~/common/modules/otp/enums';
import { OtpService } from '~/common/modules/otp/services';
@ -12,23 +11,15 @@ import { DeviceService, UserService, UserTokenService } from '~/user/services';
import { User } from '../../user/entities';
import { PASSCODE_REGEX } from '../constants';
import {
AppleLoginRequestDto,
CreateUnverifiedUserRequestDto,
CreateUnverifiedUserV2RequestDto,
DisableBiometricRequestDto,
EnableBiometricRequestDto,
ForgetPasswordRequestDto,
GoogleLoginRequestDto,
LoginRequestDto,
SendForgetPasswordOtpRequestDto,
SendLoginOtpRequestDto,
SetEmailRequestDto,
setJuniorPasswordRequestDto,
VerifyLoginOtpRequestDto,
VerifyUserRequestDto,
VerifyUserV2RequestDto,
} from '../dtos/request';
import { Roles } from '../enums';
import { IJwtPayload, ILoginResponse } from '../interfaces';
import { removePadding, verifySignature } from '../utils';
import { Oauth2Service } from './oauth2.service';
@ -49,19 +40,8 @@ export class AuthService {
private readonly cacheService: CacheService,
private readonly oauth2Service: Oauth2Service,
) {}
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.email,
scope: OtpScope.VERIFY_EMAIL,
otpType: OtpType.EMAIL,
});
}
async sendPhoneRegisterOtp(body: CreateUnverifiedUserV2RequestDto) {
if (body.email) {
const isEmailUsed = await this.userService.findUser({ email: body.email, isEmailVerified: true });
if (isEmailUsed) {
@ -70,8 +50,13 @@ export class AuthService {
}
}
if (body.password !== body.confirmPassword) {
this.logger.error('Password and confirm password do not match');
throw new BadRequestException('AUTH.PASSWORD_MISMATCH');
}
this.logger.log(`Sending OTP to ${body.countryCode + body.phoneNumber}`);
const user = await this.userService.findOrCreateByPhoneNumber(body);
const user = await this.userService.findOrCreateUser(body);
return this.otpService.generateAndSendOtp({
userId: user.id,
recipient: user.fullPhoneNumber,
@ -81,36 +66,6 @@ export class AuthService {
}
async verifyUser(verifyUserDto: VerifyUserRequestDto): Promise<[ILoginResponse, User]> {
this.logger.log(`Verifying user with email ${verifyUserDto.email}`);
const user = await this.userService.findUserOrThrow({ email: verifyUserDto.email });
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_EMAIL,
otpType: OtpType.EMAIL,
value: verifyUserDto.otp,
});
if (!isOtpValid) {
this.logger.error(`Invalid OTP for user with email ${verifyUserDto.email}`);
throw new BadRequestException('OTP.INVALID_OTP');
}
await this.userService.verifyUser(user.id, verifyUserDto);
await user.reload();
const tokens = await this.generateAuthToken(user);
this.logger.log(`User with email ${verifyUserDto.email} verified successfully`);
return [tokens, user];
}
async verifyUserV2(verifyUserDto: VerifyUserV2RequestDto): Promise<[ILoginResponse, User]> {
this.logger.log(`Verifying user with phone number ${verifyUserDto.countryCode + verifyUserDto.phoneNumber}`);
const user = await this.userService.findUserOrThrow({
phoneNumber: verifyUserDto.phoneNumber,
@ -134,7 +89,7 @@ export class AuthService {
throw new BadRequestException('OTP.INVALID_OTP');
}
await this.userService.verifyUserV2(user.id, verifyUserDto);
await this.userService.verifyUser(user.id, verifyUserDto);
await user.reload();
@ -143,81 +98,6 @@ export class AuthService {
return [tokens, user];
}
async setEmail(userId: string, { email }: SetEmailRequestDto) {
this.logger.log(`Setting email for user with id ${userId}`);
const user = await this.userService.findUserOrThrow({ id: userId });
if (user.email) {
this.logger.error(`Email already set for user with id ${userId}`);
throw new BadRequestException('USER.EMAIL_ALREADY_SET');
}
const existingUser = await this.userService.findUser({ email });
if (existingUser) {
this.logger.error(`Email ${email} already taken`);
throw new BadRequestException('USER.EMAIL_ALREADY_TAKEN');
}
return this.userService.setEmail(userId, email);
}
async setPasscode(userId: string, passcode: string) {
this.logger.log(`Setting passcode for user with id ${userId}`);
const user = await this.userService.findUserOrThrow({ id: userId });
if (user.password) {
this.logger.error(`Passcode already set for user with id ${userId}`);
throw new BadRequestException('AUTH.PASSCODE_ALREADY_SET');
}
const salt = bcrypt.genSaltSync(SALT_ROUNDS);
const hashedPasscode = bcrypt.hashSync(passcode, salt);
await this.userService.setPasscode(userId, hashedPasscode, salt);
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 });
// 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 });
// 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);
// 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,
// });
// if (!isOtpValid) {
// this.logger.error(`Invalid OTP for user with id ${userId}`);
// throw new BadRequestException('OTP.INVALID_OTP');
// }
// return this.userService.verifyPhoneNumber(userId);
// }
async enableBiometric(userId: string, { deviceId, publicKey }: EnableBiometricRequestDto) {
this.logger.log(`Enabling biometric for user with id ${userId}`);
const device = await this.deviceService.findUserDeviceById(deviceId, userId);
@ -255,48 +135,49 @@ export class AuthService {
return this.deviceService.updateDevice(deviceId, { publicKey: null });
}
async sendForgetPasswordOtp({ email }: SendForgetPasswordOtpRequestDto) {
this.logger.log(`Sending forget password OTP to ${email}`);
const user = await this.userService.findUserOrThrow({ email });
if (!user.isProfileCompleted) {
this.logger.error(`Profile not completed for user with email ${email}`);
throw new BadRequestException('USER.PROFILE_NOT_COMPLETED');
}
async sendForgetPasswordOtp({ countryCode, phoneNumber }: SendForgetPasswordOtpRequestDto) {
this.logger.log(`Sending forget password OTP to ${countryCode + phoneNumber}`);
const user = await this.userService.findUserOrThrow({ countryCode, phoneNumber });
return this.otpService.generateAndSendOtp({
userId: user.id,
recipient: user.email,
recipient: user.fullPhoneNumber,
scope: OtpScope.FORGET_PASSWORD,
otpType: OtpType.EMAIL,
otpType: OtpType.SMS,
});
}
async verifyForgetPasswordOtp({ email, otp, password, confirmPassword }: ForgetPasswordRequestDto) {
this.logger.log(`Verifying forget password OTP for ${email}`);
const user = await this.userService.findUserOrThrow({ email });
if (!user.isProfileCompleted) {
this.logger.error(`Profile not completed for user with email ${email}`);
throw new BadRequestException('USER.PROFILE_NOT_COMPLETED');
}
async verifyForgetPasswordOtp({
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 isOtpValid = await this.otpService.verifyOtp({
userId: user.id,
scope: OtpScope.FORGET_PASSWORD,
otpType: OtpType.EMAIL,
otpType: OtpType.SMS,
value: otp,
});
if (!isOtpValid) {
this.logger.error(`Invalid OTP for user with email ${email}`);
this.logger.error(`Invalid OTP for user with phone number ${user.fullPhoneNumber}`);
throw new BadRequestException('OTP.INVALID_OTP');
}
this.validatePassword(password, confirmPassword, user);
if (password !== confirmPassword) {
this.logger.error('Password and confirm password do not match');
throw new BadRequestException('AUTH.PASSWORD_MISMATCH');
}
const hashedPassword = bcrypt.hashSync(password, user.salt);
await this.userService.setPasscode(user.id, hashedPassword, user.salt);
this.logger.log(`Passcode updated successfully for user with email ${email}`);
await this.userService.setPassword(user.id, hashedPassword, user.salt);
this.logger.log(`Passcode updated successfully for user with phone number ${user.fullPhoneNumber}`);
}
async setJuniorPasscode(body: setJuniorPasswordRequestDto) {
@ -304,7 +185,7 @@ export class AuthService {
const juniorId = await this.userTokenService.validateToken(body.qrToken, UserType.JUNIOR);
const salt = bcrypt.genSaltSync(SALT_ROUNDS);
const hashedPasscode = bcrypt.hashSync(body.passcode, salt);
await this.userService.setPasscode(juniorId!, hashedPasscode, salt);
await this.userService.setPassword(juniorId!, hashedPasscode, salt);
await this.userTokenService.invalidateToken(body.qrToken);
this.logger.log(`Passcode set successfully for junior with id ${juniorId}`);
}
@ -345,40 +226,6 @@ export class AuthService {
}
}
async sendLoginOtp({ email }: SendLoginOtpRequestDto) {
const user = await this.userService.findUserOrThrow({ email });
this.logger.log(`Sending login OTP to ${email}`);
return this.otpService.generateAndSendOtp({
recipient: email,
scope: OtpScope.LOGIN,
otpType: OtpType.EMAIL,
userId: user.id,
});
}
async verifyLoginOtp({ email, otp }: VerifyLoginOtpRequestDto): Promise<[ILoginResponse, User]> {
const user = await this.userService.findUserOrThrow({ email });
this.logger.log(`Verifying login OTP for ${email}`);
const isOtpValid = await this.otpService.verifyOtp({
otpType: OtpType.EMAIL,
scope: OtpScope.LOGIN,
userId: user.id,
value: otp,
});
if (!isOtpValid) {
this.logger.error(`Invalid OTP for user with email ${email}`);
throw new BadRequestException('OTP.INVALID_OTP');
}
this.logger.log(`Login OTP verified successfully for ${email}`);
const token = await this.generateAuthToken(user);
return [token, user];
}
logout(req: Request) {
this.logger.log('Logging out');
const accessToken = req.headers.authorization?.split(' ')[1] as string;
@ -386,147 +233,68 @@ export class AuthService {
return this.cacheService.set(accessToken, 'BLACKLISTED', expiryInTtl);
}
private async loginWithPassword(loginDto: LoginRequestDto): Promise<[ILoginResponse, User]> {
const user = await this.userService.findUserOrThrow({ email: loginDto.email });
async loginWithPassword(loginDto: LoginRequestDto): Promise<[ILoginResponse, User]> {
const user = await this.userService.findUser({
countryCode: loginDto.countryCode,
phoneNumber: loginDto.phoneNumber,
});
this.logger.log(`validating password for user with email ${loginDto.email}`);
if (!user) {
this.logger.error(`User not found with phone number ${loginDto.countryCode + loginDto.phoneNumber}`);
throw new UnauthorizedException('AUTH.INVALID_CREDENTIALS');
}
if (!user.password) {
this.logger.error(`Password not set for user with phone number ${loginDto.countryCode + loginDto.phoneNumber}`);
throw new UnauthorizedException('AUTH.PHONE_NUMBER_NOT_VERIFIED');
}
this.logger.log(`validating password for user with phone ${loginDto.countryCode + loginDto.phoneNumber}`);
const isPasswordValid = bcrypt.compareSync(loginDto.password, user.password);
if (!isPasswordValid) {
this.logger.error(`Invalid password for user with email ${loginDto.email}`);
this.logger.error(`Invalid password for user with phone ${loginDto.countryCode + loginDto.phoneNumber}`);
throw new UnauthorizedException('AUTH.INVALID_CREDENTIALS');
}
const tokens = await this.generateAuthToken(user);
this.logger.log(`Password validated successfully for user with email ${loginDto.email}`);
this.logger.log(`Password validated successfully for user`);
return [tokens, user];
}
private async loginWithBiometric(loginDto: LoginRequestDto, deviceId: string): Promise<[ILoginResponse, User]> {
const user = await this.userService.findUserOrThrow({ email: loginDto.email });
// private async loginWithBiometric(loginDto: LoginRequestDto, deviceId: string): Promise<[ILoginResponse, User]> {
// const user = await this.userService.findUserOrThrow({ email: loginDto.email });
this.logger.log(`validating biometric for user with email ${loginDto.email}`);
const device = await this.deviceService.findUserDeviceById(deviceId, user.id);
// this.logger.log(`validating biometric for user with email ${loginDto.email}`);
// const device = await this.deviceService.findUserDeviceById(deviceId, user.id);
if (!device) {
this.logger.error(`Device not found for user with email ${loginDto.email} and device id ${deviceId}`);
throw new UnauthorizedException('AUTH.DEVICE_NOT_FOUND');
}
// if (!device) {
// this.logger.error(`Device not found for user with email ${loginDto.email} and device id ${deviceId}`);
// throw new UnauthorizedException('AUTH.DEVICE_NOT_FOUND');
// }
if (!device.publicKey) {
this.logger.error(`Biometric not enabled for user with email ${loginDto.email}`);
throw new UnauthorizedException('AUTH.BIOMETRIC_NOT_ENABLED');
}
// if (!device.publicKey) {
// this.logger.error(`Biometric not enabled for user with email ${loginDto.email}`);
// throw new UnauthorizedException('AUTH.BIOMETRIC_NOT_ENABLED');
// }
const cleanToken = removePadding(loginDto.signature);
const isValidToken = await verifySignature(
device.publicKey,
cleanToken,
`${user.email} - ${device.deviceId}`,
'SHA1',
);
// const cleanToken = removePadding(loginDto.signature);
// const isValidToken = await verifySignature(
// device.publicKey,
// cleanToken,
// `${user.email} - ${device.deviceId}`,
// 'SHA1',
// );
if (!isValidToken) {
this.logger.error(`Invalid biometric for user with email ${loginDto.email}`);
throw new UnauthorizedException('AUTH.INVALID_BIOMETRIC');
}
// if (!isValidToken) {
// this.logger.error(`Invalid biometric for user with email ${loginDto.email}`);
// throw new UnauthorizedException('AUTH.INVALID_BIOMETRIC');
// }
const tokens = await this.generateAuthToken(user);
this.logger.log(`Biometric validated successfully for user with email ${loginDto.email}`);
return [tokens, user];
}
async loginWithGoogle(loginDto: GoogleLoginRequestDto): Promise<[ILoginResponse, User]> {
const {
email,
sub,
given_name: firstName,
family_name: lastName,
} = await this.oauth2Service.verifyGoogleToken(loginDto.googleToken);
const [existingUser, isJunior, existingUserWithEmail] = await Promise.all([
this.userService.findUser({ googleId: sub }),
this.userService.findUser({ email, roles: ArrayContains([Roles.JUNIOR]) }),
this.userService.findUser({ email }),
]);
if (isJunior) {
this.logger.error(`User with email ${email} is an already registered junior`);
throw new BadRequestException('USER.JUNIOR_UPGRADE_NOT_SUPPORTED_YET');
}
if (!existingUser && existingUserWithEmail) {
this.logger.error(`User with email ${email} already exists adding google id to existing user`);
await this.userService.updateUser(existingUserWithEmail.id, { googleId: sub });
const tokens = await this.generateAuthToken(existingUserWithEmail);
return [tokens, existingUserWithEmail];
}
if (!existingUser && !existingUserWithEmail) {
this.logger.debug(`User with google id ${sub} or email ${email} not found, creating new user`);
const user = await this.userService.createGoogleUser(sub, email, firstName, lastName);
const tokens = await this.generateAuthToken(user);
return [tokens, user];
}
const tokens = await this.generateAuthToken(existingUser!);
return [tokens, existingUser!];
}
async loginWithApple(loginDto: AppleLoginRequestDto): Promise<[ILoginResponse, User]> {
const { sub, email } = await this.oauth2Service.verifyAppleToken(loginDto.appleToken);
const [existingUserWithSub, isJunior] = await Promise.all([
this.userService.findUser({ appleId: sub }),
this.userService.findUser({ email, roles: ArrayContains([Roles.JUNIOR]) }),
]);
if (isJunior) {
this.logger.error(`User with apple id ${sub} is an already registered junior`);
throw new BadRequestException('USER.JUNIOR_UPGRADE_NOT_SUPPORTED_YET');
}
if (email) {
const existingUserWithEmail = await this.userService.findUser({ email });
if (existingUserWithEmail && !existingUserWithSub) {
{
this.logger.error(`User with email ${email} already exists adding apple id to existing user`);
await this.userService.updateUser(existingUserWithEmail.id, { appleId: sub });
const tokens = await this.generateAuthToken(existingUserWithEmail);
return [tokens, existingUserWithEmail];
}
}
}
if (!existingUserWithSub) {
// Apple only provides email if user authorized zod for the first time
if (!email || !loginDto.additionalData) {
this.logger.error(`User authorized zod before but his email is not stored in the database`);
throw new BadRequestException('AUTH.APPLE_RE-CONSENT_REQUIRED');
}
this.logger.debug(`User with apple id ${sub} not found, creating new user`);
const user = await this.userService.createAppleUser(
sub,
email,
loginDto.additionalData.firstName,
loginDto.additionalData.lastName,
);
const tokens = await this.generateAuthToken(user);
return [tokens, user];
}
const tokens = await this.generateAuthToken(existingUserWithSub);
this.logger.log(`User with apple id ${sub} logged in successfully`);
return [tokens, existingUserWithSub];
}
// const tokens = await this.generateAuthToken(user);
// this.logger.log(`Biometric validated successfully for user with email ${loginDto.email}`);
// return [tokens, user];
// }
private async generateAuthToken(user: User) {
this.logger.log(`Generating auth token for user with id ${user.id}`);
@ -550,19 +318,4 @@ export class AuthService {
this.logger.log(`Auth token generated successfully for user with id ${user.id}`);
return { accessToken, refreshToken, expiresAt: new Date(this.jwtService.decode(accessToken).exp * ONE_THOUSAND) };
}
private validatePassword(password: string, confirmPassword: string, user: User) {
this.logger.log(`Validating password for user with id ${user.id}`);
if (password !== confirmPassword) {
this.logger.error(`Password mismatch for user with id ${user.id}`);
throw new BadRequestException('AUTH.PASSWORD_MISMATCH');
}
if (!PASSCODE_REGEX.test(password)) {
this.logger.error(`Invalid password for user with id ${user.id}`);
throw new BadRequestException('AUTH.INVALID_PASSCODE');
}
}
private validateGoogleToken(googleToken: string) {}
}

View File

@ -7,12 +7,7 @@ import { Transactional } from 'typeorm-transactional';
import { CountryIso } from '~/common/enums';
import { NotificationsService } from '~/common/modules/notification/services';
import { CustomerService } from '~/customer/services';
import {
CreateUnverifiedUserRequestDto,
CreateUnverifiedUserV2RequestDto,
VerifyUserRequestDto,
VerifyUserV2RequestDto,
} from '../../auth/dtos/request';
import { CreateUnverifiedUserRequestDto, VerifyUserRequestDto } from '../../auth/dtos/request';
import { Roles } from '../../auth/enums';
import {
CreateCheckerRequestDto,
@ -49,9 +44,9 @@ export class UserService {
return this.userRepository.update(userId, { email });
}
setPasscode(userId: string, passcode: string, salt: string) {
setPassword(userId: string, password: string, salt: string) {
this.logger.log(`Setting passcode for user ${userId}`);
return this.userRepository.update(userId, { password: passcode, salt, isProfileCompleted: true });
return this.userRepository.update(userId, { password, salt });
}
setPhoneNumber(userId: string, phoneNumber: string, countryCode: string) {
@ -60,23 +55,9 @@ export class UserService {
}
@Transactional()
async verifyUser(userId: string, body: VerifyUserRequestDto | VerifyUserV2RequestDto) {
this.logger.log(`Verifying user 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,
}),
]);
}
@Transactional()
async verifyUserV2(userId: string, body: VerifyUserV2RequestDto) {
async verifyUser(userId: string, body: VerifyUserRequestDto) {
const salt = bcrypt.genSaltSync(SALT_ROUNDS);
const hashedPassword = bcrypt.hashSync(body.password, salt);
this.logger.log(`Verifying user with id ${userId}`);
await Promise.all([
this.customerService.createGuardianCustomer(userId, {
@ -87,7 +68,8 @@ export class UserService {
}),
this.userRepository.update(userId, {
isPhoneVerified: true,
password: hashedPassword,
salt,
...(body.email && { email: body.email }),
}),
]);
@ -113,29 +95,6 @@ export class UserService {
@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 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.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 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 email ${body.email}`);
return user;
}
async findOrCreateByPhoneNumber(body: CreateUnverifiedUserV2RequestDto) {
this.logger.log(`Finding or creating user with phone number ${body.countryCode + body.phoneNumber}`);
const user = await this.userRepository.findOne({
phoneNumber: body.phoneNumber,
@ -167,25 +126,6 @@ export class UserService {
return user;
}
async findOrCreateByEmail(email: string) {
this.logger.log(`Finding or creating user with email ${email} `);
const user = await this.userRepository.findOne({ email });
if (!user) {
this.logger.log(`User with email ${email} not found, creating new user`);
return this.userRepository.createUser({ email, roles: [Roles.GUARDIAN] });
}
if (user && user.roles.includes(Roles.JUNIOR)) {
this.logger.error(`User with email ${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 email ${email} found successfully`);
return user;
}
async createUser(data: Partial<User>) {
this.logger.log(`Creating user with data ${JSON.stringify(data)}`);
const user = await this.userRepository.createUser(data);