feat: add custom domain support with SSL, DNS verification, and billing

Users can assign a custom domain to their app with automatic SSL via
cert-manager. Includes DNS verification flow (CNAME check), Persian
instructions, admin-configurable pricing via PlatformSetting, and
integration into the deploy wizard cost calculation.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
keyhan
2026-05-14 00:36:29 +03:30
parent d87b50c6a4
commit 435cf92817
18 changed files with 1082 additions and 89 deletions
@@ -20,7 +20,8 @@ import { AuthGuard } from '@nestjs/passport';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiConsumes } from '@nestjs/swagger';
import { ApplicationsService } from './applications.service';
import { CreateApplicationDto, UpdateApplicationDto, ScaleResourcesDto } from './dto/application.dto';
import { DomainService } from './domain.service';
import { CreateApplicationDto, UpdateApplicationDto, ScaleResourcesDto, SetCustomDomainDto } from './dto/application.dto';
import { RolesGuard } from '../common/guards/roles.guard';
import { Roles } from '../common/decorators/roles.decorator';
import { UserRole, DatabaseType } from '../common/enums';
@@ -36,6 +37,7 @@ export class ApplicationsController {
constructor(
private readonly applicationsService: ApplicationsService,
private readonly domainService: DomainService,
private readonly kubernetesService: KubernetesService,
@Inject(forwardRef(() => DeploymentsService))
private readonly deploymentsService: DeploymentsService,
@@ -263,6 +265,69 @@ export class ApplicationsController {
return this.kubernetesService.getPreviewInfo(app);
}
// ── Custom Domain ─────────────────────────────────────────────
@Get(':id/domain')
@ApiOperation({ summary: 'Get custom domain info and DNS setup instructions' })
async getDomainInfo(@Param('id') id: string, @Request() req: any) {
const userId = (req.user.role === UserRole.ADMIN || req.user.role === UserRole.TECHNICAL)
? (await this.applicationsService.findOne(id)).userId
: req.user.id;
return this.domainService.getDomainInfo(id, userId);
}
@Post(':id/domain')
@ApiOperation({ summary: 'Set a custom domain for the application' })
async setCustomDomain(
@Param('id') id: string,
@Request() req: any,
@Body() dto: SetCustomDomainDto,
) {
const isStaff = req.user.role === UserRole.ADMIN || req.user.role === UserRole.TECHNICAL;
const userId = isStaff
? (await this.applicationsService.findOne(id)).userId
: req.user.id;
return this.domainService.setCustomDomain(id, userId, dto.domain);
}
@Post(':id/domain/verify')
@ApiOperation({ summary: 'Verify DNS records for the custom domain' })
async verifyDomainDns(@Param('id') id: string, @Request() req: any) {
const isStaff = req.user.role === UserRole.ADMIN || req.user.role === UserRole.TECHNICAL;
const userId = isStaff
? (await this.applicationsService.findOne(id)).userId
: req.user.id;
const result = await this.domainService.verifyDns(id, userId);
if (result.verified && result.application) {
try {
await this.kubernetesService.updateIngress(result.application);
} catch (e: any) {
this.logger.warn(`Failed to update ingress for ${id} after DNS verification: ${e.message}`);
}
}
return result;
}
@Delete(':id/domain')
@ApiOperation({ summary: 'Remove custom domain from the application' })
async removeCustomDomain(@Param('id') id: string, @Request() req: any) {
const isStaff = req.user.role === UserRole.ADMIN || req.user.role === UserRole.TECHNICAL;
const userId = isStaff
? (await this.applicationsService.findOne(id)).userId
: req.user.id;
const app = await this.domainService.removeCustomDomain(id, userId);
try {
await this.kubernetesService.updateIngress(app);
} catch (e: any) {
this.logger.warn(`Failed to update ingress for ${id} after domain removal: ${e.message}`);
}
return { message: 'Custom domain removed successfully' };
}
@Delete(':id')
@ApiOperation({ summary: 'Delete an application and all its resources' })
async delete(@Param('id') id: string, @Request() req: any) {