feat: handle google login

This commit is contained in:
Abdalhamid Alhamad
2025-01-09 15:14:09 +03:00
parent db02a28b4d
commit 756e947c8a
10 changed files with 210 additions and 22 deletions

View File

@ -3,6 +3,7 @@ import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { Request } from 'express';
import { OAuth2Client } from 'google-auth-library';
import { CacheService } from '~/common/modules/cache/services';
import { OtpScope, OtpType } from '~/common/modules/otp/enums';
import { OtpService } from '~/common/modules/otp/services';
@ -30,6 +31,11 @@ const SALT_ROUNDS = 10;
@Injectable()
export class AuthService {
private readonly logger = new Logger(AuthService.name);
private readonly googleWebClientId = this.configService.getOrThrow('GOOGLE_WEB_CLIENT_ID');
private readonly googleAndroidClientId = this.configService.getOrThrow('GOOGLE_ANDROID_CLIENT_ID');
private readonly googleIosClientId = this.configService.getOrThrow('GOOGLE_IOS_CLIENT_ID');
private readonly client = new OAuth2Client();
constructor(
private readonly otpService: OtpService,
private readonly jwtService: JwtService,
@ -119,6 +125,47 @@ export class AuthService {
this.logger.log(`Passcode set successfully for user with id ${userId}`);
}
async setPhoneNumber(userId: string, { phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) {
const user = await this.userService.findUserOrThrow({ id: userId });
if (user.phoneNumber || user.countryCode) {
this.logger.error(`Phone number already set for user with id ${userId}`);
throw new BadRequestException('USERS.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('USERS.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('USERS.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);
@ -201,32 +248,38 @@ export class AuthService {
}
async login(loginDto: LoginRequestDto, deviceId: string): Promise<[ILoginResponse, User]> {
this.logger.log(`Logging in user with email ${loginDto.email}`);
const user = await this.userService.findUser({ email: loginDto.email });
let tokens;
let user: User;
let tokens: ILoginResponse;
if (!user) {
this.logger.error(`User with email ${loginDto.email} not found`);
throw new UnauthorizedException('AUTH.INVALID_CREDENTIALS');
if (loginDto.grantType === GrantType.GOOGLE) {
this.logger.log(`Logging in user with email ${loginDto.email} using google`);
[tokens, user] = await this.loginWithGoogle(loginDto);
}
if (loginDto.grantType === GrantType.APPLE) {
this.logger.log(`Logging in user with email ${loginDto.email} using apple`);
throw new BadRequestException('AUTH.APPLE_LOGIN_NOT_IMPLEMENTED');
}
if (loginDto.grantType === GrantType.PASSWORD) {
this.logger.log(`Logging in user with email ${loginDto.email} using password`);
tokens = await this.loginWithPassword(loginDto, user);
} else {
[tokens, user] = await this.loginWithPassword(loginDto);
}
if (loginDto.grantType === GrantType.BIOMETRIC) {
this.logger.log(`Logging in user with email ${loginDto.email} using biometric`);
tokens = await this.loginWithBiometric(loginDto, user, deviceId);
[tokens, user] = await this.loginWithBiometric(loginDto, deviceId);
}
await this.deviceService.updateDevice(deviceId, {
lastAccessOn: new Date(),
fcmToken: loginDto.fcmToken,
userId: user.id,
userId: user!.id,
});
this.logger.log(`User with email ${loginDto.email} logged in successfully`);
return [tokens, user];
return [tokens!, user!];
}
async setJuniorPasscode(body: setJuniorPasswordRequestDto) {
@ -268,7 +321,9 @@ export class AuthService {
return this.cacheService.set(accessToken, 'LOGOUT', expiryInTtl);
}
private async loginWithPassword(loginDto: LoginRequestDto, user: User): Promise<ILoginResponse> {
private async loginWithPassword(loginDto: LoginRequestDto): Promise<[ILoginResponse, User]> {
const user = await this.userService.findUserOrThrow({ email: loginDto.email });
this.logger.log(`validating password for user with email ${loginDto.email}`);
const isPasswordValid = bcrypt.compareSync(loginDto.password, user.password);
@ -279,10 +334,12 @@ export class AuthService {
const tokens = await this.generateAuthToken(user);
this.logger.log(`Password validated successfully for user with email ${loginDto.email}`);
return tokens;
return [tokens, user];
}
private async loginWithBiometric(loginDto: LoginRequestDto, user: User, deviceId: string): Promise<ILoginResponse> {
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);
@ -311,7 +368,36 @@ export class AuthService {
const tokens = await this.generateAuthToken(user);
this.logger.log(`Biometric validated successfully for user with email ${loginDto.email}`);
return tokens;
return [tokens, user];
}
private async loginWithGoogle(loginDto: LoginRequestDto): Promise<[ILoginResponse, User]> {
try {
const ticket = await this.client.verifyIdToken({
idToken: loginDto.googleToken,
audience: [this.googleWebClientId, this.googleAndroidClientId, this.googleIosClientId],
});
const payload = ticket.getPayload();
const existingUser = await this.userService.findUser({ googleId: payload?.sub });
if (!existingUser) {
this.logger.debug(`User with google id ${payload?.sub} not found, creating new user`);
const user = await this.userService.createGoogleUser(payload!.sub, payload!.email!);
const tokens = await this.generateAuthToken(user);
return [tokens, user];
}
const tokens = await this.generateAuthToken(existingUser);
return [tokens, existingUser];
} catch (error) {
this.logger.error(`Invalid google token`, error);
throw new UnauthorizedException();
}
}
private async generateAuthToken(user: User) {