Generate random platform subdomains for apps without custom domains.

Assign unpredictable subdomains on create when no custom domain is set, and update deploy UI CNAME hints to reference the app Platform Domain after creation.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
keyhan
2026-05-26 19:55:44 +03:30
parent d7594df9a0
commit 20c4184cde
2 changed files with 35 additions and 17 deletions
@@ -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<string> {
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<Application> {
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({
+4 -16
View File
@@ -2086,7 +2086,7 @@ export default function DeployPage() {
Type: <strong>CNAME</strong>
</p>
<p className="text-xs font-mono bg-gray-50 rounded px-2 py-1">
Value: <strong>{form.name}-{(user?.id || '').split('-')[0]}.apps.cloudhost.ir</strong>
Value: <strong>your app Platform Domain (after creation)</strong>
</p>
</div>
<p>4. If using a root domain (without www), use a registrar that supports CNAME flattening (e.g. Cloudflare).</p>
@@ -2096,20 +2096,8 @@ export default function DeployPage() {
{/* CNAME target copy box */}
<div className="mt-3 bg-blue-50 rounded-lg p-3 border border-blue-100">
<p className="text-xs text-blue-700 font-medium mb-1">CNAME Target:</p>
<div className="flex items-center gap-2">
<code className="text-sm font-mono text-blue-900 bg-blue-100 px-2 py-1 rounded flex-1 truncate">
{form.name}-{(user?.id || '').split('-')[0]}.apps.cloudhost.ir
</code>
<button
type="button"
onClick={() => {
navigator.clipboard.writeText(`${form.name}-${(user?.id || '').split('-')[0]}.apps.cloudhost.ir`);
toast.success('Copied!');
}}
className="text-blue-600 hover:text-blue-800 p-1 shrink-0"
>
<Copy className="w-4 h-4" />
</button>
<div className="text-sm text-blue-900 bg-blue-100 px-2 py-1 rounded">
After the app is created, open its page and copy the Platform Domain value shown in the Domains/Settings section.
</div>
</div>
</div>
@@ -3055,7 +3043,7 @@ export default function DeployPage() {
<div className="mt-4 bg-gray-50 rounded-lg p-3 border border-gray-200">
<p className="text-xs font-medium text-gray-600 mb-1">Required CNAME record:</p>
<code className="text-xs font-mono text-gray-800">
{customDomainInput} {form.name}-{(user?.id || '').split('-')[0]}.apps.cloudhost.ir
{customDomainInput} your app Platform Domain (after creation)
</code>
</div>
</div>