chore: change the email creation dto
This commit is contained in:
+15
-15
@@ -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": [
|
||||
|
||||
Generated
+778
-780
File diff suppressed because it is too large
Load Diff
@@ -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 = "نقش یافت نشد",
|
||||
}
|
||||
|
||||
@@ -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 }));
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user