update: domain verfication service

This commit is contained in:
mahyargdz
2025-05-23 20:59:43 +03:30
parent 7566a061e0
commit f03cf82954
4 changed files with 458 additions and 11 deletions
@@ -62,4 +62,124 @@ export class BusinessesController {
getDomainStatus(@BusinessDec() business: Business) {
return this.domainVerificationService.getDomainStatus(business.id);
}
@Post("domain/update-dns-records")
@ApiOperation({ summary: "Get detailed instructions for updating DNS records" })
@AuthGuards()
getDetailedDnsInstructions(@BusinessDec() business: Business) {
// This endpoint provides detailed DNS setup instructions for various DNS providers
if (!business.domain) {
return {
success: false,
message: "No domain configured for this business",
};
}
return {
success: true,
domain: business.domain,
instructions: {
title: "راهنمای تنظیم رکوردهای DNS برای دامنه شما",
description: "برای اتصال دامنه خود به سرویس دی‌زون، لطفا یکی از روش‌های زیر را انتخاب کنید:",
methods: [
{
title: "روش A: استفاده از رکورد A (آدرس IP)",
description: "این روش برای همه ارائه‌دهندگان DNS کار می‌کند و برای دامنه اصلی مناسب است.",
steps: [
"۱. وارد پنل مدیریت DNS خود شوید",
"۲. یک رکورد A جدید ایجاد کنید",
"۳. در فیلد Host یا Name، @ را وارد کنید (برای دامنه اصلی)",
`۴. در فیلد Value یا Points to، آدرس IP سرور ما را وارد کنید: 194.5.192.20`,
"۵. TTL را روی 3600 (یک ساعت) یا کمترین مقدار ممکن تنظیم کنید",
"۶. تغییرات را ذخیره کنید",
],
},
{
title: "روش B: استفاده از رکورد CNAME (نام مستعار)",
description: "این روش برای زیردامنه‌ها مانند www توصیه می‌شود.",
steps: [
"۱. وارد پنل مدیریت DNS خود شوید",
"۲. یک رکورد CNAME جدید ایجاد کنید",
"۳. در فیلد Host یا Name، www را وارد کنید (یا زیردامنه دلخواه)",
`۴. در فیلد Value یا Points to، آدرس سرور ما را وارد کنید: app.dzone.danakcorp.com`,
"۵. TTL را روی 3600 (یک ساعت) یا کمترین مقدار ممکن تنظیم کنید",
"۶. تغییرات را ذخیره کنید",
],
note: "توجه: برخی ارائه‌دهندگان DNS اجازه استفاده از CNAME برای دامنه اصلی (@) را نمی‌دهند. در این صورت از روش A استفاده کنید.",
},
],
providerSpecific: [
{
provider: "Cloudflare",
instructions: [
"۱. حالت ابری (پروکسی) را خاموش کنید و فقط از DNS استفاده کنید",
"۲. برای دامنه اصلی (@) از رکورد A استفاده کنید",
"۳. برای زیردامنه www از رکورد CNAME استفاده کنید",
],
},
{
provider: "cPanel",
instructions: [
"۱. به بخش Zone Editor بروید",
"۲. روی دامنه خود کلیک کنید",
"۳. برای دامنه اصلی، رکورد A با مقدار 194.5.192.20 اضافه کنید",
"۴. برای www، رکورد CNAME با مقدار app.dzone.danakcorp.com اضافه کنید",
],
},
{
provider: "سایر ارائه‌دهندگان",
instructions: [
"اگر نمی‌توانید CNAME برای دامنه اصلی تنظیم کنید، از رکورد A استفاده کنید",
"برخی ارائه‌دهندگان از رکوردهای ANAME یا ALIAS برای دامنه اصلی پشتیبانی می‌کنند",
"اگر در تنظیم رکوردها مشکل دارید، با پشتیبانی ارائه‌دهنده DNS خود تماس بگیرید",
],
},
],
troubleshooting: [
"- تغییرات DNS ممکن است بین ۱۵ دقیقه تا ۴۸ ساعت طول بکشد تا در سراسر اینترنت منتشر شود",
"- اگر پس از ۴۸ ساعت هنوز مشکل دارید، مطمئن شوید که رکوردها را درست تنظیم کرده‌اید",
"- برای بررسی وضعیت رکوردهای DNS خود می‌توانید از ابزارهایی مانند dnschecker.org استفاده کنید",
"- اگر همچنان مشکل دارید، با پشتیبانی ما تماس بگیرید",
],
},
};
}
@Post("domain/configure-caprover")
@ApiOperation({ summary: "Manually configure domain in Caprover (Admin use only)" })
@AuthGuards()
async manualCaproverConfig(@BusinessDec() business: Business) {
if (!business.domain) {
return {
success: false,
message: "No domain configured for this business",
};
}
if (!business.isDomainVerified) {
return {
success: false,
message: "Domain must be verified before configuring in Caprover",
domain: business.domain,
};
}
try {
const result = await this.domainVerificationService.configureCaproverDomain(business.domain);
return {
success: result,
message: result
? `Domain ${business.domain} successfully configured in Caprover`
: `Failed to configure domain ${business.domain} in Caprover`,
domain: business.domain,
};
} catch (error: unknown) {
return {
success: false,
message: `Error configuring domain in Caprover: ${error instanceof Error ? error.message : String(error)}`,
domain: business.domain,
};
}
}
}
@@ -1,4 +1,5 @@
import { MikroOrmModule } from "@mikro-orm/nestjs";
import { HttpModule } from "@nestjs/axios";
import { BullModule } from "@nestjs/bullmq";
import { Module } from "@nestjs/common";
@@ -16,6 +17,10 @@ import { DomainVerificationService } from "./services/domain-verification.servic
name: SUBSCRIPTIONS.PROVISIONING_QUEUE_NAME,
prefix: SUBSCRIPTIONS.PROVISIONING_QUEUE_PREFIX,
}),
HttpModule.register({
timeout: 10000,
maxRedirects: 5,
}),
],
providers: [BusinessesService, BusinessProvisioningProcessor, DomainVerificationService],
exports: [BusinessesService],
@@ -3,8 +3,12 @@ import dns from "node:dns";
import { promisify } from "node:util";
import { EntityManager } from "@mikro-orm/postgresql";
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
import { HttpService } from "@nestjs/axios";
import { BadRequestException, Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { AxiosError } from "axios";
import dayjs from "dayjs";
import { catchError, firstValueFrom } from "rxjs";
import { BusinessesService } from "./businesses.service";
import { BusinessMessage } from "../../../common/enums/message.enum";
@@ -18,12 +22,87 @@ export class DomainVerificationService {
private readonly resolveCname = promisify(dns.resolveCname);
private readonly serverIP = "194.5.192.20";
private readonly serverDomain = "app.dzone.danakcorp.com";
private readonly serverDomain = "dzone.danakcorp.com";
// Caprover configuration
private readonly caproverApiUrl: string;
private readonly caproverAppName: string;
// Caprover authentication
// private readonly caproverEmail: string;
private readonly caproverPassword: string;
private caproverAuthToken: string;
private tokenExpiry: Date | null = null;
private readonly TOKEN_REFRESH_BEFORE_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
constructor(
private readonly businessesService: BusinessesService,
private readonly em: EntityManager,
) {}
private readonly httpService: HttpService,
private readonly configService: ConfigService,
) {
this.caproverApiUrl = this.configService.getOrThrow<string>("CAPROVER_API_URL");
this.caproverAppName = this.configService.getOrThrow<string>("CAPROVER_APP_NAME");
// this.caproverEmail = this.configService.getOrThrow<string>("CAPROVER_EMAIL");
this.caproverPassword = this.configService.getOrThrow<string>("CAPROVER_PASSWORD");
this.caproverAuthToken = this.configService.get<string>("CAPROVER_AUTH_TOKEN", "");
}
//--------------------------------
private async getCaproverToken(): Promise<string> {
try {
// Check if we already have a valid token
if (this.caproverAuthToken && this.tokenExpiry && new Date() < new Date(this.tokenExpiry.getTime() - this.TOKEN_REFRESH_BEFORE_EXPIRY_MS)) {
this.logger.debug("Using existing Caprover token");
return this.caproverAuthToken;
}
this.logger.log("Getting new Caprover authentication token");
const loginUrl = `${this.caproverApiUrl}/login`;
const response = await firstValueFrom(
this.httpService
.post(
loginUrl,
{
password: this.caproverPassword,
otpToken: "",
// email: this.caproverEmail,
},
{
headers: {
"x-namespace": "captain",
"Content-Type": "application/json",
},
},
)
.pipe(
catchError((err: AxiosError) => {
this.logger.error("Error in Caprover authentication", err);
throw new InternalServerErrorException("Failed to authenticate with Caprover");
}),
),
);
if (response.status === 200 && response.data?.data?.token) {
// Set the token and expiry (default to 24 hours if not provided by Caprover)
this.caproverAuthToken = response.data.data.token;
// Set expiry to 23 hours from now to be safe (Caprover tokens typically last 24 hours)
this.tokenExpiry = new Date(Date.now() + 1 * 60 * 60 * 1000);
this.logger.log("Successfully obtained new Caprover token");
return this.caproverAuthToken;
} else {
this.logger.error(`Failed to get Caprover token: ${JSON.stringify(response.data)}`);
throw new InternalServerErrorException("Failed to get Caprover authentication token");
}
} catch (error: unknown) {
this.logger.error(`Error getting Caprover token: ${error instanceof Error ? error.message : String(error)}`);
throw new InternalServerErrorException("Failed to authenticate with Caprover");
}
}
/**
* Set Domain - Core method for setting/updating the domain and returning instructions
@@ -84,6 +163,13 @@ export class DomainVerificationService {
if (business.isDomainVerified) {
const domainPointsToServer = await this.checkDomainPointsToServer(business.domain);
try {
let caproverConfigured = false;
caproverConfigured = await this.configureCaproverDomain(business.domain);
this.logger.log(`Domain ${business.domain} configuration in Caprover: ${caproverConfigured ? "successful" : "failed"}`);
} catch (error: unknown) {
this.logger.error(`Failed to configure domain in Caprover: ${error instanceof Error ? error.message : String(error)}`);
}
return {
message: BusinessMessage.DOMAIN_ALREADY_VERIFIED,
@@ -109,6 +195,17 @@ export class DomainVerificationService {
business.domainVerifiedAt = dayjs().toDate();
await this.em.flush();
// If domain is verified, try to configure it in Caprover
let caproverConfigured = false;
if (domainPointsToServer) {
try {
caproverConfigured = await this.configureCaproverDomain(business.domain);
this.logger.log(`Domain ${business.domain} configuration in Caprover: ${caproverConfigured ? "successful" : "failed"}`);
} catch (error: unknown) {
this.logger.error(`Failed to configure domain in Caprover: ${error instanceof Error ? error.message : String(error)}`);
}
}
return {
message: BusinessMessage.DOMAIN_VERIFICATION_SUCCESS,
domain: business.domain,
@@ -171,6 +268,109 @@ export class DomainVerificationService {
};
}
/**
* Configure a domain in Caprover
* This method sends a request to Caprover API to add a custom domain to an app
*/
async configureCaproverDomain(domain: string): Promise<boolean> {
try {
// Get a fresh token before making the request
const token = await this.getCaproverToken();
this.logger.log(`Configuring domain ${domain} in Caprover for app ${this.caproverAppName}`);
const url = `${this.caproverApiUrl}/user/apps/appDefinitions/customdomain`;
const response = await firstValueFrom(
this.httpService
.post(
url,
{
appName: this.caproverAppName,
customDomain: domain,
},
{
headers: {
"X-Captain-Auth": token,
"x-namespace": "captain",
"Content-Type": "application/json",
},
},
)
.pipe(
catchError((err: AxiosError) => {
this.logger.error("error in configuring domain in caprover", err);
throw new InternalServerErrorException("error in configuring domain in caprover");
}),
),
);
if (response.status === 200 && response.data?.status === 100) {
this.logger.log(`Successfully configured domain ${domain} in Caprover`);
// Enable HTTPS for the domain
await this.enableCaproverHttps(domain);
return true;
} else {
this.logger.error(`Failed to configure domain in Caprover: ${JSON.stringify(response.data)}`);
return false;
}
} catch (error) {
this.logger.error(`Error configuring domain in Caprover: ${error instanceof Error ? error.message : String(error)}`);
return false;
}
}
/**
* Enable HTTPS for a domain in Caprover
*/
private async enableCaproverHttps(domain: string): Promise<boolean> {
try {
// Get a fresh token before making the request
const token = await this.getCaproverToken();
this.logger.log(`Enabling HTTPS for domain ${domain} in Caprover`);
const url = `${this.caproverApiUrl}/user/apps/appDefinitions/enablecustomdomainssl`;
const response = await firstValueFrom(
this.httpService
.post(
url,
{
appName: this.caproverAppName,
customDomain: domain,
},
{
headers: {
"X-Captain-Auth": token,
"x-namespace": "captain",
"Content-Type": "application/json",
},
},
)
.pipe(
catchError((err: AxiosError) => {
this.logger.error("error in enabling https in caprover", err);
throw new InternalServerErrorException("error in enabling https in caprover");
}),
),
);
if (response.status === 200 && response.data?.status === 100) {
this.logger.log(`Successfully enabled HTTPS for domain ${domain} in Caprover`);
return true;
} else {
this.logger.error(`Failed to enable HTTPS in Caprover: ${JSON.stringify(response.data)}`);
return false;
}
} catch (error: unknown) {
this.logger.error(`Error enabling HTTPS in Caprover: ${error instanceof Error ? error.message : String(error)}`);
return false;
}
}
//*********************************** */
//private methods
@@ -178,8 +378,8 @@ export class DomainVerificationService {
return `dzone-verify-${crypto.randomBytes(16).toString("hex")}`;
}
// Get domain connection instructions
private getDomainConnectionInstructions(_domain: string) {
// Get domain connection instructions with enhanced guidance for different DNS providers
private getDomainConnectionInstructions(domain: string) {
return {
title: BusinessMessage.DOMAIN_CONNECTION_TITLE,
description: BusinessMessage.DOMAIN_CONNECTION_DESCRIPTION,
@@ -209,7 +409,12 @@ export class DomainVerificationService {
BusinessMessage.DOMAIN_CONNECTION_TROUBLESHOOTING_2,
BusinessMessage.DOMAIN_CONNECTION_TROUBLESHOOTING_3,
BusinessMessage.DOMAIN_CONNECTION_TROUBLESHOOTING_4,
// Add new troubleshooting instructions for common DNS providers
"- اگر از Cloudflare استفاده می‌کنید، حالت ابری (پروکسی) را خاموش کنید و فقط از DNS استفاده کنید",
"- برای دامنه اصلی (@)، برخی ارائه دهندگان DNS به جای CNAME از رکورد ANAME یا ALIAS استفاده می‌کنند",
"- اگر نمی‌توانید CNAME برای دامنه اصلی تنظیم کنید، از رکورد A با IP سرور استفاده کنید",
],
domain: domain,
};
}
@@ -233,6 +438,9 @@ export class DomainVerificationService {
BusinessMessage.DOMAIN_SETUP_TROUBLESHOOTING_2,
BusinessMessage.DOMAIN_SETUP_TROUBLESHOOTING_3,
BusinessMessage.DOMAIN_SETUP_TROUBLESHOOTING_4,
// Add new troubleshooting instructions
"- برخی از ارائه‌دهندگان DNS ممکن است نیاز به تنظیمات خاص برای رکوردهای TXT داشته باشند",
"- اگر از Cloudflare استفاده می‌کنید، مطمئن شوید که رکوردهای TXT را بدون پروکسی تنظیم کرده‌اید",
],
},
};
@@ -258,37 +466,75 @@ export class DomainVerificationService {
}
// Check if domain points to our server via A or CNAME record
// Enhanced to handle both root domain and www subdomain
private async checkDomainPointsToServer(domain: string): Promise<boolean> {
try {
this.logger.log(`Checking if domain ${domain} points to our servers`);
// Try CNAME first
try {
const cnameRecords = await this.resolveCname(domain);
this.logger.log(`Found CNAME records for ${domain}: ${JSON.stringify(cnameRecords)}`);
// Check if any CNAME points to our server domain
if (cnameRecords.some((record) => record === this.serverDomain)) {
this.logger.log(`Domain ${domain} has valid CNAME record pointing to our server`);
return true;
}
} catch (_error) {
this.logger.debug(`No CNAME records found for ${domain}, trying A records`);
} catch (error: unknown) {
this.logger.debug(`No CNAME records found for ${domain}: ${error instanceof Error ? error.message : String(error)}`);
}
// Try www subdomain CNAME if root domain check failed
if (!domain.startsWith("www.")) {
try {
const wwwDomain = `www.${domain}`;
const wwwCnameRecords = await this.resolveCname(wwwDomain);
this.logger.log(`Found CNAME records for ${wwwDomain}: ${JSON.stringify(wwwCnameRecords)}`);
// Check if www subdomain CNAME points to our server domain
if (wwwCnameRecords.some((record) => record === this.serverDomain)) {
this.logger.log(`WWW subdomain ${wwwDomain} has valid CNAME record pointing to our server`);
return true;
}
} catch (error: unknown) {
this.logger.debug(`No CNAME records found for www.${domain}: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Try A record
try {
const aRecords = await this.resolveA(domain);
this.logger.log(`Found A records for ${domain}: ${JSON.stringify(aRecords)}`);
// Check if any A record points to our server IP
if (aRecords.some((record) => record === this.serverIP)) {
this.logger.log(`Domain ${domain} has valid A record pointing to our server`);
return true;
}
} catch (_error) {
this.logger.debug(`No A records found for ${domain}`);
} catch (error: unknown) {
this.logger.debug(`No A records found for ${domain}: ${error instanceof Error ? error.message : String(error)}`);
}
// Try www subdomain A record if root domain check failed
if (!domain.startsWith("www.")) {
try {
const wwwDomain = `www.${domain}`;
const wwwARecords = await this.resolveA(wwwDomain);
this.logger.log(`Found A records for ${wwwDomain}: ${JSON.stringify(wwwARecords)}`);
// Check if www subdomain A record points to our server IP
if (wwwARecords.some((record) => record === this.serverIP)) {
this.logger.log(`WWW subdomain ${wwwDomain} has valid A record pointing to our server`);
return true;
}
} catch (error: unknown) {
this.logger.debug(`No A records found for www.${domain}: ${error instanceof Error ? error.message : String(error)}`);
}
}
// If we get here, neither CNAME nor A records point to our server
this.logger.warn(`Domain ${domain} does not point to our servers`);
return false;
} catch (error: unknown) {
this.logger.error(`Error checking domain DNS records: ${error instanceof Error ? error.message : String(error)}`);