import { BadRequestException, forwardRef, Inject, Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as bcrypt from 'bcrypt'; import moment from 'moment'; import { FindOptionsWhere } from 'typeorm'; import { Transactional } from 'typeorm-transactional'; import { CountryIso } from '~/common/enums'; import { NotificationsService } from '~/common/modules/notification/services'; import { CustomerService } from '~/customer/services'; import { CreateUnverifiedUserRequestDto, VerifyUserRequestDto } from '../../auth/dtos/request'; import { Roles } from '../../auth/enums'; import { CreateCheckerRequestDto, SetInternalPasswordRequestDto, UpdateNotificationsSettingsRequestDto, UserFiltersRequestDto, } from '../dtos/request'; import { User } from '../entities'; import { UserType } from '../enums'; import { UserRepository } from '../repositories'; import { DeviceService } from './device.service'; import { UserTokenService } from './user-token.service'; const SALT_ROUNDS = 10; @Injectable() export class UserService { private readonly logger = new Logger(UserService.name); private adminPortalUrl = this.configService.getOrThrow('ADMIN_PORTAL_URL'); constructor( private readonly userRepository: UserRepository, @Inject(forwardRef(() => NotificationsService)) private readonly notificationsService: NotificationsService, private readonly deviceService: DeviceService, private readonly userTokenService: UserTokenService, private readonly configService: ConfigService, private customerService: CustomerService, ) {} findUser(where: FindOptionsWhere | FindOptionsWhere[]) { this.logger.log(`finding user with where clause ${JSON.stringify(where)}`); return this.userRepository.findOne(where); } setEmail(userId: string, email: string) { this.logger.log(`Setting email ${email} for user ${userId}`); return this.userRepository.update(userId, { email }); } setPasscode(userId: string, passcode: string, salt: string) { this.logger.log(`Setting passcode for user ${userId}`); return this.userRepository.update(userId, { password: passcode, salt, isProfileCompleted: true }); } setPhoneNumber(userId: string, phoneNumber: string, countryCode: string) { this.logger.log(`Setting phone number ${phoneNumber} for user ${userId}`); return this.userRepository.update(userId, { phoneNumber, countryCode }); } @Transactional() async verifyUser(userId: string, body: VerifyUserRequestDto) { this.logger.log(`Verifying user email 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 }), ]); } findUsers(filters: UserFiltersRequestDto) { this.logger.log(`Getting users with filters ${JSON.stringify(filters)}`); return this.userRepository.findUsers(filters); } async findUserOrThrow(where: FindOptionsWhere) { this.logger.log(`Finding user with where clause ${JSON.stringify(where)}`); const user = await this.findUser(where); if (!user) { this.logger.error(`User with not found with where clause ${JSON.stringify(where)}`); throw new BadRequestException('USER.NOT_FOUND'); } this.logger.log(`User with where clause ${JSON.stringify(where)} found successfully`); return user; } @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 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) { this.logger.log(`Creating user with data ${JSON.stringify(data)}`); const user = await this.userRepository.createUser(data); this.logger.log(`User with data ${JSON.stringify(data)} created successfully`); return user; } @Transactional() async createChecker(data: CreateCheckerRequestDto) { const existingUser = await this.userRepository.findOne([ { email: data.email }, { phoneNumber: data.phoneNumber, countryCode: data.countryCode }, ]); if (existingUser) { throw new BadRequestException('USER.ALREADY_EXISTS'); } const user = await this.createUser({ ...data, roles: [Roles.CHECKER], isProfileCompleted: true, }); const ONE_DAY = moment().add(1, 'day').toDate(); const token = await this.userTokenService.generateToken(user.id, ONE_DAY); await this.sendCheckerAccountCreatedEmail(data.email, token); return user; } async updateNotificationSettings(userId: string, data: UpdateNotificationsSettingsRequestDto, deviceId?: string) { this.logger.log(`Updating notification settings for user ${userId} with data ${JSON.stringify(data)}`); if (data.isPushEnabled && !data.fcmToken) { throw new BadRequestException('USER.FCM_TOKEN_REQUIRED'); } if (data.isPushEnabled && !deviceId) { throw new BadRequestException('DEVICE_ID_REQUIRED'); } if (data.isPushEnabled && deviceId && data.fcmToken) { await this.deviceService.updateDevice(deviceId, { fcmToken: data.fcmToken, userId }); } await this.userRepository.update(userId, { isPushEnabled: data.isPushEnabled, isEmailEnabled: data.isEmailEnabled, isSmsEnabled: data.isSmsEnabled, }); } async createGoogleUser(googleId: string, email: string, firstName: string, lastName: string) { this.logger.log(`Creating google user with googleId ${googleId} and email ${email}`); const user = await this.userRepository.createUser({ googleId, email, roles: [Roles.GUARDIAN], isEmailVerified: true, }); await this.customerService.createGuardianCustomer(user.id, { firstName, lastName, countryOfResidence: CountryIso.SAUDI_ARABIA, }); return this.findUserOrThrow({ id: user.id }); } async createAppleUser(appleId: string, email: string, firstName: string, lastName: string) { this.logger.log(`Creating apple user with appleId ${appleId} and email ${email}`); const user = await this.userRepository.createUser({ appleId, email, roles: [Roles.GUARDIAN], isEmailVerified: true, }); await this.customerService.createGuardianCustomer(user.id, { firstName, lastName, countryOfResidence: CountryIso.SAUDI_ARABIA, }); return this.findUserOrThrow({ id: user.id }); } async setCheckerPassword(data: SetInternalPasswordRequestDto) { const userId = await this.userTokenService.validateToken(data.token, UserType.CHECKER); this.logger.log(`Setting password for checker ${userId}`); const salt = bcrypt.genSaltSync(SALT_ROUNDS); const hashedPasscode = bcrypt.hashSync(data.password, salt); return this.userRepository.update(userId, { password: hashedPasscode, salt, isProfileCompleted: true }); } async updateUser(userId: string, data: Partial) { this.logger.log(`Updating user ${userId} with data ${JSON.stringify(data)}`); const { affected } = await this.userRepository.update(userId, data); if (affected === 0) { this.logger.error(`User with id ${userId} not found`); throw new BadRequestException('USER.NOT_FOUND'); } } private sendCheckerAccountCreatedEmail(email: string, token: string) { return this.notificationsService.sendEmailAsync({ to: email, template: 'user-invite', subject: 'Checker Account Created', data: { inviteLink: `${this.adminPortalUrl}?token=${token}` }, }); } }