import { createHmac, randomBytes } from "node:crypto"; import { BadRequestException, HttpStatus, Injectable, Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { FastifyReply } from "fastify"; import slugify from "slugify"; import { In, Not, QueryRunner } from "typeorm"; import { AdminMessage, AdsMessage, AuthMessage, CommonMessage, UserMessage } from "../../../common/enums/message.enum"; import { AddressService } from "../../address/providers/address.service"; import { CompleteRegistrationDto } from "../../auth/DTO/complete-register.dto"; import { RequestOtpDto } from "../../auth/DTO/request-otp.dto"; import { VerifyOtpDto } from "../../auth/DTO/verify-otp.dto"; import { UserSettingsService } from "../../settings/providers/user-settings.service"; import { CacheService } from "../../utils/providers/cache.service"; import { EmailService } from "../../utils/providers/email.service"; import { OTPService } from "../../utils/providers/otp.service"; import { PaginationUtils } from "../../utils/providers/pagination.utils"; import { PasswordService } from "../../utils/providers/password.service"; import { SmsService } from "../../utils/providers/sms.service"; import { WalletsService } from "../../wallets/providers/wallets.service"; import { ChangeEmailDto } from "../DTO/change-email.dto"; import { CheckValidityDTO } from "../DTO/check-validity.dto"; import { CreateAdminDto } from "../DTO/create-admin.dto"; import { CreateLegalUserDto } from "../DTO/create-legal-user.dto"; import { CreateRealUserDto } from "../DTO/create-real-user.dto"; import { CreateRoleDto } from "../DTO/create-role.dto"; import { SearchAdminQueryDto } from "../DTO/search-admins-query.dto"; import { SearchCustomersDto } from "../DTO/search-customers.dto"; import { SearchRolesQueryDto } from "../DTO/search-roles.dto"; import { UpdateProfileDto } from "../DTO/update-profile.dto"; import { CreateUserGroupDto } from "../DTO/user-group.dto"; import { EmailVerifyQueryDto } from "../DTO/verify-email-query.dto"; import { Role } from "../entities/role.entity"; import { User } from "../entities/user.entity"; import { FinancialType } from "../enums/financial-type.enum"; import { RoleEnum } from "../enums/role.enum"; import { ValidityType } from "../enums/validity-type.enum"; import { LegalUserRepository } from "../repositories/legal-user.repository"; import { PermissionsRepository } from "../repositories/permissions.repository"; import { RealUserRepository } from "../repositories/real-user.repository"; import { RoleRepository } from "../repositories/roles.repository"; import { UserGroupRepository } from "../repositories/user-group.repository"; import { UserRepository } from "../repositories/users.repository"; @Injectable() export class UsersService { private readonly logger = new Logger(UsersService.name); constructor( private readonly userRepository: UserRepository, private readonly userGroupRepository: UserGroupRepository, private readonly userSettingsService: UserSettingsService, private readonly walletsService: WalletsService, private readonly rolesRepository: RoleRepository, private readonly permissionsRepository: PermissionsRepository, private readonly passwordService: PasswordService, private readonly addressService: AddressService, private readonly realUserRepository: RealUserRepository, private readonly legalUserRepository: LegalUserRepository, private readonly emailService: EmailService, private readonly configService: ConfigService, private readonly otpService: OTPService, private readonly smsService: SmsService, private readonly cacheService: CacheService, ) {} /************************************************************ */ async findOneById(id: string) { const user = await this.userRepository.findOneBy({ id }); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); return { user }; } /************************************************************ */ async findOneByIdWithQueryRunner(id: string, queryRunner: QueryRunner) { const user = await queryRunner.manager.findOneBy(User, { id }); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); return user; } /************************************************************ */ async getMe(userId: string) { const user = await this.userRepository.findOne({ where: { id: userId }, relations: { roles: true } }); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); return { user }; } /************************************************************ */ async getRealUserData(userId: string) { const realUser = await this.realUserRepository.findOne({ where: { user: { id: userId } }, relations: { address: { city: { province: true } } }, }); return { realUser }; } /************************************************************ */ async getLegalUserData(userId: string) { const legalUser = await this.legalUserRepository.findOne({ where: { user: { id: userId } }, relations: { address: { city: { province: true } } }, }); return { legalUser }; } /************************************************************ */ async updateProfile(userId: string, updateProfileDto: UpdateProfileDto) { let city; const user = await this.userRepository.findOneBy({ id: userId }); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); if (updateProfileDto.cityId) { const { city: existCity } = await this.addressService.getCity(+updateProfileDto.cityId); if (!existCity) throw new BadRequestException(UserMessage.CITY_NOT_FOUND); city = existCity; } // if (updateProfileDto.userName) { updateProfileDto.userName = slugify(updateProfileDto.userName, { lower: true, trim: true }); const existUserName = await this.userRepository.findOneBy({ userName: updateProfileDto.userName, id: Not(userId) }); if (existUserName) throw new BadRequestException(UserMessage.USERNAME_EXIST); } await this.userRepository.save({ ...user, ...updateProfileDto, city }); // return { message: CommonMessage.UPDATE_SUCCESS, }; } /************************************************************ */ async changeEmail(userId: string, changeDto: ChangeEmailDto) { const { email } = changeDto; const user = await this.userRepository.findOneBy({ id: userId }); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); const existEmail = await this.userRepository.findOneBy({ email, id: Not(userId) }); if (existEmail) throw new BadRequestException(UserMessage.EMAIL_EXIST); const emailLink = await this.createEmailHashLink(email); await this.emailService.sendVerificationEmail(email, emailLink, `${user.firstName} ${user.lastName}`); if (user.email !== email) { user.email = email; user.emailVerified = false; } await this.userRepository.save(user); return { message: UserMessage.VERIFY_EMAIL_LINK_SENT, email, }; } /************************************************************ */ async verifyEmail(queryDto: EmailVerifyQueryDto, rep: FastifyReply) { const frontUrl = this.configService.getOrThrow("SITE_URL"); const { token, email } = queryDto; const user = await this.userRepository.findOneBy({ email, emailVerified: true }); if (user) throw new BadRequestException(UserMessage.EMAIL_EXIST); const cachedData = await this.cacheService.get(`EMAIL_VERIFY:${email}`); if (!cachedData) throw new BadRequestException(UserMessage.INVALID_EMAIL_TOKEN); const { hash, nonce } = JSON.parse(cachedData as string); const secret = this.configService.getOrThrow("EMAIL_SECRET"); if (hash !== token) throw new BadRequestException(UserMessage.INVALID_EMAIL_TOKEN); const expectedHash = createHmac("sha256", secret).update(`${email}:${nonce}`).digest("hex"); if (expectedHash !== hash) throw new BadRequestException(UserMessage.INVALID_EMAIL_TOKEN); const userToUpdate = await this.userRepository.findOneBy({ email }); if (!userToUpdate) throw new BadRequestException(UserMessage.USER_NOT_FOUND); userToUpdate.emailVerified = true; await this.userRepository.save(userToUpdate); await this.cacheService.del(`EMAIL_VERIFY:${email}`); return rep.status(HttpStatus.FOUND).redirect(`${frontUrl}/profile`); } /************************************************************ */ async changePhone(userId: string, requestOtpDto: RequestOtpDto) { const { phone } = requestOtpDto; const user = await this.userRepository.findOneBy({ id: userId }); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); const existPhone = await this.userRepository.findOneBy({ phone }); if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST); const existCode = await this.otpService.checkExistOtp(phone, "LOGIN"); if (existCode) { return { message: AuthMessage.OTP_ALREADY_SENT, ttlSecond: existCode, }; } const otpCode = await this.otpService.generateAndSetInCache(phone, "LOGIN"); // await this.smsService.sendSmsVerifyCode(phone, otpCode); this.logger.debug(`OTP sent to ${phone}: ${otpCode}`); return { message: AuthMessage.OTP_SENT, otpCode, }; } /************************************************************ */ async verifyPhone(verifyOtpDto: VerifyOtpDto, userId: string) { const { code, phone } = verifyOtpDto; const user = await this.checkUserLoginCredentialWithPhone(phone, code, userId); user.phone = phone; await this.userRepository.save(user); return { message: CommonMessage.UPDATE_SUCCESS, }; } /************************************************************ */ async findOneWithEmail(email: string): Promise { const user = await this.userRepository.findOneWithEmail(email); return user; } /************************************************************ */ async findOneWithPhone(phone: string): Promise { const user = await this.userRepository.findOneWithPhone(phone); return user; } /************************************************************ */ async createUser(registerDto: CompleteRegistrationDto, hashedPassword: string, queryRunner: QueryRunner): Promise { const role = await queryRunner.manager.findOneBy(Role, { name: RoleEnum.USER }); if (!role) throw new BadRequestException(UserMessage.ROLE_NOT_FOUND); const existUser = await queryRunner.manager.findOneBy(User, { nationalCode: registerDto.nationalCode }); if (existUser) throw new BadRequestException(UserMessage.NATIONAL_CODE_EXIST); const user = queryRunner.manager.create(User, { ...registerDto, password: hashedPassword, roles: [role], }); await queryRunner.manager.save(user); await this.userSettingsService.createUserSettings(user.id, queryRunner); await this.walletsService.createUserWallet(user.id, queryRunner); return user; } /************************************************************ */ async createAdmin(createDto: CreateAdminDto) { const existEmail = await this.userRepository.findOneBy({ email: createDto.email }); if (existEmail) throw new BadRequestException(UserMessage.EMAIL_EXIST); const existPhone = await this.userRepository.findOneBy({ phone: createDto.phone }); if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST); const role = await this.rolesRepository.findOne({ where: { id: createDto.roleId }, relations: { permissions: true } }); if (!role) throw new BadRequestException(AdsMessage.ROLE_NOT_FOUND); await this.rolesRepository.save(role); if (createDto.password !== createDto.repeatPassword) throw new BadRequestException(AdminMessage.PASSWORD_NOT_MATCH); const hashedPassword = await this.passwordService.hashPassword(createDto.password); const userName = slugify(`${createDto.firstName} ${Date.now().toString().slice(-5)}`, { lower: true, trim: true }); const adminUser = this.userRepository.create({ ...createDto, roles: [role], password: hashedPassword, userName }); await this.userRepository.save(adminUser); return { message: AdminMessage.ADMIN_CREATED, adminUser, }; } /************************************************************ */ async createRole(createDto: CreateRoleDto) { const existRole = await this.rolesRepository.findOne({ where: { name: createDto.name } }); if (existRole) throw new BadRequestException(AdminMessage.ROLE_EXIST); const permissions = await this.permissionsRepository.findBy({ id: In(createDto.permissions) }); if (permissions.length !== createDto.permissions.length) throw new BadRequestException(AdminMessage.PERMISSIONS_NOT_FOUND); const role = this.rolesRepository.create({ ...createDto, permissions, isAdmin: true }); await this.rolesRepository.save(role); return { message: AdminMessage.ROLE_CREATED, role, }; } /************************************************************ */ async checkValidity(checkDto: CheckValidityDTO, userId: string) { const { type } = checkDto; if (type === ValidityType.USERNAME) checkDto.value = slugify(checkDto.value, { lower: true, trim: true }); const user = await this.userRepository.findOneBy({ [type]: checkDto.value, id: Not(userId) }); if (user) throw new BadRequestException(UserMessage.USER_EXISTS); return { message: CommonMessage.VALID_FOR_CHOOSE }; } /************************************************************ */ async createUserGroup(createUserGroupDto: CreateUserGroupDto) { const users = await this.userRepository.find({ where: { id: In(createUserGroupDto.userIds) } }); const userGroup = this.userGroupRepository.create({ ...createUserGroupDto, users, }); await this.userGroupRepository.save(userGroup); return { message: UserMessage.USER_GROUP_CREATED, }; } ///****************************************************** */ async getUserGroups() { const usersGroup = await this.userGroupRepository.find({ relations: { users: true } }); return { usersGroup, }; } ///****************************************************** */ async getUserGroupById(id: string) { const userGroup = await this.userGroupRepository.findGroupById(id); if (!userGroup) throw new BadRequestException(UserMessage.USER_GROUP_NOT_FOUND); return { userGroup, }; } ///****************************************************** */ async getUserGroupsByIds(ids: string[]) { const userGroups = await this.userGroupRepository.find({ where: { id: In(ids) } }); if (!userGroups) throw new BadRequestException(UserMessage.USER_GROUP_NOT_FOUND); return { userGroups, }; } ///****************************************************** */ async findUsersByIds(ids: string[]) { const users = await this.userRepository.find({ where: { id: In(ids) } }); return users; } ///****************************************************** */ async updateUserPassword(userId: string, newPassword: string) { const updatedUser = await this.userRepository.update( { id: userId }, { password: newPassword, }, ); return { updatedUser, }; } /************************************************************ */ async findAllUsers() { const users = await this.userRepository.find({ relations: ["groups"], }); return { users }; } /************************************************************ */ async getCustomers(queryDto: SearchCustomersDto) { const { limit, skip } = PaginationUtils(queryDto); const queryBuilder = this.userRepository .createQueryBuilder("user") .leftJoin("user.roles", "role") .where("role.name = :roleName", { roleName: RoleEnum.USER }) .leftJoin("user.groups", "groups") .addSelect(["groups.id", "groups.name"]) .loadRelationCountAndMap("user.invoicesCount", "user.invoices") .loadRelationCountAndMap("user.subscriptionsCount", "user.subscriptions") .loadRelationCountAndMap("user.ticketsCount", "user.tickets"); if (queryDto.q) { queryBuilder .orWhere("user.firstName ILIKE :search", { search: `%${queryDto.q}%` }) .orWhere("user.lastName ILIKE :search", { search: `%${queryDto.q}%` }) .orWhere("user.userName ILIKE :search", { search: `%${queryDto.q}%` }); } const [customers, count] = await queryBuilder.skip(skip).take(limit).getManyAndCount(); return { customers, count }; } /************************************************************ */ // async createCustomer(createDto: CreateCustomerDto) { // const { cityId, address, postalCode, economicCode, companyRegisteredName, registrationId, nationalId, number, ...userData } = createDto; // const queryRunner = this.userRepository.manager.connection.createQueryRunner(); // await queryRunner.connect(); // await queryRunner.startTransaction(); // try { // const existUser = await queryRunner.manager.findOneBy(User, { email: createDto.email }); // if (existUser) throw new BadRequestException(UserMessage.EMAIL_EXIST); // const role = await queryRunner.manager.findOneBy(Role, { name: RoleEnum.USER }); // if (!role) throw new BadRequestException(UserMessage.ROLE_NOT_FOUND); // const hashedPassword = await this.passwordService.hashPassword(createDto.password); // const legalUserData = economicCode // ? { // economicCode, // companyRegisteredName, // registrationId, // nationalId, // number, // address: { // address, // city: cityId // ? { // id: +cityId, // } // : undefined, // postalCode, // }, // } // : undefined; // const user = queryRunner.manager.create(User, { // ...userData, // password: hashedPassword, // roles: [role], // legalUser: legalUserData, // }); // await queryRunner.manager.save(user); // await this.userSettingsService.createUserSettings(user.id, queryRunner); // await this.walletsService.createUserWallet(user.id, queryRunner); // await queryRunner.commitTransaction(); // return { // message: CommonMessage.CREATED, // user, // }; // } catch (error) { // await queryRunner.rollbackTransaction(); // throw error; // } finally { // await queryRunner.release(); // } // } /************************************************************ */ async findOneCustomer(userId: string) { const customer = await this.userRepository.findOne({ where: { id: userId, roles: { name: RoleEnum.USER } }, relations: { legalUser: true, realUser: true }, }); return { customer }; } /************************************************************ */ async getUsersByGroup(groupId: string) { const users = await this.userRepository.find({ where: { groups: { id: groupId } }, relations: { tickets: true } }); return { users }; } /************************************************************ */ async getAdmins(queryDto: SearchAdminQueryDto) { const { limit, skip } = PaginationUtils(queryDto); const queryBuilder = this.userRepository.createQueryBuilder("user"); queryBuilder .leftJoinAndSelect("user.roles", "role") // .addSelect(["role.id", "role.name"]) .where("role.isAdmin = :isAdmin", { isAdmin: true }) .leftJoinAndSelect("user.groups", "groups") .leftJoinAndSelect("role.permissions", "permissions"); // .where("permissions.name IN (:...adminPermissions)", { // adminPermissions: [PermissionEnum.ADMINS], // }); // .addSelect(["permissions.id", "permissions.name"]); if (queryDto.q) { queryBuilder.andWhere("user.firstName ILIKE :q OR user.lastName ILIKE :q OR user.userName ILIKE :q", { q: `%${queryDto.q}%`, }); } if (queryDto.permission) { queryBuilder.andWhere("permissions.name = :permName", { permName: queryDto.permission }); } queryBuilder.take(limit).skip(skip).orderBy("user.createdAt", "DESC"); const [users, count] = await queryBuilder.getManyAndCount(); return { users, count, paginate: true, }; } /************************************************************ */ async getPermissions() { const permissions = await this.permissionsRepository.find({ select: { id: true, name: true } }); return { permissions, }; } /************************************************************ */ async getRoles(queryDto: SearchRolesQueryDto) { const { limit, skip } = PaginationUtils(queryDto); const queryBuilder = this.rolesRepository.createQueryBuilder("role"); queryBuilder .where("role.isAdmin = :isAdmin", { isAdmin: true }) // .leftJoinAndSelect("role.permissions", "permissions") .loadRelationCountAndMap("role.userCount", "role.users"); if (queryDto.q) { queryBuilder.andWhere("role.name ILIKE :q", { q: `%${queryDto.q}%` }); } queryBuilder.take(limit).skip(skip).orderBy("role.createdAt", "DESC"); const [roles, count] = await queryBuilder.getManyAndCount(); return { roles, count, paginate: true, }; } /************************************************************ */ //TODO:query manager async createRealUserData(createDto: CreateRealUserDto, userId: string) { const user = await this.userRepository.findOneBy({ id: userId }); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); if (user.financialType && user.financialType === FinancialType.LEGAL) throw new BadRequestException(UserMessage.FINANCIAL_TYPE_IS_LEGAL); // const existRealUser = await this.realUserRepository.findOne({ where: { user: { id: user.id } } }); if (existRealUser) throw new BadRequestException(UserMessage.REAL_DATA_EXIST); const { city } = await this.addressService.getCity(+createDto.cityId); const existNationalCode = await this.realUserRepository.findOne({ where: { nationalCode: createDto.nationalCode } }); if (existNationalCode) throw new BadRequestException(UserMessage.NATIONAL_CODE_EXIST); const existPhone = await this.realUserRepository.findOne({ where: { phone: createDto.phone } }); if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST); const realUser = this.realUserRepository.create({ ...createDto, user, address: { fullAddress: createDto.address, city, postalCode: createDto.postalCode, }, }); await this.realUserRepository.save(realUser); user.financialType = FinancialType.REAL; await this.userRepository.save(user); return { message: CommonMessage.CREATED, realUser, }; } // /************************************************************ */ //TODO:query manager async createLegalUserData(createDto: CreateLegalUserDto, userId: string) { const user = await this.userRepository.findOneBy({ id: userId }); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); if (user.financialType && user.financialType === FinancialType.REAL) throw new BadRequestException(UserMessage.FINANCIAL_TYPE_IS_REAL); const existingLegalUser = await this.legalUserRepository.findOne({ where: { user: { id: user.id } } }); if (existingLegalUser) throw new BadRequestException(UserMessage.LEGAL_DATA_EXIST); const existEconomicCode = await this.legalUserRepository.findOne({ where: { economicCode: createDto.economicCode } }); if (existEconomicCode) throw new BadRequestException(UserMessage.ECONOMIC_CODE_EXIST); const existPhone = await this.legalUserRepository.findOne({ where: { phone: createDto.phone } }); if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST); const existRegistrationCode = await this.legalUserRepository.findOne({ where: { registrationCode: createDto.registrationCode } }); if (existRegistrationCode) throw new BadRequestException(UserMessage.REGISTRATION_CODE_EXIST); const existNationalIdentity = await this.legalUserRepository.findOne({ where: { nationalIdentity: createDto.nationalIdentity } }); if (existNationalIdentity) throw new BadRequestException(UserMessage.NATIONAL_IDENTITY_EXIST); const { city } = await this.addressService.getCity(+createDto.cityId); const legalUser = this.legalUserRepository.create({ ...createDto, user, address: { fullAddress: createDto.address, city, postalCode: createDto.postalCode, }, }); await this.legalUserRepository.save(legalUser); user.financialType = FinancialType.LEGAL; await this.userRepository.save(user); return { message: CommonMessage.CREATED, legalUser, }; } /************************************************************ */ async getCustomersCount() { const count = await this.userRepository.count({ where: { roles: { name: RoleEnum.USER, }, }, }); return count; } /************************************************************ */ private async createEmailHashLink(email: string) { const secret = this.configService.getOrThrow("EMAIL_SECRET"); const nonce = randomBytes(16).toString("hex"); const hash = createHmac("sha256", secret).update(`${email}:${nonce}`).digest("hex"); const url = new URL(`${this.configService.getOrThrow("API_URL")}/users/verify-email`); url.searchParams.append("token", hash); url.searchParams.append("email", email); await this.cacheService.set(`EMAIL_VERIFY:${email}`, JSON.stringify({ hash, email, nonce }), 5 * 60 * 1000); return url.toString(); } /************************************************************ */ private async checkUserLoginCredentialWithPhone(phone: string, otpCode: string, userId: string) { const isValid = await this.otpService.verifyOtp(phone, otpCode, "LOGIN"); if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP); await this.otpService.delOtpFormCache(phone, "LOGIN"); const user = await this.userRepository.findOneBy({ id: userId }); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); return user; } /************************************************************ */ }