From 9f79599ddaca9eb507fd156c91d3dcb022e5528f Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Thu, 5 Feb 2026 08:32:39 +0000 Subject: [PATCH] feat: enhance email verification and password reset processes --- ...-in-company-custom-repository-extension.ts | 106 ++++----- .../invitation-repository.interface.ts | 24 +-- ...ck-verification-link.available.use.case.ts | 46 ++-- .../invite-user-in-company.use.case.ts | 204 +++++++++--------- .../verify-invite-user-in-company.use.case.ts | 154 ++++++------- ...erification-custom-repository-extension.ts | 56 ++--- ...email-verification.repository.interface.ts | 6 +- .../request-change-user-email.use.case.ts | 67 +++--- .../request-email-verification.use.case.ts | 73 +++---- .../request-reset-user-password.use.case.ts | 69 +++--- .../verify-change-user-email.use.case.ts | 98 ++++----- .../verify-reset-user-password.use.case.ts | 85 ++++---- .../use-cases/verify-user-email.use.case.ts | 48 +++-- ...mail-change-custom-repository-extension.ts | 53 ++--- .../email-change.repository.interface.ts | 8 +- ...-invitation-custom-repository-extension.ts | 83 +++---- .../user-invitation-repository.interface.ts | 13 +- .../password-reset-repository.interface.ts | 8 +- ...er-password-custom-repository-extension.ts | 59 ++--- backend/src/helpers/encryption/encryptor.ts | 8 + .../saas-usual-register-user.use.case.ts | 155 +++++++------ 21 files changed, 725 insertions(+), 698 deletions(-) diff --git a/backend/src/entities/company-info/invitation-in-company/repository/invitation-in-company-custom-repository-extension.ts b/backend/src/entities/company-info/invitation-in-company/repository/invitation-in-company-custom-repository-extension.ts index 2c21ac2c4..414be8a64 100644 --- a/backend/src/entities/company-info/invitation-in-company/repository/invitation-in-company-custom-repository-extension.ts +++ b/backend/src/entities/company-info/invitation-in-company/repository/invitation-in-company-custom-repository-extension.ts @@ -5,58 +5,62 @@ import { InvitationInCompanyEntity } from '../invitation-in-company.entity.js'; import { IInvitationInCompanyRepository } from './invitation-repository.interface.js'; export const invitationInCompanyCustomRepositoryExtension: IInvitationInCompanyRepository = { - async createOrUpdateInvitationInCompany( - companyInfo: CompanyInfoEntity, - groupId: string | null, - inviterId: string, - newUserEmail: string, - invitedUserRole: UserRoleEnum, - ): Promise { - const qb = this.createQueryBuilder('invitation_in_company') - .leftJoinAndSelect('invitation_in_company.company', 'company') - .where('company.id = :companyId', { companyId: companyInfo.id }) - .andWhere('invitation_in_company.invitedUserEmail = :newUserEmail', { newUserEmail: newUserEmail?.toLowerCase() }); - const foundInvitation = await qb.getOne(); - if (foundInvitation) { - await this.remove(foundInvitation); - } - const newInvitation = new InvitationInCompanyEntity(); - newInvitation.verification_string = Encryptor.generateRandomString(); - newInvitation.company = companyInfo; - newInvitation.groupId = groupId ? groupId : null; - newInvitation.inviterId = inviterId; - newInvitation.invitedUserEmail = newUserEmail?.toLowerCase(); - newInvitation.role = invitedUserRole; - return await this.save(newInvitation); - }, + async createOrUpdateInvitationInCompany( + companyInfo: CompanyInfoEntity, + groupId: string | null, + inviterId: string, + newUserEmail: string, + invitedUserRole: UserRoleEnum, + ): Promise<{ entity: InvitationInCompanyEntity; rawToken: string }> { + const qb = this.createQueryBuilder('invitation_in_company') + .leftJoinAndSelect('invitation_in_company.company', 'company') + .where('company.id = :companyId', { companyId: companyInfo.id }) + .andWhere('invitation_in_company.invitedUserEmail = :newUserEmail', { + newUserEmail: newUserEmail?.toLowerCase(), + }); + const foundInvitation = await qb.getOne(); + if (foundInvitation) { + await this.remove(foundInvitation); + } + const rawToken = Encryptor.generateRandomString(); + const newInvitation = new InvitationInCompanyEntity(); + newInvitation.verification_string = Encryptor.hashVerificationToken(rawToken); + newInvitation.company = companyInfo; + newInvitation.groupId = groupId ? groupId : null; + newInvitation.inviterId = inviterId; + newInvitation.invitedUserEmail = newUserEmail?.toLowerCase(); + newInvitation.role = invitedUserRole; + const entity = await this.save(newInvitation); + return { entity, rawToken }; + }, - async deleteOldInvitationsInCompany(companyId: string): Promise { - const qb = this.createQueryBuilder('invitation_in_company') - .leftJoinAndSelect('invitation_in_company.company', 'company') - .where('company.id = :companyId', { companyId }) - .andWhere("invitation_in_company.createdAt < NOW() - INTERVAL '1 day'"); - const foundInvitations = await qb.getMany(); - if (foundInvitations.length) { - await this.remove(foundInvitations); - } - }, + async deleteOldInvitationsInCompany(companyId: string): Promise { + const qb = this.createQueryBuilder('invitation_in_company') + .leftJoinAndSelect('invitation_in_company.company', 'company') + .where('company.id = :companyId', { companyId }) + .andWhere("invitation_in_company.createdAt < NOW() - INTERVAL '1 day'"); + const foundInvitations = await qb.getMany(); + if (foundInvitations.length) { + await this.remove(foundInvitations); + } + }, - async findNonExpiredInvitationInCompanyWithUsersByVerificationString( - verificationString: string, - ): Promise { - const qb = this.createQueryBuilder('invitation_in_company') - .leftJoinAndSelect('invitation_in_company.company', 'company') - .leftJoinAndSelect('company.users', 'users') - .where("invitation_in_company.createdAt > NOW() - INTERVAL '1 day'") - .where('invitation_in_company.verification_string = :verificationString', { verificationString }); - return await qb.getOne(); - }, + async findNonExpiredInvitationInCompanyWithUsersByVerificationString( + verificationString: string, + ): Promise { + const qb = this.createQueryBuilder('invitation_in_company') + .leftJoinAndSelect('invitation_in_company.company', 'company') + .leftJoinAndSelect('company.users', 'users') + .where("invitation_in_company.createdAt > NOW() - INTERVAL '1 day'") + .where('invitation_in_company.verification_string = :verificationString', { verificationString }); + return await qb.getOne(); + }, - async countNonExpiredInvitationsInCompany(companyId: string): Promise { - const qb = this.createQueryBuilder('invitation_in_company') - .leftJoin('invitation_in_company.company', 'company') - .where('company.id = :companyId', { companyId }) - .andWhere("invitation_in_company.createdAt > NOW() - INTERVAL '1 day'"); - return await qb.getCount(); - }, + async countNonExpiredInvitationsInCompany(companyId: string): Promise { + const qb = this.createQueryBuilder('invitation_in_company') + .leftJoin('invitation_in_company.company', 'company') + .where('company.id = :companyId', { companyId }) + .andWhere("invitation_in_company.createdAt > NOW() - INTERVAL '1 day'"); + return await qb.getCount(); + }, }; diff --git a/backend/src/entities/company-info/invitation-in-company/repository/invitation-repository.interface.ts b/backend/src/entities/company-info/invitation-in-company/repository/invitation-repository.interface.ts index 0165b48ce..86d5296f6 100644 --- a/backend/src/entities/company-info/invitation-in-company/repository/invitation-repository.interface.ts +++ b/backend/src/entities/company-info/invitation-in-company/repository/invitation-repository.interface.ts @@ -3,19 +3,19 @@ import { CompanyInfoEntity } from '../../company-info.entity.js'; import { InvitationInCompanyEntity } from '../invitation-in-company.entity.js'; export interface IInvitationInCompanyRepository { - createOrUpdateInvitationInCompany( - companyInfo: CompanyInfoEntity, - groupId: string | null, - inviterId: string, - newUserEmail: string, - invitedUserRole: UserRoleEnum, - ): Promise; + createOrUpdateInvitationInCompany( + companyInfo: CompanyInfoEntity, + groupId: string | null, + inviterId: string, + newUserEmail: string, + invitedUserRole: UserRoleEnum, + ): Promise<{ entity: InvitationInCompanyEntity; rawToken: string }>; - deleteOldInvitationsInCompany(companyId: string): Promise; + deleteOldInvitationsInCompany(companyId: string): Promise; - findNonExpiredInvitationInCompanyWithUsersByVerificationString( - verificationString: string, - ): Promise; + findNonExpiredInvitationInCompanyWithUsersByVerificationString( + verificationString: string, + ): Promise; - countNonExpiredInvitationsInCompany(companyId: string): Promise; + countNonExpiredInvitationsInCompany(companyId: string): Promise; } diff --git a/backend/src/entities/company-info/use-cases/check-verification-link.available.use.case.ts b/backend/src/entities/company-info/use-cases/check-verification-link.available.use.case.ts index 1a0a493c9..a167802c2 100644 --- a/backend/src/entities/company-info/use-cases/check-verification-link.available.use.case.ts +++ b/backend/src/entities/company-info/use-cases/check-verification-link.available.use.case.ts @@ -4,31 +4,33 @@ import { IGlobalDatabaseContext } from '../../../common/application/global-datab import { BaseType } from '../../../common/data-injection.tokens.js'; import { SuccessResponse } from '../../../microservices/saas-microservice/data-structures/common-responce.ds.js'; import { ICheckVerificationLinkAvailable } from './company-info-use-cases.interface.js'; +import { Encryptor } from '../../../helpers/encryption/encryptor.js'; @Injectable() export class CheckIsVerificationLinkAvailable - extends AbstractUseCase - implements ICheckVerificationLinkAvailable + extends AbstractUseCase + implements ICheckVerificationLinkAvailable { - constructor( - @Inject(BaseType.GLOBAL_DB_CONTEXT) - protected _dbContext: IGlobalDatabaseContext, - ) { - super(); - } + constructor( + @Inject(BaseType.GLOBAL_DB_CONTEXT) + protected _dbContext: IGlobalDatabaseContext, + ) { + super(); + } - protected async implementation(verificationString: string): Promise { - const foundInvitation = - await this._dbContext.invitationInCompanyRepository.findNonExpiredInvitationInCompanyWithUsersByVerificationString( - verificationString, - ); - if (!foundInvitation) { - return { - success: false, - }; - } - return { - success: true, - }; - } + protected async implementation(verificationString: string): Promise { + const hashedToken = Encryptor.hashVerificationToken(verificationString); + const foundInvitation = + await this._dbContext.invitationInCompanyRepository.findNonExpiredInvitationInCompanyWithUsersByVerificationString( + hashedToken, + ); + if (!foundInvitation) { + return { + success: false, + }; + } + return { + success: true, + }; + } } diff --git a/backend/src/entities/company-info/use-cases/invite-user-in-company.use.case.ts b/backend/src/entities/company-info/use-cases/invite-user-in-company.use.case.ts index 221a46ef7..d447486fb 100644 --- a/backend/src/entities/company-info/use-cases/invite-user-in-company.use.case.ts +++ b/backend/src/entities/company-info/use-cases/invite-user-in-company.use.case.ts @@ -15,116 +15,116 @@ import { WinstonLogger } from '../../logging/winston-logger.js'; @Injectable({ scope: Scope.REQUEST }) export class InviteUserInCompanyAndConnectionGroupUseCase - extends AbstractUseCase - implements IInviteUserInCompanyAndConnectionGroup + extends AbstractUseCase + implements IInviteUserInCompanyAndConnectionGroup { - constructor( - @Inject(BaseType.GLOBAL_DB_CONTEXT) - protected _dbContext: IGlobalDatabaseContext, - private readonly saasCompanyGatewayService: SaasCompanyGatewayService, - private readonly emailService: EmailService, - private readonly companyInfoHelperService: CompanyInfoHelperService, - private readonly logger: WinstonLogger, - ) { - super(); - } + constructor( + @Inject(BaseType.GLOBAL_DB_CONTEXT) + protected _dbContext: IGlobalDatabaseContext, + private readonly saasCompanyGatewayService: SaasCompanyGatewayService, + private readonly emailService: EmailService, + private readonly companyInfoHelperService: CompanyInfoHelperService, + private readonly logger: WinstonLogger, + ) { + super(); + } - protected async implementation( - inputData: InviteUserInCompanyAndConnectionGroupDs, - ): Promise { - const { inviterId, companyId, groupId, invitedUserCompanyRole } = inputData; - const invitedUserEmail = inputData.invitedUserEmail.toLowerCase(); - const foundCompany = await this._dbContext.companyInfoRepository.findOneBy({ id: companyId }); - if (!foundCompany) { - throw new HttpException( - { - message: Messages.COMPANY_NOT_FOUND, - }, - HttpStatus.BAD_REQUEST, - ); - } + protected async implementation( + inputData: InviteUserInCompanyAndConnectionGroupDs, + ): Promise { + const { inviterId, companyId, groupId, invitedUserCompanyRole } = inputData; + const invitedUserEmail = inputData.invitedUserEmail.toLowerCase(); + const foundCompany = await this._dbContext.companyInfoRepository.findOneBy({ id: companyId }); + if (!foundCompany) { + throw new HttpException( + { + message: Messages.COMPANY_NOT_FOUND, + }, + HttpStatus.BAD_REQUEST, + ); + } - if (isSaaS()) { - const canInviteMoreUsers = await this.companyInfoHelperService.canInviteMoreUsers(companyId); - if (!canInviteMoreUsers) { - throw new HttpException( - { - message: Messages.MAXIMUM_INVITATIONS_COUNT_REACHED_CANT_INVITE, - }, - HttpStatus.BAD_REQUEST, - ); - } - } + if (isSaaS()) { + const canInviteMoreUsers = await this.companyInfoHelperService.canInviteMoreUsers(companyId); + if (!canInviteMoreUsers) { + throw new HttpException( + { + message: Messages.MAXIMUM_INVITATIONS_COUNT_REACHED_CANT_INVITE, + }, + HttpStatus.BAD_REQUEST, + ); + } + } - const foundInvitedUser = await this._dbContext.userRepository.findOneUserByEmailAndCompanyId( - invitedUserEmail, - companyId, - ); + const foundInvitedUser = await this._dbContext.userRepository.findOneUserByEmailAndCompanyId( + invitedUserEmail, + companyId, + ); - if (foundInvitedUser?.isActive) { - throw new HttpException( - { - message: Messages.USER_ALREADY_ADDED_IN_COMPANY, - }, - HttpStatus.BAD_REQUEST, - ); - } + if (foundInvitedUser?.isActive) { + throw new HttpException( + { + message: Messages.USER_ALREADY_ADDED_IN_COMPANY, + }, + HttpStatus.BAD_REQUEST, + ); + } - if (foundInvitedUser && !foundInvitedUser.isActive) { - const companyCustomDomain = await this.saasCompanyGatewayService.getCompanyCustomDomainById(companyId); - const renewedEmailVerification = - await this._dbContext.emailVerificationRepository.createOrUpdateEmailVerification(foundInvitedUser); + if (foundInvitedUser && !foundInvitedUser.isActive) { + const companyCustomDomain = await this.saasCompanyGatewayService.getCompanyCustomDomainById(companyId); + const { rawToken } = + await this._dbContext.emailVerificationRepository.createOrUpdateEmailVerification(foundInvitedUser); - const sendEmailResult = await this.emailService.sendEmailConfirmation( - foundInvitedUser.email, - renewedEmailVerification.verification_string, - companyCustomDomain, - ); + const sendEmailResult = await this.emailService.sendEmailConfirmation( + foundInvitedUser.email, + rawToken, + companyCustomDomain, + ); - if (!sendEmailResult && !isTest() && !isSaaS()) { - throw new HttpException( - { - message: Messages.EMAIL_SEND_FAILED(invitedUserEmail), - }, - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } + if (!sendEmailResult && !isTest() && !isSaaS()) { + throw new HttpException( + { + message: Messages.EMAIL_SEND_FAILED(invitedUserEmail), + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } - if (!isSaaS()) { - this.logger.printTechString(`Invitation verification string: ${renewedEmailVerification.verification_string}`); - } - throw new HttpException( - { - message: Messages.USER_ALREADY_ADDED_BUT_NOT_ACTIVE_IN_COMPANY, - }, - HttpStatus.BAD_REQUEST, - ); - } + if (!isSaaS()) { + this.logger.printTechString(`Invitation verification string: ${rawToken}`); + } + throw new HttpException( + { + message: Messages.USER_ALREADY_ADDED_BUT_NOT_ACTIVE_IN_COMPANY, + }, + HttpStatus.BAD_REQUEST, + ); + } - const newInvitation = await this._dbContext.invitationInCompanyRepository.createOrUpdateInvitationInCompany( - foundCompany, - groupId, - inviterId, - invitedUserEmail, - invitedUserCompanyRole, - ); - const companyCustomDomain = await this.saasCompanyGatewayService.getCompanyCustomDomainById(companyId); - await this.emailService.sendInvitationToCompany( - invitedUserEmail, - newInvitation.verification_string, - companyId, - foundCompany.name, - companyCustomDomain, - ); - const invitationRO: any = { - companyId: companyId, - groupId: groupId, - email: invitedUserEmail, - role: invitedUserCompanyRole, - }; - if (process.env.NODE_ENV === 'test') { - invitationRO.verificationString = newInvitation.verification_string; - } - return invitationRO; - } + const { rawToken } = await this._dbContext.invitationInCompanyRepository.createOrUpdateInvitationInCompany( + foundCompany, + groupId, + inviterId, + invitedUserEmail, + invitedUserCompanyRole, + ); + const companyCustomDomain = await this.saasCompanyGatewayService.getCompanyCustomDomainById(companyId); + await this.emailService.sendInvitationToCompany( + invitedUserEmail, + rawToken, + companyId, + foundCompany.name, + companyCustomDomain, + ); + const invitationRO: any = { + companyId: companyId, + groupId: groupId, + email: invitedUserEmail, + role: invitedUserCompanyRole, + }; + if (process.env.NODE_ENV === 'test') { + invitationRO.verificationString = rawToken; + } + return invitationRO; + } } diff --git a/backend/src/entities/company-info/use-cases/verify-invite-user-in-company.use.case.ts b/backend/src/entities/company-info/use-cases/verify-invite-user-in-company.use.case.ts index 8c06c4e66..ca755f978 100644 --- a/backend/src/entities/company-info/use-cases/verify-invite-user-in-company.use.case.ts +++ b/backend/src/entities/company-info/use-cases/verify-invite-user-in-company.use.case.ts @@ -8,85 +8,87 @@ import { get2FaScope } from '../../user/utils/is-jwt-scope-need.util.js'; import { AcceptUserValidationInCompany } from '../application/data-structures/accept-user-invitation-in-company.ds.js'; import { IVerifyInviteUserInCompanyAndConnectionGroup } from './company-info-use-cases.interface.js'; import { SaasCompanyGatewayService } from '../../../microservices/gateways/saas-gateway.ts/saas-company-gateway.service.js'; +import { Encryptor } from '../../../helpers/encryption/encryptor.js'; @Injectable({ scope: Scope.REQUEST }) export class VerifyInviteUserInCompanyAndConnectionGroupUseCase - extends AbstractUseCase - implements IVerifyInviteUserInCompanyAndConnectionGroup + extends AbstractUseCase + implements IVerifyInviteUserInCompanyAndConnectionGroup { - constructor( - @Inject(BaseType.GLOBAL_DB_CONTEXT) - protected _dbContext: IGlobalDatabaseContext, - private readonly saasCompanyGatewayService: SaasCompanyGatewayService, - ) { - super(); - } + constructor( + @Inject(BaseType.GLOBAL_DB_CONTEXT) + protected _dbContext: IGlobalDatabaseContext, + private readonly saasCompanyGatewayService: SaasCompanyGatewayService, + ) { + super(); + } - protected async implementation(inputData: AcceptUserValidationInCompany): Promise { - const { verificationString, userPassword, userName } = inputData; - const foundInvitation = - await this._dbContext.invitationInCompanyRepository.findNonExpiredInvitationInCompanyWithUsersByVerificationString( - verificationString, - ); - if (!foundInvitation) { - throw new HttpException( - { - message: Messages.VERIFICATION_LINK_INCORRECT, - }, - HttpStatus.BAD_REQUEST, - ); - } - const { - company: { users, id: companyId }, - groupId, - role, - } = foundInvitation; - const invitedUserEmail = foundInvitation.invitedUserEmail.toLowerCase(); - const foundUser = users.find((user) => user.email === invitedUserEmail); - if (foundUser?.isActive) { - throw new HttpException( - { - message: Messages.USER_ALREADY_ADDED_IN_COMPANY, - }, - HttpStatus.BAD_REQUEST, - ); - } - if (foundUser) { - foundUser.isActive = true; - foundUser.role = role; - await this._dbContext.userRepository.saveUserEntity(foundUser); - return generateGwtToken(foundUser, get2FaScope(foundUser, foundInvitation.company)); - } - const newUser = await this._dbContext.userRepository.saveRegisteringUser({ - email: invitedUserEmail, - gclidValue: null, - password: userPassword, - isActive: true, - role, - name: userName, - }); - newUser.company = foundInvitation.company; - const savedUser = await this._dbContext.userRepository.saveUserEntity(newUser); - if (groupId) { - const foundGroup = await this._dbContext.groupRepository.findGroupByIdWithConnectionAndUsers(groupId); - if (!foundGroup) { - throw new HttpException( - { - message: Messages.GROUP_NOT_FOUND, - }, - HttpStatus.BAD_REQUEST, - ); - } - const userAlreadyIngroup = foundGroup.users.find( - (user) => user.email.toLowerCase() === savedUser.email.toLowerCase(), - ); - if (!userAlreadyIngroup) { - foundGroup.users.push(savedUser); - await this._dbContext.groupRepository.saveNewOrUpdatedGroup(foundGroup); - } - } - await this._dbContext.invitationInCompanyRepository.remove(foundInvitation); - await this.saasCompanyGatewayService.recountUsersInCompanyRequest(companyId); - return generateGwtToken(newUser, get2FaScope(newUser, foundInvitation.company)); - } + protected async implementation(inputData: AcceptUserValidationInCompany): Promise { + const { verificationString, userPassword, userName } = inputData; + const hashedToken = Encryptor.hashVerificationToken(verificationString); + const foundInvitation = + await this._dbContext.invitationInCompanyRepository.findNonExpiredInvitationInCompanyWithUsersByVerificationString( + hashedToken, + ); + if (!foundInvitation) { + throw new HttpException( + { + message: Messages.VERIFICATION_LINK_INCORRECT, + }, + HttpStatus.BAD_REQUEST, + ); + } + const { + company: { users, id: companyId }, + groupId, + role, + } = foundInvitation; + const invitedUserEmail = foundInvitation.invitedUserEmail.toLowerCase(); + const foundUser = users.find((user) => user.email === invitedUserEmail); + if (foundUser?.isActive) { + throw new HttpException( + { + message: Messages.USER_ALREADY_ADDED_IN_COMPANY, + }, + HttpStatus.BAD_REQUEST, + ); + } + if (foundUser) { + foundUser.isActive = true; + foundUser.role = role; + await this._dbContext.userRepository.saveUserEntity(foundUser); + return generateGwtToken(foundUser, get2FaScope(foundUser, foundInvitation.company)); + } + const newUser = await this._dbContext.userRepository.saveRegisteringUser({ + email: invitedUserEmail, + gclidValue: null, + password: userPassword, + isActive: true, + role, + name: userName, + }); + newUser.company = foundInvitation.company; + const savedUser = await this._dbContext.userRepository.saveUserEntity(newUser); + if (groupId) { + const foundGroup = await this._dbContext.groupRepository.findGroupByIdWithConnectionAndUsers(groupId); + if (!foundGroup) { + throw new HttpException( + { + message: Messages.GROUP_NOT_FOUND, + }, + HttpStatus.BAD_REQUEST, + ); + } + const userAlreadyIngroup = foundGroup.users.find( + (user) => user.email.toLowerCase() === savedUser.email.toLowerCase(), + ); + if (!userAlreadyIngroup) { + foundGroup.users.push(savedUser); + await this._dbContext.groupRepository.saveNewOrUpdatedGroup(foundGroup); + } + } + await this._dbContext.invitationInCompanyRepository.remove(foundInvitation); + await this.saasCompanyGatewayService.recountUsersInCompanyRequest(companyId); + return generateGwtToken(newUser, get2FaScope(newUser, foundInvitation.company)); + } } diff --git a/backend/src/entities/email/repository/email-verification-custom-repository-extension.ts b/backend/src/entities/email/repository/email-verification-custom-repository-extension.ts index 6d769d424..639cb481a 100644 --- a/backend/src/entities/email/repository/email-verification-custom-repository-extension.ts +++ b/backend/src/entities/email/repository/email-verification-custom-repository-extension.ts @@ -3,31 +3,37 @@ import { UserEntity } from '../../user/user.entity.js'; import { EmailVerificationEntity } from '../email-verification.entity.js'; export const emailVerificationRepositoryExtension = { - async findVerificationWithVerificationString(verificationString: string): Promise { - const qb = this.createQueryBuilder('email_verification') - .leftJoinAndSelect('email_verification.user', 'user') - .where('email_verification.verification_string = :ver_string', { - ver_string: verificationString, - }); - return await qb.getOne(); - }, + async findVerificationWithVerificationString(verificationString: string): Promise { + const qb = this.createQueryBuilder('email_verification') + .leftJoinAndSelect('email_verification.user', 'user') + .where('email_verification.verification_string = :ver_string', { + ver_string: verificationString, + }); + return await qb.getOne(); + }, - async removeVerificationEntity(verification: EmailVerificationEntity): Promise { - return await this.remove(verification); - }, + async removeVerificationEntity(verification: EmailVerificationEntity): Promise { + return await this.remove(verification); + }, - async createOrUpdateEmailVerification(user: UserEntity): Promise { - if (!user.email_verification) { - const newEmailVerification = new EmailVerificationEntity(); - newEmailVerification.verification_string = Encryptor.generateRandomString(); - newEmailVerification.user = user; - return await this.save(newEmailVerification); - } - const foundEmailVerification = await this.findOne({ where: { id: user.email_verification.id } }); - await this.remove(foundEmailVerification); - const newEmailVerification = new EmailVerificationEntity(); - newEmailVerification.verification_string = Encryptor.generateRandomString(); - newEmailVerification.user = user; - return await this.save(newEmailVerification); - }, + async createOrUpdateEmailVerification( + user: UserEntity, + ): Promise<{ entity: EmailVerificationEntity; rawToken: string }> { + if (!user.email_verification) { + const rawToken = Encryptor.generateRandomString(); + const newEmailVerification = new EmailVerificationEntity(); + newEmailVerification.verification_string = Encryptor.hashVerificationToken(rawToken); + newEmailVerification.user = user; + const entity = await this.save(newEmailVerification); + return { entity, rawToken }; + } + const foundEmailVerification = await this.findOne({ where: { id: user.email_verification.id } }); + await this.remove(foundEmailVerification); + const rawToken = Encryptor.generateRandomString(); + const newEmailVerification = new EmailVerificationEntity(); + newEmailVerification.verification_string = Encryptor.hashVerificationToken(rawToken); + newEmailVerification.user = user; + const entity = await this.save(newEmailVerification); + return { entity, rawToken }; + }, }; diff --git a/backend/src/entities/email/repository/email-verification.repository.interface.ts b/backend/src/entities/email/repository/email-verification.repository.interface.ts index 32d0e8f15..664b711fe 100644 --- a/backend/src/entities/email/repository/email-verification.repository.interface.ts +++ b/backend/src/entities/email/repository/email-verification.repository.interface.ts @@ -2,9 +2,9 @@ import { EmailVerificationEntity } from '../email-verification.entity.js'; import { UserEntity } from '../../user/user.entity.js'; export interface IEmailVerificationRepository { - findVerificationWithVerificationString(verificationString: string): Promise; + findVerificationWithVerificationString(verificationString: string): Promise; - removeVerificationEntity(verification: EmailVerificationEntity): Promise; + removeVerificationEntity(verification: EmailVerificationEntity): Promise; - createOrUpdateEmailVerification(user: UserEntity): Promise; + createOrUpdateEmailVerification(user: UserEntity): Promise<{ entity: EmailVerificationEntity; rawToken: string }>; } diff --git a/backend/src/entities/user/use-cases/request-change-user-email.use.case.ts b/backend/src/entities/user/use-cases/request-change-user-email.use.case.ts index 343bd6801..05dd830e9 100644 --- a/backend/src/entities/user/use-cases/request-change-user-email.use.case.ts +++ b/backend/src/entities/user/use-cases/request-change-user-email.use.case.ts @@ -10,40 +10,39 @@ import { EmailService } from '../../email/email/email.service.js'; @Injectable() export class RequestChangeUserEmailUseCase - extends AbstractUseCase - implements IRequestEmailChange + extends AbstractUseCase + implements IRequestEmailChange { - constructor( - @Inject(BaseType.GLOBAL_DB_CONTEXT) - protected _dbContext: IGlobalDatabaseContext, - private readonly saasCompanyGatewayService: SaasCompanyGatewayService, - private readonly emailService: EmailService, - ) { - super(); - } + constructor( + @Inject(BaseType.GLOBAL_DB_CONTEXT) + protected _dbContext: IGlobalDatabaseContext, + private readonly saasCompanyGatewayService: SaasCompanyGatewayService, + private readonly emailService: EmailService, + ) { + super(); + } - protected async implementation(userId: string): Promise { - const foundUser = await this._dbContext.userRepository.findOneUserById(userId); - if (!foundUser.isActive) { - throw new HttpException( - { - message: Messages.EMAIL_NOT_CONFIRMED, - }, - HttpStatus.FORBIDDEN, - ); - } - const savedEmailChangeRequest = - await this._dbContext.emailChangeRepository.createOrUpdateEmailChangeEntity(foundUser); - const userCompanyInfo = await this._dbContext.companyInfoRepository.findCompanyInfoByUserId(userId); - const companyCustomDomain = await this.saasCompanyGatewayService.getCompanyCustomDomainById(userCompanyInfo.id); - const mailingResult = await this.emailService.sendEmailChangeRequest( - foundUser.email, - savedEmailChangeRequest.verification_string, - companyCustomDomain, - ); - const resultMessage = mailingResult.messageId - ? Messages.EMAIL_CHANGE_REQUESTED_SUCCESSFULLY - : Messages.EMAIL_CHANGE_REQUESTED; - return { message: resultMessage }; - } + protected async implementation(userId: string): Promise { + const foundUser = await this._dbContext.userRepository.findOneUserById(userId); + if (!foundUser.isActive) { + throw new HttpException( + { + message: Messages.EMAIL_NOT_CONFIRMED, + }, + HttpStatus.FORBIDDEN, + ); + } + const { rawToken } = await this._dbContext.emailChangeRepository.createOrUpdateEmailChangeEntity(foundUser); + const userCompanyInfo = await this._dbContext.companyInfoRepository.findCompanyInfoByUserId(userId); + const companyCustomDomain = await this.saasCompanyGatewayService.getCompanyCustomDomainById(userCompanyInfo.id); + const mailingResult = await this.emailService.sendEmailChangeRequest( + foundUser.email, + rawToken, + companyCustomDomain, + ); + const resultMessage = mailingResult.messageId + ? Messages.EMAIL_CHANGE_REQUESTED_SUCCESSFULLY + : Messages.EMAIL_CHANGE_REQUESTED; + return { message: resultMessage }; + } } diff --git a/backend/src/entities/user/use-cases/request-email-verification.use.case.ts b/backend/src/entities/user/use-cases/request-email-verification.use.case.ts index c4434bf76..0998f652a 100644 --- a/backend/src/entities/user/use-cases/request-email-verification.use.case.ts +++ b/backend/src/entities/user/use-cases/request-email-verification.use.case.ts @@ -10,46 +10,41 @@ import { EmailService } from '../../email/email/email.service.js'; @Injectable() export class RequestEmailVerificationUseCase - extends AbstractUseCase - implements IRequestEmailVerification + extends AbstractUseCase + implements IRequestEmailVerification { - constructor( - @Inject(BaseType.GLOBAL_DB_CONTEXT) - protected _dbContext: IGlobalDatabaseContext, - private readonly saasCompanyGatewayService: SaasCompanyGatewayService, - private readonly emailService: EmailService, - ) { - super(); - } + constructor( + @Inject(BaseType.GLOBAL_DB_CONTEXT) + protected _dbContext: IGlobalDatabaseContext, + private readonly saasCompanyGatewayService: SaasCompanyGatewayService, + private readonly emailService: EmailService, + ) { + super(); + } - protected async implementation(userId: string): Promise { - const foundUser = await this._dbContext.userRepository.findOneUserWithEmailVerification(userId); - if (!foundUser) { - throw new HttpException( - { - message: Messages.USER_NOT_FOUND, - }, - HttpStatus.BAD_REQUEST, - ); - } - if (foundUser.isActive) { - throw new HttpException( - { - message: Messages.EMAIL_ALREADY_CONFIRMED, - }, - HttpStatus.BAD_REQUEST, - ); - } - const foundUserCompany = await this._dbContext.companyInfoRepository.finOneCompanyInfoByUserId(foundUser.id); - const companyCustomDomain = await this.saasCompanyGatewayService.getCompanyCustomDomainById(foundUserCompany.id); + protected async implementation(userId: string): Promise { + const foundUser = await this._dbContext.userRepository.findOneUserWithEmailVerification(userId); + if (!foundUser) { + throw new HttpException( + { + message: Messages.USER_NOT_FOUND, + }, + HttpStatus.BAD_REQUEST, + ); + } + if (foundUser.isActive) { + throw new HttpException( + { + message: Messages.EMAIL_ALREADY_CONFIRMED, + }, + HttpStatus.BAD_REQUEST, + ); + } + const foundUserCompany = await this._dbContext.companyInfoRepository.finOneCompanyInfoByUserId(foundUser.id); + const companyCustomDomain = await this.saasCompanyGatewayService.getCompanyCustomDomainById(foundUserCompany.id); - const newEmailVerification = - await this._dbContext.emailVerificationRepository.createOrUpdateEmailVerification(foundUser); - await this.emailService.sendEmailConfirmation( - foundUser.email, - newEmailVerification.verification_string, - companyCustomDomain, - ); - return { message: Messages.EMAIL_VERIFICATION_REQUESTED }; - } + const { rawToken } = await this._dbContext.emailVerificationRepository.createOrUpdateEmailVerification(foundUser); + await this.emailService.sendEmailConfirmation(foundUser.email, rawToken, companyCustomDomain); + return { message: Messages.EMAIL_VERIFICATION_REQUESTED }; + } } diff --git a/backend/src/entities/user/use-cases/request-reset-user-password.use.case.ts b/backend/src/entities/user/use-cases/request-reset-user-password.use.case.ts index 42f2d7753..9ed84e0f3 100644 --- a/backend/src/entities/user/use-cases/request-reset-user-password.use.case.ts +++ b/backend/src/entities/user/use-cases/request-reset-user-password.use.case.ts @@ -10,43 +10,42 @@ import { IRequestPasswordReset } from './user-use-cases.interfaces.js'; import { EmailService } from '../../email/email/email.service.js'; export class RequestResetUserPasswordUseCase - extends AbstractUseCase - implements IRequestPasswordReset + extends AbstractUseCase + implements IRequestPasswordReset { - constructor( - @Inject(BaseType.GLOBAL_DB_CONTEXT) - protected _dbContext: IGlobalDatabaseContext, - private readonly saasCompanyGatewayService: SaasCompanyGatewayService, - private readonly emailService: EmailService, - ) { - super(); - } + constructor( + @Inject(BaseType.GLOBAL_DB_CONTEXT) + protected _dbContext: IGlobalDatabaseContext, + private readonly saasCompanyGatewayService: SaasCompanyGatewayService, + private readonly emailService: EmailService, + ) { + super(); + } - protected async implementation(emailData: RequestRestUserPasswordDto): Promise { - const { companyId } = emailData; - const email = emailData.email.toLowerCase(); - const foundUser = await this._dbContext.userRepository.findOneUserByEmailAndCompanyId(email, companyId); - if (!foundUser) { - throw new HttpException( - { - message: Messages.USER_MISSING_EMAIL_OR_SOCIAL_REGISTERED, - }, - HttpStatus.FORBIDDEN, - ); - } - const companyCustomDomain = await this.saasCompanyGatewayService.getCompanyCustomDomainById(companyId); + protected async implementation(emailData: RequestRestUserPasswordDto): Promise { + const { companyId } = emailData; + const email = emailData.email.toLowerCase(); + const foundUser = await this._dbContext.userRepository.findOneUserByEmailAndCompanyId(email, companyId); + if (!foundUser) { + throw new HttpException( + { + message: Messages.USER_MISSING_EMAIL_OR_SOCIAL_REGISTERED, + }, + HttpStatus.FORBIDDEN, + ); + } + const companyCustomDomain = await this.saasCompanyGatewayService.getCompanyCustomDomainById(companyId); - const savedResetPasswordRequest = - await this._dbContext.passwordResetRepository.createOrUpdatePasswordResetEntity(foundUser); + const { rawToken } = await this._dbContext.passwordResetRepository.createOrUpdatePasswordResetEntity(foundUser); - const mailingResult = await this.emailService.sendPasswordResetRequest( - foundUser.email, - savedResetPasswordRequest.verification_string, - companyCustomDomain, - ); - const resultMessage = mailingResult.messageId - ? Messages.PASSWORD_RESET_REQUESTED_SUCCESSFULLY - : Messages.PASSWORD_RESET_REQUESTED; - return { message: resultMessage }; - } + const mailingResult = await this.emailService.sendPasswordResetRequest( + foundUser.email, + rawToken, + companyCustomDomain, + ); + const resultMessage = mailingResult.messageId + ? Messages.PASSWORD_RESET_REQUESTED_SUCCESSFULLY + : Messages.PASSWORD_RESET_REQUESTED; + return { message: resultMessage }; + } } diff --git a/backend/src/entities/user/use-cases/verify-change-user-email.use.case.ts b/backend/src/entities/user/use-cases/verify-change-user-email.use.case.ts index 5cbda0c57..3ae1963c6 100644 --- a/backend/src/entities/user/use-cases/verify-change-user-email.use.case.ts +++ b/backend/src/entities/user/use-cases/verify-change-user-email.use.case.ts @@ -7,60 +7,62 @@ import { EmailService } from '../../email/email/email.service.js'; import { ChangeUserEmailDs } from '../application/data-structures/change-user-email.ds.js'; import { OperationResultMessageDs } from '../application/data-structures/operation-result-message.ds.js'; import { IVerifyEmailChange } from './user-use-cases.interfaces.js'; +import { Encryptor } from '../../../helpers/encryption/encryptor.js'; @Injectable() export class VerifyChangeUserEmailUseCase - extends AbstractUseCase - implements IVerifyEmailChange + extends AbstractUseCase + implements IVerifyEmailChange { - constructor( - @Inject(BaseType.GLOBAL_DB_CONTEXT) - protected _dbContext: IGlobalDatabaseContext, - private readonly emailService: EmailService, - ) { - super(); - } + constructor( + @Inject(BaseType.GLOBAL_DB_CONTEXT) + protected _dbContext: IGlobalDatabaseContext, + private readonly emailService: EmailService, + ) { + super(); + } - protected async implementation(inputData: ChangeUserEmailDs): Promise { - const { verificationString } = inputData; - const newEmail = inputData.newEmail.toLowerCase(); - const verificationEntity = - await this._dbContext.emailChangeRepository.findEmailChangeWithVerificationString(verificationString); - if (!verificationEntity || !verificationEntity.user) { - throw new HttpException( - { - message: Messages.PASSWORD_RESET_VERIFICATION_FAILED, - }, - HttpStatus.BAD_REQUEST, - ); - } + protected async implementation(inputData: ChangeUserEmailDs): Promise { + const { verificationString } = inputData; + const newEmail = inputData.newEmail.toLowerCase(); + const hashedToken = Encryptor.hashVerificationToken(verificationString); + const verificationEntity = + await this._dbContext.emailChangeRepository.findEmailChangeWithVerificationString(hashedToken); + if (!verificationEntity || !verificationEntity.user) { + throw new HttpException( + { + message: Messages.PASSWORD_RESET_VERIFICATION_FAILED, + }, + HttpStatus.BAD_REQUEST, + ); + } - const foundExistingUsersWithThisEmail = await this._dbContext.userRepository.find({ - where: { email: newEmail }, - }); + const foundExistingUsersWithThisEmail = await this._dbContext.userRepository.find({ + where: { email: newEmail }, + }); - if (foundExistingUsersWithThisEmail.length > 0) { - throw new HttpException( - { - message: Messages.CANNOT_SET_THIS_EMAIL, - }, - HttpStatus.BAD_REQUEST, - ); - } + if (foundExistingUsersWithThisEmail.length > 0) { + throw new HttpException( + { + message: Messages.CANNOT_SET_THIS_EMAIL, + }, + HttpStatus.BAD_REQUEST, + ); + } - const foundUser = await this._dbContext.userRepository.findOneUserById(verificationEntity.user.id); - if (!foundUser) { - throw new HttpException( - { - message: Messages.USER_NOT_FOUND, - }, - HttpStatus.NOT_FOUND, - ); - } - foundUser.email = newEmail; - await this._dbContext.userRepository.saveUserEntity(foundUser); - await this._dbContext.emailChangeRepository.removeEmailChangeEntity(verificationEntity); - await this.emailService.sendEmailChanged(newEmail); - return { message: Messages.EMAIL_CHANGED }; - } + const foundUser = await this._dbContext.userRepository.findOneUserById(verificationEntity.user.id); + if (!foundUser) { + throw new HttpException( + { + message: Messages.USER_NOT_FOUND, + }, + HttpStatus.NOT_FOUND, + ); + } + foundUser.email = newEmail; + await this._dbContext.userRepository.saveUserEntity(foundUser); + await this._dbContext.emailChangeRepository.removeEmailChangeEntity(verificationEntity); + await this.emailService.sendEmailChanged(newEmail); + return { message: Messages.EMAIL_CHANGED }; + } } diff --git a/backend/src/entities/user/use-cases/verify-reset-user-password.use.case.ts b/backend/src/entities/user/use-cases/verify-reset-user-password.use.case.ts index df63c60e5..3b53b0ad2 100644 --- a/backend/src/entities/user/use-cases/verify-reset-user-password.use.case.ts +++ b/backend/src/entities/user/use-cases/verify-reset-user-password.use.case.ts @@ -13,48 +13,49 @@ import { get2FaScope } from '../utils/is-jwt-scope-need.util.js'; @Injectable() export class VerifyResetUserPasswordUseCase - extends AbstractUseCase - implements IVerifyPasswordReset + extends AbstractUseCase + implements IVerifyPasswordReset { - constructor( - @Inject(BaseType.GLOBAL_DB_CONTEXT) - protected _dbContext: IGlobalDatabaseContext, - ) { - super(); - } + constructor( + @Inject(BaseType.GLOBAL_DB_CONTEXT) + protected _dbContext: IGlobalDatabaseContext, + ) { + super(); + } - protected async implementation(inputData: ResetUsualUserPasswordDs): Promise { - const { verificationString, newUserPassword } = inputData; - ValidationHelper.isPasswordStrongOrThrowError(newUserPassword); - const verificationEntity = - await this._dbContext.passwordResetRepository.findPasswordResetWidthVerificationString(verificationString); - if (!verificationEntity || !verificationEntity.user) { - throw new HttpException( - { - message: Messages.PASSWORD_RESET_VERIFICATION_FAILED, - }, - HttpStatus.BAD_REQUEST, - ); - } - const foundUser = await this._dbContext.userRepository.findOneUserById(verificationEntity.user.id); - if (!foundUser) { - throw new HttpException( - { - message: Messages.USER_NOT_FOUND, - }, - HttpStatus.NOT_FOUND, - ); - } - foundUser.password = await Encryptor.hashUserPassword(newUserPassword); - await this._dbContext.passwordResetRepository.removePasswordResetEntity(verificationEntity); - const savedUser = await this._dbContext.userRepository.saveUserEntity(foundUser); - const foundUserCompany = await this._dbContext.companyInfoRepository.finOneCompanyInfoByUserId(savedUser.id); - return { - id: foundUser.id, - email: foundUser.email, - token: generateGwtToken(foundUser, get2FaScope(foundUser, foundUserCompany)), - name: foundUser.name, - externalRegistrationProvider: foundUser.externalRegistrationProvider, - }; - } + protected async implementation(inputData: ResetUsualUserPasswordDs): Promise { + const { verificationString, newUserPassword } = inputData; + ValidationHelper.isPasswordStrongOrThrowError(newUserPassword); + const hashedToken = Encryptor.hashVerificationToken(verificationString); + const verificationEntity = + await this._dbContext.passwordResetRepository.findPasswordResetWidthVerificationString(hashedToken); + if (!verificationEntity || !verificationEntity.user) { + throw new HttpException( + { + message: Messages.PASSWORD_RESET_VERIFICATION_FAILED, + }, + HttpStatus.BAD_REQUEST, + ); + } + const foundUser = await this._dbContext.userRepository.findOneUserById(verificationEntity.user.id); + if (!foundUser) { + throw new HttpException( + { + message: Messages.USER_NOT_FOUND, + }, + HttpStatus.NOT_FOUND, + ); + } + foundUser.password = await Encryptor.hashUserPassword(newUserPassword); + await this._dbContext.passwordResetRepository.removePasswordResetEntity(verificationEntity); + const savedUser = await this._dbContext.userRepository.saveUserEntity(foundUser); + const foundUserCompany = await this._dbContext.companyInfoRepository.finOneCompanyInfoByUserId(savedUser.id); + return { + id: foundUser.id, + email: foundUser.email, + token: generateGwtToken(foundUser, get2FaScope(foundUser, foundUserCompany)), + name: foundUser.name, + externalRegistrationProvider: foundUser.externalRegistrationProvider, + }; + } } diff --git a/backend/src/entities/user/use-cases/verify-user-email.use.case.ts b/backend/src/entities/user/use-cases/verify-user-email.use.case.ts index 89bbc0460..d347e5f5a 100644 --- a/backend/src/entities/user/use-cases/verify-user-email.use.case.ts +++ b/backend/src/entities/user/use-cases/verify-user-email.use.case.ts @@ -5,31 +5,33 @@ import { BaseType } from '../../../common/data-injection.tokens.js'; import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js'; import { Messages } from '../../../exceptions/text/messages.js'; import { OperationResultMessageDs } from '../application/data-structures/operation-result-message.ds.js'; +import { Encryptor } from '../../../helpers/encryption/encryptor.js'; @Injectable() export class VerifyUserEmailUseCase extends AbstractUseCase implements IVerifyEmail { - constructor( - @Inject(BaseType.GLOBAL_DB_CONTEXT) - protected _dbContext: IGlobalDatabaseContext, - ) { - super(); - } + constructor( + @Inject(BaseType.GLOBAL_DB_CONTEXT) + protected _dbContext: IGlobalDatabaseContext, + ) { + super(); + } - protected async implementation(verificationString: string): Promise { - const foundVerificationEntity = - await this._dbContext.emailVerificationRepository.findVerificationWithVerificationString(verificationString); - if (!foundVerificationEntity || !foundVerificationEntity.user) { - throw new HttpException( - { - message: Messages.EMAIL_VERIFICATION_FAILED, - }, - HttpStatus.BAD_REQUEST, - ); - } - const foundUser = await this._dbContext.userRepository.findOneUserById(foundVerificationEntity.user.id); - foundUser.isActive = true; - await this._dbContext.userRepository.saveUserEntity(foundUser); - await this._dbContext.emailVerificationRepository.removeVerificationEntity(foundVerificationEntity); - return { message: Messages.EMAIL_VERIFIED_SUCCESSFULLY }; - } + protected async implementation(verificationString: string): Promise { + const hashedToken = Encryptor.hashVerificationToken(verificationString); + const foundVerificationEntity = + await this._dbContext.emailVerificationRepository.findVerificationWithVerificationString(hashedToken); + if (!foundVerificationEntity || !foundVerificationEntity.user) { + throw new HttpException( + { + message: Messages.EMAIL_VERIFICATION_FAILED, + }, + HttpStatus.BAD_REQUEST, + ); + } + const foundUser = await this._dbContext.userRepository.findOneUserById(foundVerificationEntity.user.id); + foundUser.isActive = true; + await this._dbContext.userRepository.saveUserEntity(foundUser); + await this._dbContext.emailVerificationRepository.removeVerificationEntity(foundVerificationEntity); + return { message: Messages.EMAIL_VERIFIED_SUCCESSFULLY }; + } } diff --git a/backend/src/entities/user/user-email/repository/email-change-custom-repository-extension.ts b/backend/src/entities/user/user-email/repository/email-change-custom-repository-extension.ts index de01e3283..f1f09e619 100644 --- a/backend/src/entities/user/user-email/repository/email-change-custom-repository-extension.ts +++ b/backend/src/entities/user/user-email/repository/email-change-custom-repository-extension.ts @@ -3,33 +3,34 @@ import { UserEntity } from '../../user.entity.js'; import { EmailChangeEntity } from '../email-change.entity.js'; export const emailChangeCustomRepositoryExtension = { - async findEmailChangeWithVerificationString(verificationString: string): Promise { - const qb = this.createQueryBuilder('email_change') - .leftJoinAndSelect('email_change.user', 'user') - .where('email_change.verification_string = :ver_string', { ver_string: verificationString }); - return await qb.getOne(); - }, + async findEmailChangeWithVerificationString(verificationString: string): Promise { + const qb = this.createQueryBuilder('email_change') + .leftJoinAndSelect('email_change.user', 'user') + .where('email_change.verification_string = :ver_string', { ver_string: verificationString }); + return await qb.getOne(); + }, - async removeEmailChangeEntity(emailChangeEntity: EmailChangeEntity): Promise { - return await this.remove(emailChangeEntity); - }, + async removeEmailChangeEntity(emailChangeEntity: EmailChangeEntity): Promise { + return await this.remove(emailChangeEntity); + }, - async saveEmailChangeEntity(emailChangeEntity: EmailChangeEntity): Promise { - return await this.save(emailChangeEntity); - }, + async saveEmailChangeEntity(emailChangeEntity: EmailChangeEntity): Promise { + return await this.save(emailChangeEntity); + }, - async createOrUpdateEmailChangeEntity(user: UserEntity): Promise { - const qb = this.createQueryBuilder('email_change') - .leftJoinAndSelect('email_change.user', 'user') - .where('user.id = :userId', { userId: user.id }); - const foundChange = await qb.getOne(); - if (foundChange) { - await this.remove(foundChange); - } - const verificationString = Encryptor.generateRandomString(); - const newEmailChangeRequest = new EmailChangeEntity(); - newEmailChangeRequest.verification_string = verificationString; - newEmailChangeRequest.user = user; - return await this.save(newEmailChangeRequest); - }, + async createOrUpdateEmailChangeEntity(user: UserEntity): Promise<{ entity: EmailChangeEntity; rawToken: string }> { + const qb = this.createQueryBuilder('email_change') + .leftJoinAndSelect('email_change.user', 'user') + .where('user.id = :userId', { userId: user.id }); + const foundChange = await qb.getOne(); + if (foundChange) { + await this.remove(foundChange); + } + const rawToken = Encryptor.generateRandomString(); + const newEmailChangeRequest = new EmailChangeEntity(); + newEmailChangeRequest.verification_string = Encryptor.hashVerificationToken(rawToken); + newEmailChangeRequest.user = user; + const entity = await this.save(newEmailChangeRequest); + return { entity, rawToken }; + }, }; diff --git a/backend/src/entities/user/user-email/repository/email-change.repository.interface.ts b/backend/src/entities/user/user-email/repository/email-change.repository.interface.ts index 12bb831da..d2605302d 100644 --- a/backend/src/entities/user/user-email/repository/email-change.repository.interface.ts +++ b/backend/src/entities/user/user-email/repository/email-change.repository.interface.ts @@ -2,11 +2,11 @@ import { EmailChangeEntity } from '../email-change.entity.js'; import { UserEntity } from '../../user.entity.js'; export interface IEmailChangeRepository { - findEmailChangeWithVerificationString(verificationString: string): Promise; + findEmailChangeWithVerificationString(verificationString: string): Promise; - removeEmailChangeEntity(emailChangeEntity: EmailChangeEntity): Promise; + removeEmailChangeEntity(emailChangeEntity: EmailChangeEntity): Promise; - saveEmailChangeEntity(emailChangeEntity: EmailChangeEntity): Promise; + saveEmailChangeEntity(emailChangeEntity: EmailChangeEntity): Promise; - createOrUpdateEmailChangeEntity(user: UserEntity): Promise; + createOrUpdateEmailChangeEntity(user: UserEntity): Promise<{ entity: EmailChangeEntity; rawToken: string }>; } diff --git a/backend/src/entities/user/user-invitation/repository/user-invitation-custom-repository-extension.ts b/backend/src/entities/user/user-invitation/repository/user-invitation-custom-repository-extension.ts index 0564a972f..ccc505dd3 100644 --- a/backend/src/entities/user/user-invitation/repository/user-invitation-custom-repository-extension.ts +++ b/backend/src/entities/user/user-invitation/repository/user-invitation-custom-repository-extension.ts @@ -5,47 +5,52 @@ import { UserInvitationEntity } from '../user-invitation.entity.js'; import { IUserInvitationRepository } from './user-invitation-repository.interface.js'; export const userInvitationCustomRepositoryExtension: IUserInvitationRepository = { - async findUserInvitationWithVerificationString(verificationString: string): Promise { - const qb = this.createQueryBuilder('user_invitation') - .leftJoinAndSelect('user_invitation.user', 'user') - .where('user_invitation.verification_string = :ver_string', { - ver_string: verificationString, - }); - return await qb.getOne(); - }, + async findUserInvitationWithVerificationString(verificationString: string): Promise { + const qb = this.createQueryBuilder('user_invitation') + .leftJoinAndSelect('user_invitation.user', 'user') + .where('user_invitation.verification_string = :ver_string', { + ver_string: verificationString, + }); + return await qb.getOne(); + }, - async removeInvitationEntity(entity: UserInvitationEntity): Promise { - return await this.remove(entity); - }, + async removeInvitationEntity(entity: UserInvitationEntity): Promise { + return await this.remove(entity); + }, - async removeOldInvitationEntities(): Promise> { - const qb = this.createQueryBuilder('user_invitation').where('user_invitation.createdAt <= :date_ago', { - date_ago: Constants.ONE_WEEK_AGO(), - }); - const foundEntities = await qb.getMany(); - return await Promise.all( - foundEntities.map(async (invitation: UserInvitationEntity): Promise => { - return await this.remove(invitation); - }), - ); - }, + async removeOldInvitationEntities(): Promise> { + const qb = this.createQueryBuilder('user_invitation').where('user_invitation.createdAt <= :date_ago', { + date_ago: Constants.ONE_WEEK_AGO(), + }); + const foundEntities = await qb.getMany(); + return await Promise.all( + foundEntities.map(async (invitation: UserInvitationEntity): Promise => { + return await this.remove(invitation); + }), + ); + }, - async saveInvitationEntity(entity: UserInvitationEntity): Promise { - return await this.save(entity); - }, + async saveInvitationEntity(entity: UserInvitationEntity): Promise { + return await this.save(entity); + }, - async createOrUpdateInvitationEntity(user: UserEntity, connectionOwnerId: string): Promise { - const qb = this.createQueryBuilder('user_invitation') - .leftJoinAndSelect('user_invitation.user', 'user') - .where('user.id = :userId', { userId: user.id }); - const foundInvitation = await qb.getOne(); - if (foundInvitation) { - await this.remove(foundInvitation); - } - const newInvitation = new UserInvitationEntity(); - newInvitation.verification_string = Encryptor.generateRandomString(); - newInvitation.user = user; - newInvitation.ownerId = connectionOwnerId ? connectionOwnerId : null; - return await this.save(newInvitation); - }, + async createOrUpdateInvitationEntity( + user: UserEntity, + connectionOwnerId: string, + ): Promise<{ entity: UserInvitationEntity; rawToken: string }> { + const qb = this.createQueryBuilder('user_invitation') + .leftJoinAndSelect('user_invitation.user', 'user') + .where('user.id = :userId', { userId: user.id }); + const foundInvitation = await qb.getOne(); + if (foundInvitation) { + await this.remove(foundInvitation); + } + const rawToken = Encryptor.generateRandomString(); + const newInvitation = new UserInvitationEntity(); + newInvitation.verification_string = Encryptor.hashVerificationToken(rawToken); + newInvitation.user = user; + newInvitation.ownerId = connectionOwnerId ? connectionOwnerId : null; + const entity = await this.save(newInvitation); + return { entity, rawToken }; + }, }; diff --git a/backend/src/entities/user/user-invitation/repository/user-invitation-repository.interface.ts b/backend/src/entities/user/user-invitation/repository/user-invitation-repository.interface.ts index c64b9581a..7a99a3f3a 100644 --- a/backend/src/entities/user/user-invitation/repository/user-invitation-repository.interface.ts +++ b/backend/src/entities/user/user-invitation/repository/user-invitation-repository.interface.ts @@ -2,13 +2,16 @@ import { UserEntity } from '../../user.entity.js'; import { UserInvitationEntity } from '../user-invitation.entity.js'; export interface IUserInvitationRepository { - createOrUpdateInvitationEntity(user: UserEntity, connectionOwnerId: string): Promise; + createOrUpdateInvitationEntity( + user: UserEntity, + connectionOwnerId: string, + ): Promise<{ entity: UserInvitationEntity; rawToken: string }>; - findUserInvitationWithVerificationString(verificationString: string): Promise; + findUserInvitationWithVerificationString(verificationString: string): Promise; - removeInvitationEntity(entity: UserInvitationEntity): Promise; + removeInvitationEntity(entity: UserInvitationEntity): Promise; - saveInvitationEntity(entity: UserInvitationEntity): Promise; + saveInvitationEntity(entity: UserInvitationEntity): Promise; - removeOldInvitationEntities(): Promise>; + removeOldInvitationEntities(): Promise>; } diff --git a/backend/src/entities/user/user-password/repository/password-reset-repository.interface.ts b/backend/src/entities/user/user-password/repository/password-reset-repository.interface.ts index 28fce0344..58913ec11 100644 --- a/backend/src/entities/user/user-password/repository/password-reset-repository.interface.ts +++ b/backend/src/entities/user/user-password/repository/password-reset-repository.interface.ts @@ -2,11 +2,11 @@ import { PasswordResetEntity } from '../password-reset.entity.js'; import { UserEntity } from '../../user.entity.js'; export interface IPasswordResetRepository { - findPasswordResetWidthVerificationString(verificationString: string): Promise; + findPasswordResetWidthVerificationString(verificationString: string): Promise; - removePasswordResetEntity(entity: PasswordResetEntity): Promise; + removePasswordResetEntity(entity: PasswordResetEntity): Promise; - savePasswordResetEntity(entity: PasswordResetEntity): Promise; + savePasswordResetEntity(entity: PasswordResetEntity): Promise; - createOrUpdatePasswordResetEntity(user: UserEntity): Promise; + createOrUpdatePasswordResetEntity(user: UserEntity): Promise<{ entity: PasswordResetEntity; rawToken: string }>; } diff --git a/backend/src/entities/user/user-password/repository/user-password-custom-repository-extension.ts b/backend/src/entities/user/user-password/repository/user-password-custom-repository-extension.ts index 5f38fb2a2..af0c03b5f 100644 --- a/backend/src/entities/user/user-password/repository/user-password-custom-repository-extension.ts +++ b/backend/src/entities/user/user-password/repository/user-password-custom-repository-extension.ts @@ -3,35 +3,38 @@ import { UserEntity } from '../../user.entity.js'; import { PasswordResetEntity } from '../password-reset.entity.js'; export const userPasswordResetCustomRepositoryExtension = { - async findPasswordResetWidthVerificationString(verificationString: string): Promise { - const qb = this.createQueryBuilder('password_reset') - .leftJoinAndSelect('password_reset.user', 'user') - .where('password_reset.verification_string = :ver_string', { - ver_string: verificationString, - }); - return await qb.getOne(); - }, + async findPasswordResetWidthVerificationString(verificationString: string): Promise { + const qb = this.createQueryBuilder('password_reset') + .leftJoinAndSelect('password_reset.user', 'user') + .where('password_reset.verification_string = :ver_string', { + ver_string: verificationString, + }); + return await qb.getOne(); + }, - async removePasswordResetEntity(entity: PasswordResetEntity): Promise { - return await this.remove(entity); - }, + async removePasswordResetEntity(entity: PasswordResetEntity): Promise { + return await this.remove(entity); + }, - async savePasswordResetEntity(entity: PasswordResetEntity): Promise { - return await this.save(entity); - }, + async savePasswordResetEntity(entity: PasswordResetEntity): Promise { + return await this.save(entity); + }, - async createOrUpdatePasswordResetEntity(user: UserEntity): Promise { - const qb = this.createQueryBuilder('password_reset') - .leftJoinAndSelect('password_reset.user', 'user') - .where('user.id = :userId', { userId: user.id }); - const foundReset = await qb.getOne(); - if (foundReset) { - await this.remove(foundReset); - } - const verificationString = Encryptor.generateRandomString(); - const newResetPasswordRequest = new PasswordResetEntity(); - newResetPasswordRequest.verification_string = verificationString; - newResetPasswordRequest.user = user; - return await this.save(newResetPasswordRequest); - }, + async createOrUpdatePasswordResetEntity( + user: UserEntity, + ): Promise<{ entity: PasswordResetEntity; rawToken: string }> { + const qb = this.createQueryBuilder('password_reset') + .leftJoinAndSelect('password_reset.user', 'user') + .where('user.id = :userId', { userId: user.id }); + const foundReset = await qb.getOne(); + if (foundReset) { + await this.remove(foundReset); + } + const rawToken = Encryptor.generateRandomString(); + const newResetPasswordRequest = new PasswordResetEntity(); + newResetPasswordRequest.verification_string = Encryptor.hashVerificationToken(rawToken); + newResetPasswordRequest.user = user; + const entity = await this.save(newResetPasswordRequest); + return { entity, rawToken }; + }, }; diff --git a/backend/src/helpers/encryption/encryptor.ts b/backend/src/helpers/encryption/encryptor.ts index 5a8cb460f..54bdfbd1c 100644 --- a/backend/src/helpers/encryption/encryptor.ts +++ b/backend/src/helpers/encryption/encryptor.ts @@ -164,6 +164,14 @@ export class Encryptor { return hmac.digest('hex'); } + static hashVerificationToken(token: string): string { + const privateKey = Encryptor.getPrivateKey(); + if (!privateKey) { + throw new Error('PRIVATE_KEY environment variable is required for token hashing'); + } + return Encryptor.hashDataHMAC(token); + } + static hashDataHMACexternalKey(key: string, dataToHash: string): string { const hmac = createHmac('sha256', key); hmac.update(dataToHash); diff --git a/backend/src/microservices/saas-microservice/use-cases/saas-usual-register-user.use.case.ts b/backend/src/microservices/saas-microservice/use-cases/saas-usual-register-user.use.case.ts index 71c4e1a55..cefc52480 100644 --- a/backend/src/microservices/saas-microservice/use-cases/saas-usual-register-user.use.case.ts +++ b/backend/src/microservices/saas-microservice/use-cases/saas-usual-register-user.use.case.ts @@ -17,95 +17,90 @@ import { ISaasRegisterUser } from './saas-use-cases.interface.js'; @Injectable() export class SaasUsualRegisterUseCase - extends AbstractUseCase - implements ISaasRegisterUser + extends AbstractUseCase + implements ISaasRegisterUser { - constructor( - @Inject(BaseType.GLOBAL_DB_CONTEXT) - protected _dbContext: IGlobalDatabaseContext, - private readonly saasCompanyGatewayService: SaasCompanyGatewayService, - private readonly emailService: EmailService, - private readonly demoDataService: DemoDataService, - ) { - super(); - } + constructor( + @Inject(BaseType.GLOBAL_DB_CONTEXT) + protected _dbContext: IGlobalDatabaseContext, + private readonly saasCompanyGatewayService: SaasCompanyGatewayService, + private readonly emailService: EmailService, + private readonly demoDataService: DemoDataService, + ) { + super(); + } - protected async implementation(userData: SaasUsualUserRegisterDS): Promise { - const { email, password, gclidValue, name, companyId, companyName } = userData; - const foundUser = await this._dbContext.userRepository.findOneUserByEmailAndCompanyId(email, companyId); - const userCompany = await this._dbContext.companyInfoRepository.findCompanyInfoWithUsersById(companyId); + protected async implementation(userData: SaasUsualUserRegisterDS): Promise { + const { email, password, gclidValue, name, companyId, companyName } = userData; + const foundUser = await this._dbContext.userRepository.findOneUserByEmailAndCompanyId(email, companyId); + const userCompany = await this._dbContext.companyInfoRepository.findCompanyInfoWithUsersById(companyId); - if (foundUser) { - throw new HttpException( - { - message: Messages.USER_ALREADY_REGISTERED(email), - }, - HttpStatus.BAD_REQUEST, - ); - } + if (foundUser) { + throw new HttpException( + { + message: Messages.USER_ALREADY_REGISTERED(email), + }, + HttpStatus.BAD_REQUEST, + ); + } - const registerUserData: RegisterUserDs = { - email: email, - password: password, - isActive: false, - gclidValue: gclidValue, - name: name, - }; + const registerUserData: RegisterUserDs = { + email: email, + password: password, + isActive: false, + gclidValue: gclidValue, + name: name, + }; - const savedUser = await this._dbContext.userRepository.saveRegisteringUser(registerUserData); + const savedUser = await this._dbContext.userRepository.saveRegisteringUser(registerUserData); - const createdTestConnections = await this.demoDataService.createDemoDataForUser(savedUser.id); + const createdTestConnections = await this.demoDataService.createDemoDataForUser(savedUser.id); - if (userCompany) { - userCompany.users.push(savedUser); - await this._dbContext.companyInfoRepository.save(userCompany); - } else { - await this.registerEmptyCompany(savedUser, createdTestConnections, companyId, companyName); - } + if (userCompany) { + userCompany.users.push(savedUser); + await this._dbContext.companyInfoRepository.save(userCompany); + } else { + await this.registerEmptyCompany(savedUser, createdTestConnections, companyId, companyName); + } - const createdEmailVerification = - await this._dbContext.emailVerificationRepository.createOrUpdateEmailVerification(savedUser); - const companyCustomDomain = await this.saasCompanyGatewayService.getCompanyCustomDomainById(companyId); + const { rawToken } = await this._dbContext.emailVerificationRepository.createOrUpdateEmailVerification(savedUser); + const companyCustomDomain = await this.saasCompanyGatewayService.getCompanyCustomDomainById(companyId); - await this.emailService.sendEmailConfirmation( - savedUser.email, - createdEmailVerification.verification_string, - companyCustomDomain, - ); + await this.emailService.sendEmailConfirmation(savedUser.email, rawToken, companyCustomDomain); - return { - id: savedUser.id, - createdAt: savedUser.createdAt, - isActive: savedUser.isActive, - email: savedUser.email, - intercom_hash: null, - name: savedUser.name, - role: savedUser.role, - is_2fa_enabled: false, - suspended: false, - externalRegistrationProvider: savedUser.externalRegistrationProvider, - show_test_connections: savedUser.showTestConnections, - }; - } + return { + id: savedUser.id, + createdAt: savedUser.createdAt, + isActive: savedUser.isActive, + email: savedUser.email, + intercom_hash: null, + name: savedUser.name, + role: savedUser.role, + is_2fa_enabled: false, + suspended: false, + externalRegistrationProvider: savedUser.externalRegistrationProvider, + show_test_connections: savedUser.showTestConnections, + }; + } - private async registerEmptyCompany( - savedUser: UserEntity, - testConnections: Array, - companyId: string, - companyName: string, - ): Promise { - if (!companyName) { - companyName = 'New Company'; - } - const newCompanyInfo = new CompanyInfoEntity(); - newCompanyInfo.id = companyId; - newCompanyInfo.name = companyName; - newCompanyInfo.show_test_connections = true; - newCompanyInfo.connections = [...testConnections]; - const savedCompanyInfo = await this._dbContext.companyInfoRepository.save(newCompanyInfo); - savedUser.company = savedCompanyInfo; - savedUser.role = UserRoleEnum.ADMIN; - await this._dbContext.userRepository.saveUserEntity(savedUser); - return savedCompanyInfo; - } + private async registerEmptyCompany( + savedUser: UserEntity, + testConnections: Array, + companyId: string, + companyName: string, + ): Promise { + if (!companyName) { + companyName = 'New Company'; + } + const newCompanyInfo = new CompanyInfoEntity(); + newCompanyInfo.id = companyId; + newCompanyInfo.name = companyName; + newCompanyInfo.show_test_connections = true; + newCompanyInfo.connections = [...testConnections]; + const savedCompanyInfo = await this._dbContext.companyInfoRepository.save(newCompanyInfo); + savedUser.company = savedCompanyInfo; + savedUser.role = UserRoleEnum.ADMIN; + await this._dbContext.userRepository.saveUserEntity(savedUser); + return savedCompanyInfo; + } }