diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 0bb6718..b022261 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -66,10 +66,9 @@ export const enum AuthMessage { } export const enum UserMessage { - USER_NOT_FOUND = "کاربری با این شماره وجود ندارد", + USER_NOT_FOUND = "کاربری با این مشخصات یافت نشد", USER_EXISTS = "با این شماره قبلا ثبت نام شده است", USER_REGISTER_SUCCESS = "ثبت نام موفقیت آمیز بود", - INVALID_USER = "کاربری با این مشخصات یافت نشد", EMAIL_ADDRESS_ALREADY_EXISTS = "ایمیل قبلا ثبت شده است", EMAIL_USER_NOT_FOUND = "ایمیل یافت نشد", EMAIL_USER_DELETED_SUCCESSFULLY = "ایمیل با موفقیت حذف شد", @@ -337,6 +336,7 @@ export const enum BusinessMessage { DOMAIN_NOT_FOUND = "دامنه یافت نشد", DOMAIN_VERIFICATION_TOKEN_REQUIRED = "توکن تایید دامنه مورد نیاز است", INSUFFICIENT_QUOTA = "حجم شما برای ساخت ایمیل کافی نمی باشد", + QUOTA_DOWNGRADE_NOT_ALLOWED = "کاهش حجم ایمیل مجاز نیست، فقط می‌توانید حجم را افزایش دهید", } export const enum DomainMessage { @@ -424,10 +424,18 @@ export const enum RoleMessage { } export const enum SignatureMessage { - NAME_NOT_EMPTY = "نام علامت توضیحی نمیتواند خالی باشد", - NAME_MAX_LENGTH = "NAME_MAX_LENGTH", - NAME_STRING = "NAME_STRING", - TEXT_CONTENT_NOT_EMPTY = "TEXT_CONTENT_NOT_EMPTY", - TEXT_CONTENT_STRING = "TEXT_CONTENT_STRING", - TEXT_CONTENT_MAX_LENGTH = "TEXT_CONTENT_MAX_LENGTH", + NAME_NOT_EMPTY = "نام امضاء نمیتواند خالی باشد", + NAME_MAX_LENGTH = "نام امضاء نمیتواند بیشتر از حداکثر طول مجاز باشد", + NAME_STRING = "نام امضاء باید یک رشته باشد", + TEXT_CONTENT_NOT_EMPTY = "محتوای متنی امضاء نمیتواند خالی باشد", + TEXT_CONTENT_STRING = "محتوای متنی امضاء باید یک رشته باشد", + TEXT_CONTENT_MAX_LENGTH = "محتوای متنی امضاء نمیتواند بیشتر از حداکثر طول مجاز باشد", + IS_ACTIVE_NOT_EMPTY = "وضعیت فعال بودن نمیتواند خالی باشد", + IS_ACTIVE_BOOLEAN = "وضعیت فعال بودن باید بولین باشد", + IS_DEFAULT_NOT_EMPTY = "وضعیت پیش فرض نمیتواند خالی باشد", + IS_DEFAULT_BOOLEAN = "وضعیت پیش فرض باید بولین باشد", + CREATED = "امضاء با موفقیت ایجاد شد", + NOT_FOUND = "امضاء یافت نشد", + DELETED = "امضاء با موفقیت حذف شد", + TOGGLE_ACTIVE = "وضعیت امضاء با موفقیت تغییر کرد", } diff --git a/src/main.ts b/src/main.ts index b9768a0..17acb39 100644 --- a/src/main.ts +++ b/src/main.ts @@ -29,7 +29,7 @@ async function bootstrap() { // Setup WebSocket adapter app.useWebSocketAdapter(new IoAdapter(app)); - app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true })); + app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true })); app.useGlobalInterceptors(new ResponseInterceptor(), new PaginationInterceptor()); diff --git a/src/modules/mail-server/mail-server.controller.ts b/src/modules/mail-server/mail-server.controller.ts new file mode 100644 index 0000000..a98cb31 --- /dev/null +++ b/src/modules/mail-server/mail-server.controller.ts @@ -0,0 +1,18 @@ +import { Controller, Get } from "@nestjs/common"; +import { ApiOperation, ApiResponse } from "@nestjs/swagger"; + +import { MailServerService } from "./services/mail-server.service"; +import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; + +@AuthGuards() +@Controller("mail-server") +export class MailServerController { + constructor(private readonly mailServerService: MailServerService) {} + + @Get("info") + @ApiOperation({ summary: "Get mail server info" }) + @ApiResponse({ status: 200, description: "Mail server info" }) + getMailServerInfo() { + return this.mailServerService.getMailServerInfo(); + } +} diff --git a/src/modules/mail-server/mail-server.module.ts b/src/modules/mail-server/mail-server.module.ts index 59e7b77..312291e 100644 --- a/src/modules/mail-server/mail-server.module.ts +++ b/src/modules/mail-server/mail-server.module.ts @@ -1,6 +1,7 @@ import { HttpModule } from "@nestjs/axios"; import { Module } from "@nestjs/common"; +import { MailServerController } from "./mail-server.controller"; import { MailServerService } from "./services/mail-server.service"; import { WildDuckAddressesService } from "./services/wildduck-addresses.service"; import { WildDuckApplicationPasswordsService } from "./services/wildduck-application-passwords.service"; @@ -25,6 +26,7 @@ import { wildduckConfig } from "../../configs/wildduck.config"; @Module({ imports: [HttpModule.registerAsync(wildduckConfig())], + controllers: [MailServerController], providers: [ WildDuckBaseService, MailServerService, diff --git a/src/modules/mail-server/services/mail-server.service.ts b/src/modules/mail-server/services/mail-server.service.ts index 573c5c3..c55de27 100644 --- a/src/modules/mail-server/services/mail-server.service.ts +++ b/src/modules/mail-server/services/mail-server.service.ts @@ -19,6 +19,8 @@ import { WildDuckSubmissionService } from "./wildduck-submission.service"; import { WildDuckTwoFactorService } from "./wildduck-two-factor.service"; import { WildDuckUsersService } from "./wildduck-users.service"; import { WildDuckWebhooksService } from "./wildduck-webhooks.service"; +import { DANAK_IMAPS_ENCRYPTION, DANAK_IMAPS_PORT, DANAK_SMTPS_ENCRYPTION, DANAK_SMTPS_PORT, DANAK_SMTPS_SERVER } from "../../../common/constants"; +import { DANAK_IMAPS_SERVER } from "../../../common/constants"; @Injectable() export class MailServerService { @@ -45,6 +47,23 @@ export class MailServerService { private readonly twoFactorService: WildDuckTwoFactorService, ) {} + getMailServerInfo() { + return { + setupInfo: { + imapSettings: { + server: DANAK_IMAPS_SERVER, + port: DANAK_IMAPS_PORT, + encryption: DANAK_IMAPS_ENCRYPTION, + }, + smtpSettings: { + server: DANAK_SMTPS_SERVER, + port: DANAK_SMTPS_PORT, + encryption: DANAK_SMTPS_ENCRYPTION, + }, + }, + }; + } + // Expose individual services for direct access get users() { return this.usersService; diff --git a/src/modules/quota-sync/services/quota-sync.service.ts b/src/modules/quota-sync/services/quota-sync.service.ts index 563e69d..16e6c4f 100644 --- a/src/modules/quota-sync/services/quota-sync.service.ts +++ b/src/modules/quota-sync/services/quota-sync.service.ts @@ -27,7 +27,7 @@ export class QuotaSyncService { /** * Sync quota usage every 2 hours */ - @Cron(CronExpression.EVERY_2_HOURS) + @Cron(CronExpression.EVERY_10_SECONDS) async syncAllUsersQuota(): Promise { const em = this.em.fork(); this.logger.log("Starting quota synchronization for all users"); @@ -66,7 +66,7 @@ export class QuotaSyncService { /** * Sync business quota usage every 4 hours */ - @Cron(CronExpression.EVERY_4_HOURS) + @Cron(CronExpression.EVERY_10_SECONDS) async syncAllBusinessQuotas(): Promise { const em = this.em.fork(); this.logger.log("Starting business quota synchronization"); diff --git a/src/modules/signatures/DTO/create-email-signature.dto.ts b/src/modules/signatures/DTO/create-email-signature.dto.ts index 82a705f..ecb215a 100644 --- a/src/modules/signatures/DTO/create-email-signature.dto.ts +++ b/src/modules/signatures/DTO/create-email-signature.dto.ts @@ -10,40 +10,29 @@ export class CreateEmailSignatureDto { @ApiProperty({ description: "Name for the email signature", example: "Business Signature", maxLength: 100 }) name: string; - @IsOptional() @IsNotEmpty({ message: SignatureMessage.TEXT_CONTENT_NOT_EMPTY }) @IsString({ message: SignatureMessage.TEXT_CONTENT_STRING }) @MaxLength(5000, { message: SignatureMessage.TEXT_CONTENT_MAX_LENGTH }) - @ApiPropertyOptional({ + @ApiProperty({ description: "Plain text content of the signature", example: "John Doe\nSoftware Engineer\nCompany Inc.\njohn@company.com", maxLength: 5000, }) - textContent?: string; + textContent: string; + + // @IsOptional() + // @IsString() + // @MaxLength(10000) + // @ApiPropertyOptional({ + // description: "HTML content of the signature", + // example: "
John Doe
Software Engineer
Company Inc.
john@company.com
", + // maxLength: 10000, + // }) + // htmlContent?: string; @IsOptional() - @IsString() - @MaxLength(10000) - @ApiPropertyOptional({ - description: "HTML content of the signature", - example: "
John Doe
Software Engineer
Company Inc.
john@company.com
", - maxLength: 10000, - }) - htmlContent?: string; - - @IsOptional() - @IsBoolean() - @ApiPropertyOptional({ - description: "Whether this signature is active", - default: true, - }) + @IsNotEmpty({ message: SignatureMessage.IS_ACTIVE_NOT_EMPTY }) + @IsBoolean({ message: SignatureMessage.IS_ACTIVE_BOOLEAN }) + @ApiPropertyOptional({ description: "Whether this signature is active", default: true }) isActive?: boolean; - - @IsOptional() - @IsBoolean() - @ApiPropertyOptional({ - description: "Whether this is the default signature for the user", - default: false, - }) - isDefault?: boolean; } diff --git a/src/modules/signatures/email-signatures.controller.ts b/src/modules/signatures/email-signatures.controller.ts index 5c28c0b..1992e22 100644 --- a/src/modules/signatures/email-signatures.controller.ts +++ b/src/modules/signatures/email-signatures.controller.ts @@ -1,18 +1,16 @@ -import { Body, Controller, Delete, Get, Param, Patch, Post, Put, UseGuards } from "@nestjs/common"; -import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from "@nestjs/common"; +import { ApiOperation, ApiResponse } from "@nestjs/swagger"; import { CreateEmailSignatureDto } from "./DTO/create-email-signature.dto"; import { UpdateEmailSignatureDto } from "./DTO/update-email-signature.dto"; import { EmailSignature } from "./entities/email-signature.entity"; import { EmailSignaturesService } from "./services/email-signatures.service"; +import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { UserDec } from "../../common/decorators/user.decorator"; -import { JwtAuthGuard } from "../auth/guards/auth.guard"; import { User } from "../users/entities/user.entity"; -@ApiTags("Email Signatures") -@ApiBearerAuth() -@UseGuards(JwtAuthGuard) @Controller("email-signatures") +@AuthGuards() export class EmailSignaturesController { constructor(private readonly emailSignaturesService: EmailSignaturesService) {} @@ -20,37 +18,22 @@ export class EmailSignaturesController { @ApiOperation({ summary: "Create a new email signature" }) @ApiResponse({ status: 201, description: "Email signature created successfully", type: EmailSignature }) @ApiResponse({ status: 400, description: "Bad request" }) - async create(@UserDec() user: User, @Body() createEmailSignatureDto: CreateEmailSignatureDto): Promise { + create(@UserDec() user: User, @Body() createEmailSignatureDto: CreateEmailSignatureDto) { return this.emailSignaturesService.create(user.id, createEmailSignatureDto); } - @Get() - @ApiOperation({ summary: "Get all email signatures for the user" }) - @ApiResponse({ status: 200, description: "Email signatures retrieved successfully", type: [EmailSignature] }) - async findAll(@UserDec() user: User): Promise { - return this.emailSignaturesService.findAll(user.id); - } - @Get("active") @ApiOperation({ summary: "Get all active email signatures for the user" }) @ApiResponse({ status: 200, description: "Active email signatures retrieved successfully", type: [EmailSignature] }) - async findActive(@UserDec() user: User): Promise { + findActive(@UserDec() user: User) { return this.emailSignaturesService.findActive(user.id); } - @Get("default") - @ApiOperation({ summary: "Get the default email signature for the user" }) - @ApiResponse({ status: 200, description: "Default email signature retrieved successfully", type: EmailSignature }) - @ApiResponse({ status: 404, description: "No default signature found" }) - async findDefault(@UserDec() user: User): Promise { - return this.emailSignaturesService.findDefault(user.id); - } - @Get(":id") @ApiOperation({ summary: "Get a specific email signature" }) @ApiResponse({ status: 200, description: "Email signature retrieved successfully", type: EmailSignature }) @ApiResponse({ status: 404, description: "Email signature not found" }) - async findOne(@Param("id") id: string, @UserDec() user: User): Promise { + findOne(@Param("id") id: string, @UserDec() user: User) { return this.emailSignaturesService.findOne(id, user.id); } @@ -58,40 +41,23 @@ export class EmailSignaturesController { @ApiOperation({ summary: "Update an email signature" }) @ApiResponse({ status: 200, description: "Email signature updated successfully", type: EmailSignature }) @ApiResponse({ status: 404, description: "Email signature not found" }) - async update(@Param("id") id: string, @UserDec() user: User, @Body() updateEmailSignatureDto: UpdateEmailSignatureDto): Promise { + update(@Param("id") id: string, @UserDec() user: User, @Body() updateEmailSignatureDto: UpdateEmailSignatureDto) { return this.emailSignaturesService.update(id, user.id, updateEmailSignatureDto); } - @Put(":id/set-default") - @ApiOperation({ summary: "Set an email signature as default" }) - @ApiResponse({ status: 200, description: "Email signature set as default successfully", type: EmailSignature }) - @ApiResponse({ status: 400, description: "Cannot set inactive signature as default" }) - @ApiResponse({ status: 404, description: "Email signature not found" }) - async setAsDefault(@Param("id") id: string, @UserDec() user: User): Promise { - return this.emailSignaturesService.setAsDefault(id, user.id); - } - - @Put(":id/activate") + @Put(":id/toggle-active") @ApiOperation({ summary: "Activate an email signature" }) @ApiResponse({ status: 200, description: "Email signature activated successfully", type: EmailSignature }) @ApiResponse({ status: 404, description: "Email signature not found" }) - async activate(@Param("id") id: string, @UserDec() user: User): Promise { - return this.emailSignaturesService.activate(id, user.id); - } - - @Put(":id/deactivate") - @ApiOperation({ summary: "Deactivate an email signature" }) - @ApiResponse({ status: 200, description: "Email signature deactivated successfully", type: EmailSignature }) - @ApiResponse({ status: 404, description: "Email signature not found" }) - async deactivate(@Param("id") id: string, @UserDec() user: User): Promise { - return this.emailSignaturesService.deactivate(id, user.id); + toggleActive(@Param("id") id: string, @UserDec() user: User) { + return this.emailSignaturesService.toggleActive(id, user.id); } @Delete(":id") @ApiOperation({ summary: "Delete an email signature" }) @ApiResponse({ status: 200, description: "Email signature deleted successfully" }) @ApiResponse({ status: 404, description: "Email signature not found" }) - async remove(@Param("id") id: string, @UserDec() user: User): Promise { + remove(@Param("id") id: string, @UserDec() user: User) { return this.emailSignaturesService.remove(id, user.id); } } diff --git a/src/modules/signatures/entities/email-signature.entity.ts b/src/modules/signatures/entities/email-signature.entity.ts index f9cdad1..ad204a3 100644 --- a/src/modules/signatures/entities/email-signature.entity.ts +++ b/src/modules/signatures/entities/email-signature.entity.ts @@ -22,8 +22,5 @@ export class EmailSignature extends BaseEntity { @Property({ type: "boolean", default: true }) isActive!: boolean & Opt; - @Property({ type: "boolean", default: false }) - isDefault!: boolean & Opt; - [EntityRepositoryType]?: EmailSignatureRepository; } diff --git a/src/modules/signatures/repositories/email-signature.repository.ts b/src/modules/signatures/repositories/email-signature.repository.ts index ab758a6..7308673 100644 --- a/src/modules/signatures/repositories/email-signature.repository.ts +++ b/src/modules/signatures/repositories/email-signature.repository.ts @@ -4,25 +4,14 @@ import { EmailSignature } from "../entities/email-signature.entity"; export class EmailSignatureRepository extends EntityRepository { async findByUserId(userId: string): Promise { - return this.find({ user: userId }, { orderBy: { isDefault: "DESC", createdAt: "DESC" } }); + return this.find({ user: userId }, { orderBy: { createdAt: "DESC" } }); } async findActiveByUserId(userId: string): Promise { - return this.find({ user: userId, isActive: true }, { orderBy: { isDefault: "DESC", createdAt: "DESC" } }); - } - - async findDefaultByUserId(userId: string): Promise { - return this.findOne({ user: userId, isDefault: true, isActive: true }); + return this.find({ user: userId, isActive: true }, { orderBy: { createdAt: "DESC" } }); } async findByIdAndUserId(id: string, userId: string): Promise { return this.findOne({ id, user: userId }); } - - async updateDefaultSignature(userId: string, newDefaultId: string): Promise { - // remove default from all signatures for this user - await this.nativeUpdate({ user: userId }, { isDefault: false }); - // set the new default - await this.nativeUpdate({ id: newDefaultId, user: userId }, { isDefault: true }); - } } diff --git a/src/modules/signatures/services/email-signatures.service.ts b/src/modules/signatures/services/email-signatures.service.ts index 1b563cd..b2997f6 100644 --- a/src/modules/signatures/services/email-signatures.service.ts +++ b/src/modules/signatures/services/email-signatures.service.ts @@ -1,10 +1,10 @@ import { EntityManager } from "@mikro-orm/core"; import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; +import { SignatureMessage, UserMessage } from "../../../common/enums/message.enum"; import { User } from "../../users/entities/user.entity"; import { CreateEmailSignatureDto } from "../DTO/create-email-signature.dto"; import { UpdateEmailSignatureDto } from "../DTO/update-email-signature.dto"; -import { EmailSignature } from "../entities/email-signature.entity"; import { EmailSignatureRepository } from "../repositories/email-signature.repository"; @Injectable() @@ -13,94 +13,66 @@ export class EmailSignaturesService { private readonly emailSignatureRepository: EmailSignatureRepository, private readonly em: EntityManager, ) {} - - async create(userId: string, createDto: CreateEmailSignatureDto): Promise { - // Validate that at least one content type is provided - if (!createDto.textContent && !createDto.htmlContent) { - throw new BadRequestException("At least one content type (text or HTML) must be provided"); - } - - if (createDto.isDefault) await this.emailSignatureRepository.updateDefaultSignature(userId, ""); + //######################### + async create(userId: string, createDto: CreateEmailSignatureDto) { + const user = await this.em.findOne(User, { id: userId, deletedAt: null }); + if (!user) throw new NotFoundException(UserMessage.USER_NOT_FOUND); const signature = this.emailSignatureRepository.create({ ...createDto, - user: { id: userId } as User, + user, }); await this.em.persistAndFlush(signature); - return signature; + return { + message: SignatureMessage.CREATED, + signature, + }; + } + //######################### + + async findActive(userId: string) { + const signatures = await this.emailSignatureRepository.findActiveByUserId(userId); + return { + signatures, + }; } - async findAll(userId: string): Promise { - return this.emailSignatureRepository.findByUserId(userId); - } + //######################### - async findActive(userId: string): Promise { - return this.emailSignatureRepository.findActiveByUserId(userId); - } + async update(id: string, userId: string, updateDto: UpdateEmailSignatureDto) { + const { signature } = await this.findOne(id, userId); - async findDefault(userId: string): Promise { - return this.emailSignatureRepository.findDefaultByUserId(userId); - } - - async findOne(id: string, userId: string): Promise { - const signature = await this.emailSignatureRepository.findByIdAndUserId(id, userId); - if (!signature) { - throw new NotFoundException("Email signature not found"); - } - return signature; - } - - async update(id: string, userId: string, updateDto: UpdateEmailSignatureDto): Promise { - const signature = await this.findOne(id, userId); - - // If updating to default, remove default from other signatures - if (updateDto.isDefault && !signature.isDefault) { - await this.emailSignatureRepository.updateDefaultSignature(userId, id); - } - - // Update the signature - Object.assign(signature, updateDto); + this.emailSignatureRepository.assign(signature, updateDto); await this.em.flush(); - - return signature; + return { signature }; } + //######################### - async setAsDefault(id: string, userId: string): Promise { - const signature = await this.findOne(id, userId); - - if (!signature.isActive) { - throw new BadRequestException("Cannot set inactive signature as default"); - } - - await this.emailSignatureRepository.updateDefaultSignature(userId, id); - signature.isDefault = true; - - return signature; - } - - async remove(id: string, userId: string): Promise { - const signature = await this.findOne(id, userId); + async remove(id: string, userId: string) { + const { signature } = await this.findOne(id, userId); await this.em.removeAndFlush(signature); + return { message: SignatureMessage.DELETED }; + } + //######################### + + async toggleActive(id: string, userId: string) { + const { signature } = await this.findOne(id, userId); + signature.isActive = !signature.isActive; + + await this.em.flush(); + + return { + message: SignatureMessage.TOGGLE_ACTIVE, + signature, + }; } - async activate(id: string, userId: string): Promise { - const signature = await this.findOne(id, userId); - signature.isActive = true; - await this.em.flush(); - return signature; - } + //######################### + async findOne(id: string, userId: string) { + const signature = await this.emailSignatureRepository.findByIdAndUserId(id, userId); + if (!signature) throw new BadRequestException(SignatureMessage.NOT_FOUND); - async deactivate(id: string, userId: string): Promise { - const signature = await this.findOne(id, userId); - - // If this is the default signature, we need to handle it - if (signature.isDefault) { - signature.isDefault = false; - } - - signature.isActive = false; - await this.em.flush(); - return signature; + return { signature }; } } diff --git a/src/modules/users/entities/refresh-token.entity.ts b/src/modules/users/entities/refresh-token.entity.ts index af8c51b..f7231d2 100644 --- a/src/modules/users/entities/refresh-token.entity.ts +++ b/src/modules/users/entities/refresh-token.entity.ts @@ -8,7 +8,7 @@ export class RefreshToken extends BaseEntity { @Property({ type: "varchar", length: 255 }) token!: string; - @ManyToOne(() => User) + @ManyToOne(() => User, { deleteRule: "cascade" }) user!: User; @Property({ type: "timestamptz" }) diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index 9c22791..fd26fd3 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -1,4 +1,4 @@ -import { Collection, Entity, EntityRepositoryType, ManyToOne, OneToMany, OneToOne, Opt, Property } from "@mikro-orm/core"; +import { Collection, Entity, EntityRepositoryType, ManyToOne, OneToMany, OneToOne, Opt, Property, Unique } from "@mikro-orm/core"; import { RefreshToken } from "./refresh-token.entity"; import { Role } from "./role.entity"; @@ -9,6 +9,7 @@ import { EmailSignature } from "../../signatures/entities/email-signature.entity import { UserRepository } from "../repositories/user.repository"; @Entity({ repository: () => UserRepository }) +@Unique({ properties: ["userName", "domain"], name: "unique_user_name_domain", options: { where: "deleted_at is null" } }) export class User extends BaseEntity { @Property({ type: "varchar", length: 150, nullable: false }) title!: string; diff --git a/src/modules/users/services/users.service.ts b/src/modules/users/services/users.service.ts index 6639b1f..833b93d 100644 --- a/src/modules/users/services/users.service.ts +++ b/src/modules/users/services/users.service.ts @@ -2,14 +2,6 @@ import { EntityManager } from "@mikro-orm/postgresql"; import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common"; import { firstValueFrom } from "rxjs"; -import { - DANAK_IMAPS_ENCRYPTION, - DANAK_IMAPS_PORT, - DANAK_IMAPS_SERVER, - DANAK_SMTPS_ENCRYPTION, - DANAK_SMTPS_PORT, - DANAK_SMTPS_SERVER, -} from "../../../common/constants"; import { BusinessMessage, DomainMessage, MailServerMessage, RoleMessage, UserMessage } from "../../../common/enums/message.enum"; import { QUOTA_CONSTANTS } from "../../businesses/constant"; import { Business } from "../../businesses/entities/business.entity"; @@ -106,6 +98,7 @@ export class UsersService { if (domain.status !== DomainStatus.VERIFIED) throw new BadRequestException(DomainMessage.NOT_VERIFIED); const emailAddress = `${username}@${domain.name}`; + const finalUserName = `${username}-${domain.name}`; const existingUser = await this.userRepository.findOne({ emailAddress }); if (existingUser) throw new BadRequestException(UserMessage.EMAIL_ADDRESS_ALREADY_EXISTS); @@ -120,7 +113,7 @@ export class UsersService { const mailServerUser = await firstValueFrom( this.mailServerService.users.createUser({ - username: `${username}-${domain.name}`, + username: finalUserName, password, address: emailAddress, name: displayName || username, @@ -132,7 +125,7 @@ export class UsersService { const user = this.userRepository.create({ title: title || displayName || username, - userName: username, + userName: finalUserName, password: hashedPassword, emailAddress, emailEnabled: true, @@ -182,19 +175,6 @@ export class UsersService { createdAt: user.createdAt, isActive: user.isActive, }, - setupInfo: { - emailAddress: user.emailAddress!, - imapSettings: { - server: DANAK_IMAPS_SERVER, - port: DANAK_IMAPS_PORT, - encryption: DANAK_IMAPS_ENCRYPTION, - }, - smtpSettings: { - server: DANAK_SMTPS_SERVER, - port: DANAK_SMTPS_PORT, - encryption: DANAK_SMTPS_ENCRYPTION, - }, - }, message: UserMessage.EMAIL_USER_CREATED_SUCCESSFULLY, }; } catch (error) { @@ -245,14 +225,23 @@ 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, - }); + const user = await this.userRepository.findOne( + { + id: userId, + business: { id: businessId }, + emailEnabled: true, + }, + { populate: ["business"] }, + ); if (!user) throw new NotFoundException(UserMessage.EMAIL_USER_NOT_FOUND); + // Validate business quota if quota is being updated + if (quota) { + const currentQuota = user.emailQuota || 0; + this.validateBusinessQuotaAvailability(user.business, currentQuota, quota); + } + try { const updateData: UpdateUserDto = {}; @@ -299,7 +288,13 @@ export class UsersService { if (displayName) user.displayName = displayName; - if (quota) user.emailQuota = quota; + 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; @@ -330,14 +325,21 @@ export class UsersService { * 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, - }); + const user = await this.userRepository.findOne( + { + id: userId, + business: { id: businessId }, + emailEnabled: true, + }, + { populate: ["business"] }, + ); if (!user) throw new NotFoundException(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( @@ -348,7 +350,11 @@ export class UsersService { } user.emailQuota = newQuota; - await this.em.persistAndFlush(user); + + // Update business quota tracking + this.updateBusinessQuotaTracking(user.business, currentQuota, newQuota); + + await this.em.persistAndFlush([user, user.business]); this.logger.log(`Email user quota updated: ${user.emailAddress} to ${newQuota} bytes`); @@ -373,4 +379,33 @@ export class UsersService { status: user.isActive, }; } + + /*******************************/ + private validateBusinessQuotaAvailability(business: Business, currentUserQuota: number, newUserQuota: number): void { + // Prevent quota downgrade - users can only upgrade their quota + if (newUserQuota < currentUserQuota) { + throw new BadRequestException(BusinessMessage.QUOTA_DOWNGRADE_NOT_ALLOWED); + } + + const quotaDifference = newUserQuota - currentUserQuota; + + if (quotaDifference > 0) { + const remainingQuota = Number(business.remainingQuota) || 0; + + if (quotaDifference > remainingQuota) { + throw new BadRequestException(BusinessMessage.INSUFFICIENT_QUOTA); + } + } + } + + //===================================================== + + private updateBusinessQuotaTracking(business: Business, currentUserQuota: number, newUserQuota: number): void { + const quotaDifference = newUserQuota - currentUserQuota; + + business.usedQuota = Number(business.usedQuota) + quotaDifference; + business.remainingQuota = Number(business.quota) - Number(business.usedQuota); + + this.logger.log(`Updated business quota for ${business.name}: used=${business.usedQuota}, remaining=${business.remainingQuota}`); + } }