Files
backend/src/auth/services/user-auth.service.ts
ZaydSkaff e5970c02c1 SP-1757, SP-1758, SP-1809, SP-1810: Feat/implement booking (#469)
* fix: commission device API

* task: add create booking API

* add get All api for dashboard & mobile

* add Find APIs for bookings

* implement sending email updates on update bookable space

* move email interfaces to separate files
2025-07-15 10:11:36 +03:00

358 lines
11 KiB
TypeScript

import { RoleType } from '@app/common/constants/role.type.enum';
import { differenceInSeconds } from '@app/common/helper/differenceInSeconds';
import {
BadRequestException,
ForbiddenException,
Injectable,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as argon2 from 'argon2';
import { RoleService } from 'src/role/services';
import { LessThan, MoreThan } from 'typeorm';
import { AuthService } from '../../../libs/common/src/auth/services/auth.service';
import { OtpType } from '../../../libs/common/src/constants/otp-type.enum';
import { HelperHashService } from '../../../libs/common/src/helper/services';
import { UserSessionRepository } from '../../../libs/common/src/modules/session/repositories/session.repository';
import { UserEntity } from '../../../libs/common/src/modules/user/entities/user.entity';
import { UserRepository } from '../../../libs/common/src/modules/user/repositories';
import { UserOtpRepository } from '../../../libs/common/src/modules/user/repositories/user.repository';
import { EmailService } from '../../../libs/common/src/util/email/email.service';
import { ForgetPasswordDto, UserOtpDto, VerifyOtpDto } from '../dtos';
import { UserSignUpDto } from '../dtos/user-auth.dto';
import { UserLoginDto } from '../dtos/user-login.dto';
@Injectable()
export class UserAuthService {
constructor(
private readonly userRepository: UserRepository,
private readonly sessionRepository: UserSessionRepository,
private readonly otpRepository: UserOtpRepository,
private readonly helperHashService: HelperHashService,
private readonly authService: AuthService,
private readonly emailService: EmailService,
private readonly roleService: RoleService,
private readonly configService: ConfigService,
) {}
async signUp(
userSignUpDto: UserSignUpDto,
clientUuid?: string,
): Promise<UserEntity> {
const findUser = await this.findUser(userSignUpDto.email);
if (findUser) {
if (!findUser.isActive) {
throw new BadRequestException('User is not active');
}
throw new BadRequestException('User already registered with given email');
}
const salt = this.helperHashService.randomSalt(10); // Hash the password using bcrypt
const hashedPassword = await this.helperHashService.bcrypt(
userSignUpDto.password,
salt,
);
try {
const { regionUuid, hasAcceptedAppAgreement, ...rest } = userSignUpDto;
if (!hasAcceptedAppAgreement) {
throw new BadRequestException('Please accept the terms and conditions');
}
const spaceMemberRole = await this.roleService.findRoleByType(
RoleType.SPACE_MEMBER,
);
const user = await this.userRepository.save({
...rest,
appAgreementAcceptedAt: new Date(),
hasAcceptedAppAgreement,
password: hashedPassword,
roleType: { uuid: spaceMemberRole.uuid },
client: { uuid: clientUuid },
region: regionUuid
? {
uuid: regionUuid,
}
: {
regionName: 'United Arab Emirates',
},
});
return user;
} catch (error) {
throw new BadRequestException(error.message || 'Failed to register user');
}
}
async findUser(email: string) {
return await this.userRepository.findOne({
where: {
email,
},
});
}
async forgetPassword(forgetPasswordDto: ForgetPasswordDto) {
const findUser = await this.findUser(forgetPasswordDto.email);
if (!findUser) {
throw new BadRequestException('User not found');
}
const salt = this.helperHashService.randomSalt(10);
const password = this.helperHashService.bcrypt(
forgetPasswordDto.password,
salt,
);
return await this.userRepository.update(
{ uuid: findUser.uuid },
{ password },
);
}
async userLogin(data: UserLoginDto) {
try {
let user: Omit<UserEntity, 'password'>;
if (data.googleCode) {
const googleUserData = await this.authService.login({
googleCode: data.googleCode,
});
const userExists = await this.userRepository.exists({
where: {
email: googleUserData['email'],
},
});
user = await this.userRepository.findOne({
where: {
email: googleUserData['email'],
},
});
if (!userExists) {
await this.signUp({
email: googleUserData['email'],
firstName: googleUserData['given_name'],
lastName: googleUserData['family_name'],
password: googleUserData['email'],
hasAcceptedAppAgreement: true,
});
}
data.email = googleUserData['email'];
data.password = googleUserData['password'];
}
if (!data.googleCode) {
user = await this.authService.validateUser(
data.email,
data.password,
data.regionUuid,
data.platform,
);
}
const session = await Promise.all([
await this.sessionRepository.update(
{ userId: user?.['id'] },
{
isLoggedOut: true,
},
),
await this.authService.createSession({
userId: user.uuid,
loginTime: new Date(),
isLoggedOut: false,
}),
]);
const res = await this.authService.login({
email: user.email,
userId: user.uuid,
uuid: user.uuid,
role: user.roleType,
hasAcceptedWebAgreement: user.hasAcceptedWebAgreement,
hasAcceptedAppAgreement: user.hasAcceptedAppAgreement,
project: user.project,
sessionId: session[1].uuid,
bookingPoints: user.bookingPoints,
});
return res;
} catch (error) {
throw new BadRequestException(error.message || 'Invalid credentials');
}
}
async findOneById(id: string): Promise<UserEntity> {
return await this.userRepository.findOne({ where: { uuid: id } });
}
async generateOTP(data: UserOtpDto): Promise<{
otpCode: string;
cooldown: number;
}> {
try {
const otpLimiter = new Date();
otpLimiter.setDate(
otpLimiter.getDate() - this.configService.get<number>('OTP_LIMITER'),
);
const user = await this.userRepository.findOne({
where: {
region: data.regionUuid ? { uuid: data.regionUuid } : undefined,
email: data.email,
isUserVerified: data.type === OtpType.PASSWORD ? true : undefined,
},
});
if (!user) {
throw new BadRequestException('User not found');
}
await this.otpRepository.softDelete({
email: data.email,
type: data.type,
});
await this.otpRepository.delete({
email: data.email,
type: data.type,
createdAt: LessThan(otpLimiter),
});
const countOfOtp = await this.otpRepository.count({
withDeleted: true,
where: {
email: data.email,
type: data.type,
createdAt: MoreThan(otpLimiter),
},
});
const lastOtp = await this.otpRepository.findOne({
where: { email: data.email, type: data.type },
order: { createdAt: 'DESC' },
withDeleted: true,
});
let cooldown = 30 * Math.pow(2, countOfOtp - 1);
if (lastOtp) {
const now = new Date();
const timeSinceLastOtp = differenceInSeconds(now, lastOtp.createdAt);
if (timeSinceLastOtp < cooldown) {
throw new BadRequestException({
message: `Please wait ${cooldown - timeSinceLastOtp} more seconds before requesting a new OTP.`,
data: {
cooldown: cooldown - timeSinceLastOtp,
},
});
}
}
const otpCode = Math.floor(100000 + Math.random() * 900000).toString();
const expiryTime = new Date();
expiryTime.setMinutes(expiryTime.getMinutes() + 10);
await this.otpRepository.save({
email: data.email,
otpCode,
expiryTime,
type: data.type,
});
const countOfOtpToReturn = await this.otpRepository.count({
withDeleted: true,
where: {
email: data.email,
type: data.type,
createdAt: MoreThan(otpLimiter),
},
});
cooldown = 30 * Math.pow(2, countOfOtpToReturn - 1);
const [otp1, otp2, otp3, otp4, otp5, otp6] = otpCode.split('');
await this.emailService.sendOtpEmailWithTemplate(data.email, {
name: user.firstName,
otp1,
otp2,
otp3,
otp4,
otp5,
otp6,
});
return { otpCode, cooldown };
} catch (error) {
if (error instanceof BadRequestException) {
throw error;
}
console.error('OTP generation error:', error);
throw new BadRequestException(
'An unexpected error occurred while generating the OTP.',
);
}
}
async verifyOTP(
data: VerifyOtpDto,
fromNewPassword: boolean = false,
): Promise<boolean> {
const otp = await this.otpRepository.findOne({
where: { email: data.email, type: data.type, otpCode: data.otpCode },
});
if (!otp) {
const user = await this.userRepository.findOne({
where: {
email: data.email,
},
});
if (!user) {
throw new BadRequestException('this email is not registered');
}
throw new BadRequestException('You entered wrong otp');
}
if (otp.otpCode !== data.otpCode) {
throw new BadRequestException('You entered wrong otp');
}
if (otp.expiryTime < new Date()) {
await this.otpRepository.delete(otp.uuid);
throw new BadRequestException('OTP expired');
}
if (fromNewPassword) {
await this.otpRepository.delete(otp.uuid);
}
if (data.type == OtpType.VERIFICATION) {
await this.userRepository.update(
{ email: data.email },
{ isUserVerified: true },
);
}
return true;
}
async userList(): Promise<UserEntity[]> {
return await this.userRepository.find({
where: { isActive: true },
select: {
firstName: true,
lastName: true,
email: true,
isActive: true,
},
});
}
async refreshToken(
userId: string,
refreshToken: string,
type: string,
sessionId: string,
) {
const user = await this.userRepository.findOne({ where: { uuid: userId } });
if (!user || !user.refreshToken)
throw new ForbiddenException('Access Denied');
const refreshTokenMatches = await argon2.verify(
user.refreshToken,
refreshToken,
);
if (!refreshTokenMatches) throw new ForbiddenException('Access Denied');
const tokens = await this.authService.getTokens({
email: user.email,
userId: user.uuid,
uuid: user.uuid,
type,
bookingPoints: user.bookingPoints,
sessionId,
});
await this.authService.updateRefreshToken(user.uuid, tokens.refreshToken);
return tokens;
}
}