From 9b3b5b41d7e7a7166e77050d7a386bbee4ee6823 Mon Sep 17 00:00:00 2001 From: Mahyargdz Date: Wed, 21 May 2025 10:06:29 +0330 Subject: [PATCH] fix: the bug in the payment service --- database/seeders/index.seeder.ts | 2 + database/seeders/payment-gateway.seeder.ts | 34 ++ .../businesses/DTO/business-domain.dto.ts | 4 +- .../businesses/businesses.controller.ts | 51 +- .../businesses/services/businesses.service.ts | 4 - .../services/domain-verification.service.ts | 438 ++++++++++++++---- .../payments/DTO/checkout-payment.dto.ts | 4 +- .../payments/queue/payment.processor.ts | 2 +- .../payments/services/payments.service.ts | 15 +- src/modules/uploader/uploader.controller.ts | 3 +- 10 files changed, 454 insertions(+), 103 deletions(-) create mode 100644 database/seeders/payment-gateway.seeder.ts diff --git a/database/seeders/index.seeder.ts b/database/seeders/index.seeder.ts index 77d3736..74153a2 100644 --- a/database/seeders/index.seeder.ts +++ b/database/seeders/index.seeder.ts @@ -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 { this.logger.log("Starting seeding process..."); await new RoleSeeder().run(em); + await new PaymentGatewaySeeder().run(em); this.logger.log("Seeding process completed successfully."); } } diff --git a/database/seeders/payment-gateway.seeder.ts b/database/seeders/payment-gateway.seeder.ts new file mode 100644 index 0000000..6d9ecc7 --- /dev/null +++ b/database/seeders/payment-gateway.seeder.ts @@ -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 { + const paymentGateways: RequiredEntityData[] = [ + { + 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}`); + } + } + } +} diff --git a/src/modules/businesses/DTO/business-domain.dto.ts b/src/modules/businesses/DTO/business-domain.dto.ts index 3fbf957..24b81cd 100644 --- a/src/modules/businesses/DTO/business-domain.dto.ts +++ b/src/modules/businesses/DTO/business-domain.dto.ts @@ -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; } diff --git a/src/modules/businesses/businesses.controller.ts b/src/modules/businesses/businesses.controller.ts index 9ca4653..2e3b2ef 100644 --- a/src/modules/businesses/businesses.controller.ts +++ b/src/modules/businesses/businesses.controller.ts @@ -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); + } } diff --git a/src/modules/businesses/services/businesses.service.ts b/src/modules/businesses/services/businesses.service.ts index 578911b..4b77c69 100644 --- a/src/modules/businesses/services/businesses.service.ts +++ b/src/modules/businesses/services/businesses.service.ts @@ -27,8 +27,4 @@ export class BusinessesService { return business; } - - async flushChanges() { - await this.businessRepository.getEntityManager().flush(); - } } diff --git a/src/modules/businesses/services/domain-verification.service.ts b/src/modules/businesses/services/domain-verification.service.ts index f5a24a3..3ff1342 100644 --- a/src/modules/businesses/services/domain-verification.service.ts +++ b/src/modules/businesses/services/domain-verification.service.ts @@ -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 { + 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 { + 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(`]*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(`]*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 { + private async checkDnsTxtRecord(domain: string, token: string): Promise { 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 { + private async checkDomainPointsToServer(domain: string): Promise { 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 { - 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(`]*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(`]*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; } } diff --git a/src/modules/payments/DTO/checkout-payment.dto.ts b/src/modules/payments/DTO/checkout-payment.dto.ts index 6abdcd6..6a6066e 100755 --- a/src/modules/payments/DTO/checkout-payment.dto.ts +++ b/src/modules/payments/DTO/checkout-payment.dto.ts @@ -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; } diff --git a/src/modules/payments/queue/payment.processor.ts b/src/modules/payments/queue/payment.processor.ts index a28c0e4..a153eea 100755 --- a/src/modules/payments/queue/payment.processor.ts +++ b/src/modules/payments/queue/payment.processor.ts @@ -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); diff --git a/src/modules/payments/services/payments.service.ts b/src/modules/payments/services/payments.service.ts index 7d108c0..58431f2 100644 --- a/src/modules/payments/services/payments.service.ts +++ b/src/modules/payments/services/payments.service.ts @@ -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 { - 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; } diff --git a/src/modules/uploader/uploader.controller.ts b/src/modules/uploader/uploader.controller.ts index 40c0f36..2e19b2e 100755 --- a/src/modules/uploader/uploader.controller.ts +++ b/src/modules/uploader/uploader.controller.ts @@ -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) {}