From ed0e14c89abbbe94dd34c1ac9edbe85d3dec7ae3 Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Sun, 13 Jul 2025 09:45:18 +0330 Subject: [PATCH] chore: add update users --- src/common/enums/message.enum.ts | 1 + src/modules/email/email.controller.ts | 2 +- .../mail-server/DTO/update-user.dto.ts | 70 ++++----------- .../users/DTO/update-email-user.dto.ts | 41 +++++++++ src/modules/users/services/users.service.ts | 88 +++++++++++++++++++ src/modules/users/users.controller.ts | 8 ++ 6 files changed, 154 insertions(+), 56 deletions(-) create mode 100644 src/modules/users/DTO/update-email-user.dto.ts diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index c23edd3..0bb6718 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -99,6 +99,7 @@ export const enum UserMessage { TITLE_OPTIONAL = "عنوان اختیاری است", TITLE_NOT_EMPTY = "عنوان نمیتواند خالی باشد", EMAIL_USER_CREATED_SUCCESSFULLY = "ایمیل با موفقیت ایجاد شد", + EMAIL_USER_UPDATED_SUCCESSFULLY = "ایمیل کاربر با موفقیت به‌روزرسانی شد", STATUS_UPDATED = "وضعیت کاربر با موفقیت تغییر کرد", } diff --git a/src/modules/email/email.controller.ts b/src/modules/email/email.controller.ts index 1e4b82a..0ac171d 100644 --- a/src/modules/email/email.controller.ts +++ b/src/modules/email/email.controller.ts @@ -213,7 +213,7 @@ export class EmailController { @Post("messages/bulk-action") @ApiOperation({ summary: "Perform bulk actions on multiple messages", - description: "Apply actions (seen, delete, archive, favorite, junk, trash) to multiple messages at once", + description: "Apply actions (seen, delete, archive, unarchive, favorite, unfavorite, junk, trash, restore) to multiple messages at once", }) @ApiResponse({ status: 200, description: "Bulk action completed successfully" }) @ApiResponse({ status: 206, description: "Bulk action partially completed" }) diff --git a/src/modules/mail-server/DTO/update-user.dto.ts b/src/modules/mail-server/DTO/update-user.dto.ts index 8140cee..06d1bcd 100644 --- a/src/modules/mail-server/DTO/update-user.dto.ts +++ b/src/modules/mail-server/DTO/update-user.dto.ts @@ -2,18 +2,12 @@ import { ApiPropertyOptional } from "@nestjs/swagger"; import { IsArray, IsBoolean, IsNumber, IsOptional, IsString, IsUrl } from "class-validator"; export class UpdateUserDto { - @ApiPropertyOptional({ - description: "Updated display name for the user", - example: "Updated User Name", - }) + @ApiPropertyOptional({ description: "Updated display name for the user", example: "Updated User Name" }) @IsOptional() @IsString() name?: string; - @ApiPropertyOptional({ - description: "Updated password for the user", - example: "newsecretpassword123", - }) + @ApiPropertyOptional({ description: "Updated password for the user", example: "newsecretpassword123" }) @IsOptional() @IsString() password?: string; @@ -27,102 +21,68 @@ export class UpdateUserDto { @IsString({ each: true }) forward?: string[]; - @ApiPropertyOptional({ - description: "URL to upload all messages to", - example: "https://example.com/webhook", - }) + @ApiPropertyOptional({ description: "URL to upload all messages to", example: "https://example.com/webhook" }) @IsOptional() @IsUrl() targetUrl?: string; - @ApiPropertyOptional({ - description: "Maximum storage in bytes allowed for this user", - example: 2147483648, - }) + @ApiPropertyOptional({ description: "Maximum storage in bytes allowed for this user", example: 2147483648 }) @IsOptional() @IsNumber() quota?: number; - @ApiPropertyOptional({ - description: "Default retention time in milliseconds for mailboxes", - example: 2592000000, - }) + @ApiPropertyOptional({ description: "Default retention time in milliseconds for mailboxes", example: 2592000000 }) @IsOptional() @IsNumber() retention?: number; - @ApiPropertyOptional({ - description: "Language code for the user", - example: "en", - }) + @ApiPropertyOptional({ description: "Language code for the user", example: "en" }) @IsOptional() @IsString() language?: string; - @ApiPropertyOptional({ - description: "Maximum number of recipients allowed to send mail to in 24h", - example: 3000, - }) + @ApiPropertyOptional({ description: "Maximum number of recipients allowed to send mail to in 24h", example: 3000 }) @IsOptional() @IsNumber() recipients?: number; - @ApiPropertyOptional({ - description: "Maximum number of forwarded emails in 24h", - example: 3000, - }) + @ApiPropertyOptional({ description: "Maximum number of forwarded emails in 24h", example: 3000 }) @IsOptional() @IsNumber() forwards?: number; - @ApiPropertyOptional({ - description: "Array of tags to associate with the user", - example: ["premium", "customer"], - }) + @ApiPropertyOptional({ description: "Array of tags to associate with the user", example: ["premium", "customer"] }) @IsOptional() @IsArray() @IsString({ each: true }) tags?: string[]; - @ApiPropertyOptional({ - description: "PGP public key for encryption", - }) + @ApiPropertyOptional({ description: "PGP public key for encryption" }) @IsOptional() @IsString() pubKey?: string; - @ApiPropertyOptional({ - description: "Whether to encrypt stored messages", - }) + @ApiPropertyOptional({ description: "Whether to encrypt stored messages" }) @IsOptional() @IsBoolean() encryptMessages?: boolean; - @ApiPropertyOptional({ - description: "Whether to encrypt forwarded messages", - }) + @ApiPropertyOptional({ description: "Whether to encrypt forwarded messages" }) @IsOptional() @IsBoolean() encryptForwarded?: boolean; - @ApiPropertyOptional({ - description: "Current password for verification", - }) + @ApiPropertyOptional({ description: "Current password for verification" }) @IsOptional() @IsString() existingPassword?: string; - @ApiPropertyOptional({ - description: "Session identifier for logging", - }) + @ApiPropertyOptional({ description: "Session identifier for logging" }) @IsOptional() @IsString() sess?: string; - @ApiPropertyOptional({ - description: "IP address the request was made from", - example: "192.168.1.1", - }) + @ApiPropertyOptional({ description: "IP address the request was made from", example: "192.168.1.1" }) @IsOptional() @IsString() ip?: string; diff --git a/src/modules/users/DTO/update-email-user.dto.ts b/src/modules/users/DTO/update-email-user.dto.ts new file mode 100644 index 0000000..8e1a7ad --- /dev/null +++ b/src/modules/users/DTO/update-email-user.dto.ts @@ -0,0 +1,41 @@ +import { ApiPropertyOptional } from "@nestjs/swagger"; +import { Transform } from "class-transformer"; +import { IsArray, IsNotEmpty, IsNumber, IsOptional, IsString, Matches, MinLength } from "class-validator"; + +import { UserMessage } from "../../../common/enums/message.enum"; + +export class UpdateEmailUserDto { + @IsOptional() + @IsString({ message: UserMessage.PASSWORD_STRING }) + @MinLength(8, { message: UserMessage.PASSWORD_MIN_LENGTH }) + @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/, { message: UserMessage.PASSWORD_MATCHES }) + @ApiPropertyOptional({ description: "New password for email account", example: "NewSecurePassword123!", minLength: 8 }) + password?: string; + + @IsOptional() + @IsString({ message: UserMessage.DISPLAY_NAME_STRING }) + @Transform(({ value }) => value?.trim()) + @ApiPropertyOptional({ description: "Display name for the user", example: "John Doe" }) + displayName?: string; + + @IsOptional() + @IsNotEmpty({ message: UserMessage.QUOTA_NOT_EMPTY }) + @IsNumber({ allowNaN: false, allowInfinity: false }, { message: UserMessage.QUOTA_NUMBER }) + @ApiPropertyOptional({ description: "Email quota in bytes", example: 2147483648 }) + quota?: number; + + @IsOptional() + @IsArray({ message: UserMessage.ALIASES_ARRAY }) + @IsString({ each: true, message: UserMessage.ALIASES_STRING }) + @ApiPropertyOptional({ + description: "Email aliases for this user (will replace existing aliases)", + example: ["johndoe@example.com", "j.doe@example.com"], + }) + aliases?: string[]; + + @IsOptional() + @IsString({ message: UserMessage.TITLE_STRING }) + @Transform(({ value }) => value?.trim()) + @ApiPropertyOptional({ description: "User role/title", example: "Senior Marketing Manager" }) + title?: string; +} diff --git a/src/modules/users/services/users.service.ts b/src/modules/users/services/users.service.ts index 722a714..6639b1f 100644 --- a/src/modules/users/services/users.service.ts +++ b/src/modules/users/services/users.service.ts @@ -16,9 +16,11 @@ import { Business } from "../../businesses/entities/business.entity"; 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"; import { MailServerService } from "../../mail-server/services/mail-server.service"; import { PasswordService } from "../../utils/services/password.service"; import { CreateEmailUserDto } from "../DTO/create-email-user.dto"; +import { UpdateEmailUserDto } from "../DTO/update-email-user.dto"; import { UserListQueryDto } from "../DTO/user-list-query.dto"; import { Role } from "../entities/role.entity"; import { User } from "../entities/user.entity"; @@ -238,6 +240,92 @@ export class UsersService { } } + /*******************************/ + + async updateEmailUser(userId: string, businessId: string, updateEmailUserDto: UpdateEmailUserDto) { + const { password, displayName, quota, aliases, title } = updateEmailUserDto; + + const user = await this.userRepository.findOne({ + id: userId, + business: { id: businessId }, + emailEnabled: true, + }); + + if (!user) throw new NotFoundException(UserMessage.EMAIL_USER_NOT_FOUND); + + try { + const updateData: UpdateUserDto = {}; + + if (password) { + updateData.password = password; + const hashedPassword = await this.passwordService.hashPassword(password); + user.password = hashedPassword; + } + + if (displayName) updateData.name = displayName; + + if (quota) updateData.quota = quota; + + if (Object.keys(updateData).length > 0) { + await firstValueFrom(this.mailServerService.users.updateUser(user.wildduckUserId!, updateData)); + } + + if (aliases && aliases.length > 0) { + 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); + } + } + } + + 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); + } + } + } + + if (displayName) user.displayName = displayName; + + if (quota) user.emailQuota = quota; + + if (title) user.title = title; + + await this.em.flush(); + + 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, + isActive: user.isActive, + updatedAt: user.updatedAt, + }, + }; + } catch (error) { + this.logger.error(`Failed to update email user ${user.emailAddress}:`, error); + throw new BadRequestException("Failed to update email user"); + } + } + /** * Update email user quota */ diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index 900b891..0b8e19f 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseIntercepto import { ApiHeader, ApiOperation, ApiResponse } from "@nestjs/swagger"; import { CreateEmailUserDto } from "./DTO/create-email-user.dto"; +import { UpdateEmailUserDto } from "./DTO/update-email-user.dto"; import { UserListQueryDto } from "./DTO/user-list-query.dto"; import { UsersService } from "./services/users.service"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; @@ -45,6 +46,13 @@ export class UsersController { return this.usersService.deleteEmailUser(paramDto.id, businessId); } + @Patch(":id") + @ApiOperation({ summary: "Update email user" }) + @ApiResponse({ status: 200, description: "Email user updated successfully" }) + updateEmailUser(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string, @Body() body: UpdateEmailUserDto) { + return this.usersService.updateEmailUser(paramDto.id, businessId, body); + } + @Patch(":id/quota") @ApiOperation({ summary: "Update email user quota" }) @ApiResponse({ status: 200, description: "Email user quota updated successfully" })