fix: the bug in the payment service
This commit is contained in:
@@ -2,6 +2,7 @@ import { EntityManager } from "@mikro-orm/postgresql";
|
||||
import { Seeder } from "@mikro-orm/seeder";
|
||||
import { Logger } from "@nestjs/common";
|
||||
|
||||
import { PaymentGatewaySeeder } from "./payment-gateway.seeder";
|
||||
import { RoleSeeder } from "./role.seeder";
|
||||
|
||||
export class IndexSeeder extends Seeder {
|
||||
@@ -10,6 +11,7 @@ export class IndexSeeder extends Seeder {
|
||||
async run(em: EntityManager): Promise<void> {
|
||||
this.logger.log("Starting seeding process...");
|
||||
await new RoleSeeder().run(em);
|
||||
await new PaymentGatewaySeeder().run(em);
|
||||
this.logger.log("Seeding process completed successfully.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { EntityManager, RequiredEntityData } from "@mikro-orm/postgresql";
|
||||
import { Seeder } from "@mikro-orm/seeder";
|
||||
import { Logger } from "@nestjs/common";
|
||||
|
||||
import { PaymentGateway } from "../../src/modules/payments/entities/payment-gateway.entity";
|
||||
import { GatewayEnum } from "../../src/modules/payments/enums/gateway.enum";
|
||||
|
||||
export class PaymentGatewaySeeder extends Seeder {
|
||||
private readonly logger = new Logger(PaymentGatewaySeeder.name);
|
||||
|
||||
async run(em: EntityManager): Promise<void> {
|
||||
const paymentGateways: RequiredEntityData<PaymentGateway>[] = [
|
||||
{
|
||||
name: GatewayEnum.ZARINPAL,
|
||||
nameFa: "زرین پال",
|
||||
logoUrl: "https://storage.danakcorp.com/logo/fa-dark.png",
|
||||
description: "درگاه پرداخت زرین پال",
|
||||
},
|
||||
];
|
||||
|
||||
for (const paymentGateway of paymentGateways) {
|
||||
const existingPaymentGateway = await em.findOne(PaymentGateway, { name: paymentGateway.name });
|
||||
|
||||
if (!existingPaymentGateway) {
|
||||
const createdPaymentGateway = em.create(PaymentGateway, paymentGateway);
|
||||
|
||||
await em.persistAndFlush(createdPaymentGateway);
|
||||
this.logger.log(`[PaymentGatewaySeeder] Created payment gateway: ${createdPaymentGateway.name}`);
|
||||
} else {
|
||||
this.logger.log(`[PaymentGatewaySeeder] Payment gateway already exists: ${paymentGateway.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,7 @@ export enum DomainVerificationMethod {
|
||||
export class UpdateBusinessDomainDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@Matches(/^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/, {
|
||||
message: "Domain must be a valid domain name (e.g., example.com)",
|
||||
})
|
||||
@Matches(/^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/, { message: "Domain must be a valid domain name (e.g., example.com)" })
|
||||
domain: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Body, Controller, Get, Param, Post } from "@nestjs/common";
|
||||
import { ApiOperation } from "@nestjs/swagger";
|
||||
|
||||
import { UpdateBusinessDomainDto, VerifyDomainDto } from "./DTO/business-domain.dto";
|
||||
import { BusinessSlugParamDto } from "./DTO/business-slug-param.dto";
|
||||
import { Business } from "./entities/business.entity";
|
||||
import { BusinessesService } from "./services/businesses.service";
|
||||
import { DomainVerificationService } from "./services/domain-verification.service";
|
||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||
import { BusinessDec } from "../../common/decorators/business.decorator";
|
||||
|
||||
@Controller("business")
|
||||
@@ -15,37 +17,74 @@ export class BusinessesController {
|
||||
) {}
|
||||
|
||||
@Get("slug/:slug")
|
||||
@ApiOperation({ summary: "Get business by slug" })
|
||||
async getBusinessBySlug(@Param() params: BusinessSlugParamDto) {
|
||||
return this.businessesService.getBusinessBySlug(params.slug);
|
||||
}
|
||||
|
||||
@Post()
|
||||
// Domain management endpoints
|
||||
@Post("domain")
|
||||
@ApiOperation({ summary: "Update business domain" })
|
||||
@AuthGuards()
|
||||
async updateBusinessDomain(@BusinessDec() business: Business, @Body() domainDto: UpdateBusinessDomainDto) {
|
||||
return this.domainVerificationService.updateBusinessDomain(business.id, domainDto);
|
||||
}
|
||||
|
||||
@Get("status")
|
||||
@Get("domain/status")
|
||||
@ApiOperation({ summary: "Get business domain status" })
|
||||
@AuthGuards()
|
||||
async getDomainStatus(@BusinessDec() business: Business) {
|
||||
return this.domainVerificationService.getDomainVerificationStatus(business.id);
|
||||
}
|
||||
|
||||
@Post("verify")
|
||||
@Post("domain/verify")
|
||||
@ApiOperation({ summary: "Initiate domain verification" })
|
||||
@AuthGuards()
|
||||
async verifyDomain(@BusinessDec() business: Business, @Body() verifyDto: VerifyDomainDto) {
|
||||
return this.domainVerificationService.initiateVerification(business.id, verifyDto);
|
||||
}
|
||||
|
||||
@Post("verify/check")
|
||||
@Post("domain/verify/check")
|
||||
@ApiOperation({ summary: "Check domain verification status" })
|
||||
@AuthGuards()
|
||||
async checkVerification(@BusinessDec() business: Business) {
|
||||
return this.domainVerificationService.verifyDomain(business.id);
|
||||
}
|
||||
|
||||
@Post("admin/:businessId/verify")
|
||||
// DNS-specific domain verification endpoints
|
||||
@Post("domain/set-domain")
|
||||
@ApiOperation({ summary: "Set domain and get verification instructions" })
|
||||
@AuthGuards()
|
||||
async setDomainAndGetInstructions(@BusinessDec() business: Business, @Body() domainDto: UpdateBusinessDomainDto) {
|
||||
return this.domainVerificationService.setDomainAndGetVerificationInstructions(business.id, domainDto.domain);
|
||||
}
|
||||
|
||||
@Get("domain/check-dns")
|
||||
@ApiOperation({ summary: "Check DNS verification" })
|
||||
@AuthGuards()
|
||||
async checkDnsVerification(@BusinessDec() business: Business) {
|
||||
return this.domainVerificationService.verifyDnsTxtRecord(business.id);
|
||||
}
|
||||
|
||||
// Admin domain management endpoints
|
||||
@Post("admin/:businessId/domain/verify")
|
||||
@ApiOperation({ summary: "Admin initiate domain verification" })
|
||||
@AuthGuards()
|
||||
async adminVerifyDomain(@Param("businessId") businessId: string, @Body() verifyDto: VerifyDomainDto) {
|
||||
return this.domainVerificationService.initiateVerification(businessId, verifyDto);
|
||||
}
|
||||
|
||||
@Post("admin/:businessId/verify/check")
|
||||
@Post("admin/:businessId/domain/verify/check")
|
||||
@ApiOperation({ summary: "Admin check domain verification status" })
|
||||
@AuthGuards()
|
||||
async adminCheckVerification(@Param("businessId") businessId: string) {
|
||||
return this.domainVerificationService.verifyDomain(businessId);
|
||||
}
|
||||
|
||||
@Post("admin/:businessId/domain/check-dns")
|
||||
@ApiOperation({ summary: "Admin check DNS verification" })
|
||||
@AuthGuards()
|
||||
async adminCheckDnsVerification(@Param("businessId") businessId: string) {
|
||||
return this.domainVerificationService.verifyDnsTxtRecord(businessId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,4 @@ export class BusinessesService {
|
||||
|
||||
return business;
|
||||
}
|
||||
|
||||
async flushChanges() {
|
||||
await this.businessRepository.getEntityManager().flush();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import * as crypto from "node:crypto";
|
||||
import * as dns from "node:dns";
|
||||
import crypto from "node:crypto";
|
||||
import dns from "node:dns";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { EntityManager } from "@mikro-orm/postgresql";
|
||||
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
||||
import axios from "axios";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { BusinessesService } from "./businesses.service";
|
||||
import { BusinessMessage } from "../../../common/enums/message.enum";
|
||||
@@ -13,8 +15,17 @@ import { DomainVerificationMethod, UpdateBusinessDomainDto, VerifyDomainDto } fr
|
||||
export class DomainVerificationService {
|
||||
private readonly logger = new Logger(DomainVerificationService.name);
|
||||
private readonly resolveTxt = promisify(dns.resolveTxt);
|
||||
private readonly resolveA = promisify(dns.resolve4);
|
||||
private readonly resolveCname = promisify(dns.resolveCname);
|
||||
|
||||
constructor(private readonly businessesService: BusinessesService) {}
|
||||
// The server IP or domain users should point their domain to
|
||||
private readonly serverIP = "IP_OF_YOUR_SERVER"; // Replace with actual IP
|
||||
private readonly serverDomain = "app.dzone.danakcorp.com"; // Replace with actual domain
|
||||
|
||||
constructor(
|
||||
private readonly businessesService: BusinessesService,
|
||||
private readonly em: EntityManager,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Update business domain
|
||||
@@ -41,13 +52,13 @@ export class DomainVerificationService {
|
||||
business.domainVerifiedAt = undefined;
|
||||
business.domainVerificationMethod = undefined;
|
||||
|
||||
await this.businessesService.flushChanges();
|
||||
await this.em.flush();
|
||||
|
||||
return {
|
||||
message: BusinessMessage.DOMAIN_UPDATED,
|
||||
domain: business.domain,
|
||||
isVerified: business.isDomainVerified,
|
||||
verificationMethods: this.getVerificationInstructions(business.domain, verificationToken),
|
||||
dnsInstructions: this.getDomainSetupInstructions(business.domain, verificationToken),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -73,12 +84,12 @@ export class DomainVerificationService {
|
||||
// If no token exists, generate one
|
||||
if (!business.domainVerificationToken) {
|
||||
business.domainVerificationToken = this.generateVerificationToken();
|
||||
await this.businessesService.flushChanges();
|
||||
await this.em.flush();
|
||||
}
|
||||
|
||||
// Update verification method
|
||||
business.domainVerificationMethod = verifyDto.method;
|
||||
await this.businessesService.flushChanges();
|
||||
await this.em.flush();
|
||||
|
||||
return {
|
||||
message: BusinessMessage.DOMAIN_VERIFICATION_INITIATED,
|
||||
@@ -88,6 +99,44 @@ export class DomainVerificationService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set domain and get verification and connection instructions
|
||||
*/
|
||||
async setDomainAndGetVerificationInstructions(businessId: string, domain: string) {
|
||||
const business = await this.businessesService.getBusinessById(businessId);
|
||||
|
||||
// Domain hasn't changed, check if it's already verified
|
||||
if (business.domain === domain && business.isDomainVerified) {
|
||||
return {
|
||||
message: BusinessMessage.DOMAIN_ALREADY_VERIFIED,
|
||||
domain: business.domain,
|
||||
isVerified: business.isDomainVerified,
|
||||
dnsInstructions: business.domainVerificationToken
|
||||
? this.getDomainSetupInstructions(domain, business.domainVerificationToken)
|
||||
: this.getDomainSetupInstructions(domain, ""),
|
||||
};
|
||||
}
|
||||
|
||||
// Generate verification token
|
||||
const verificationToken = this.generateVerificationToken();
|
||||
|
||||
// Update the domain and verification data
|
||||
business.domain = domain;
|
||||
business.isDomainVerified = false;
|
||||
business.domainVerificationToken = verificationToken;
|
||||
business.domainVerifiedAt = undefined;
|
||||
|
||||
await this.em.flush();
|
||||
|
||||
// Return DNS verification and setup instructions
|
||||
return {
|
||||
message: BusinessMessage.DOMAIN_VERIFICATION_INITIATED,
|
||||
domain: business.domain,
|
||||
isVerified: false,
|
||||
dnsInstructions: this.getDomainSetupInstructions(domain, verificationToken),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check domain verification status
|
||||
*/
|
||||
@@ -112,7 +161,7 @@ export class DomainVerificationService {
|
||||
|
||||
switch (business.domainVerificationMethod) {
|
||||
case DomainVerificationMethod.DNS_TXT:
|
||||
isVerified = await this.verifyDnsTxtRecord(business.domain, business.domainVerificationToken);
|
||||
isVerified = await this.checkDnsTxtRecord(business.domain, business.domainVerificationToken);
|
||||
break;
|
||||
case DomainVerificationMethod.FILE:
|
||||
isVerified = await this.verifyFileMethod(business.domain, business.domainVerificationToken);
|
||||
@@ -126,8 +175,8 @@ export class DomainVerificationService {
|
||||
|
||||
if (isVerified) {
|
||||
business.isDomainVerified = true;
|
||||
business.domainVerifiedAt = new Date();
|
||||
await this.businessesService.flushChanges();
|
||||
business.domainVerifiedAt = dayjs().toDate();
|
||||
await this.em.flush();
|
||||
|
||||
return {
|
||||
message: BusinessMessage.DOMAIN_VERIFICATION_SUCCESS,
|
||||
@@ -150,6 +199,82 @@ export class DomainVerificationService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify DNS TXT record and domain pointing
|
||||
*/
|
||||
async verifyDnsTxtRecord(businessId: string) {
|
||||
const business = await this.businessesService.getBusinessById(businessId);
|
||||
|
||||
if (!business.domain || !business.domainVerificationToken) {
|
||||
throw new BadRequestException(BusinessMessage.DOMAIN_INVALID);
|
||||
}
|
||||
|
||||
if (business.isDomainVerified) {
|
||||
const domainPointsToServer = await this.checkDomainPointsToServer(business.domain);
|
||||
|
||||
return {
|
||||
message: BusinessMessage.DOMAIN_ALREADY_VERIFIED,
|
||||
domain: business.domain,
|
||||
isVerified: true,
|
||||
verifiedAt: business.domainVerifiedAt,
|
||||
domainPointsToServer,
|
||||
serverConnectionStatus: domainPointsToServer
|
||||
? "Your domain is properly set up and pointing to our servers"
|
||||
: "Your domain is verified, but not properly pointing to our servers",
|
||||
setupInstructions: !domainPointsToServer ? this.getDomainConnectionInstructions(business.domain) : null,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// First check domain ownership verification via TXT record
|
||||
const isVerified = await this.checkDnsTxtRecord(business.domain, business.domainVerificationToken);
|
||||
|
||||
// Then check if domain points to our server (A or CNAME record)
|
||||
const domainPointsToServer = await this.checkDomainPointsToServer(business.domain);
|
||||
|
||||
// If TXT verification passes, mark the domain as verified regardless of A/CNAME
|
||||
if (isVerified) {
|
||||
// Update domain verification status
|
||||
business.isDomainVerified = true;
|
||||
business.domainVerifiedAt = dayjs().toDate();
|
||||
await this.em.flush();
|
||||
|
||||
return {
|
||||
message: BusinessMessage.DOMAIN_VERIFICATION_SUCCESS,
|
||||
domain: business.domain,
|
||||
isVerified: true,
|
||||
verifiedAt: business.domainVerifiedAt,
|
||||
domainPointsToServer,
|
||||
serverConnectionStatus: domainPointsToServer
|
||||
? "Your domain is properly set up and pointing to our servers"
|
||||
: "Your domain is verified, but not properly pointing to our servers",
|
||||
connectionInstructions: !domainPointsToServer ? this.getDomainConnectionInstructions(business.domain) : null,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
message: BusinessMessage.DOMAIN_VERIFICATION_FAILED,
|
||||
domain: business.domain,
|
||||
isVerified: false,
|
||||
domainPointsToServer,
|
||||
serverConnectionStatus: domainPointsToServer
|
||||
? "Your domain is pointing to our servers, but ownership verification failed"
|
||||
: "Both ownership verification and server connection failed",
|
||||
setupInstructions: this.getDomainSetupInstructions(business.domain, business.domainVerificationToken || ""),
|
||||
};
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(`Domain verification failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
|
||||
return {
|
||||
message: BusinessMessage.DOMAIN_VERIFICATION_FAILED,
|
||||
domain: business.domain,
|
||||
isVerified: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
setupInstructions: this.getDomainSetupInstructions(business.domain, business.domainVerificationToken || ""),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get domain verification status
|
||||
*/
|
||||
@@ -162,13 +287,139 @@ export class DomainVerificationService {
|
||||
};
|
||||
}
|
||||
|
||||
// Check if domain points to our server (A or CNAME record)
|
||||
const domainPointsToServer = await this.checkDomainPointsToServer(business.domain);
|
||||
|
||||
return {
|
||||
hasDomain: true,
|
||||
domain: business.domain,
|
||||
isVerified: business.isDomainVerified,
|
||||
verificationMethod: business.domainVerificationMethod,
|
||||
verifiedAt: business.domainVerifiedAt,
|
||||
instructions: business.domainVerificationToken ? this.getVerificationInstructions(business.domain, business.domainVerificationToken) : null,
|
||||
domainPointsToServer,
|
||||
verificationStatus: business.isDomainVerified ? "Verified" : "Not Verified",
|
||||
connectionStatus: domainPointsToServer ? "Connected" : "Not Connected",
|
||||
fullSetupComplete: business.isDomainVerified && domainPointsToServer,
|
||||
connectionInstructions: !domainPointsToServer ? this.getDomainConnectionInstructions(business.domain) : null,
|
||||
setupInstructions: !business.isDomainVerified ? this.getDomainSetupInstructions(business.domain, business.domainVerificationToken || "") : null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify domain using file method
|
||||
*/
|
||||
private async verifyFileMethod(domain: string, token: string): Promise<boolean> {
|
||||
try {
|
||||
// Try HTTPS first
|
||||
try {
|
||||
const response = await axios.get(`https://${domain}/.well-known/${token}.html`, {
|
||||
timeout: 5000, // 5 seconds timeout
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (compatible; DzoneVerification/1.0; +https://dzone.danakcorp.com/domain-verification)",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 200 && response.data.toString().trim() === token.trim()) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.debug(`HTTPS file verification failed, trying HTTP: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
|
||||
// Try HTTP if HTTPS fails
|
||||
const httpResponse = await axios.get(`http://${domain}/.well-known/${token}.html`, {
|
||||
timeout: 5000,
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (compatible; DzoneVerification/1.0; +https://dzone.danakcorp.com/domain-verification)",
|
||||
},
|
||||
});
|
||||
|
||||
if (httpResponse.status === 200 && httpResponse.data.toString().trim() === token.trim()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(`Error verifying file method: ${error instanceof Error ? error.message : String(error)}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify domain using meta tag method
|
||||
*/
|
||||
private async verifyMetaTagMethod(domain: string, token: string): Promise<boolean> {
|
||||
try {
|
||||
// Try HTTPS first
|
||||
try {
|
||||
const response = await axios.get(`https://${domain}`, {
|
||||
timeout: 5000, // 5 seconds timeout
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (compatible; DzoneVerification/1.0; +https://dzone.danakcorp.com/domain-verification)",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
// Check for meta tag in the HTML
|
||||
const content = response.data.toString();
|
||||
const metaTagRegex = new RegExp(`<meta[^>]*name=["']dzone-verification["'][^>]*content=["']${token}["']`, "i");
|
||||
if (metaTagRegex.test(content)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.debug(`HTTPS meta tag verification failed, trying HTTP: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
|
||||
// Try HTTP if HTTPS fails
|
||||
const httpResponse = await axios.get(`http://${domain}`, {
|
||||
timeout: 5000,
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (compatible; DzoneVerification/1.0; +https://dzone.danakcorp.com/domain-verification)",
|
||||
},
|
||||
});
|
||||
|
||||
if (httpResponse.status === 200) {
|
||||
// Check for meta tag in the HTML
|
||||
const content = httpResponse.data.toString();
|
||||
const metaTagRegex = new RegExp(`<meta[^>]*name=["']dzone-verification["'][^>]*content=["']${token}["']`, "i");
|
||||
return metaTagRegex.test(content);
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(`Error verifying meta tag method: ${error instanceof Error ? error.message : String(error)}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get domain verification and connection status
|
||||
*/
|
||||
async getDomainStatus(businessId: string) {
|
||||
const business = await this.businessesService.getBusinessById(businessId);
|
||||
|
||||
if (!business.domain) {
|
||||
return {
|
||||
hasDomain: false,
|
||||
message: "No domain set for this business",
|
||||
};
|
||||
}
|
||||
|
||||
const domainPointsToServer = await this.checkDomainPointsToServer(business.domain);
|
||||
|
||||
return {
|
||||
hasDomain: true,
|
||||
domain: business.domain,
|
||||
isVerified: business.isDomainVerified,
|
||||
verifiedAt: business.domainVerifiedAt,
|
||||
domainPointsToServer,
|
||||
verificationStatus: business.isDomainVerified ? "Verified" : "Not Verified",
|
||||
connectionStatus: domainPointsToServer ? "Connected" : "Not Connected",
|
||||
fullSetupComplete: business.isDomainVerified && domainPointsToServer,
|
||||
setupInstructions:
|
||||
!business.isDomainVerified || !domainPointsToServer
|
||||
? this.getDomainSetupInstructions(business.domain, business.domainVerificationToken || "")
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -206,14 +457,86 @@ export class DomainVerificationService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get domain connection instructions
|
||||
*/
|
||||
private getDomainConnectionInstructions(_domain: string) {
|
||||
// The domain parameter is used for contextual purposes in some implementations
|
||||
// but not directly in this template
|
||||
return {
|
||||
title: "Point your domain to our servers",
|
||||
description: "Choose ONE of these options to connect your domain:",
|
||||
options: [
|
||||
{
|
||||
title: "Option A: Using A Record (Direct IP)",
|
||||
steps: [
|
||||
"1. Add an A record in your DNS settings",
|
||||
"2. Set the host to @ or your subdomain (e.g., www)",
|
||||
"3. Set the value to our server IP: " + this.serverIP,
|
||||
"4. Set TTL to 3600 (1 hour) or the lowest available",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Option B: Using CNAME Record (Recommended)",
|
||||
steps: [
|
||||
"1. Add a CNAME record in your DNS settings",
|
||||
"2. Set the host to @ or your subdomain (e.g., www)",
|
||||
"3. Set the value to: " + this.serverDomain,
|
||||
"4. Set TTL to 3600 (1 hour) or the lowest available",
|
||||
"Note: Some DNS providers don't allow CNAME for root domains (@)",
|
||||
],
|
||||
},
|
||||
],
|
||||
troubleshooting: [
|
||||
"- Make sure you've added the records to the correct domain",
|
||||
"- Some DNS providers require the @ symbol for root domains",
|
||||
"- Check that you've copied the values exactly as shown",
|
||||
"- DNS changes can take time to propagate (typically 15 minutes to 48 hours)",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comprehensive domain setup instructions
|
||||
*/
|
||||
private getDomainSetupInstructions(domain: string, token: string) {
|
||||
return {
|
||||
verificationInstructions: {
|
||||
title: "Step 1: Verify domain ownership",
|
||||
description: "Add this TXT record to prove you own the domain:",
|
||||
host: "@", // Root domain
|
||||
type: "TXT", // Record type
|
||||
value: token, // Token value
|
||||
ttl: "3600", // Time to live (1 hour)
|
||||
},
|
||||
connectionInstructions: this.getDomainConnectionInstructions(domain),
|
||||
general: {
|
||||
verificationEndpoint: `/business/domain/verify/check-dns`,
|
||||
propagationNote: "DNS changes can take 15 minutes to 48 hours to propagate worldwide",
|
||||
troubleshooting: [
|
||||
"- Make sure you've added the records to the correct domain",
|
||||
"- Some DNS providers require the @ symbol for root domains",
|
||||
"- Check that you've copied the values exactly as shown",
|
||||
"- Wait at least 15 minutes before checking verification status",
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify domain using DNS TXT record
|
||||
*/
|
||||
private async verifyDnsTxtRecord(domain: string, token: string): Promise<boolean> {
|
||||
private async checkDnsTxtRecord(domain: string, token: string): Promise<boolean> {
|
||||
try {
|
||||
this.logger.log(`Checking TXT records for domain: ${domain}`);
|
||||
|
||||
// Try to resolve TXT records
|
||||
const txtRecords = await this.resolveTxt(domain);
|
||||
|
||||
// Flatten the results and check if any match our token
|
||||
const records = txtRecords.flat();
|
||||
this.logger.log(`Found TXT records: ${JSON.stringify(records)}`);
|
||||
|
||||
return records.some((record) => record === token);
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(`Error verifying DNS TXT record: ${error instanceof Error ? error.message : String(error)}`);
|
||||
@@ -222,89 +545,42 @@ export class DomainVerificationService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify domain using file method
|
||||
* Check if domain points to our server via A or CNAME record
|
||||
*/
|
||||
private async verifyFileMethod(domain: string, token: string): Promise<boolean> {
|
||||
private async checkDomainPointsToServer(domain: string): Promise<boolean> {
|
||||
try {
|
||||
// Try HTTPS first
|
||||
// Try CNAME first
|
||||
try {
|
||||
const response = await axios.get(`https://${domain}/.well-known/${token}.html`, {
|
||||
timeout: 5000, // 5 seconds timeout
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (compatible; DzoneVerification/1.0; +https://dzone.com/domain-verification)",
|
||||
},
|
||||
});
|
||||
const cnameRecords = await this.resolveCname(domain);
|
||||
|
||||
if (response.status === 200 && response.data.toString().trim() === token.trim()) {
|
||||
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)) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.debug(`HTTPS file verification failed, trying HTTP: ${error instanceof Error ? error.message : String(error)}`);
|
||||
} catch (_error) {
|
||||
this.logger.debug(`No CNAME records found for ${domain}, trying A records`);
|
||||
}
|
||||
|
||||
// Try HTTP if HTTPS fails
|
||||
const httpResponse = await axios.get(`http://${domain}/.well-known/${token}.html`, {
|
||||
timeout: 5000,
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (compatible; DzoneVerification/1.0; +https://dzone.com/domain-verification)",
|
||||
},
|
||||
});
|
||||
|
||||
if (httpResponse.status === 200 && httpResponse.data.toString().trim() === token.trim()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(`Error verifying file method: ${error instanceof Error ? error.message : String(error)}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify domain using meta tag method
|
||||
*/
|
||||
private async verifyMetaTagMethod(domain: string, token: string): Promise<boolean> {
|
||||
try {
|
||||
// Try HTTPS first
|
||||
// Try A record
|
||||
try {
|
||||
const response = await axios.get(`https://${domain}`, {
|
||||
timeout: 5000, // 5 seconds timeout
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (compatible; DzoneVerification/1.0; +https://dzone.com/domain-verification)",
|
||||
},
|
||||
});
|
||||
const aRecords = await this.resolveA(domain);
|
||||
|
||||
if (response.status === 200) {
|
||||
// Check for meta tag in the HTML
|
||||
const content = response.data.toString();
|
||||
const metaTagRegex = new RegExp(`<meta[^>]*name=["']dzone-verification["'][^>]*content=["']${token}["']`, "i");
|
||||
if (metaTagRegex.test(content)) {
|
||||
return true;
|
||||
}
|
||||
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)) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.debug(`HTTPS meta tag verification failed, trying HTTP: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
|
||||
// Try HTTP if HTTPS fails
|
||||
const httpResponse = await axios.get(`http://${domain}`, {
|
||||
timeout: 5000,
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (compatible; DzoneVerification/1.0; +https://dzone.com/domain-verification)",
|
||||
},
|
||||
});
|
||||
|
||||
if (httpResponse.status === 200) {
|
||||
// Check for meta tag in the HTML
|
||||
const content = httpResponse.data.toString();
|
||||
const metaTagRegex = new RegExp(`<meta[^>]*name=["']dzone-verification["'][^>]*content=["']${token}["']`, "i");
|
||||
return metaTagRegex.test(content);
|
||||
} catch (_error) {
|
||||
this.logger.debug(`No A records found for ${domain}`);
|
||||
}
|
||||
|
||||
// If we get here, neither CNAME nor A records point to our server
|
||||
return false;
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(`Error verifying meta tag method: ${error instanceof Error ? error.message : String(error)}`);
|
||||
this.logger.error(`Error checking domain DNS records: ${error instanceof Error ? error.message : String(error)}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import { PaymentMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class CheckoutPaymentDto {
|
||||
@IsNotEmpty({ message: PaymentMessage.GATEWAY_REQUIRED })
|
||||
@IsUUID("4", { message: PaymentMessage.GATEWAY_ID_SHOULD_BE_UUID })
|
||||
@IsUUID("7", { message: PaymentMessage.GATEWAY_ID_SHOULD_BE_UUID })
|
||||
@ApiProperty({ description: "Gateway id to pay", example: "e6fdce2a-9f91-47c4-8561-48368fc275b5" })
|
||||
gatewayId: string;
|
||||
|
||||
@IsNotEmpty({ message: PaymentMessage.INVOICE_ID_REQUIRED })
|
||||
@IsUUID("4", { message: PaymentMessage.INVOICE_ID_SHOULD_BE_UUID })
|
||||
@IsUUID("7", { message: PaymentMessage.INVOICE_ID_SHOULD_BE_UUID })
|
||||
@ApiProperty({ description: "Invoice id to pay", example: "e6fdce2a-9f91-47c4-8561-48368fc275b5" })
|
||||
invoiceId: string;
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ export class PaymentProcessor extends WorkerProcessor {
|
||||
const verifyData = await gateway.verifyPayment({
|
||||
reference: payment.reference,
|
||||
amount: payment.amount,
|
||||
merchantId: payment.business.zarinpalMerchantId || this.zarinpalMerchantId,
|
||||
merchantId: payment.business.zarinpalMerchantId ?? this.zarinpalMerchantId,
|
||||
});
|
||||
|
||||
const result = await this.paymentsService.processVerificationResult(verifyData, payment, em);
|
||||
|
||||
@@ -58,17 +58,17 @@ export class PaymentsService {
|
||||
if (invoice.company.user.id !== user.id) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID_OR_NOT_BELONG_TO_USER);
|
||||
|
||||
const gatewayData = await this.processPayment(paymentGateway.name, {
|
||||
amount: invoice.totalPrice.toNumber(),
|
||||
amount: new Decimal(invoice.totalPrice).toNumber(),
|
||||
description: PaymentMessage.CHECKOUT_INVOICE_MESSAGE.replace("[invoiceNumber]", invoice.numericId.toString()),
|
||||
email: user.email,
|
||||
mobile: user.phone,
|
||||
callBackPath: invoice.business.slug,
|
||||
merchantId: invoice.business.zarinpalMerchantId || this.zarinpalMerchantId,
|
||||
merchantId: invoice.business.zarinpalMerchantId ?? this.zarinpalMerchantId,
|
||||
});
|
||||
|
||||
const payment = await this.createGatewayPaymentForUser(
|
||||
user,
|
||||
invoice.totalPrice.toNumber(),
|
||||
new Decimal(invoice.totalPrice).toNumber(),
|
||||
gatewayData.reference,
|
||||
paymentGateway.id,
|
||||
invoice.business.id,
|
||||
@@ -84,6 +84,7 @@ export class PaymentsService {
|
||||
} catch (error) {
|
||||
await em.rollback();
|
||||
if (error instanceof HttpException) throw error;
|
||||
this.logger.error("Payment checkout failed:", error);
|
||||
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_PAYMENT);
|
||||
}
|
||||
}
|
||||
@@ -113,7 +114,7 @@ export class PaymentsService {
|
||||
const verifyData = await paymentGateway.verifyPayment({
|
||||
reference: queryDto.Authority,
|
||||
amount: payment.amount,
|
||||
merchantId: payment.business.zarinpalMerchantId || this.zarinpalMerchantId,
|
||||
merchantId: payment.business.zarinpalMerchantId ?? this.zarinpalMerchantId,
|
||||
});
|
||||
|
||||
return await this.handlePaymentVerificationWithResponse(verifyData, payment, rep, frontUrl, em);
|
||||
@@ -289,8 +290,12 @@ export class PaymentsService {
|
||||
//===============================================
|
||||
|
||||
private async getPaymentByReference(reference: string, em: EntityManager): Promise<Payment> {
|
||||
const payment = await em.findOne(Payment, { reference }, { populate: ["user", "invoice", "business"], lockMode: LockMode.PESSIMISTIC_WRITE });
|
||||
// First get the payment with a lock, but without joins
|
||||
const payment = await em.findOne(Payment, { reference }, { lockMode: LockMode.PESSIMISTIC_WRITE });
|
||||
if (!payment) throw new BadRequestException(PaymentMessage.PAYMENT_NOT_FOUND_WITH_REF);
|
||||
|
||||
// Then populate relations after acquiring the lock
|
||||
await em.populate(payment, ["user", "invoice", "business"]);
|
||||
return payment;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@ import { ApiBody, ApiConsumes, ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
|
||||
import { UploadMultipleFileDto, UploadSingleFileDto } from "./DTO/upload-file.dto";
|
||||
import { UploaderService } from "./uploader.service";
|
||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||
import { IFile } from "../utils/interfaces/IFile";
|
||||
|
||||
@ApiTags("Uploader")
|
||||
@Controller("uploader")
|
||||
// @AuthGuards()
|
||||
@AuthGuards()
|
||||
export class UploaderController {
|
||||
constructor(private readonly uploaderService: UploaderService) {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user