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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user