chore: change the email creation dto

This commit is contained in:
mahyargdz
2025-07-07 10:13:49 +03:30
parent 9ae6f260a8
commit 6565d2ae8b
12 changed files with 1169 additions and 925 deletions
+15 -15
View File
@@ -31,7 +31,7 @@
},
"dependencies": {
"@fastify/static": "^8.2.0",
"@keyv/redis": "^4.4.0",
"@keyv/redis": "^4.5.0",
"@mikro-orm/cli": "^6.4.16",
"@mikro-orm/core": "^6.4.16",
"@mikro-orm/nestjs": "^6.1.1",
@@ -54,14 +54,14 @@
"@nestjs/throttler": "^6.4.0",
"axios": "^1.10.0",
"bcrypt": "^5.1.1",
"bullmq": "^5.54.1",
"bullmq": "^5.56.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"dayjs": "^1.11.13",
"decimal.js": "^10.5.0",
"dotenv": "^16.5.0",
"dotenv": "^16.6.1",
"fastify": "^5.4.0",
"nodemailer": "^7.0.3",
"nodemailer": "^7.0.4",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
@@ -72,30 +72,30 @@
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.29.0",
"@eslint/js": "^9.30.1",
"@nestjs/cli": "^11.0.7",
"@nestjs/schematics": "^11.0.5",
"@nestjs/testing": "^11.1.3",
"@swc/cli": "^0.6.0",
"@swc/core": "^1.12.1",
"@swc/core": "^1.12.9",
"@types/bcrypt": "^5.0.2",
"@types/jest": "^29.5.14",
"@types/node": "^22.15.32",
"@types/node": "^22.16.0",
"@types/nodemailer": "^6.4.17",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.3",
"@typescript-eslint/eslint-plugin": "^8.34.1",
"@typescript-eslint/parser": "^8.34.1",
"eslint": "^9.29.0",
"@typescript-eslint/eslint-plugin": "^8.35.1",
"@typescript-eslint/parser": "^8.35.1",
"eslint": "^9.30.1",
"eslint-config-prettier": "^10.1.5",
"eslint-import-resolver-typescript": "^3.10.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.5.0",
"globals": "^16.2.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.1",
"globals": "^16.3.0",
"husky": "^9.1.7",
"jest": "^29.7.0",
"lint-staged": "^15.5.2",
"prettier": "^3.5.3",
"prettier": "^3.6.2",
"source-map-support": "^0.5.21",
"supertest": "^7.1.1",
"ts-jest": "^29.4.0",
@@ -103,7 +103,7 @@
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.34.1"
"typescript-eslint": "^8.35.1"
},
"jest": {
"moduleFileExtensions": [
+778 -780
View File
File diff suppressed because it is too large Load Diff
+28
View File
@@ -75,6 +75,30 @@ export const enum UserMessage {
FAILED_TO_DELETE_EMAIL_USER = "خطا در حذف ایمیل",
EMAIL_USER_QUOTA_UPDATED_SUCCESSFULLY = "ایمیل با موفقیت به روز رسانی شد",
FAILED_TO_UPDATE_EMAIL_USER_QUOTA = "خطا در به روز رسانی حجم ایمیل",
USERNAME_REQUIRED = "نام کاربری الزامی است",
USERNAME_STRING = "نام کاربری باید یک رشته باشد",
USERNAME_MIN_LENGTH = "نام کاربری باید حداقل ۲ کاراکتر باشد",
USERNAME_MATCHES = "نام کاربری فقط میتواند شامل حروف، اعداد، نقطه، خط زیر و خط فاصله باشد",
PASSWORD_MATCHES = "رمز عبور باید حداقل ۸ کاراکتر باشد و شامل حروف بزرگ، حروف کوچک، اعداد و علائم خاص باشد",
PASSWORD_STRING = "رمز عبور باید یک رشته باشد",
PASSWORD_REQUIRED = "رمز عبور الزامی است",
PASSWORD_MIN_LENGTH = "رمز عبور باید حداقل ۸ کاراکتر باشد",
DOMAIN_ID_STRING = "شناسه دامنه باید یک رشته باشد",
DOMAIN_ID_REQUIRED = "شناسه دامنه الزامی است",
DOMAIN_ID_UUID = "شناسه دامنه باید یک یو یو آی دی باشد",
DISPLAY_NAME_STRING = "نام نمایشی باید یک رشته باشد",
DISPLAY_NAME_OPTIONAL = "نام نمایشی اختیاری است",
QUOTA_NUMBER = "حجم ایمیل باید یک عدد باشد",
QUOTA_OPTIONAL = "حجم ایمیل اختیاری است",
QUOTA_NOT_EMPTY = "حجم ایمیل نمیتواند خالی باشد",
ALIASES_ARRAY = "الیاس ها باید یک آرایه باشد",
ALIASES_STRING = "الیاس ها باید یک رشته باشد",
ALIASES_OPTIONAL = "الیاس ها اختیاری است",
TITLE_STRING = "عنوان باید یک رشته باشد",
TITLE_OPTIONAL = "عنوان اختیاری است",
TITLE_NOT_EMPTY = "عنوان نمیتواند خالی باشد",
EMAIL_USER_CREATED_SUCCESSFULLY = "ایمیل با موفقیت ایجاد شد",
STATUS_UPDATED = "وضعیت کاربر با موفقیت تغییر کرد",
}
export const enum CommonMessage {
@@ -220,3 +244,7 @@ export const enum DnsRecordMessage {
DNS_RECORD_NOT_FOUND = "رکورد DNS یافت نشد",
DNS_RECORD_DELETED = "رکورد DNS با موفقیت حذف شد",
}
export const enum RoleMessage {
NOT_FOUND = "نقش یافت نشد",
}
-4
View File
@@ -23,10 +23,6 @@ async function bootstrap() {
done(null);
});
fastifyAdapter.getInstance().addContentTypeParser("text/plain", (_request, _payload, done) => {
done(null);
});
const app = await NestFactory.create<NestFastifyApplication>(AppModule, fastifyAdapter);
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true }));
+4 -4
View File
@@ -34,15 +34,15 @@ export class DomainsController {
@Patch(":id")
@ApiOperation({ summary: "Update domain" })
@ApiResponse({ status: 200, description: "Domain updated successfully" })
updateDomain(@Param() paramDto: ParamDto, @Body() updateDomainDto: UpdateDomainDto) {
return this.domainsService.updateDomain(paramDto.id, updateDomainDto);
updateDomain(@Param() paramDto: ParamDto, @Body() updateDomainDto: UpdateDomainDto, @BusinessDec("id") businessId: string) {
return this.domainsService.updateDomain(paramDto.id, updateDomainDto, businessId);
}
@Delete(":id")
@ApiOperation({ summary: "Delete domain" })
@ApiResponse({ status: 200, description: "Domain deleted successfully" })
deleteDomain(@Param() paramDto: ParamDto) {
return this.domainsService.deleteDomain(paramDto.id);
deleteDomain(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
return this.domainsService.deleteDomain(paramDto.id, businessId);
}
@Get("setup")
+15 -12
View File
@@ -74,8 +74,11 @@ export class DomainsService {
/**
* Get domain by ID
*/
async getDomainById(id: string): Promise<Domain> {
const domain = await this.domainRepository.findOne({ id, deletedAt: null }, { populate: ["business", "dnsRecords"] });
async getDomainById(id: string, businessId: string): Promise<Domain> {
const domain = await this.domainRepository.findOne(
{ id, deletedAt: null, business: { id: businessId } },
{ populate: ["business", "dnsRecords"] },
);
if (!domain) throw new NotFoundException(DomainMessage.DOMAIN_NOT_FOUND);
@@ -93,8 +96,8 @@ export class DomainsService {
/**
* Update domain
*/
async updateDomain(id: string, updateDomainDto: UpdateDomainDto) {
const domain = await this.getDomainById(id);
async updateDomain(id: string, updateDomainDto: UpdateDomainDto, businessId: string) {
const domain = await this.getDomainById(id, businessId);
this.em.assign(domain, updateDomainDto);
@@ -107,8 +110,8 @@ export class DomainsService {
/**
* Delete domain
*/
async deleteDomain(id: string) {
const domain = await this.getDomainById(id);
async deleteDomain(id: string, businessId: string) {
const domain = await this.getDomainById(id, businessId);
// Soft delete
domain.deletedAt = new Date();
@@ -122,8 +125,8 @@ export class DomainsService {
/**
* Verify domain ownership
*/
async verifyDomain(id: string) {
const domain = await this.getDomainById(id);
async verifyDomain(id: string, businessId: string) {
const domain = await this.getDomainById(id, businessId);
// Check DNS records for verification
const isVerified = await this.dnsService.verifyDomainDNS(domain);
@@ -208,7 +211,7 @@ export class DomainsService {
const businessDomain = await this.domainRepository.findOne({ business: { id: businessId }, deletedAt: null });
if (!businessDomain) throw new NotFoundException(DomainMessage.DOMAIN_NOT_FOUND);
const domain = await this.getDomainById(businessDomain.id);
const domain = await this.getDomainById(businessDomain.id, businessId);
const { dnsRecords } = await this.dnsService.getDomainDnsRecords(businessDomain.id);
@@ -222,7 +225,7 @@ export class DomainsService {
const businessDomain = await this.domainRepository.findOne({ business: { id: businessId }, deletedAt: null });
if (!businessDomain) throw new BadRequestException(DomainMessage.DOMAIN_NOT_FOUND);
const domain = await this.getDomainById(businessDomain.id);
const domain = await this.getDomainById(businessDomain.id, businessId);
const { overallStatus, recommendations } = await this.checkDomainVerificationStatus(businessId);
@@ -241,10 +244,10 @@ export class DomainsService {
const businessDomain = await this.domainRepository.findOne({ business: { id: businessId }, deletedAt: null });
if (!businessDomain) throw new BadRequestException(DomainMessage.DOMAIN_NOT_FOUND);
const domain = await this.getDomainById(businessDomain.id);
const domain = await this.getDomainById(businessDomain.id, businessId);
// Trigger DNS verification
await this.verifyDomain(domain.id);
await this.verifyDomain(domain.id, businessId);
const { dnsRecords } = await this.dnsService.getDomainDnsRecords(domain.id);
+262 -32
View File
@@ -1,96 +1,326 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsArray, IsBoolean, IsEmail, IsNumber, IsOptional, IsString, IsUrl } from "class-validator";
import { Type } from "class-transformer";
import { IsArray, IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsObject, IsOptional, IsString, ValidateNested } from "class-validator";
class MailboxesDto {
@ApiPropertyOptional({ description: "Sent messages mailbox", example: "string" })
@IsOptional()
@IsString()
sent?: string;
@ApiPropertyOptional({ description: "Trash mailbox", example: "string" })
@IsOptional()
@IsString()
trash?: string;
@ApiPropertyOptional({ description: "Junk/Spam mailbox", example: "string" })
@IsOptional()
@IsString()
junk?: string;
@ApiPropertyOptional({ description: "Drafts mailbox", example: "string" })
@IsOptional()
@IsString()
drafts?: string;
}
class FeatureFlagsDto {
@ApiPropertyOptional({ description: "Enable indexing feature", example: false })
@IsOptional()
@IsBoolean()
indexing?: boolean;
}
export class CreateUserDto {
@ApiProperty({ description: "Username for the user (letters and numbers only)", example: "testuser" })
@IsNotEmpty()
@IsString()
@ApiProperty({ description: "This is not an email address but authentication username, use only letters and numbers", example: "testuser" })
username: string;
@ApiProperty({ description: "Password for the user", example: "secretpassword123" })
@IsNotEmpty()
@IsString()
@ApiProperty({ description: "Password for the user. You can use false to create an account without a password", example: "secretpass" })
password: string;
@ApiPropertyOptional({ description: "Main email address for the user", example: "testuser@example.com" })
@ApiPropertyOptional({
description: "Bcrypt hash for password. If provided, plain text password is ignored",
example: false,
})
@IsOptional()
hashedPassword?: string | boolean;
@ApiPropertyOptional({
description: "Allow weak passwords",
default: true,
})
@IsOptional()
@IsBoolean()
allowUnsafe?: boolean;
@ApiPropertyOptional({
description: "Main email address for the user. If address is not set then a new one is generated based on the username and current domain name",
example: "testuser@example.com",
})
@IsOptional()
@IsEmail()
address?: string;
@ApiPropertyOptional({ description: "If true, do not set up an address for the user", default: false })
@ApiPropertyOptional({
description: "If true, then do not set up an address for the user",
default: false,
})
@IsOptional()
@IsBoolean()
emptyAddress?: boolean;
@ApiPropertyOptional({ description: "Display name for the user", example: "Test User" })
@ApiPropertyOptional({
description: 'Language code for the user, eg. "en" or "et". Mailbox names for the default mailboxes (eg. "Trash") depend on the language',
example: "en",
})
@IsOptional()
@IsString()
language?: string;
@ApiPropertyOptional({
description: "Default retention time in ms for mailboxes. Messages in Trash and Junk folders have a capped retention time of 30 days",
example: 0,
})
@IsOptional()
@IsNumber()
retention?: number;
@ApiPropertyOptional({
description: "Name for the user",
example: "Test User",
})
@IsOptional()
@IsString()
name?: string;
@ApiPropertyOptional({
description: "Array of email addresses to forward all messages to",
example: ["forward1@example.com", "forward2@example.com"],
description: "List of forwarding targets",
example: ["forward@example.com"],
})
@IsOptional()
@IsArray()
@IsEmail({}, { each: true })
forward?: string[];
@IsString({ each: true })
targets?: string[];
@ApiPropertyOptional({ description: "URL to upload all messages to", example: "https://example.com/webhook" })
@ApiPropertyOptional({
description: "Relay server for outbound messages",
example: "smtp://relay.example.com:587",
})
@IsOptional()
@IsUrl()
targetUrl?: string;
@IsString()
mtaRelay?: string;
@ApiPropertyOptional({ description: "Maximum storage in bytes allowed for this user", example: 1073741824 })
@ApiPropertyOptional({
description: "Spam level threshold (0-100)",
example: 50,
})
@IsOptional()
@IsNumber()
spamLevel?: number;
@ApiPropertyOptional({
description: "Maximum storage in bytes allowed for this user. If not set then the default value is used",
example: 1073741824,
})
@IsOptional()
@IsNumber()
quota?: number;
@ApiPropertyOptional({ description: "Default retention time in milliseconds for mailboxes", example: 2592000000 })
@IsOptional()
@IsNumber()
retention?: number;
@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: 2000 })
@ApiPropertyOptional({
description: "Maximum number of recipients allowed to send mail to in a 24h window. Requires ZoneMTA with the Wild Duck plugin",
example: 2000,
})
@IsOptional()
@IsNumber()
recipients?: number;
@ApiPropertyOptional({ description: "Maximum number of forwarded emails in 24h", example: 2000 })
@ApiPropertyOptional({
description: "Maximum number of forwarded emails in a 24h window. Requires ZoneMTA with the Wild Duck plugin",
example: 2000,
})
@IsOptional()
@IsNumber()
forwards?: number;
@ApiPropertyOptional({ description: "Array of tags to associate with the user", example: ["vip", "customer"] })
@ApiPropertyOptional({
description: "Maximum number of filters allowed for this user",
example: 0,
})
@IsOptional()
@IsNumber()
filters?: number;
@ApiPropertyOptional({
description: "If true then requires the user to change password on next login",
default: false,
})
@IsOptional()
@IsBoolean()
requirePasswordChange?: boolean;
@ApiPropertyOptional({
description: "Maximum upload size per day for IMAP",
example: 0,
})
@IsOptional()
@IsNumber()
imapMaxUpload?: number;
@ApiPropertyOptional({
description: "Maximum download size per day for IMAP",
example: 0,
})
@IsOptional()
@IsNumber()
imapMaxDownload?: number;
@ApiPropertyOptional({
description: "Maximum download size per day for POP3",
example: 0,
})
@IsOptional()
@IsNumber()
pop3MaxDownload?: number;
@ApiPropertyOptional({
description: "Maximum number of messages to download for POP3",
example: 0,
})
@IsOptional()
@IsNumber()
pop3MaxMessages?: number;
@ApiPropertyOptional({
description: "Maximum number of parallel IMAP connections allowed",
example: 0,
})
@IsOptional()
@IsNumber()
imapMaxConnections?: number;
@ApiPropertyOptional({
description: "Maximum number of messages received per day",
example: 0,
})
@IsOptional()
@IsNumber()
receivedMax?: number;
@ApiPropertyOptional({
description: "List of allowed From addresses",
example: ["allowed@example.com"],
})
@IsOptional()
@IsArray()
@IsString({ each: true })
fromWhitelist?: string[];
@ApiPropertyOptional({
description: "Array of tags to be associated with that user. Tags can be used for filtering",
example: ["vip", "customer"],
})
@IsOptional()
@IsArray()
@IsString({ each: true })
tags?: string[];
@ApiPropertyOptional({ description: "PGP public key for encryption" })
@ApiPropertyOptional({
description: "If true then autogenerated tags are added to the main address",
default: false,
})
@IsOptional()
@IsBoolean()
addTagsToAddress?: boolean;
@ApiPropertyOptional({
description: "If true then sent messages are uploaded to Sent folder",
default: false,
})
@IsOptional()
@IsBoolean()
uploadSentMessages?: boolean;
@ApiPropertyOptional({
description: "Mailbox configuration",
type: MailboxesDto,
})
@IsOptional()
@ValidateNested()
@Type(() => MailboxesDto)
mailboxes?: MailboxesDto;
@ApiPropertyOptional({
description: "List of disabled scopes for this user",
example: ["imap"],
})
@IsOptional()
@IsArray()
@IsString({ each: true })
disabledScopes?: string[];
@ApiPropertyOptional({
description: "Custom metadata object",
example: {},
})
@IsOptional()
@IsObject()
metaData?: Record<string, any>;
@ApiPropertyOptional({
description: "Internal data object",
example: {},
})
@IsOptional()
@IsObject()
internalData?: Record<string, any>;
@ApiPropertyOptional({
description: "Optional PGP public key. Wild Duck uses OpenPGP.js so keys using EC might not pass validation",
})
@IsOptional()
@IsString()
pubKey?: string;
@ApiPropertyOptional({ description: "Whether to encrypt stored messages", default: false })
@ApiPropertyOptional({
description: "Indicates if messages stored should be encrypted using the PGP key",
default: false,
})
@IsOptional()
@IsBoolean()
encryptMessages?: boolean;
@ApiPropertyOptional({ description: "Whether to encrypt forwarded messages", default: false })
@ApiPropertyOptional({
description: "Indicates if forwarded messages should be encrypted using the PGP key. Encryption applies after filter rules",
default: false,
})
@IsOptional()
@IsBoolean()
encryptForwarded?: boolean;
@ApiPropertyOptional({ description: "Session identifier for logging" })
@ApiPropertyOptional({
description: "Feature flags configuration",
type: FeatureFlagsDto,
})
@IsOptional()
@ValidateNested()
@Type(() => FeatureFlagsDto)
featureFlags?: FeatureFlagsDto;
@ApiPropertyOptional({
description: "Optional string of user session to be stored in security log",
})
@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;
@@ -24,7 +24,7 @@ export class WildDuckSettingsService extends WildDuckBaseService {
/**
* Set server setting value
*/
setSetting(key: string, value: any): Observable<WildDuckSuccessResponse> {
setSetting(key: string, value: unknown): Observable<WildDuckSuccessResponse> {
return this.post<WildDuckSuccessResponse>(`/settings/${key}`, { value });
}
}
+31 -54
View File
@@ -1,73 +1,50 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsArray, IsNotEmpty, IsNumber, IsOptional, IsString, Matches, MinLength } from "class-validator";
import { IsArray, IsNotEmpty, IsNumber, IsOptional, IsString, IsUUID, Matches, MinLength } from "class-validator";
import { UserMessage } from "../../../common/enums/message.enum";
export class CreateEmailUserDto {
@ApiProperty({
description: "Username part of the email address (before @domain.com)",
example: "john.doe",
})
@IsString()
@IsNotEmpty()
@MinLength(2)
@Matches(/^[a-zA-Z0-9._-]+$/, {
message: "Username can only contain letters, numbers, dots, underscores, and hyphens",
})
@IsNotEmpty({ message: UserMessage.USERNAME_REQUIRED })
@IsString({ message: UserMessage.USERNAME_STRING })
@MinLength(2, { message: UserMessage.USERNAME_MIN_LENGTH })
@Matches(/^[a-zA-Z0-9._-]+$/, { message: UserMessage.USERNAME_MATCHES })
@ApiProperty({ description: "Username part of the email address (before @domain.com)", example: "john.doe" })
username: string;
@ApiProperty({
description: "User password for email account",
example: "SecurePassword123!",
minLength: 8,
})
@IsString()
@IsNotEmpty()
@MinLength(8)
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/, {
message: "Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character",
})
@IsNotEmpty({ message: UserMessage.PASSWORD_REQUIRED })
@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 })
@ApiProperty({ description: "User password for email account", example: "SecurePassword123!", minLength: 8 })
password: string;
@ApiProperty({
description: "Domain ID for the email address",
example: "12345678-1234-1234-1234-123456789012",
})
@IsString()
@IsNotEmpty()
@IsNotEmpty({ message: UserMessage.DOMAIN_ID_REQUIRED })
@IsUUID("7", { message: UserMessage.DOMAIN_ID_UUID })
@ApiProperty({ description: "Domain ID for the email address", example: "12345678-1234-1234-1234-123456789012" })
domainId: string;
@ApiPropertyOptional({
description: "Display name for the user",
example: "John Doe",
})
@IsString()
@IsOptional()
@ApiPropertyOptional({ description: "Display name for the user", example: "John Doe" })
@IsString({ message: UserMessage.DISPLAY_NAME_STRING })
@IsOptional({ message: UserMessage.DISPLAY_NAME_OPTIONAL })
@Transform(({ value }) => value?.trim())
displayName?: string;
@ApiPropertyOptional({
description: "Email quota in bytes (default: 1GB)",
example: 1073741824,
})
@IsNumber()
@IsOptional()
@IsOptional({ message: UserMessage.QUOTA_OPTIONAL })
@IsNotEmpty({ message: UserMessage.QUOTA_NOT_EMPTY })
@IsNumber({ allowNaN: false, allowInfinity: false }, { message: UserMessage.QUOTA_NUMBER })
@ApiPropertyOptional({ description: "Email quota in bytes (default: 1GB)", example: 1073741824 })
quota?: number;
@ApiPropertyOptional({
description: "Additional email aliases for this user",
example: ["johndoe@example.com", "j.doe@example.com"],
})
@IsArray()
@IsString({ each: true })
@IsOptional()
@IsArray({ message: UserMessage.ALIASES_ARRAY })
@IsString({ each: true, message: UserMessage.ALIASES_STRING })
@IsOptional({ message: UserMessage.ALIASES_OPTIONAL })
@ApiPropertyOptional({ description: "Additional email aliases for this user", example: ["johndoe@example.com", "j.doe@example.com"] })
aliases?: string[];
@ApiPropertyOptional({
description: "User role/title",
example: "Marketing Manager",
})
@IsString()
@IsOptional()
@IsNotEmpty({ message: UserMessage.TITLE_NOT_EMPTY })
@IsString({ message: UserMessage.TITLE_STRING })
@Transform(({ value }) => value?.trim())
title?: string;
@ApiProperty({ description: "User role/title", example: "Marketing Manager" })
title: string;
}
+2 -2
View File
@@ -48,8 +48,8 @@ export class User extends BaseEntity {
@ManyToOne(() => Business, { deleteRule: "restrict" })
business!: Business;
@ManyToOne(() => Domain, { nullable: true })
domain?: Domain; // The domain this email user belongs to
@ManyToOne(() => Domain, { deleteRule: "restrict" })
domain!: Domain; // The domain this email user belongs to
[EntityRepositoryType]?: UserRepository;
}
+24 -18
View File
@@ -10,7 +10,7 @@ import {
DANAK_SMTPS_PORT,
DANAK_SMTPS_SERVER,
} from "../../../common/constants";
import { BusinessMessage, DomainMessage, MailServerMessage, UserMessage } from "../../../common/enums/message.enum";
import { BusinessMessage, DomainMessage, MailServerMessage, RoleMessage, UserMessage } from "../../../common/enums/message.enum";
import { Business } from "../../businesses/entities/business.entity";
import { DomainStatus } from "../../domains/enums/domain-status.enum";
import { DomainsService } from "../../domains/services/domains.service";
@@ -66,29 +66,21 @@ export class UsersService {
const business = await this.em.findOne(Business, { id: businessId });
if (!business) throw new BadRequestException(BusinessMessage.NOT_FOUND);
const domain = await this.domainsService.getDomainById(domainId);
const domain = await this.domainsService.getDomainById(domainId, businessId);
if (domain.business.id !== businessId) {
throw new BadRequestException(DomainMessage.NOT_BELONG_TO_BUSINESS);
}
if (domain.business.id !== businessId) throw new BadRequestException(DomainMessage.NOT_BELONG_TO_BUSINESS);
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}`;
// Check if email address already exists
const existingUser = await this.userRepository.findOne({ emailAddress });
if (existingUser) throw new BadRequestException(UserMessage.EMAIL_ADDRESS_ALREADY_EXISTS);
const hashedPassword = await this.passwordService.hashPassword(password);
let userRole = await this.em.findOne(Role, { name: RoleEnum.USER });
if (!userRole) {
userRole = this.em.create(Role, { name: RoleEnum.USER });
await this.em.persistAndFlush(userRole);
}
const userRole = await this.em.findOne(Role, { name: RoleEnum.USER });
if (!userRole) throw new BadRequestException(RoleMessage.NOT_FOUND);
try {
const mailServerUser = await firstValueFrom(
@@ -97,8 +89,8 @@ export class UsersService {
password,
address: emailAddress,
name: displayName || username,
quota: quota || 1073741824, // 1GB default
tags: ["dmail-user"],
quota: quota || 1073741824,
language: "fa",
}),
);
@@ -121,7 +113,6 @@ export class UsersService {
await this.em.persistAndFlush(user);
// Create aliases if provided
if (aliases && aliases.length > 0) {
for (const alias of aliases) {
try {
@@ -164,7 +155,7 @@ export class UsersService {
encryption: DANAK_SMTPS_ENCRYPTION,
},
},
message: "Email user created successfully",
message: UserMessage.EMAIL_USER_CREATED_SUCCESSFULLY,
};
} catch (error) {
this.logger.error(`Failed to create email user ${emailAddress}:`, error);
@@ -252,4 +243,19 @@ export class UsersService {
throw new BadRequestException(UserMessage.FAILED_TO_UPDATE_EMAIL_USER_QUOTA);
}
}
//=====================================================
async updateEmailUserStatus(userId: string, businessId: string) {
const user = await this.userRepository.findOne({ id: userId, business: { id: businessId }, emailEnabled: true });
if (!user) throw new NotFoundException(UserMessage.EMAIL_USER_NOT_FOUND);
user.isActive = !user.isActive;
await this.em.flush();
return {
message: UserMessage.STATUS_UPDATED,
status: user.isActive,
};
}
}
+9 -3
View File
@@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiResponse } from "@nestjs/swagger";
import { CreateEmailUserDto } from "./DTO/create-email-user.dto";
import { UsersService } from "./services/users.service";
@@ -7,8 +7,7 @@ import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
import { BusinessDec } from "../../common/decorators/business.decorator";
import { ParamDto } from "../../common/DTO/param.dto";
@ApiTags("email-users")
@Controller("email-users")
@Controller("users")
@ApiHeader({ name: "x-business-id", description: "Business ID" })
@AuthGuards()
export class UsersController {
@@ -41,4 +40,11 @@ export class UsersController {
updateEmailUserQuota(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string, @Body() body: { quota: number }) {
return this.usersService.updateEmailUserQuota(paramDto.id, businessId, body.quota);
}
@Patch(":id/toggle-status")
@ApiOperation({ summary: "Update email user status" })
@ApiResponse({ status: 200, description: "Email user status updated successfully" })
updateEmailUserStatus(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
return this.usersService.updateEmailUserStatus(paramDto.id, businessId);
}
}