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