chore: update user fix

This commit is contained in:
mahyargdz
2025-07-13 13:16:40 +03:30
parent ed0e14c89a
commit 00dd77377d
14 changed files with 205 additions and 209 deletions
+16 -8
View File
@@ -66,10 +66,9 @@ export const enum AuthMessage {
} }
export const enum UserMessage { export const enum UserMessage {
USER_NOT_FOUND = "کاربری با این شماره وجود ندارد", USER_NOT_FOUND = "کاربری با این مشخصات یافت نشد",
USER_EXISTS = "با این شماره قبلا ثبت نام شده است", USER_EXISTS = "با این شماره قبلا ثبت نام شده است",
USER_REGISTER_SUCCESS = "ثبت نام موفقیت آمیز بود", USER_REGISTER_SUCCESS = "ثبت نام موفقیت آمیز بود",
INVALID_USER = "کاربری با این مشخصات یافت نشد",
EMAIL_ADDRESS_ALREADY_EXISTS = "ایمیل قبلا ثبت شده است", EMAIL_ADDRESS_ALREADY_EXISTS = "ایمیل قبلا ثبت شده است",
EMAIL_USER_NOT_FOUND = "ایمیل یافت نشد", EMAIL_USER_NOT_FOUND = "ایمیل یافت نشد",
EMAIL_USER_DELETED_SUCCESSFULLY = "ایمیل با موفقیت حذف شد", EMAIL_USER_DELETED_SUCCESSFULLY = "ایمیل با موفقیت حذف شد",
@@ -337,6 +336,7 @@ export const enum BusinessMessage {
DOMAIN_NOT_FOUND = "دامنه یافت نشد", DOMAIN_NOT_FOUND = "دامنه یافت نشد",
DOMAIN_VERIFICATION_TOKEN_REQUIRED = "توکن تایید دامنه مورد نیاز است", DOMAIN_VERIFICATION_TOKEN_REQUIRED = "توکن تایید دامنه مورد نیاز است",
INSUFFICIENT_QUOTA = "حجم شما برای ساخت ایمیل کافی نمی باشد", INSUFFICIENT_QUOTA = "حجم شما برای ساخت ایمیل کافی نمی باشد",
QUOTA_DOWNGRADE_NOT_ALLOWED = "کاهش حجم ایمیل مجاز نیست، فقط می‌توانید حجم را افزایش دهید",
} }
export const enum DomainMessage { export const enum DomainMessage {
@@ -424,10 +424,18 @@ export const enum RoleMessage {
} }
export const enum SignatureMessage { export const enum SignatureMessage {
NAME_NOT_EMPTY = "نام علامت توضیحی نمیتواند خالی باشد", NAME_NOT_EMPTY = "نام امضاء نمیتواند خالی باشد",
NAME_MAX_LENGTH = "NAME_MAX_LENGTH", NAME_MAX_LENGTH = "نام امضاء نمیتواند بیشتر از حداکثر طول مجاز باشد",
NAME_STRING = "NAME_STRING", NAME_STRING = "نام امضاء باید یک رشته باشد",
TEXT_CONTENT_NOT_EMPTY = "TEXT_CONTENT_NOT_EMPTY", TEXT_CONTENT_NOT_EMPTY = "محتوای متنی امضاء نمیتواند خالی باشد",
TEXT_CONTENT_STRING = "TEXT_CONTENT_STRING", TEXT_CONTENT_STRING = "محتوای متنی امضاء باید یک رشته باشد",
TEXT_CONTENT_MAX_LENGTH = "TEXT_CONTENT_MAX_LENGTH", TEXT_CONTENT_MAX_LENGTH = "محتوای متنی امضاء نمیتواند بیشتر از حداکثر طول مجاز باشد",
IS_ACTIVE_NOT_EMPTY = "وضعیت فعال بودن نمیتواند خالی باشد",
IS_ACTIVE_BOOLEAN = "وضعیت فعال بودن باید بولین باشد",
IS_DEFAULT_NOT_EMPTY = "وضعیت پیش فرض نمیتواند خالی باشد",
IS_DEFAULT_BOOLEAN = "وضعیت پیش فرض باید بولین باشد",
CREATED = "امضاء با موفقیت ایجاد شد",
NOT_FOUND = "امضاء یافت نشد",
DELETED = "امضاء با موفقیت حذف شد",
TOGGLE_ACTIVE = "وضعیت امضاء با موفقیت تغییر کرد",
} }
+1 -1
View File
@@ -29,7 +29,7 @@ async function bootstrap() {
// Setup WebSocket adapter // Setup WebSocket adapter
app.useWebSocketAdapter(new IoAdapter(app)); 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()); app.useGlobalInterceptors(new ResponseInterceptor(), new PaginationInterceptor());
@@ -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();
}
}
@@ -1,6 +1,7 @@
import { HttpModule } from "@nestjs/axios"; import { HttpModule } from "@nestjs/axios";
import { Module } from "@nestjs/common"; import { Module } from "@nestjs/common";
import { MailServerController } from "./mail-server.controller";
import { MailServerService } from "./services/mail-server.service"; import { MailServerService } from "./services/mail-server.service";
import { WildDuckAddressesService } from "./services/wildduck-addresses.service"; import { WildDuckAddressesService } from "./services/wildduck-addresses.service";
import { WildDuckApplicationPasswordsService } from "./services/wildduck-application-passwords.service"; import { WildDuckApplicationPasswordsService } from "./services/wildduck-application-passwords.service";
@@ -25,6 +26,7 @@ import { wildduckConfig } from "../../configs/wildduck.config";
@Module({ @Module({
imports: [HttpModule.registerAsync(wildduckConfig())], imports: [HttpModule.registerAsync(wildduckConfig())],
controllers: [MailServerController],
providers: [ providers: [
WildDuckBaseService, WildDuckBaseService,
MailServerService, MailServerService,
@@ -19,6 +19,8 @@ import { WildDuckSubmissionService } from "./wildduck-submission.service";
import { WildDuckTwoFactorService } from "./wildduck-two-factor.service"; import { WildDuckTwoFactorService } from "./wildduck-two-factor.service";
import { WildDuckUsersService } from "./wildduck-users.service"; import { WildDuckUsersService } from "./wildduck-users.service";
import { WildDuckWebhooksService } from "./wildduck-webhooks.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() @Injectable()
export class MailServerService { export class MailServerService {
@@ -45,6 +47,23 @@ export class MailServerService {
private readonly twoFactorService: WildDuckTwoFactorService, 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 // Expose individual services for direct access
get users() { get users() {
return this.usersService; return this.usersService;
@@ -27,7 +27,7 @@ export class QuotaSyncService {
/** /**
* Sync quota usage every 2 hours * Sync quota usage every 2 hours
*/ */
@Cron(CronExpression.EVERY_2_HOURS) @Cron(CronExpression.EVERY_10_SECONDS)
async syncAllUsersQuota(): Promise<void> { async syncAllUsersQuota(): Promise<void> {
const em = this.em.fork(); const em = this.em.fork();
this.logger.log("Starting quota synchronization for all users"); this.logger.log("Starting quota synchronization for all users");
@@ -66,7 +66,7 @@ export class QuotaSyncService {
/** /**
* Sync business quota usage every 4 hours * Sync business quota usage every 4 hours
*/ */
@Cron(CronExpression.EVERY_4_HOURS) @Cron(CronExpression.EVERY_10_SECONDS)
async syncAllBusinessQuotas(): Promise<void> { async syncAllBusinessQuotas(): Promise<void> {
const em = this.em.fork(); const em = this.em.fork();
this.logger.log("Starting business quota synchronization"); this.logger.log("Starting business quota synchronization");
@@ -10,40 +10,29 @@ export class CreateEmailSignatureDto {
@ApiProperty({ description: "Name for the email signature", example: "Business Signature", maxLength: 100 }) @ApiProperty({ description: "Name for the email signature", example: "Business Signature", maxLength: 100 })
name: string; name: string;
@IsOptional()
@IsNotEmpty({ message: SignatureMessage.TEXT_CONTENT_NOT_EMPTY }) @IsNotEmpty({ message: SignatureMessage.TEXT_CONTENT_NOT_EMPTY })
@IsString({ message: SignatureMessage.TEXT_CONTENT_STRING }) @IsString({ message: SignatureMessage.TEXT_CONTENT_STRING })
@MaxLength(5000, { message: SignatureMessage.TEXT_CONTENT_MAX_LENGTH }) @MaxLength(5000, { message: SignatureMessage.TEXT_CONTENT_MAX_LENGTH })
@ApiPropertyOptional({ @ApiProperty({
description: "Plain text content of the signature", description: "Plain text content of the signature",
example: "John Doe\nSoftware Engineer\nCompany Inc.\njohn@company.com", example: "John Doe\nSoftware Engineer\nCompany Inc.\njohn@company.com",
maxLength: 5000, maxLength: 5000,
}) })
textContent?: string; textContent: string;
// @IsOptional()
// @IsString()
// @MaxLength(10000)
// @ApiPropertyOptional({
// description: "HTML content of the signature",
// example: "<div><strong>John Doe</strong><br>Software Engineer<br>Company Inc.<br><a href='mailto:john@company.com'>john@company.com</a></div>",
// maxLength: 10000,
// })
// htmlContent?: string;
@IsOptional() @IsOptional()
@IsString() @IsNotEmpty({ message: SignatureMessage.IS_ACTIVE_NOT_EMPTY })
@MaxLength(10000) @IsBoolean({ message: SignatureMessage.IS_ACTIVE_BOOLEAN })
@ApiPropertyOptional({ @ApiPropertyOptional({ description: "Whether this signature is active", default: true })
description: "HTML content of the signature",
example: "<div><strong>John Doe</strong><br>Software Engineer<br>Company Inc.<br><a href='mailto:john@company.com'>john@company.com</a></div>",
maxLength: 10000,
})
htmlContent?: string;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({
description: "Whether this signature is active",
default: true,
})
isActive?: boolean; isActive?: boolean;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({
description: "Whether this is the default signature for the user",
default: false,
})
isDefault?: boolean;
} }
@@ -1,18 +1,16 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, UseGuards } from "@nestjs/common"; import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from "@nestjs/common";
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; import { ApiOperation, ApiResponse } from "@nestjs/swagger";
import { CreateEmailSignatureDto } from "./DTO/create-email-signature.dto"; import { CreateEmailSignatureDto } from "./DTO/create-email-signature.dto";
import { UpdateEmailSignatureDto } from "./DTO/update-email-signature.dto"; import { UpdateEmailSignatureDto } from "./DTO/update-email-signature.dto";
import { EmailSignature } from "./entities/email-signature.entity"; import { EmailSignature } from "./entities/email-signature.entity";
import { EmailSignaturesService } from "./services/email-signatures.service"; import { EmailSignaturesService } from "./services/email-signatures.service";
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
import { UserDec } from "../../common/decorators/user.decorator"; import { UserDec } from "../../common/decorators/user.decorator";
import { JwtAuthGuard } from "../auth/guards/auth.guard";
import { User } from "../users/entities/user.entity"; import { User } from "../users/entities/user.entity";
@ApiTags("Email Signatures")
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller("email-signatures") @Controller("email-signatures")
@AuthGuards()
export class EmailSignaturesController { export class EmailSignaturesController {
constructor(private readonly emailSignaturesService: EmailSignaturesService) {} constructor(private readonly emailSignaturesService: EmailSignaturesService) {}
@@ -20,37 +18,22 @@ export class EmailSignaturesController {
@ApiOperation({ summary: "Create a new email signature" }) @ApiOperation({ summary: "Create a new email signature" })
@ApiResponse({ status: 201, description: "Email signature created successfully", type: EmailSignature }) @ApiResponse({ status: 201, description: "Email signature created successfully", type: EmailSignature })
@ApiResponse({ status: 400, description: "Bad request" }) @ApiResponse({ status: 400, description: "Bad request" })
async create(@UserDec() user: User, @Body() createEmailSignatureDto: CreateEmailSignatureDto): Promise<EmailSignature> { create(@UserDec() user: User, @Body() createEmailSignatureDto: CreateEmailSignatureDto) {
return this.emailSignaturesService.create(user.id, 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<EmailSignature[]> {
return this.emailSignaturesService.findAll(user.id);
}
@Get("active") @Get("active")
@ApiOperation({ summary: "Get all active email signatures for the user" }) @ApiOperation({ summary: "Get all active email signatures for the user" })
@ApiResponse({ status: 200, description: "Active email signatures retrieved successfully", type: [EmailSignature] }) @ApiResponse({ status: 200, description: "Active email signatures retrieved successfully", type: [EmailSignature] })
async findActive(@UserDec() user: User): Promise<EmailSignature[]> { findActive(@UserDec() user: User) {
return this.emailSignaturesService.findActive(user.id); 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<EmailSignature | null> {
return this.emailSignaturesService.findDefault(user.id);
}
@Get(":id") @Get(":id")
@ApiOperation({ summary: "Get a specific email signature" }) @ApiOperation({ summary: "Get a specific email signature" })
@ApiResponse({ status: 200, description: "Email signature retrieved successfully", type: EmailSignature }) @ApiResponse({ status: 200, description: "Email signature retrieved successfully", type: EmailSignature })
@ApiResponse({ status: 404, description: "Email signature not found" }) @ApiResponse({ status: 404, description: "Email signature not found" })
async findOne(@Param("id") id: string, @UserDec() user: User): Promise<EmailSignature> { findOne(@Param("id") id: string, @UserDec() user: User) {
return this.emailSignaturesService.findOne(id, user.id); return this.emailSignaturesService.findOne(id, user.id);
} }
@@ -58,40 +41,23 @@ export class EmailSignaturesController {
@ApiOperation({ summary: "Update an email signature" }) @ApiOperation({ summary: "Update an email signature" })
@ApiResponse({ status: 200, description: "Email signature updated successfully", type: EmailSignature }) @ApiResponse({ status: 200, description: "Email signature updated successfully", type: EmailSignature })
@ApiResponse({ status: 404, description: "Email signature not found" }) @ApiResponse({ status: 404, description: "Email signature not found" })
async update(@Param("id") id: string, @UserDec() user: User, @Body() updateEmailSignatureDto: UpdateEmailSignatureDto): Promise<EmailSignature> { update(@Param("id") id: string, @UserDec() user: User, @Body() updateEmailSignatureDto: UpdateEmailSignatureDto) {
return this.emailSignaturesService.update(id, user.id, updateEmailSignatureDto); return this.emailSignaturesService.update(id, user.id, updateEmailSignatureDto);
} }
@Put(":id/set-default") @Put(":id/toggle-active")
@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<EmailSignature> {
return this.emailSignaturesService.setAsDefault(id, user.id);
}
@Put(":id/activate")
@ApiOperation({ summary: "Activate an email signature" }) @ApiOperation({ summary: "Activate an email signature" })
@ApiResponse({ status: 200, description: "Email signature activated successfully", type: EmailSignature }) @ApiResponse({ status: 200, description: "Email signature activated successfully", type: EmailSignature })
@ApiResponse({ status: 404, description: "Email signature not found" }) @ApiResponse({ status: 404, description: "Email signature not found" })
async activate(@Param("id") id: string, @UserDec() user: User): Promise<EmailSignature> { toggleActive(@Param("id") id: string, @UserDec() user: User) {
return this.emailSignaturesService.activate(id, user.id); return this.emailSignaturesService.toggleActive(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<EmailSignature> {
return this.emailSignaturesService.deactivate(id, user.id);
} }
@Delete(":id") @Delete(":id")
@ApiOperation({ summary: "Delete an email signature" }) @ApiOperation({ summary: "Delete an email signature" })
@ApiResponse({ status: 200, description: "Email signature deleted successfully" }) @ApiResponse({ status: 200, description: "Email signature deleted successfully" })
@ApiResponse({ status: 404, description: "Email signature not found" }) @ApiResponse({ status: 404, description: "Email signature not found" })
async remove(@Param("id") id: string, @UserDec() user: User): Promise<void> { remove(@Param("id") id: string, @UserDec() user: User) {
return this.emailSignaturesService.remove(id, user.id); return this.emailSignaturesService.remove(id, user.id);
} }
} }
@@ -22,8 +22,5 @@ export class EmailSignature extends BaseEntity {
@Property({ type: "boolean", default: true }) @Property({ type: "boolean", default: true })
isActive!: boolean & Opt; isActive!: boolean & Opt;
@Property({ type: "boolean", default: false })
isDefault!: boolean & Opt;
[EntityRepositoryType]?: EmailSignatureRepository; [EntityRepositoryType]?: EmailSignatureRepository;
} }
@@ -4,25 +4,14 @@ import { EmailSignature } from "../entities/email-signature.entity";
export class EmailSignatureRepository extends EntityRepository<EmailSignature> { export class EmailSignatureRepository extends EntityRepository<EmailSignature> {
async findByUserId(userId: string): Promise<EmailSignature[]> { async findByUserId(userId: string): Promise<EmailSignature[]> {
return this.find({ user: userId }, { orderBy: { isDefault: "DESC", createdAt: "DESC" } }); return this.find({ user: userId }, { orderBy: { createdAt: "DESC" } });
} }
async findActiveByUserId(userId: string): Promise<EmailSignature[]> { async findActiveByUserId(userId: string): Promise<EmailSignature[]> {
return this.find({ user: userId, isActive: true }, { orderBy: { isDefault: "DESC", createdAt: "DESC" } }); return this.find({ user: userId, isActive: true }, { orderBy: { createdAt: "DESC" } });
}
async findDefaultByUserId(userId: string): Promise<EmailSignature | null> {
return this.findOne({ user: userId, isDefault: true, isActive: true });
} }
async findByIdAndUserId(id: string, userId: string): Promise<EmailSignature | null> { async findByIdAndUserId(id: string, userId: string): Promise<EmailSignature | null> {
return this.findOne({ id, user: userId }); return this.findOne({ id, user: userId });
} }
async updateDefaultSignature(userId: string, newDefaultId: string): Promise<void> {
// 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 });
}
} }
@@ -1,10 +1,10 @@
import { EntityManager } from "@mikro-orm/core"; import { EntityManager } from "@mikro-orm/core";
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { SignatureMessage, UserMessage } from "../../../common/enums/message.enum";
import { User } from "../../users/entities/user.entity"; import { User } from "../../users/entities/user.entity";
import { CreateEmailSignatureDto } from "../DTO/create-email-signature.dto"; import { CreateEmailSignatureDto } from "../DTO/create-email-signature.dto";
import { UpdateEmailSignatureDto } from "../DTO/update-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"; import { EmailSignatureRepository } from "../repositories/email-signature.repository";
@Injectable() @Injectable()
@@ -13,94 +13,66 @@ export class EmailSignaturesService {
private readonly emailSignatureRepository: EmailSignatureRepository, private readonly emailSignatureRepository: EmailSignatureRepository,
private readonly em: EntityManager, private readonly em: EntityManager,
) {} ) {}
//#########################
async create(userId: string, createDto: CreateEmailSignatureDto): Promise<EmailSignature> { async create(userId: string, createDto: CreateEmailSignatureDto) {
// Validate that at least one content type is provided const user = await this.em.findOne(User, { id: userId, deletedAt: null });
if (!createDto.textContent && !createDto.htmlContent) { if (!user) throw new NotFoundException(UserMessage.USER_NOT_FOUND);
throw new BadRequestException("At least one content type (text or HTML) must be provided");
}
if (createDto.isDefault) await this.emailSignatureRepository.updateDefaultSignature(userId, "");
const signature = this.emailSignatureRepository.create({ const signature = this.emailSignatureRepository.create({
...createDto, ...createDto,
user: { id: userId } as User, user,
}); });
await this.em.persistAndFlush(signature); 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<EmailSignature[]> { //#########################
return this.emailSignatureRepository.findByUserId(userId);
}
async findActive(userId: string): Promise<EmailSignature[]> { async update(id: string, userId: string, updateDto: UpdateEmailSignatureDto) {
return this.emailSignatureRepository.findActiveByUserId(userId); const { signature } = await this.findOne(id, userId);
}
async findDefault(userId: string): Promise<EmailSignature | null> { this.emailSignatureRepository.assign(signature, updateDto);
return this.emailSignatureRepository.findDefaultByUserId(userId);
}
async findOne(id: string, userId: string): Promise<EmailSignature> {
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<EmailSignature> {
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);
await this.em.flush(); await this.em.flush();
return { signature };
return signature;
} }
//#########################
async setAsDefault(id: string, userId: string): Promise<EmailSignature> { async remove(id: string, userId: string) {
const signature = await this.findOne(id, userId); 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<void> {
const signature = await this.findOne(id, userId);
await this.em.removeAndFlush(signature); 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<EmailSignature> { //#########################
const signature = await this.findOne(id, userId); async findOne(id: string, userId: string) {
signature.isActive = true; const signature = await this.emailSignatureRepository.findByIdAndUserId(id, userId);
await this.em.flush(); if (!signature) throw new BadRequestException(SignatureMessage.NOT_FOUND);
return signature;
}
async deactivate(id: string, userId: string): Promise<EmailSignature> { return { signature };
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;
} }
} }
@@ -8,7 +8,7 @@ export class RefreshToken extends BaseEntity {
@Property({ type: "varchar", length: 255 }) @Property({ type: "varchar", length: 255 })
token!: string; token!: string;
@ManyToOne(() => User) @ManyToOne(() => User, { deleteRule: "cascade" })
user!: User; user!: User;
@Property({ type: "timestamptz" }) @Property({ type: "timestamptz" })
+2 -1
View File
@@ -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 { RefreshToken } from "./refresh-token.entity";
import { Role } from "./role.entity"; import { Role } from "./role.entity";
@@ -9,6 +9,7 @@ import { EmailSignature } from "../../signatures/entities/email-signature.entity
import { UserRepository } from "../repositories/user.repository"; import { UserRepository } from "../repositories/user.repository";
@Entity({ repository: () => UserRepository }) @Entity({ repository: () => UserRepository })
@Unique({ properties: ["userName", "domain"], name: "unique_user_name_domain", options: { where: "deleted_at is null" } })
export class User extends BaseEntity { export class User extends BaseEntity {
@Property({ type: "varchar", length: 150, nullable: false }) @Property({ type: "varchar", length: 150, nullable: false })
title!: string; title!: string;
+70 -35
View File
@@ -2,14 +2,6 @@ import { EntityManager } from "@mikro-orm/postgresql";
import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common"; import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
import { firstValueFrom } from "rxjs"; 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 { BusinessMessage, DomainMessage, MailServerMessage, RoleMessage, UserMessage } from "../../../common/enums/message.enum";
import { QUOTA_CONSTANTS } from "../../businesses/constant"; import { QUOTA_CONSTANTS } from "../../businesses/constant";
import { Business } from "../../businesses/entities/business.entity"; 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); if (domain.status !== DomainStatus.VERIFIED) throw new BadRequestException(DomainMessage.NOT_VERIFIED);
const emailAddress = `${username}@${domain.name}`; const emailAddress = `${username}@${domain.name}`;
const finalUserName = `${username}-${domain.name}`;
const existingUser = await this.userRepository.findOne({ emailAddress }); const existingUser = await this.userRepository.findOne({ emailAddress });
if (existingUser) throw new BadRequestException(UserMessage.EMAIL_ADDRESS_ALREADY_EXISTS); if (existingUser) throw new BadRequestException(UserMessage.EMAIL_ADDRESS_ALREADY_EXISTS);
@@ -120,7 +113,7 @@ export class UsersService {
const mailServerUser = await firstValueFrom( const mailServerUser = await firstValueFrom(
this.mailServerService.users.createUser({ this.mailServerService.users.createUser({
username: `${username}-${domain.name}`, username: finalUserName,
password, password,
address: emailAddress, address: emailAddress,
name: displayName || username, name: displayName || username,
@@ -132,7 +125,7 @@ export class UsersService {
const user = this.userRepository.create({ const user = this.userRepository.create({
title: title || displayName || username, title: title || displayName || username,
userName: username, userName: finalUserName,
password: hashedPassword, password: hashedPassword,
emailAddress, emailAddress,
emailEnabled: true, emailEnabled: true,
@@ -182,19 +175,6 @@ export class UsersService {
createdAt: user.createdAt, createdAt: user.createdAt,
isActive: user.isActive, 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, message: UserMessage.EMAIL_USER_CREATED_SUCCESSFULLY,
}; };
} catch (error) { } catch (error) {
@@ -245,14 +225,23 @@ export class UsersService {
async updateEmailUser(userId: string, businessId: string, updateEmailUserDto: UpdateEmailUserDto) { async updateEmailUser(userId: string, businessId: string, updateEmailUserDto: UpdateEmailUserDto) {
const { password, displayName, quota, aliases, title } = updateEmailUserDto; const { password, displayName, quota, aliases, title } = updateEmailUserDto;
const user = await this.userRepository.findOne({ const user = await this.userRepository.findOne(
id: userId, {
business: { id: businessId }, id: userId,
emailEnabled: true, business: { id: businessId },
}); emailEnabled: true,
},
{ populate: ["business"] },
);
if (!user) throw new NotFoundException(UserMessage.EMAIL_USER_NOT_FOUND); 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 { try {
const updateData: UpdateUserDto = {}; const updateData: UpdateUserDto = {};
@@ -299,7 +288,13 @@ export class UsersService {
if (displayName) user.displayName = displayName; 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; if (title) user.title = title;
@@ -330,14 +325,21 @@ export class UsersService {
* Update email user quota * Update email user quota
*/ */
async updateEmailUserQuota(userId: string, businessId: string, newQuota: number) { async updateEmailUserQuota(userId: string, businessId: string, newQuota: number) {
const user = await this.userRepository.findOne({ const user = await this.userRepository.findOne(
id: userId, {
business: { id: businessId }, id: userId,
emailEnabled: true, business: { id: businessId },
}); emailEnabled: true,
},
{ populate: ["business"] },
);
if (!user) throw new NotFoundException(UserMessage.EMAIL_USER_NOT_FOUND); 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 { try {
if (user.wildduckUserId) { if (user.wildduckUserId) {
await firstValueFrom( await firstValueFrom(
@@ -348,7 +350,11 @@ export class UsersService {
} }
user.emailQuota = newQuota; 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`); this.logger.log(`Email user quota updated: ${user.emailAddress} to ${newQuota} bytes`);
@@ -373,4 +379,33 @@ export class UsersService {
status: user.isActive, 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}`);
}
} }