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": { "dependencies": {
"@fastify/static": "^8.2.0", "@fastify/static": "^8.2.0",
"@keyv/redis": "^4.4.0", "@keyv/redis": "^4.5.0",
"@mikro-orm/cli": "^6.4.16", "@mikro-orm/cli": "^6.4.16",
"@mikro-orm/core": "^6.4.16", "@mikro-orm/core": "^6.4.16",
"@mikro-orm/nestjs": "^6.1.1", "@mikro-orm/nestjs": "^6.1.1",
@@ -54,14 +54,14 @@
"@nestjs/throttler": "^6.4.0", "@nestjs/throttler": "^6.4.0",
"axios": "^1.10.0", "axios": "^1.10.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"bullmq": "^5.54.1", "bullmq": "^5.56.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.2", "class-validator": "^0.14.2",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"decimal.js": "^10.5.0", "decimal.js": "^10.5.0",
"dotenv": "^16.5.0", "dotenv": "^16.6.1",
"fastify": "^5.4.0", "fastify": "^5.4.0",
"nodemailer": "^7.0.3", "nodemailer": "^7.0.4",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
@@ -72,30 +72,30 @@
"@commitlint/cli": "^19.8.1", "@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1", "@commitlint/config-conventional": "^19.8.1",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.29.0", "@eslint/js": "^9.30.1",
"@nestjs/cli": "^11.0.7", "@nestjs/cli": "^11.0.7",
"@nestjs/schematics": "^11.0.5", "@nestjs/schematics": "^11.0.5",
"@nestjs/testing": "^11.1.3", "@nestjs/testing": "^11.1.3",
"@swc/cli": "^0.6.0", "@swc/cli": "^0.6.0",
"@swc/core": "^1.12.1", "@swc/core": "^1.12.9",
"@types/bcrypt": "^5.0.2", "@types/bcrypt": "^5.0.2",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/node": "^22.15.32", "@types/node": "^22.16.0",
"@types/nodemailer": "^6.4.17", "@types/nodemailer": "^6.4.17",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.3", "@types/supertest": "^6.0.3",
"@typescript-eslint/eslint-plugin": "^8.34.1", "@typescript-eslint/eslint-plugin": "^8.35.1",
"@typescript-eslint/parser": "^8.34.1", "@typescript-eslint/parser": "^8.35.1",
"eslint": "^9.29.0", "eslint": "^9.30.1",
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.1.5",
"eslint-import-resolver-typescript": "^3.10.1", "eslint-import-resolver-typescript": "^3.10.1",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.0", "eslint-plugin-prettier": "^5.5.1",
"globals": "^16.2.0", "globals": "^16.3.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"jest": "^29.7.0", "jest": "^29.7.0",
"lint-staged": "^15.5.2", "lint-staged": "^15.5.2",
"prettier": "^3.5.3", "prettier": "^3.6.2",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"supertest": "^7.1.1", "supertest": "^7.1.1",
"ts-jest": "^29.4.0", "ts-jest": "^29.4.0",
@@ -103,7 +103,7 @@
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0", "tsconfig-paths": "^4.2.0",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"typescript-eslint": "^8.34.1" "typescript-eslint": "^8.35.1"
}, },
"jest": { "jest": {
"moduleFileExtensions": [ "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 = "خطا در حذف ایمیل", FAILED_TO_DELETE_EMAIL_USER = "خطا در حذف ایمیل",
EMAIL_USER_QUOTA_UPDATED_SUCCESSFULLY = "ایمیل با موفقیت به روز رسانی شد", EMAIL_USER_QUOTA_UPDATED_SUCCESSFULLY = "ایمیل با موفقیت به روز رسانی شد",
FAILED_TO_UPDATE_EMAIL_USER_QUOTA = "خطا در به روز رسانی حجم ایمیل", 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 { export const enum CommonMessage {
@@ -220,3 +244,7 @@ export const enum DnsRecordMessage {
DNS_RECORD_NOT_FOUND = "رکورد DNS یافت نشد", DNS_RECORD_NOT_FOUND = "رکورد DNS یافت نشد",
DNS_RECORD_DELETED = "رکورد DNS با موفقیت حذف شد", DNS_RECORD_DELETED = "رکورد DNS با موفقیت حذف شد",
} }
export const enum RoleMessage {
NOT_FOUND = "نقش یافت نشد",
}
-4
View File
@@ -23,10 +23,6 @@ async function bootstrap() {
done(null); done(null);
}); });
fastifyAdapter.getInstance().addContentTypeParser("text/plain", (_request, _payload, done) => {
done(null);
});
const app = await NestFactory.create<NestFastifyApplication>(AppModule, fastifyAdapter); const app = await NestFactory.create<NestFastifyApplication>(AppModule, fastifyAdapter);
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true })); app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true }));
+4 -4
View File
@@ -34,15 +34,15 @@ export class DomainsController {
@Patch(":id") @Patch(":id")
@ApiOperation({ summary: "Update domain" }) @ApiOperation({ summary: "Update domain" })
@ApiResponse({ status: 200, description: "Domain updated successfully" }) @ApiResponse({ status: 200, description: "Domain updated successfully" })
updateDomain(@Param() paramDto: ParamDto, @Body() updateDomainDto: UpdateDomainDto) { updateDomain(@Param() paramDto: ParamDto, @Body() updateDomainDto: UpdateDomainDto, @BusinessDec("id") businessId: string) {
return this.domainsService.updateDomain(paramDto.id, updateDomainDto); return this.domainsService.updateDomain(paramDto.id, updateDomainDto, businessId);
} }
@Delete(":id") @Delete(":id")
@ApiOperation({ summary: "Delete domain" }) @ApiOperation({ summary: "Delete domain" })
@ApiResponse({ status: 200, description: "Domain deleted successfully" }) @ApiResponse({ status: 200, description: "Domain deleted successfully" })
deleteDomain(@Param() paramDto: ParamDto) { deleteDomain(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
return this.domainsService.deleteDomain(paramDto.id); return this.domainsService.deleteDomain(paramDto.id, businessId);
} }
@Get("setup") @Get("setup")
+15 -12
View File
@@ -74,8 +74,11 @@ export class DomainsService {
/** /**
* Get domain by ID * Get domain by ID
*/ */
async getDomainById(id: string): Promise<Domain> { async getDomainById(id: string, businessId: string): Promise<Domain> {
const domain = await this.domainRepository.findOne({ id, deletedAt: null }, { populate: ["business", "dnsRecords"] }); const domain = await this.domainRepository.findOne(
{ id, deletedAt: null, business: { id: businessId } },
{ populate: ["business", "dnsRecords"] },
);
if (!domain) throw new NotFoundException(DomainMessage.DOMAIN_NOT_FOUND); if (!domain) throw new NotFoundException(DomainMessage.DOMAIN_NOT_FOUND);
@@ -93,8 +96,8 @@ export class DomainsService {
/** /**
* Update domain * Update domain
*/ */
async updateDomain(id: string, updateDomainDto: UpdateDomainDto) { async updateDomain(id: string, updateDomainDto: UpdateDomainDto, businessId: string) {
const domain = await this.getDomainById(id); const domain = await this.getDomainById(id, businessId);
this.em.assign(domain, updateDomainDto); this.em.assign(domain, updateDomainDto);
@@ -107,8 +110,8 @@ export class DomainsService {
/** /**
* Delete domain * Delete domain
*/ */
async deleteDomain(id: string) { async deleteDomain(id: string, businessId: string) {
const domain = await this.getDomainById(id); const domain = await this.getDomainById(id, businessId);
// Soft delete // Soft delete
domain.deletedAt = new Date(); domain.deletedAt = new Date();
@@ -122,8 +125,8 @@ export class DomainsService {
/** /**
* Verify domain ownership * Verify domain ownership
*/ */
async verifyDomain(id: string) { async verifyDomain(id: string, businessId: string) {
const domain = await this.getDomainById(id); const domain = await this.getDomainById(id, businessId);
// Check DNS records for verification // Check DNS records for verification
const isVerified = await this.dnsService.verifyDomainDNS(domain); 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 }); const businessDomain = await this.domainRepository.findOne({ business: { id: businessId }, deletedAt: null });
if (!businessDomain) throw new NotFoundException(DomainMessage.DOMAIN_NOT_FOUND); 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); 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 }); const businessDomain = await this.domainRepository.findOne({ business: { id: businessId }, deletedAt: null });
if (!businessDomain) throw new BadRequestException(DomainMessage.DOMAIN_NOT_FOUND); 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); 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 }); const businessDomain = await this.domainRepository.findOne({ business: { id: businessId }, deletedAt: null });
if (!businessDomain) throw new BadRequestException(DomainMessage.DOMAIN_NOT_FOUND); 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 // Trigger DNS verification
await this.verifyDomain(domain.id); await this.verifyDomain(domain.id, businessId);
const { dnsRecords } = await this.dnsService.getDomainDnsRecords(domain.id); const { dnsRecords } = await this.dnsService.getDomainDnsRecords(domain.id);
+262 -32
View File
@@ -1,96 +1,326 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; 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 { export class CreateUserDto {
@ApiProperty({ description: "Username for the user (letters and numbers only)", example: "testuser" }) @IsNotEmpty()
@IsString() @IsString()
@ApiProperty({ description: "This is not an email address but authentication username, use only letters and numbers", example: "testuser" })
username: string; username: string;
@ApiProperty({ description: "Password for the user", example: "secretpassword123" }) @IsNotEmpty()
@IsString() @IsString()
@ApiProperty({ description: "Password for the user. You can use false to create an account without a password", example: "secretpass" })
password: string; 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() @IsOptional()
@IsEmail() @IsEmail()
address?: string; 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() @IsOptional()
@IsBoolean() @IsBoolean()
emptyAddress?: boolean; 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() @IsOptional()
@IsString() @IsString()
name?: string; name?: string;
@ApiPropertyOptional({ @ApiPropertyOptional({
description: "Array of email addresses to forward all messages to", description: "List of forwarding targets",
example: ["forward1@example.com", "forward2@example.com"], example: ["forward@example.com"],
}) })
@IsOptional() @IsOptional()
@IsArray() @IsArray()
@IsEmail({}, { each: true }) @IsString({ each: true })
forward?: string[]; 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() @IsOptional()
@IsUrl() @IsString()
targetUrl?: string; 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() @IsOptional()
@IsNumber() @IsNumber()
quota?: number; quota?: number;
@ApiPropertyOptional({ description: "Default retention time in milliseconds for mailboxes", example: 2592000000 }) @ApiPropertyOptional({
@IsOptional() description: "Maximum number of recipients allowed to send mail to in a 24h window. Requires ZoneMTA with the Wild Duck plugin",
@IsNumber() example: 2000,
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 })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
recipients?: number; 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() @IsOptional()
@IsNumber() @IsNumber()
forwards?: number; 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() @IsOptional()
@IsArray() @IsArray()
@IsString({ each: true }) @IsString({ each: true })
tags?: string[]; 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() @IsOptional()
@IsString() @IsString()
pubKey?: string; 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() @IsOptional()
@IsBoolean() @IsBoolean()
encryptMessages?: boolean; 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() @IsOptional()
@IsBoolean() @IsBoolean()
encryptForwarded?: boolean; 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() @IsOptional()
@IsString() @IsString()
sess?: string; 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() @IsOptional()
@IsString() @IsString()
ip?: string; ip?: string;
@@ -24,7 +24,7 @@ export class WildDuckSettingsService extends WildDuckBaseService {
/** /**
* Set server setting value * 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 }); return this.post<WildDuckSuccessResponse>(`/settings/${key}`, { value });
} }
} }
+31 -54
View File
@@ -1,73 +1,50 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Transform } from "class-transformer"; 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 { export class CreateEmailUserDto {
@ApiProperty({ @IsNotEmpty({ message: UserMessage.USERNAME_REQUIRED })
description: "Username part of the email address (before @domain.com)", @IsString({ message: UserMessage.USERNAME_STRING })
example: "john.doe", @MinLength(2, { message: UserMessage.USERNAME_MIN_LENGTH })
}) @Matches(/^[a-zA-Z0-9._-]+$/, { message: UserMessage.USERNAME_MATCHES })
@IsString() @ApiProperty({ description: "Username part of the email address (before @domain.com)", example: "john.doe" })
@IsNotEmpty()
@MinLength(2)
@Matches(/^[a-zA-Z0-9._-]+$/, {
message: "Username can only contain letters, numbers, dots, underscores, and hyphens",
})
username: string; username: string;
@ApiProperty({ @IsNotEmpty({ message: UserMessage.PASSWORD_REQUIRED })
description: "User password for email account", @IsString({ message: UserMessage.PASSWORD_STRING })
example: "SecurePassword123!", @MinLength(8, { message: UserMessage.PASSWORD_MIN_LENGTH })
minLength: 8, @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 })
@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",
})
password: string; password: string;
@ApiProperty({ @IsNotEmpty({ message: UserMessage.DOMAIN_ID_REQUIRED })
description: "Domain ID for the email address", @IsUUID("7", { message: UserMessage.DOMAIN_ID_UUID })
example: "12345678-1234-1234-1234-123456789012", @ApiProperty({ description: "Domain ID for the email address", example: "12345678-1234-1234-1234-123456789012" })
})
@IsString()
@IsNotEmpty()
domainId: string; domainId: string;
@ApiPropertyOptional({ @ApiPropertyOptional({ description: "Display name for the user", example: "John Doe" })
description: "Display name for the user", @IsString({ message: UserMessage.DISPLAY_NAME_STRING })
example: "John Doe", @IsOptional({ message: UserMessage.DISPLAY_NAME_OPTIONAL })
})
@IsString()
@IsOptional()
@Transform(({ value }) => value?.trim()) @Transform(({ value }) => value?.trim())
displayName?: string; displayName?: string;
@ApiPropertyOptional({ @IsOptional({ message: UserMessage.QUOTA_OPTIONAL })
description: "Email quota in bytes (default: 1GB)", @IsNotEmpty({ message: UserMessage.QUOTA_NOT_EMPTY })
example: 1073741824, @IsNumber({ allowNaN: false, allowInfinity: false }, { message: UserMessage.QUOTA_NUMBER })
}) @ApiPropertyOptional({ description: "Email quota in bytes (default: 1GB)", example: 1073741824 })
@IsNumber()
@IsOptional()
quota?: number; quota?: number;
@ApiPropertyOptional({ @IsArray({ message: UserMessage.ALIASES_ARRAY })
description: "Additional email aliases for this user", @IsString({ each: true, message: UserMessage.ALIASES_STRING })
example: ["johndoe@example.com", "j.doe@example.com"], @IsOptional({ message: UserMessage.ALIASES_OPTIONAL })
}) @ApiPropertyOptional({ description: "Additional email aliases for this user", example: ["johndoe@example.com", "j.doe@example.com"] })
@IsArray()
@IsString({ each: true })
@IsOptional()
aliases?: string[]; aliases?: string[];
@ApiPropertyOptional({ @IsNotEmpty({ message: UserMessage.TITLE_NOT_EMPTY })
description: "User role/title", @IsString({ message: UserMessage.TITLE_STRING })
example: "Marketing Manager",
})
@IsString()
@IsOptional()
@Transform(({ value }) => value?.trim()) @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" }) @ManyToOne(() => Business, { deleteRule: "restrict" })
business!: Business; business!: Business;
@ManyToOne(() => Domain, { nullable: true }) @ManyToOne(() => Domain, { deleteRule: "restrict" })
domain?: Domain; // The domain this email user belongs to domain!: Domain; // The domain this email user belongs to
[EntityRepositoryType]?: UserRepository; [EntityRepositoryType]?: UserRepository;
} }
+24 -18
View File
@@ -10,7 +10,7 @@ import {
DANAK_SMTPS_PORT, DANAK_SMTPS_PORT,
DANAK_SMTPS_SERVER, DANAK_SMTPS_SERVER,
} from "../../../common/constants"; } 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 { Business } from "../../businesses/entities/business.entity";
import { DomainStatus } from "../../domains/enums/domain-status.enum"; import { DomainStatus } from "../../domains/enums/domain-status.enum";
import { DomainsService } from "../../domains/services/domains.service"; import { DomainsService } from "../../domains/services/domains.service";
@@ -66,29 +66,21 @@ export class UsersService {
const business = await this.em.findOne(Business, { id: businessId }); const business = await this.em.findOne(Business, { id: businessId });
if (!business) throw new BadRequestException(BusinessMessage.NOT_FOUND); 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) { if (domain.business.id !== businessId) throw new BadRequestException(DomainMessage.NOT_BELONG_TO_BUSINESS);
throw new BadRequestException(DomainMessage.NOT_BELONG_TO_BUSINESS);
}
if (domain.status !== DomainStatus.VERIFIED) { if (domain.status !== DomainStatus.VERIFIED) throw new BadRequestException(DomainMessage.NOT_VERIFIED);
throw new BadRequestException(DomainMessage.NOT_VERIFIED);
}
const emailAddress = `${username}@${domain.name}`; const emailAddress = `${username}@${domain.name}`;
// Check if email address already exists
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);
const hashedPassword = await this.passwordService.hashPassword(password); const hashedPassword = await this.passwordService.hashPassword(password);
let userRole = await this.em.findOne(Role, { name: RoleEnum.USER }); const userRole = await this.em.findOne(Role, { name: RoleEnum.USER });
if (!userRole) { if (!userRole) throw new BadRequestException(RoleMessage.NOT_FOUND);
userRole = this.em.create(Role, { name: RoleEnum.USER });
await this.em.persistAndFlush(userRole);
}
try { try {
const mailServerUser = await firstValueFrom( const mailServerUser = await firstValueFrom(
@@ -97,8 +89,8 @@ export class UsersService {
password, password,
address: emailAddress, address: emailAddress,
name: displayName || username, name: displayName || username,
quota: quota || 1073741824, // 1GB default quota: quota || 1073741824,
tags: ["dmail-user"], language: "fa",
}), }),
); );
@@ -121,7 +113,6 @@ export class UsersService {
await this.em.persistAndFlush(user); await this.em.persistAndFlush(user);
// Create aliases if provided
if (aliases && aliases.length > 0) { if (aliases && aliases.length > 0) {
for (const alias of aliases) { for (const alias of aliases) {
try { try {
@@ -164,7 +155,7 @@ export class UsersService {
encryption: DANAK_SMTPS_ENCRYPTION, encryption: DANAK_SMTPS_ENCRYPTION,
}, },
}, },
message: "Email user created successfully", message: UserMessage.EMAIL_USER_CREATED_SUCCESSFULLY,
}; };
} catch (error) { } catch (error) {
this.logger.error(`Failed to create email user ${emailAddress}:`, 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); 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 { 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 { CreateEmailUserDto } from "./DTO/create-email-user.dto";
import { UsersService } from "./services/users.service"; 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 { BusinessDec } from "../../common/decorators/business.decorator";
import { ParamDto } from "../../common/DTO/param.dto"; import { ParamDto } from "../../common/DTO/param.dto";
@ApiTags("email-users") @Controller("users")
@Controller("email-users")
@ApiHeader({ name: "x-business-id", description: "Business ID" }) @ApiHeader({ name: "x-business-id", description: "Business ID" })
@AuthGuards() @AuthGuards()
export class UsersController { export class UsersController {
@@ -41,4 +40,11 @@ export class UsersController {
updateEmailUserQuota(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string, @Body() body: { quota: number }) { updateEmailUserQuota(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string, @Body() body: { quota: number }) {
return this.usersService.updateEmailUserQuota(paramDto.id, businessId, body.quota); 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);
}
} }