From 8609d8d4fd339df605cad436b12fb7740a1ae0f9 Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Thu, 20 Aug 2026 11:50:57 +0330 Subject: [PATCH] Make Arvan DNS updates opt-in and persist English user names. Domain add/edit only upserts Arvan records when updateDns is true, and super-admin user PATCH now accepts firstNameEn and lastNameEn. Co-authored-by: Cursor --- src/business-admin/business-admin.service.ts | 4 +-- src/business-admin/dto/add-domain.dto.ts | 5 +++ src/business-admin/dto/update-domain.dto.ts | 7 +++- src/domain-admin/domain-admin.module.ts | 3 +- src/domain-admin/domain-admin.service.ts | 33 ++++++++++++++++++- .../dto/update-domain-admin.dto.ts | 7 +++- src/users/dto/update-user.dto.ts | 24 +++++++++++++- src/users/users.service.ts | 13 ++++++++ 8 files changed, 89 insertions(+), 7 deletions(-) diff --git a/src/business-admin/business-admin.service.ts b/src/business-admin/business-admin.service.ts index 79c98a7..3ed9753 100644 --- a/src/business-admin/business-admin.service.ts +++ b/src/business-admin/business-admin.service.ts @@ -548,7 +548,7 @@ export class BusinessAdminService { }, }); - const dnsError = await this.applyArvanDns(host); + const dnsError = dto.updateDns ? await this.applyArvanDns(host) : null; return { ...domain, @@ -648,7 +648,7 @@ export class BusinessAdminService { }, }); - const dnsError = await this.applyArvanDns(host); + const dnsError = dto.updateDns ? await this.applyArvanDns(host) : null; return { ...updated, diff --git a/src/business-admin/dto/add-domain.dto.ts b/src/business-admin/dto/add-domain.dto.ts index 34e5534..f18d0a8 100644 --- a/src/business-admin/dto/add-domain.dto.ts +++ b/src/business-admin/dto/add-domain.dto.ts @@ -10,6 +10,11 @@ export class AddDomainDto { @IsBoolean() isPrimary?: boolean; + /** When true, upsert Meshkee tenant records in Arvan DNS. */ + @IsOptional() + @IsBoolean() + updateDns?: boolean; + /** HTTPS or SSH git URL — when set, provisions storefront deploy on the websites VM. */ @IsOptional() @IsString() diff --git a/src/business-admin/dto/update-domain.dto.ts b/src/business-admin/dto/update-domain.dto.ts index 227080f..ebcabd6 100644 --- a/src/business-admin/dto/update-domain.dto.ts +++ b/src/business-admin/dto/update-domain.dto.ts @@ -1,4 +1,4 @@ -import { IsOptional, IsString, Matches, MinLength } from 'class-validator'; +import { IsBoolean, IsOptional, IsString, Matches, MinLength } from 'class-validator'; import { GIT_REPO_URL_RE } from '../../website-deploy/website-deploy.util'; export class UpdateDomainDto { @@ -6,6 +6,11 @@ export class UpdateDomainDto { @MinLength(1) host!: string; + /** When true, upsert Meshkee tenant records in Arvan DNS. */ + @IsOptional() + @IsBoolean() + updateDns?: boolean; + /** HTTPS or SSH git URL — when set, provisions storefront deploy on the websites VM. */ @IsOptional() @IsString() diff --git a/src/domain-admin/domain-admin.module.ts b/src/domain-admin/domain-admin.module.ts index 001c62b..35b0faf 100644 --- a/src/domain-admin/domain-admin.module.ts +++ b/src/domain-admin/domain-admin.module.ts @@ -1,12 +1,13 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AuthModule } from '../auth/auth.module'; +import { ArvanDnsModule } from '../arvan-dns/arvan-dns.module'; import { WebsiteDeployModule } from '../website-deploy/website-deploy.module'; import { DomainAdminController } from './domain-admin.controller'; import { DomainAdminService } from './domain-admin.service'; @Module({ - imports: [AuthModule, ConfigModule, WebsiteDeployModule], + imports: [AuthModule, ConfigModule, WebsiteDeployModule, ArvanDnsModule], controllers: [DomainAdminController], providers: [DomainAdminService], }) diff --git a/src/domain-admin/domain-admin.service.ts b/src/domain-admin/domain-admin.service.ts index 93ef445..8145b14 100644 --- a/src/domain-admin/domain-admin.service.ts +++ b/src/domain-admin/domain-admin.service.ts @@ -2,12 +2,14 @@ import { BadRequestException, ConflictException, ForbiddenException, + HttpException, Injectable, NotFoundException, ServiceUnavailableException, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Prisma } from '@prisma/client'; +import { ArvanDnsService } from '../arvan-dns/arvan-dns.service'; import { AuthUser } from '../auth/auth.types'; import { PermissionsService } from '../auth/permissions.service'; import { probeTlsHost } from '../common/tls-probe'; @@ -39,6 +41,7 @@ export class DomainAdminService { private readonly permissions: PermissionsService, private readonly config: ConfigService, private readonly websiteDeployAgent: WebsiteDeployAgentService, + private readonly arvanDns: ArvanDnsService, ) {} private async assertSuperAdmin(actor: AuthUser) { @@ -478,13 +481,41 @@ export class DomainAdminService { } } - return this.prisma.domain.update({ + const updated = await this.prisma.domain.update({ where: { id: domainId }, data: { host: dto.host?.trim(), expiresAt: dto.expiresAt !== undefined ? new Date(dto.expiresAt) : undefined, }, }); + + const host = updated.host; + const dnsError = dto.updateDns ? await this.applyArvanDns(host) : null; + + return { + ...updated, + dnsError, + }; + } + + private async applyArvanDns(host: string): Promise { + try { + await this.arvanDns.ensureTenantRecords(host); + return null; + } catch (err) { + if (err instanceof HttpException) { + const res = err.getResponse(); + if (typeof res === 'string') return res; + if (res && typeof res === 'object' && 'message' in res) { + const message = (res as { message: unknown }).message; + return Array.isArray(message) ? message.map(String).join(', ') : String(message); + } + } + if (err && typeof err === 'object' && 'message' in err && typeof err.message === 'string') { + return err.message; + } + return 'Arvan DNS update failed'; + } } async disable(domainIdRaw: string, dto: DisableDomainDto, actor: AuthUser) { diff --git a/src/domain-admin/dto/update-domain-admin.dto.ts b/src/domain-admin/dto/update-domain-admin.dto.ts index 6f8c163..a3dc6cc 100644 --- a/src/domain-admin/dto/update-domain-admin.dto.ts +++ b/src/domain-admin/dto/update-domain-admin.dto.ts @@ -1,4 +1,4 @@ -import { IsDateString, IsOptional, IsString, MinLength } from 'class-validator'; +import { IsBoolean, IsDateString, IsOptional, IsString, MinLength } from 'class-validator'; export class UpdateDomainAdminDto { @IsOptional() @@ -9,4 +9,9 @@ export class UpdateDomainAdminDto { @IsOptional() @IsDateString() expiresAt?: string; + + /** When true, upsert Meshkee tenant records in Arvan DNS. */ + @IsOptional() + @IsBoolean() + updateDns?: boolean; } diff --git a/src/users/dto/update-user.dto.ts b/src/users/dto/update-user.dto.ts index 23a7de0..b40dee5 100644 --- a/src/users/dto/update-user.dto.ts +++ b/src/users/dto/update-user.dto.ts @@ -1,16 +1,38 @@ -import { IsOptional, IsString, MinLength } from 'class-validator'; +import { + IsOptional, + IsString, + MaxLength, + MinLength, + ValidateIf, +} from 'class-validator'; export class UpdateUserDto { @IsOptional() @IsString() @MinLength(1) + @MaxLength(100) firstName?: string; @IsOptional() @IsString() @MinLength(1) + @MaxLength(100) lastName?: string; + @IsOptional() + @IsString() + @ValidateIf((_, value) => typeof value === 'string' && value.trim().length > 0) + @MinLength(2) + @MaxLength(100) + firstNameEn?: string; + + @IsOptional() + @IsString() + @ValidateIf((_, value) => typeof value === 'string' && value.trim().length > 0) + @MinLength(2) + @MaxLength(100) + lastNameEn?: string; + @IsOptional() @IsString() email?: string; diff --git a/src/users/users.service.ts b/src/users/users.service.ts index b3713cf..1ef923f 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -24,6 +24,8 @@ type UserListRow = { id: bigint; firstName: string | null; lastName: string | null; + firstNameEn: string | null; + lastNameEn: string | null; cellNumber: string; roles: string | null; roleSlug: string | null; @@ -171,7 +173,10 @@ export class UsersService { AND ( u.first_name ILIKE ${nameLike} OR u.last_name ILIKE ${nameLike} + OR u.first_name_en ILIKE ${nameLike} + OR u.last_name_en ILIKE ${nameLike} OR (COALESCE(u.first_name, '') || ' ' || COALESCE(u.last_name, '')) ILIKE ${nameLike} + OR (COALESCE(u.first_name_en, '') || ' ' || COALESCE(u.last_name_en, '')) ILIKE ${nameLike} ) ` : Prisma.empty} ${cellNumberIlikeFilter(query.cellNumber)} @@ -209,6 +214,8 @@ export class UsersService { u.id AS "id", u.first_name AS "firstName", u.last_name AS "lastName", + u.first_name_en AS "firstNameEn", + u.last_name_en AS "lastNameEn", u.cell_number AS "cellNumber", u.created_at AS "createdAt", u.is_active AS "isActive", @@ -464,6 +471,10 @@ export class UsersService { data: { firstName: dto.firstName?.trim(), lastName: dto.lastName?.trim(), + firstNameEn: + dto.firstNameEn !== undefined ? dto.firstNameEn.trim() || null : undefined, + lastNameEn: + dto.lastNameEn !== undefined ? dto.lastNameEn.trim() || null : undefined, email: dto.email !== undefined ? dto.email.trim() || null : undefined, cellNumber: dto.cellNumber?.trim(), }, @@ -473,6 +484,8 @@ export class UsersService { id: updated.id, firstName: updated.firstName, lastName: updated.lastName, + firstNameEn: updated.firstNameEn, + lastNameEn: updated.lastNameEn, email: updated.email, cellNumber: updated.cellNumber, };