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,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) {}
}