feat: registration jounrey for parents

This commit is contained in:
Abdalhamid Alhamad
2024-12-05 11:14:18 +03:00
parent e4b69a406f
commit 2577f2dcac
97 changed files with 2269 additions and 17 deletions

View File

@ -0,0 +1,202 @@
import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { OtpScope, OtpType } from '~/common/modules/otp/enums';
import { OtpService } from '~/common/modules/otp/services';
import {
CreateUnverifiedUserRequestDto,
DisableBiometricRequestDto,
EnableBiometricRequestDto,
LoginRequestDto,
SetEmailRequestDto,
} from '../dtos/request';
import { VerifyUserRequestDto } from '../dtos/request/verify-user.request.dto';
import { User } from '../entities';
import { GrantType } from '../enums';
import { ILoginResponse } from '../interfaces';
import { removePadding, verifySignature } from '../utils';
import { DeviceService } from './device.service';
import { UserService } from './user.service';
const ONE_THOUSAND = 1000;
const SALT_ROUNDS = 10;
@Injectable()
export class AuthService {
constructor(
private readonly otpService: OtpService,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
private readonly userService: UserService,
private readonly deviceService: DeviceService,
) {}
async sendRegisterOtp({ phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) {
const user = await this.userService.findOrCreateUser({ phoneNumber, countryCode });
return this.otpService.generateAndSendOtp({
userId: user.id,
phoneNumber: user.phoneNumber,
scope: OtpScope.VERIFY_PHONE,
otpType: OtpType.SMS,
});
}
async verifyUser(verifyUserDto: VerifyUserRequestDto): Promise<[ILoginResponse, User]> {
const user = await this.userService.findUserOrThrow({ phoneNumber: verifyUserDto.phoneNumber });
if (user.isPasswordSet) {
throw new BadRequestException('USERS.PHONE_ALREADY_VERIFIED');
}
const isOtpValid = await this.otpService.verifyOtp({
userId: user.id,
scope: OtpScope.VERIFY_PHONE,
otpType: OtpType.SMS,
value: verifyUserDto.otp,
});
if (!isOtpValid) {
throw new BadRequestException('USERS.INVALID_OTP');
}
const updatedUser = await this.userService.verifyUserAndCreateCustomer(user);
const tokens = await this.generateAuthToken(updatedUser);
return [tokens, updatedUser];
}
async setEmail(userId: string, { email }: SetEmailRequestDto) {
const user = await this.userService.findUserOrThrow({ id: userId });
if (user.email) {
throw new BadRequestException('USERS.EMAIL_ALREADY_SET');
}
return this.userService.setEmail(userId, email);
}
async setPasscode(userId: string, passcode: string) {
const user = await this.userService.findUserOrThrow({ id: userId });
if (user.password) {
throw new BadRequestException('USERS.PASSCODE_ALREADY_SET');
}
const salt = bcrypt.genSaltSync(SALT_ROUNDS);
const hashedPasscode = bcrypt.hashSync(passcode, salt);
await this.userService.setPasscode(userId, hashedPasscode, salt);
}
async enableBiometric(userId: string, { deviceId, publicKey }: EnableBiometricRequestDto) {
const device = await this.deviceService.findUserDeviceById(deviceId, userId);
if (!device) {
return this.deviceService.createDevice({
deviceId,
userId,
publicKey,
});
}
if (device.publicKey) {
throw new BadRequestException('AUTH.BIOMETRIC_ALREADY_ENABLED');
}
return this.deviceService.updateDevice(deviceId, { publicKey });
}
async disableBiometric(userId: string, { deviceId }: DisableBiometricRequestDto) {
const device = await this.deviceService.findUserDeviceById(deviceId, userId);
if (!device) {
throw new BadRequestException('AUTH.DEVICE_NOT_FOUND');
}
if (!device.publicKey) {
throw new BadRequestException('AUTH.BIOMETRIC_ALREADY_DISABLED');
}
return this.deviceService.updateDevice(deviceId, { publicKey: null });
}
async login(loginDto: LoginRequestDto, deviceId: string): Promise<[ILoginResponse, User]> {
const user = await this.userService.findUser({ email: loginDto.email });
let tokens;
if (!user) {
throw new UnauthorizedException('AUTH.INVALID_CREDENTIALS');
}
if (loginDto.grantType === GrantType.PASSWORD) {
tokens = await this.loginWithPassword(loginDto, user);
} else {
tokens = await this.loginWithBiometric(loginDto, user, deviceId);
}
this.deviceService.updateDevice(deviceId, { lastAccessOn: new Date() });
return [tokens, user];
}
private async loginWithPassword(loginDto: LoginRequestDto, user: User): Promise<ILoginResponse> {
const isPasswordValid = bcrypt.compareSync(loginDto.password, user.password);
if (!isPasswordValid) {
throw new UnauthorizedException('AUTH.INVALID_CREDENTIALS');
}
const tokens = await this.generateAuthToken(user);
return tokens;
}
private async loginWithBiometric(loginDto: LoginRequestDto, user: User, deviceId: string): Promise<ILoginResponse> {
const device = await this.deviceService.findUserDeviceById(deviceId, user.id);
if (!device) {
throw new UnauthorizedException('AUTH.DEVICE_NOT_FOUND');
}
if (!device.publicKey) {
throw new UnauthorizedException('AUTH.BIOMETRIC_NOT_ENABLED');
}
const cleanToken = removePadding(loginDto.deviceToken);
const isValidToken = await verifySignature(
device.publicKey,
cleanToken,
`${user.email} - ${device.deviceId}`,
'SHA1',
);
if (!isValidToken) {
throw new UnauthorizedException('AUTH.INVALID_BIOMETRIC');
}
const tokens = await this.generateAuthToken(user);
return tokens;
}
private async generateAuthToken(user: User) {
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.sign(
{ sub: user.id, roles: user.roles },
{
expiresIn: this.configService.getOrThrow('JWT_ACCESS_TOKEN_EXPIRY'),
secret: this.configService.getOrThrow('JWT_ACCESS_TOKEN_SECRET'),
},
),
this.jwtService.sign(
{ sub: user.id, roles: user.roles },
{
expiresIn: this.configService.getOrThrow('JWT_REFRESH_TOKEN_EXPIRY'),
secret: this.configService.getOrThrow('JWT_REFRESH_TOKEN_SECRET'),
},
),
]);
return { accessToken, refreshToken, expiresAt: new Date(this.jwtService.decode(accessToken).exp * ONE_THOUSAND) };
}
}