chore: complete the mail domain setup

This commit is contained in:
mahyargdz
2025-07-03 12:25:31 +03:30
parent ccbf743ecb
commit 9ae6f260a8
6 changed files with 147 additions and 150 deletions
+11 -21
View File
@@ -45,34 +45,24 @@ export class DomainsController {
return this.domainsService.deleteDomain(paramDto.id);
}
@Get(":id/setup")
@Get("setup")
@ApiOperation({ summary: "Get domain setup instructions" })
@ApiResponse({ status: 200, description: "Domain setup instructions retrieved" })
getDomainSetupInstructions(@Param() paramDto: ParamDto) {
return this.domainsService.getDomainSetupInstructions(paramDto.id);
getDomainSetupInstructions(@BusinessDec("id") businessId: string) {
return this.domainsService.getDomainSetupInstructions(businessId);
}
@Get(":id/dns-records")
@Get("dns-records")
@ApiOperation({ summary: "Get DNS records for domain" })
@ApiResponse({ status: 200, description: "DNS records retrieved successfully" })
getDomainDnsRecords(@Param() paramDto: ParamDto) {
return this.domainsService.getDomainDnsRecords(paramDto.id);
getDomainDnsRecords(@BusinessDec("id") businessId: string) {
return this.domainsService.getDomainDnsRecords(businessId);
}
@Get(":id/verification-status")
@ApiOperation({ summary: "Check domain verification status" })
@ApiResponse({ status: 200, description: "Domain verification status checked" })
checkDomainVerificationStatus(@Param() paramDto: ParamDto) {
return this.domainsService.checkDomainVerificationStatus(paramDto.id);
}
@Post(":id/check-dns")
@ApiOperation({ summary: "Check DNS records and update verification status" })
@ApiResponse({ status: 200, description: "DNS records checked and updated" })
async checkDnsRecords(@Param() paramDto: ParamDto) {
// Trigger DNS verification
await this.domainsService.verifyDomain(paramDto.id);
// Return updated status
return this.domainsService.checkDomainVerificationStatus(paramDto.id);
@Get("verify-dns")
@ApiOperation({ summary: "Check DNS records, verify domain configuration and return verification status" })
@ApiResponse({ status: 200, description: "DNS records verified and status returned" })
verifyDnsRecords(@BusinessDec("id") businessId: string) {
return this.domainsService.checkDomainVerificationStatus(businessId);
}
}
+10 -4
View File
@@ -20,8 +20,9 @@ export class DnsService {
private readonly logger = new Logger(DnsService.name);
private readonly mailServerDomain: string = "mail.danakcorp.com";
private readonly spfRecord: string = `v=spf1 include:${this.mailServerDomain} ~all`;
private readonly dmarcRecord: string = `v=DMARC1; p=quarantine; rua=mailto:dmarc@${this.mailServerDomain}; ruf=mailto:dmarc@${this.mailServerDomain}`;
private readonly serverDomain: string = "danakcorp.com";
private readonly spfRecord: string = `v=spf1 mx include:${this.serverDomain} ~all`;
// private readonly dmarcRecord: string = `v=DMARC1; p=quarantine; rua=mailto:dmarc@${this.serverDomain}; ruf=mailto:dmarc@${this.serverDomain}`;
private readonly resolveTxt = promisify(dns.resolveTxt);
private readonly resolveCname = promisify(dns.resolveCname);
@@ -107,10 +108,11 @@ export class DnsService {
});
// DMARC Record
const dmarcRecord = `v=DMARC1; p=quarantine; rua=mailto:dmarc@${domain.name}; ruf=mailto:dmarc@${domain.name}`;
requiredRecords.push({
name: `_dmarc.${domain.name}`,
type: DNSRecordType.DMARC,
value: this.dmarcRecord,
value: dmarcRecord,
isRequired: true,
description: "DMARC policy record",
});
@@ -269,7 +271,11 @@ export class DnsService {
const txtRecords = await this.resolveTxt(dnsRecord.name);
const flatRecords = txtRecords.flat();
return flatRecords.some((record) => record.includes(dnsRecord.value) || record === dnsRecord.value);
// Join all TXT record parts together (handles multi-part TXT records like DKIM)
const joinedRecord = flatRecords.join("");
// Check if the expected value matches either the joined record or any individual part
return joinedRecord === dnsRecord.value || flatRecords.some((record) => record.includes(dnsRecord.value) || record === dnsRecord.value);
} catch (error) {
this.logger.debug(`TXT record verification failed for ${dnsRecord.name}:`, error instanceof Error ? error.message : "Unknown error");
return false;
+116 -112
View File
@@ -56,25 +56,9 @@ export class DomainsService {
await this.dnsService.generateRequiredDnsRecords(domain);
// Generate and create DKIM record
try {
const dkimKeys = await this.dnsService.generateDKIMKeysByMailServer(domain.name, "default");
await this.setupDomainDkim(domain, "default");
// Update domain with DKIM keys
domain.dkimPrivateKey = dkimKeys.privateKey;
domain.dkimPublicKey = dkimKeys.publicKey;
// Create DKIM DNS record
await this.dnsService.createDKIMRecord(domain, "default", dkimKeys.dnsTxt.name, dkimKeys.dnsTxt.value, dkimKeys.wildduckId);
await this.em.persistAndFlush(domain);
this.logger.log(`DKIM automatically enabled for domain ${name}`);
} catch (error) {
this.logger.warn(`Failed to create DKIM for domain ${name}:`, error);
// Continue with domain creation even if DKIM fails
domain.dkimEnabled = false;
await this.em.persistAndFlush(domain);
}
await this.em.persistAndFlush(domain);
// Create initial verification
@@ -98,16 +82,6 @@ export class DomainsService {
return domain;
}
/**
* Get domain by name
*/
async getDomainByName(name: string) {
const domain = await this.domainRepository.findByName(name);
if (!domain) throw new BadRequestException(DomainMessage.DOMAIN_NOT_FOUND);
return { domain };
}
/**
* Get domains for a business
*/
@@ -173,13 +147,40 @@ export class DomainsService {
return { domain };
}
/**
* Setup DKIM for domain
*/
private async setupDomainDkim(domain: Domain, selector: string) {
try {
const dkimKeys = await this.dnsService.generateDKIMKeysByMailServer(domain.name, selector);
// Update domain with DKIM keys
domain.dkimPrivateKey = dkimKeys.privateKey;
domain.dkimPublicKey = dkimKeys.publicKey;
// Create DKIM DNS record
await this.dnsService.createDKIMRecord(domain, selector, dkimKeys.dnsTxt.name, dkimKeys.dnsTxt.value, dkimKeys.wildduckId);
this.logger.log(`DKIM automatically enabled for domain ${domain.name}`);
} catch (error) {
this.logger.warn(`Failed to create DKIM for domain ${domain.name}:`, error);
// Continue with domain creation even if DKIM fails
domain.dkimEnabled = false;
domain.dkimPrivateKey = undefined;
domain.dkimPublicKey = undefined;
domain.dkimSelector = undefined;
}
}
/**
* Setup domain in mail server
*/
private async setupDomainInMailServer(domain: Domain) {
try {
// Create DKIM keys if enabled
if (domain.dkimEnabled && domain.dkimSelector) {
// Only create DKIM keys if enabled and not already created
if (domain.dkimEnabled && domain.dkimSelector && !domain.dkimPrivateKey) {
this.logger.log(`Creating DKIM keys for domain ${domain.name} in mail server`);
const dkimKey = await firstValueFrom(
this.mailServerService.dkim.createDKIMKey({
domain: domain.name,
@@ -189,6 +190,8 @@ export class DomainsService {
);
if (!dkimKey.success) throw new BadRequestException(MailServerMessage.FAILED_TO_CREATE_DKIM_KEY);
} else if (domain.dkimEnabled && domain.dkimPrivateKey) {
this.logger.log(`DKIM already exists for domain ${domain.name}, skipping creation`);
}
this.logger.log(`Mail server setup completed for domain ${domain.name}`);
@@ -198,56 +201,16 @@ export class DomainsService {
}
}
/**
* Enable DKIM for domain
*/
async enableDKIM(id: string, selector?: string) {
const domain = await this.getDomainById(id);
const dkimSelector = selector || "default";
// Generate DKIM keys
const dkimKeys = await this.dnsService.generateDKIMKeysByMailServer(domain.name, dkimSelector);
domain.dkimEnabled = true;
domain.dkimSelector = dkimSelector;
domain.dkimPrivateKey = dkimKeys.privateKey;
domain.dkimPublicKey = dkimKeys.publicKey;
// Create DKIM DNS record
await this.dnsService.createDKIMRecord(domain, dkimSelector, dkimKeys.dnsTxt.name, dkimKeys.dnsTxt.value, dkimKeys.wildduckId);
await this.em.persistAndFlush(domain);
this.logger.log(`DKIM enabled for domain ${domain.name}`);
return { domain };
}
/**
* Get domain statistics
*/
async getDomainStats(businessId: string) {
return this.domainRepository.getBusinessDomainStats(businessId);
}
/**
* Check all domains that need verification
*/
async checkDomainsNeedingVerification() {
const domain = await this.domainRepository.findNeedingCheck();
if (!domain) return { message: DomainMessage.NO_DOMAIN_FOUND };
await this.verifyDomain(domain.id);
return { message: DomainMessage.DOMAIN_CHECKED, domain: domain };
}
/**
* Get domain setup instructions
*/
async getDomainSetupInstructions(domainId: string) {
const domain = await this.getDomainById(domainId);
const { dnsRecords } = await this.dnsService.getDomainDnsRecords(domainId);
async getDomainSetupInstructions(businessId: string) {
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 { dnsRecords } = await this.dnsService.getDomainDnsRecords(businessDomain.id);
return this.formatDomainSetupResponse(domain, dnsRecords);
}
@@ -255,19 +218,57 @@ export class DomainsService {
/**
* Get DNS records for domain
*/
async getDomainDnsRecords(domainId: string) {
// Verify domain exists
await this.getDomainById(domainId);
return this.dnsService.getDomainDnsRecords(domainId);
async getDomainDnsRecords(businessId: string) {
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 { overallStatus, recommendations } = await this.checkDomainVerificationStatus(businessId);
const { dnsRecords } = await this.dnsService.getDomainDnsRecords(domain.id);
return {
dnsRecords,
overallStatus,
recommendations,
};
}
/**
* Check domain verification status
*/
async checkDomainVerificationStatus(domainId: string) {
await this.getDomainById(domainId);
const { dnsRecords } = await this.dnsService.getDomainDnsRecords(domainId);
async checkDomainVerificationStatus(businessId: string) {
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);
// Trigger DNS verification
await this.verifyDomain(domain.id);
const { dnsRecords } = await this.dnsService.getDomainDnsRecords(domain.id);
// Process DNS records for verification status
const { verificationStatuses, statusCounts, recommendations } = await this.processDnsRecordsForVerification(dnsRecords);
const isVerified = statusCounts.failed === 0 && statusCounts.pending === 0 && statusCounts.verified > 0;
return {
dnsRecords: verificationStatuses,
overallStatus: {
isVerified,
...statusCounts,
total: dnsRecords.length,
lastChecked: new Date(),
},
recommendations,
};
}
/**
* Process DNS records for verification status
*/
private async processDnsRecordsForVerification(dnsRecords: DnsRecord[]) {
const verificationStatuses = [];
let verified = 0;
let pending = 0;
@@ -316,22 +317,24 @@ export class DomainsService {
}
}
const isVerified = failed === 0 && pending === 0 && verified > 0;
return {
dnsRecords: verificationStatuses,
overallStatus: {
isVerified,
verified,
pending,
failed,
total: dnsRecords.length,
lastChecked: new Date(),
},
verificationStatuses,
statusCounts: { verified, pending, failed },
recommendations,
};
}
/**
* Get DNS record status counts
*/
private getDnsRecordStatusCounts(dnsRecords: DnsRecord[]) {
const verified = dnsRecords.filter((r) => r.status === VerificationStatus.VERIFIED).length;
const pending = dnsRecords.filter((r) => r.status === VerificationStatus.PENDING).length;
const failed = dnsRecords.filter((r) => r.status === VerificationStatus.FAILED).length;
return { verified, pending, failed };
}
/**
* Format domain setup response with instructions
*/
@@ -353,20 +356,15 @@ export class DomainsService {
instructions: this.generateRecordInstructions(record),
}));
const verified = dnsRecords.filter((r) => r.status === VerificationStatus.VERIFIED).length;
const pending = dnsRecords.filter((r) => r.status === VerificationStatus.PENDING).length;
const failed = dnsRecords.filter((r) => r.status === VerificationStatus.FAILED).length;
const statusCounts = this.getDnsRecordStatusCounts(dnsRecords);
const nextSteps = this.generateNextSteps(domain, dnsRecords);
return {
domain,
dnsRecords: dnsInstructions,
setupStatus: {
isComplete: failed === 0 && pending === 0 && verified > 0,
verified,
pending,
failed,
isComplete: statusCounts.failed === 0 && statusCounts.pending === 0 && statusCounts.verified > 0,
...statusCounts,
total: dnsRecords.length,
},
nextSteps,
@@ -420,19 +418,25 @@ export class DomainsService {
*/
private generateNextSteps(domain: Domain, dnsRecords: DnsRecord[]): string[] {
const steps: string[] = [];
const statusCounts = this.getDnsRecordStatusCounts(dnsRecords);
if (domain.status === DomainStatus.PENDING) {
steps.push(`1. ${DomainMessage.STEP_LOGIN_DNS_PANEL}`);
steps.push(`2. ${DomainMessage.STEP_ADD_ALL_RECORDS}`);
steps.push(`3. ${DomainMessage.STEP_INCLUDE_ALL_TYPES}`);
steps.push(`4. ${DomainMessage.STEP_WAIT_PROPAGATION}`);
steps.push(`5. ${DomainMessage.STEP_USE_CHECK_ENDPOINT}`);
steps.push(`6. ${DomainMessage.STEP_DOMAIN_READY}`);
const setupSteps = [
DomainMessage.STEP_LOGIN_DNS_PANEL,
DomainMessage.STEP_ADD_ALL_RECORDS,
DomainMessage.STEP_INCLUDE_ALL_TYPES,
DomainMessage.STEP_WAIT_PROPAGATION,
DomainMessage.STEP_USE_CHECK_ENDPOINT,
DomainMessage.STEP_DOMAIN_READY,
];
setupSteps.forEach((step, index) => {
steps.push(`${index + 1}. ${step}`);
});
}
const pendingRecords = dnsRecords.filter((r) => r.status === VerificationStatus.PENDING);
if (pendingRecords.length > 0) {
steps.push(`📋 ${formatDomainMessage(DomainMessage.RECORDS_PENDING_VERIFICATION, { count: pendingRecords.length })}`);
if (statusCounts.pending > 0) {
steps.push(`📋 ${formatDomainMessage(DomainMessage.RECORDS_PENDING_VERIFICATION, { count: statusCounts.pending })}`);
}
const requiredRecords = dnsRecords.filter((r) => r.isRequired);