mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-08-25 05:42:27 +00:00
feat: add login and forget password and refactor code
This commit is contained in:
@ -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 {}
|
||||
|
@ -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);
|
||||
// }
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
@ -1,2 +1 @@
|
||||
export * from './auth.controller';
|
||||
export * from './auth.v2.controller';
|
||||
|
@ -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']) {}
|
||||
|
@ -1,4 +0,0 @@
|
||||
import { OmitType } from '@nestjs/swagger';
|
||||
import { VerifyUserV2RequestDto } from './verify-user.v2.request.dto';
|
||||
|
||||
export class CreateUnverifiedUserV2RequestDto extends OmitType(VerifyUserV2RequestDto, ['otp']) {}
|
@ -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' })
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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']) {}
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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 },
|
||||
|
@ -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;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
export class SendForgetPasswordOtpResponseDto {
|
||||
email!: string;
|
||||
maskedNumber!: string;
|
||||
|
||||
constructor(email: string) {
|
||||
this.email = email;
|
||||
constructor(maskedNumber: string) {
|
||||
this.maskedNumber = maskedNumber;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,4 @@
|
||||
export enum GrantType {
|
||||
PASSWORD = 'PASSWORD',
|
||||
BIOMETRIC = 'BIOMETRIC',
|
||||
GOOGLE = 'GOOGLE',
|
||||
APPLE = 'APPLE',
|
||||
}
|
||||
|
@ -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) {}
|
||||
}
|
||||
|
@ -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);
|
||||
|
Reference in New Issue
Block a user