diff --git a/backend/src/applications/applications.service.ts b/backend/src/applications/applications.service.ts index 2887b16..5e7e213 100644 --- a/backend/src/applications/applications.service.ts +++ b/backend/src/applications/applications.service.ts @@ -30,6 +30,33 @@ export class ApplicationsService { private configService: ConfigService, ) {} + private toDnsLabel(value: string): string { + return (value || '') + .toLowerCase() + .replace(/[^a-z0-9-]/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, '') + .slice(0, 63); + } + + private async generateRandomSubdomain(baseLabel: string): Promise { + const base = this.toDnsLabel(baseLabel) || 'app'; + const suffixLenBytes = 5; // 10 hex chars + const suffixLen = suffixLenBytes * 2; + + for (let attempt = 0; attempt < 10; attempt++) { + const suffix = crypto.randomBytes(suffixLenBytes).toString('hex'); // [0-9a-f] + const maxBaseLen = 63 - 1 - suffixLen; // base + '-' + suffix + const truncatedBase = base.slice(0, Math.max(1, maxBaseLen)).replace(/-+$/g, ''); + const candidate = `${truncatedBase}-${suffix}`; + + const exists = await this.appsRepository.findOne({ where: { subdomain: candidate } }); + if (!exists) return candidate; + } + + throw new BadRequestException('Failed to generate a unique subdomain. Please try again.'); + } + async create(userId: string, dto: CreateApplicationDto, userRole?: string): Promise { dto = normalizeCreateApplicationDto(dto); const productType = dto.productType ?? ProductType.APPLICATION; @@ -83,7 +110,10 @@ export class ApplicationsService { ? 80 : 3000; - const subdomain = `${dto.name}-${userId.split('-')[0]}`; + const baseLabel = dto.name; + const subdomain = customDomain + ? `${this.toDnsLabel(baseLabel)}-${this.toDnsLabel(userId.split('-')[0])}` + : await this.generateRandomSubdomain(baseLabel); const platformDomain = this.configService.get('platform.domain') || 'apps.cloudhost.ir'; const app = this.appsRepository.create({ diff --git a/frontend/src/app/dashboard/deploy/page.tsx b/frontend/src/app/dashboard/deploy/page.tsx index 9d4bc8c..412cc1a 100644 --- a/frontend/src/app/dashboard/deploy/page.tsx +++ b/frontend/src/app/dashboard/deploy/page.tsx @@ -2086,7 +2086,7 @@ export default function DeployPage() { Type: CNAME

- Value: {form.name}-{(user?.id || '').split('-')[0]}.apps.cloudhost.ir + Value: your app Platform Domain (after creation)

4. If using a root domain (without www), use a registrar that supports CNAME flattening (e.g. Cloudflare).

@@ -2096,20 +2096,8 @@ export default function DeployPage() { {/* CNAME target copy box */}

CNAME Target:

-
- - {form.name}-{(user?.id || '').split('-')[0]}.apps.cloudhost.ir - - +
+ After the app is created, open its page and copy the “Platform Domain” value shown in the Domains/Settings section.
@@ -3055,7 +3043,7 @@ export default function DeployPage() {

Required CNAME record:

- {customDomainInput} → {form.name}-{(user?.id || '').split('-')[0]}.apps.cloudhost.ir + {customDomainInput} → your app Platform Domain (after creation)