diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 52e2a3d..1dac965 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -64,6 +64,7 @@ export const enum AuthMessage { USER_ACCOUNT_INACTIVE = "حساب کاربری شما غیر فعال است", TOKEN_MISSED_OR_EXPIRED = "توکن مفقود یا منقضی شده است", OLD_PASSWORD_REQUIRED = "رمز عبور قبلی الزامی است", + PASSWORD_CHANGE_FAILED = "تغییر رمز عبور با مشکل روبرد شد", } export const enum UserMessage { diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index af0689b..0b4f611 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -1,9 +1,13 @@ -import { BadRequestException, Injectable } from "@nestjs/common"; +import { EntityManager } from "@mikro-orm/postgresql"; +import { BadRequestException, Injectable, Logger } from "@nestjs/common"; +import { firstValueFrom } from "rxjs"; import { TokensService } from "./tokens.service"; import { TwoFactorService } from "./two-factor.service"; import { AuthMessage, TwoFactorMessage } from "../../../common/enums/message.enum"; +import { MailServerService } from "../../mail-server/services/mail-server.service"; import { NotificationQueue } from "../../notifications/queue/notification.queue"; +import { User } from "../../users/entities/user.entity"; import { UserRepository } from "../../users/repositories/user.repository"; import { PasswordService } from "../../utils/services/password.service"; import { ChangePasswordDto } from "../DTO/change-password.dto"; @@ -11,12 +15,15 @@ import { LoginDto } from "../DTO/login.dto"; @Injectable() export class AuthService { + private readonly logger = new Logger(AuthService.name); constructor( private readonly notificationQueue: NotificationQueue, private readonly userRepository: UserRepository, private readonly tokensService: TokensService, private readonly passwordService: PasswordService, private readonly twoFactorService: TwoFactorService, + private readonly mailServerService: MailServerService, + private readonly em: EntityManager, ) {} //============================================== @@ -69,20 +76,38 @@ export class AuthService { //============================================== //============================================== async changePassword(userId: string, changePasswordDto: ChangePasswordDto) { - const user = await this.userRepository.findOne({ id: userId, deletedAt: null }); - if (!user) throw new BadRequestException(AuthMessage.USER_NOT_FOUND); + const em = this.em.fork(); - const passCompare = await this.passwordService.comparePasswords(changePasswordDto.oldPassword, user.password); - if (!passCompare) throw new BadRequestException(AuthMessage.CURRENT_PASSWORD_INVALID); + try { + em.begin(); - if (changePasswordDto.newPassword !== changePasswordDto.repeatPassword) throw new BadRequestException(AuthMessage.INVALID_REPEAT_PASSWORD); + const user = await em.findOne(User, { id: userId, deletedAt: null }); + if (!user) throw new BadRequestException(AuthMessage.USER_NOT_FOUND); - const hashedPassword = await this.passwordService.hashPassword(changePasswordDto.newPassword); - await this.userRepository.nativeUpdate({ id: userId }, { password: hashedPassword }); + const passCompare = await this.passwordService.comparePasswords(changePasswordDto.oldPassword, user.password); + if (!passCompare) throw new BadRequestException(AuthMessage.CURRENT_PASSWORD_INVALID); - return { - message: AuthMessage.PASSWORD_CHANGED_SUCCESSFULLY, - }; + if (changePasswordDto.newPassword !== changePasswordDto.repeatPassword) throw new BadRequestException(AuthMessage.INVALID_REPEAT_PASSWORD); + + const hashedPassword = await this.passwordService.hashPassword(changePasswordDto.newPassword); + + user.password = hashedPassword; + + const res = await firstValueFrom(this.mailServerService.users.updateUser(user.wildduckUserId, { password: changePasswordDto.newPassword })); + if (!res.success) throw new BadRequestException(AuthMessage.PASSWORD_CHANGE_FAILED); + + await em.flush(); + + await em.commit(); + + return { + message: AuthMessage.PASSWORD_CHANGED_SUCCESSFULLY, + }; + } catch (error) { + await em.rollback(); + this.logger.error("Error in changePassword of user", error); + throw error; + } } //============================================== diff --git a/src/modules/email-utils/services/email-polling.service.ts b/src/modules/email-utils/services/email-polling.service.ts index b668995..d19c424 100644 --- a/src/modules/email-utils/services/email-polling.service.ts +++ b/src/modules/email-utils/services/email-polling.service.ts @@ -55,7 +55,6 @@ export class EmailPollingService { async stopPollingForUser(userId: string): Promise { try { const activeJob = this.activeJobs.get(userId); - console.log(activeJob); if (!activeJob) { this.logger.debug(`No active polling job found for user ${userId}`); return; diff --git a/src/modules/mail-server/DTO/create-user.dto.ts b/src/modules/mail-server/DTO/create-user.dto.ts index 9c12436..ee5e058 100644 --- a/src/modules/mail-server/DTO/create-user.dto.ts +++ b/src/modules/mail-server/DTO/create-user.dto.ts @@ -42,17 +42,11 @@ export class CreateUserDto { @ApiProperty({ description: "Password for the user. You can use false to create an account without a password", example: "secretpass" }) password: string; - @ApiPropertyOptional({ - description: "Bcrypt hash for password. If provided, plain text password is ignored", - example: false, - }) + @ApiPropertyOptional({ description: "Bcrypt hash for password indicate if the password is hashed", example: false }) @IsOptional() - hashedPassword?: string | boolean; + hashedPassword?: boolean; - @ApiPropertyOptional({ - description: "Allow weak passwords", - default: true, - }) + @ApiPropertyOptional({ description: "Allow weak passwords", default: true }) @IsOptional() @IsBoolean() allowUnsafe?: boolean; diff --git a/src/modules/mail-server/DTO/update-user.dto.ts b/src/modules/mail-server/DTO/update-user.dto.ts index dc48109..e542906 100644 --- a/src/modules/mail-server/DTO/update-user.dto.ts +++ b/src/modules/mail-server/DTO/update-user.dto.ts @@ -1,5 +1,5 @@ import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsArray, IsBoolean, IsNumber, IsOptional, IsString, IsUrl } from "class-validator"; +import { IsArray, IsBoolean, IsInt, IsNumber, IsOptional, IsString, IsUrl, Max, Min } from "class-validator"; export class UpdateUserDto { @ApiPropertyOptional({ description: "Updated display name for the user", example: "Updated User Name" }) @@ -7,11 +7,6 @@ export class UpdateUserDto { @IsString() name?: string; - @ApiPropertyOptional({ description: "Updated password for the user", example: "newsecretpassword123" }) - @IsOptional() - @IsString() - password?: string; - @ApiPropertyOptional({ description: "Array of email addresses to forward all messages to", example: ["forward1@example.com", "forward2@example.com"], @@ -57,11 +52,6 @@ export class UpdateUserDto { @IsString({ each: true }) tags?: string[]; - @ApiPropertyOptional({ description: "PGP public key for encryption" }) - @IsOptional() - @IsString() - pubKey?: string; - @ApiPropertyOptional({ description: "Whether to encrypt stored messages" }) @IsOptional() @IsBoolean() @@ -72,18 +62,40 @@ export class UpdateUserDto { @IsBoolean() encryptForwarded?: boolean; + @ApiPropertyOptional({ description: "Updated password for the user", example: "newsecretpassword123" }) + @IsOptional() + @IsString() + password?: string; + @ApiPropertyOptional({ description: "Current password for verification" }) @IsOptional() @IsString() existingPassword?: string; - @ApiPropertyOptional({ description: "Session identifier for logging" }) + @ApiPropertyOptional({ description: "Hashed password for the user indicate if the password is hashed" }) @IsOptional() - @IsString() - sess?: string; + @IsBoolean() + hashedPassword?: boolean; - @ApiPropertyOptional({ description: "IP address the request was made from", example: "192.168.1.1" }) + @ApiPropertyOptional({ description: "Spam level for the user" }) @IsOptional() - @IsString() - ip?: string; + @IsInt() + @Min(0) + @Max(100) + spamLevel?: number; + + // @ApiPropertyOptional({ description: "PGP public key for encryption" }) + // @IsOptional() + // @IsString() + // pubKey?: string; + + // @ApiPropertyOptional({ description: "Session identifier for logging" }) + // @IsOptional() + // @IsString() + // sess?: string; + + // @ApiPropertyOptional({ description: "IP address the request was made from", example: "192.168.1.1" }) + // @IsOptional() + // @IsString() + // ip?: string; } diff --git a/src/modules/mail-server/services/wildduck-base.service.ts b/src/modules/mail-server/services/wildduck-base.service.ts index 1c69d75..64b0f5d 100644 --- a/src/modules/mail-server/services/wildduck-base.service.ts +++ b/src/modules/mail-server/services/wildduck-base.service.ts @@ -50,7 +50,8 @@ export class WildDuckBaseService { return response.data as T; }), catchError((error) => { - this.logger.error(`WildDuck API error: ${error.message}`, error.stack); + this.logger.error(`WildDuck API error message: ${error.message}`, error.stack); + this.logger.error(`WildDuck API error data: ${JSON.stringify(error.response?.data)}`); if (error.response?.data?.error) { return throwError(() => this.handleMailServerError(error.response.data.error, error.response.status)); diff --git a/src/modules/users/services/users.service.ts b/src/modules/users/services/users.service.ts index b59a014..d886ad1 100644 --- a/src/modules/users/services/users.service.ts +++ b/src/modules/users/services/users.service.ts @@ -9,6 +9,7 @@ import { DomainStatus } from "../../domains/enums/domain-status.enum"; import { DomainAutomationService } from "../../domains/services/domain-automation.service"; import { DomainsService } from "../../domains/services/domains.service"; import { UpdateUserDto } from "../../mail-server/DTO/update-user.dto"; +import { AddressInfo } from "../../mail-server/interfaces/addresses-response.interface"; import { MailServerService } from "../../mail-server/services/mail-server.service"; import { QuotaSyncService } from "../../quota-sync/services/quota-sync.service"; import { UserSettingsService } from "../../settings/services/user-settings.service"; @@ -260,12 +261,7 @@ export class UsersService { if (aliases && aliases.length > 0) { for (const alias of aliases) { try { - await firstValueFrom( - this.mailServerService.addresses.createUserAddress(mailServerUser.id, { - address: alias, - main: false, - }), - ); + await firstValueFrom(this.mailServerService.addresses.createUserAddress(mailServerUser.id, { address: alias, main: false })); } catch (error) { this.logger.warn(`Failed to create alias ${alias} for user ${emailAddress}:`, error); } @@ -325,9 +321,7 @@ export class UsersService { await this.em.persistAndFlush(user); - //sync quota await this.quotaSyncService.scheduleUserQuotaSync(user.id, businessId, user.wildduckUserId); - //sync business quota await this.quotaSyncService.scheduleBusinessQuotaSync(businessId, user.business.name); this.logger.log(`Email user deleted: ${user.emailAddress} for business ${businessId}`); @@ -342,165 +336,112 @@ export class UsersService { /*******************************/ async updateEmailUser(userId: string, businessId: string, updateEmailUserDto: UpdateEmailUserDto) { + const em = this.em.fork(); + const { password, displayName, quota, aliases, title, templateId, forwarders } = updateEmailUserDto; - const user = await this.userRepository.findOne( - { - id: userId, - business: { id: businessId }, - emailEnabled: true, - }, - { populate: ["business"] }, - ); - - if (!user) throw new BadRequestException(UserMessage.EMAIL_USER_NOT_FOUND); - - if (templateId) { - const template = await this.em.findOne(Template, { id: templateId, business: { id: businessId } }); - if (!template) throw new BadRequestException(TemplateMessage.TEMPLATE_NOT_FOUND); - user.template = template; - } - - // Validate business quota if quota is being updated - if (quota) { - const currentQuota = user.emailQuota || 0; - this.validateBusinessQuotaAvailability(user.business, currentQuota, quota); - } - try { + em.begin(); + + const user = await em.findOne(User, { id: userId, business: { id: businessId }, emailEnabled: true }, { populate: ["business"] }); + + if (!user) throw new BadRequestException(UserMessage.EMAIL_USER_NOT_FOUND); + + if (templateId) { + const template = await em.findOne(Template, { id: templateId, business: { id: businessId } }); + if (!template) throw new BadRequestException(TemplateMessage.TEMPLATE_NOT_FOUND); + user.template = template; + } + + if (quota) { + const currentQuota = user.emailQuota || 0; + this.validateBusinessQuotaAvailability(user.business, currentQuota, quota); + } + const updateData: UpdateUserDto = {}; if (password) { - updateData.password = password; + // updateData.password = password; const hashedPassword = await this.passwordService.hashPassword(password); user.password = hashedPassword; + updateData.password = hashedPassword; } if (displayName) updateData.name = displayName; if (quota) updateData.quota = quota; - if (forwarders !== undefined) { + if (forwarders !== undefined && forwarders.length > 0) { updateData.targets = forwarders; user.forwarders = forwarders; } + this.logger.warn(`Updating email user ${user.emailAddress} with data: ${JSON.stringify(updateData)}`); + if (Object.keys(updateData).length > 0) { - await firstValueFrom(this.mailServerService.users.updateUser(user.wildduckUserId!, updateData)); + await firstValueFrom(this.mailServerService.users.updateUser(user.wildduckUserId, { ...updateData, hashedPassword: true })); } if (aliases && aliases.length > 0) { - const currentAliases = await firstValueFrom(this.mailServerService.addresses.listUserAddresses(user.wildduckUserId!)); + const currentAliases = await firstValueFrom(this.mailServerService.addresses.listUserAddresses(user.wildduckUserId)); - for (const alias of currentAliases.results) { - if (!alias.main && alias.address !== user.emailAddress) { - try { - await firstValueFrom(this.mailServerService.addresses.deleteUserAddress(user.wildduckUserId!, alias.id)); - } catch (error) { - this.logger.warn(`Failed to delete alias ${alias.address}:`, error); - } - } - } + await this.deleteUserAliases(user, currentAliases.results); - for (const alias of aliases) { - try { - await firstValueFrom( - this.mailServerService.addresses.createUserAddress(user.wildduckUserId!, { - address: alias, - main: false, - }), - ); - } catch (error) { - this.logger.warn(`Failed to create alias ${alias}:`, error); - } - } + await this.createUserAliases(user, aliases); } if (displayName) user.displayName = displayName; + if (title) user.title = title; if (quota) { const currentQuota = user.emailQuota || 0; user.emailQuota = quota; - // Update business quota tracking this.updateBusinessQuotaTracking(user.business, currentQuota, quota); } - if (title) user.title = title; + await em.flush(); - await this.em.flush(); + await this.quotaSyncService.scheduleUserQuotaSync(user.id, businessId, user.wildduckUserId); - //sync quota - if (user.wildduckUserId) { - await this.quotaSyncService.scheduleUserQuotaSync(user.id, businessId, user.wildduckUserId); - } - - //sync business quota await this.quotaSyncService.scheduleBusinessQuotaSync(businessId, user.business.name); this.logger.log(`Email user updated: ${user.emailAddress} for business ${businessId}`); - return { - message: UserMessage.EMAIL_USER_UPDATED_SUCCESSFULLY, - user: { - id: user.id, - emailAddress: user.emailAddress!, - displayName: user.displayName!, - title: user.title, - emailQuota: user.emailQuota!, - emailEnabled: user.emailEnabled, - forwarders: user.forwarders || [], - isActive: user.isActive, - updatedAt: user.updatedAt, - }, - }; + await em.commit(); + + return { message: UserMessage.EMAIL_USER_UPDATED_SUCCESSFULLY, user: { id: user.id, emailAddress: user.emailAddress } }; } catch (error) { - this.logger.error(`Failed to update email user ${user.emailAddress}:`, error); + await em.rollback(); + this.logger.error(`Failed to update email user ${userId}:`, error); throw new BadRequestException("Failed to update email user"); } } - /** - * Update email user quota - */ + /*******************************/ async updateEmailUserQuota(userId: string, businessId: string, newQuota: number) { - const user = await this.userRepository.findOne( - { - id: userId, - business: { id: businessId }, - emailEnabled: true, - }, - { populate: ["business"] }, - ); + const user = await this.userRepository.findOne({ id: userId, business: { id: businessId }, emailEnabled: true }, { populate: ["business"] }); if (!user) throw new BadRequestException(UserMessage.EMAIL_USER_NOT_FOUND); - // Validate business quota before updating const currentQuota = user.emailQuota || 0; this.validateBusinessQuotaAvailability(user.business, currentQuota, newQuota); try { if (user.wildduckUserId) { - await firstValueFrom( - this.mailServerService.users.updateUser(user.wildduckUserId, { - quota: newQuota, - }), - ); + await firstValueFrom(this.mailServerService.users.updateUser(user.wildduckUserId, { quota: newQuota })); } user.emailQuota = newQuota; - // Update business quota tracking this.updateBusinessQuotaTracking(user.business, currentQuota, newQuota); await this.em.persistAndFlush([user, user.business]); - //sync quota if (user.wildduckUserId) { await this.quotaSyncService.scheduleUserQuotaSync(user.id, businessId, user.wildduckUserId); } - //sync business quota await this.quotaSyncService.scheduleBusinessQuotaSync(businessId, user.business.name); this.logger.log(`Email user quota updated: ${user.emailAddress} to ${newQuota} bytes`); @@ -545,7 +486,7 @@ export class UsersService { } } - //===================================================== + //##################################################### private updateBusinessQuotaTracking(business: Business, currentUserQuota: number, newUserQuota: number): void { const quotaDifference = newUserQuota - currentUserQuota; @@ -555,4 +496,30 @@ export class UsersService { this.logger.log(`Updated business quota for ${business.name}: used=${business.usedQuota}, remaining=${business.remainingQuota}`); } + + //##################################################### + + private async createUserAliases(user: User, aliases: string[]) { + for (const alias of aliases) { + try { + await firstValueFrom(this.mailServerService.addresses.createUserAddress(user.wildduckUserId, { address: alias, main: false })); + } catch (error) { + this.logger.warn(`Failed to create alias ${alias} for user ${user.emailAddress}:`, error); + } + } + } + + //##################################################### + + private async deleteUserAliases(user: User, aliases: AddressInfo[]) { + for (const alias of aliases) { + if (!alias.main && alias.address !== user.emailAddress) { + try { + await firstValueFrom(this.mailServerService.addresses.deleteUserAddress(user.wildduckUserId, alias.id)); + } catch (error) { + this.logger.warn(`Failed to delete alias ${alias} for user ${user.emailAddress}:`, error); + } + } + } + } }