feat(deploy): add DNS verification with modal before proceeding to next step

Add standalone DNS check endpoint and verify DNS on Next click in the
deploy wizard (step 2). Show error modal with CNAME instructions when
DNS is not configured instead of disabling the Next button. Also make
the custom domain clickable in the app detail header.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
keyhan
2026-05-14 01:20:57 +03:30
parent e8be7fbf03
commit 583c4c4c90
5 changed files with 320 additions and 29 deletions
@@ -21,7 +21,7 @@ import { FileInterceptor } from '@nestjs/platform-express';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiConsumes } from '@nestjs/swagger';
import { ApplicationsService } from './applications.service';
import { DomainService } from './domain.service';
import { CreateApplicationDto, UpdateApplicationDto, ScaleResourcesDto, SetCustomDomainDto } from './dto/application.dto';
import { CreateApplicationDto, UpdateApplicationDto, ScaleResourcesDto, SetCustomDomainDto, CheckDnsDto } from './dto/application.dto';
import { RolesGuard } from '../common/guards/roles.guard';
import { Roles } from '../common/decorators/roles.decorator';
import { UserRole, DatabaseType } from '../common/enums';
@@ -267,6 +267,12 @@ export class ApplicationsController {
// ── Custom Domain ─────────────────────────────────────────────
@Post('domain/check-dns')
@ApiOperation({ summary: 'Standalone DNS check (no app needed — for deploy wizard)' })
async checkDnsStandalone(@Body() dto: CheckDnsDto) {
return this.domainService.checkDnsStandalone(dto.domain, dto.appName);
}
@Get(':id/domain')
@ApiOperation({ summary: 'Get custom domain info and DNS setup instructions' })
async getDomainInfo(@Param('id') id: string, @Request() req: any) {
@@ -167,6 +167,68 @@ export class DomainService {
};
}
async checkDnsStandalone(
domain: string,
appName: string,
): Promise<{
verified: boolean;
message: string;
cnameTarget: string;
instructions: string[];
}> {
const platformDomain =
this.configService.get('platform.domain') || 'apps.cloudhost.ir';
const cnameTarget = await this.getPlatformCnameTarget();
const fullPlatformUrl = `${appName}.${platformDomain}`;
const instructions = [
`1. Log in to your domain registrar (e.g. Cloudflare, Namecheap, GoDaddy)`,
`2. Go to DNS management for your domain`,
`3. Add a CNAME record:`,
` - Name/Host: @ or www (depending on your domain)`,
` - Type: CNAME`,
` - Value/Target: ${fullPlatformUrl}`,
`4. If using a root domain (without www), some registrars support CNAME flattening (e.g. Cloudflare). Otherwise use www.`,
`5. Wait 5-30 minutes for DNS propagation (may take up to 48 hours)`,
`6. Click the "Verify DNS" button`,
];
try {
const resolved = await this.resolveDomain(domain);
const isValid = resolved.some(
(r) =>
r === cnameTarget ||
r === fullPlatformUrl ||
r.endsWith(`.${cnameTarget}`),
);
if (isValid) {
return {
verified: true,
message:
'DNS verification successful! Your domain points to our servers.',
cnameTarget: fullPlatformUrl,
instructions,
};
}
return {
verified: false,
message: `DNS records do not point to ${fullPlatformUrl}. Please check your CNAME record and try again.`,
cnameTarget: fullPlatformUrl,
instructions,
};
} catch (err: any) {
return {
verified: false,
message: `Could not resolve DNS for ${domain}. Make sure the CNAME record is set and DNS has propagated (may take up to 48 hours).`,
cnameTarget: fullPlatformUrl,
instructions,
};
}
}
private async resolveDomain(domain: string): Promise<string[]> {
const resolver = new dns.promises.Resolver();
resolver.setServers(['8.8.8.8', '1.1.1.1']);
@@ -269,6 +269,19 @@ export class SetCustomDomainDto {
domain: string;
}
export class CheckDnsDto {
@ApiProperty({ example: 'www.example.com', description: 'The domain to check DNS for' })
@IsString()
@Matches(/^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/, {
message: 'Invalid domain format (e.g. example.com or www.example.com)',
})
domain: string;
@ApiProperty({ example: 'my-app', description: 'The application name (used to compute CNAME target)' })
@IsString()
appName: string;
}
export class ScaleResourcesDto {
@ApiPropertyOptional({ example: '100m' })
@IsOptional()