feat(domains): support apex domains and non-.com TLDs
Loosen the custom-domain regex to accept apex domains (1+ labels) in addition to subdomains; the TLD already allowed any letters. Suppress the CNAME record for apex hostnames since a CNAME is invalid at the zone apex, and surface only the A record there. Drop the frontend apex rejection and the misleading ".com" input suffix, and render the domain-input prefix/suffix addons only when set.
This commit is contained in:
@@ -67,6 +67,7 @@
|
||||
"katex": "^0.16.10",
|
||||
"marked": "^7.0.3",
|
||||
"nanoid": "^5.1.5",
|
||||
"tldts": "^6.1.86",
|
||||
"zod": "^4.1.8",
|
||||
"zod-openapi": "^5.4.1"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AppError, ErrorCodes } from '@api/utils/errors';
|
||||
|
||||
import { env } from '@cio/core/config/env';
|
||||
import { parse } from 'tldts';
|
||||
|
||||
export type DomainSetupStatus =
|
||||
| 'reconnect_required'
|
||||
@@ -40,7 +41,7 @@ interface ApproximatedVhost {
|
||||
ssl_active_until?: string | null;
|
||||
}
|
||||
|
||||
const SUPPORTED_CUSTOM_DOMAIN_PATTERN = /^(?=.{4,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.){2,}[a-z]{2,63}$/i;
|
||||
const SUPPORTED_CUSTOM_DOMAIN_PATTERN = /^(?=.{4,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i;
|
||||
|
||||
const APPROXIMATED_BASE_URL = 'https://cloud.approximated.app/api';
|
||||
|
||||
@@ -110,6 +111,7 @@ async function approximatedRequest<T>(path: string, init: RequestInit = {}): Pro
|
||||
function buildDnsRecords(hostname: string, vhost?: ApproximatedVhost | null): DomainDnsRecord[] {
|
||||
const targetIp = env.APPROXIMATED_DNS_TARGET_IP;
|
||||
const targetCname = env.APPROXIMATED_DNS_TARGET_CNAME;
|
||||
const isApex = !parse(hostname).subdomain;
|
||||
|
||||
const records: DomainDnsRecord[] = [];
|
||||
const isResolvingToTarget = Boolean(
|
||||
@@ -120,7 +122,9 @@ function buildDnsRecords(hostname: string, vhost?: ApproximatedVhost | null): Do
|
||||
if (targetIp) {
|
||||
records.push({ type: 'A', name: hostname, value: targetIp, status });
|
||||
}
|
||||
if (targetCname) {
|
||||
|
||||
// A CNAME is invalid at the zone apex, so only offer it for subdomains.
|
||||
if (targetCname && !isApex) {
|
||||
records.push({ type: 'CNAME', name: hostname, value: targetCname, status });
|
||||
}
|
||||
|
||||
@@ -190,7 +194,7 @@ export function normalizeCustomDomain(domain: string) {
|
||||
export function assertSupportedCustomDomain(domain: string) {
|
||||
if (!SUPPORTED_CUSTOM_DOMAIN_PATTERN.test(domain)) {
|
||||
throw new AppError(
|
||||
'Only custom subdomains are supported. Use a subdomain like courses.yourwebsite.com.',
|
||||
'Enter a valid domain like yourwebsite.com or courses.yourwebsite.com.',
|
||||
ErrorCodes.VALIDATION_ERROR,
|
||||
400,
|
||||
'domain'
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
if (!domainApex) return recordName;
|
||||
|
||||
const suffix = '.' + domainApex;
|
||||
if (recordName === domainApex) return recordName;
|
||||
if (recordName === domainApex) return '@';
|
||||
if (recordName.endsWith(suffix)) return recordName.slice(0, -suffix.length);
|
||||
|
||||
return recordName;
|
||||
@@ -190,13 +190,6 @@
|
||||
}
|
||||
|
||||
const sanitizedDomain = sanitizeDomain(customDomain);
|
||||
const { parse } = await import('tldts/dist/es6/index.js');
|
||||
const details = parse(sanitizedDomain);
|
||||
|
||||
if (!details.subdomain) {
|
||||
errors.customDomain = $t('components.settings.domains.custom_domain_error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (sanitizedDomain.includes(BRAND_ROOT_DOMAIN) || sanitizedDomain.includes(TENANT_ROOT_DOMAIN)) {
|
||||
errors.customDomain = $t('components.settings.domains.custom_domain_not_classroomio');
|
||||
@@ -549,7 +542,7 @@
|
||||
{:else}
|
||||
<Field.Field>
|
||||
<Field.Label class="font-bold">{$t('components.settings.domains.your_domain')}</Field.Label>
|
||||
<DomainInput bind:value={customDomain} placeholder="courses.yourwebsite" disabled={$isFreePlan} />
|
||||
<DomainInput bind:value={customDomain} placeholder="yourwebsite.com" suffix="" disabled={$isFreePlan} />
|
||||
{#if errors.customDomain}
|
||||
<Field.Error>{errors.customDomain}</Field.Error>
|
||||
{/if}
|
||||
|
||||
@@ -24,11 +24,15 @@
|
||||
</script>
|
||||
|
||||
<InputGroup.Root class={cn('ui:w-full', className)} data-slot="domain-input">
|
||||
<InputGroup.Addon>
|
||||
<InputGroup.Text>{prefix}</InputGroup.Text>
|
||||
</InputGroup.Addon>
|
||||
{#if prefix}
|
||||
<InputGroup.Addon>
|
||||
<InputGroup.Text>{prefix}</InputGroup.Text>
|
||||
</InputGroup.Addon>
|
||||
{/if}
|
||||
<InputGroup.Input {...restProps} {placeholder} bind:value {disabled} class="!ps-0.5" />
|
||||
<InputGroup.Addon align="inline-end">
|
||||
<InputGroup.Text>{suffix}</InputGroup.Text>
|
||||
</InputGroup.Addon>
|
||||
{#if suffix}
|
||||
<InputGroup.Addon align="inline-end">
|
||||
<InputGroup.Text>{suffix}</InputGroup.Text>
|
||||
</InputGroup.Addon>
|
||||
{/if}
|
||||
</InputGroup.Root>
|
||||
|
||||
Generated
+3
@@ -144,6 +144,9 @@ importers:
|
||||
nanoid:
|
||||
specifier: ^5.1.5
|
||||
version: 5.1.6
|
||||
tldts:
|
||||
specifier: ^6.1.86
|
||||
version: 6.1.86
|
||||
zod:
|
||||
specifier: 4.2.1
|
||||
version: 4.2.1
|
||||
|
||||
Reference in New Issue
Block a user