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
+76
View File
@@ -0,0 +1,76 @@
# Caprover Integration Setup
This document explains how to set up the integration between the DZone API and Caprover for automatic domain configuration.
## Overview
When a user successfully verifies their domain, the system will automatically configure that domain in Caprover, enabling the domain to be properly routed to the frontend application.
## Configuration Steps
### 1. Set Environment Variables
Add the following environment variables to your `.env` file:
```
CAPROVER_API_URL=https://captain.dev.danakcorp.com/api/v2
CAPROVER_EMAIL=your_caprover_email@example.com
CAPROVER_PASSWORD=your_caprover_password
CAPROVER_APP_NAME=dzone-front
```
The system will automatically handle authentication with Caprover using these credentials and refresh tokens as needed.
### 2. Verify Configuration
1. Restart the API service to apply the new environment variables
2. Try verifying a domain through the API
3. Check the logs to ensure the Caprover API calls are successful
## How Token Refresh Works
The system automatically handles Caprover authentication:
1. When a domain configuration request is made, the system checks if it has a valid token
2. If no token exists or the token is about to expire (within 5 minutes), it requests a new one
3. The system uses the email and password to authenticate with Caprover
4. The new token is stored and used for subsequent requests
5. Tokens are typically valid for 24 hours, but the system refreshes them after 23 hours to be safe
This approach ensures that the integration continues to work even if tokens expire.
## Troubleshooting
### Authentication Issues
If you see authentication errors in the logs:
1. Verify that the `CAPROVER_EMAIL` and `CAPROVER_PASSWORD` are correct
2. Check that the user has sufficient permissions in Caprover
3. Ensure the Caprover instance is accessible at the URL specified in `CAPROVER_API_URL`
### API Connection Issues
If the API cannot connect to Caprover, check:
1. That the Caprover instance is running and accessible
2. That the `CAPROVER_API_URL` is correct
3. That there are no network issues between the API server and Caprover
### Domain Configuration Issues
If domains are not being properly configured in Caprover:
1. Check that the domain has proper DNS records pointing to your server
2. Verify that the app name specified in `CAPROVER_APP_NAME` exists in Caprover
3. Ensure the Caprover user associated with the credentials has sufficient permissions
## Manual Configuration
If automatic configuration fails, you can manually configure domains using the admin endpoint:
```
POST /business/domain/configure-caprover
```
This endpoint requires authentication and a business with a verified domain.
@@ -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)}`);