chore: complete the mail domain setup
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@
|
||||
FROM node:22-alpine AS base
|
||||
|
||||
RUN npm install -g corepack@latest
|
||||
RUN corepack enable && corepack prepare pnpm --activate
|
||||
RUN corepack enable && corepack prepare pnpm@9 --activate
|
||||
|
||||
# Install tzdata to support timezone settings
|
||||
RUN apk add --no-cache tzdata
|
||||
|
||||
+1
-1
@@ -129,5 +129,5 @@
|
||||
"git add ."
|
||||
]
|
||||
},
|
||||
"packageManager": "pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac"
|
||||
"packageManager": "pnpm@9.15.9+sha512.68046141893c66fad01c079231128e9afb89ef87e2691d69e4d40eee228988295fd4682181bae55b58418c3a253bde65a505ec7c5f9403ece5cc3cd37dcf2531"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
+8
-11
@@ -1,11 +1,11 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import * as request from 'supertest';
|
||||
import { App } from 'supertest/types';
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import * as request from "supertest";
|
||||
import { App } from "supertest/types";
|
||||
|
||||
import { AppModule } from './../src/app.module';
|
||||
import { AppModule } from "./../src/app.module";
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
describe("AppController (e2e)", () => {
|
||||
let app: INestApplication<App>;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -17,10 +17,7 @@ describe('AppController (e2e)', () => {
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/ (GET)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect(200)
|
||||
.expect('Hello World!');
|
||||
it("/ (GET)", () => {
|
||||
return request(app.getHttpServer()).get("/").expect(200).expect("Hello World!");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user