diff --git a/src/docs/src/Objects/customdomain.md b/src/docs/src/Objects/customdomain.md index a7950774c..2675fea08 100644 --- a/src/docs/src/Objects/customdomain.md +++ b/src/docs/src/Objects/customdomain.md @@ -39,11 +39,11 @@ Why the domain isn't active yet, when that's known. The DNS records to set at the domain's DNS provider. Each record is an object with these attributes: -- `type` (String): `CNAME` or `TXT`. +- `type` (String): `CNAME`, `TXT` or `A`. - `name` (String): the record's name. - `value` (String): the record's value. -- `purpose` (String): `routing` for the record that sends traffic to the website, `ownership` for the record that proves the user owns the domain, or `certificate` for a record that speeds up issuing the HTTPS certificate. -- `required` (Boolean): `true` for the `routing` and `ownership` records, which the user has to set. A `certificate` record is optional. +- `purpose` (String): `routing` for the record that sends traffic to the website, `ownership` for the record that proves the user owns the domain, `certificate` for a record that speeds up issuing the HTTPS certificate, or `redirect` for the `A` record that forwards a root domain to its `www.` hostname. +- `required` (Boolean): `true` for the `routing`, `ownership` and `redirect` records, which the user has to set. A `certificate` record is optional. #### `createdAt` (Number) @@ -52,3 +52,11 @@ When the domain was added, in milliseconds since the Unix epoch. #### `activatedAt` (Number | null) When the domain became active, in milliseconds since the Unix epoch, or `null` if it isn't active. + +#### `dnsProvider` (String, optional) + +Who hosts the DNS of a root domain: `route53`, `godaddy` or `other`. Puter looks this up when a root domain is added, and only includes it in that response. Route 53 and GoDaddy can't point a root domain at another host name, so a root domain on either is added as its `www.` hostname (see `redirectFrom`). + +#### `redirectFrom` (String, optional) + +The root domain that forwards to this `www.` hostname. A root domain hosted on Route 53 or GoDaddy is added as its `www.` hostname, and the user can switch any other root domain to `www.` in the dialog. The `records` then include an `A` record that forwards the root domain. diff --git a/src/gui/src/UI/UIWindowAttachCustomDomain.js b/src/gui/src/UI/UIWindowAttachCustomDomain.js index aa09bcf05..5bd13545c 100644 --- a/src/gui/src/UI/UIWindowAttachCustomDomain.js +++ b/src/gui/src/UI/UIWindowAttachCustomDomain.js @@ -136,7 +136,9 @@ async function UIWindowAttachCustomDomain (options) { ${i18n('custom_domain_record_type')}${i18n('name')}${i18n('custom_domain_record_value')} + +
@@ -371,7 +373,12 @@ async function UIWindowAttachCustomDomain (options) { if ( domain.statusDetail && domain.status !== 'active' ) { $status.append($('').text(domain.statusDetail)); } + const $redirect = $overlay.find('.custom-domain-redirect').toggle(!!domain.redirectFrom); + if ( domain.redirectFrom ) { + $redirect.html(i18n('custom_domain_redirect_note', [domain.redirectFrom, domain.hostname])); + } $overlay.find('.custom-domain-apex').toggle(domain.isApex === true); + $overlay.find('.custom-domain-use-www').toggle(domain.isApex === true && domain.status !== 'active'); const $tbody = $overlay.find('.custom-domain-records tbody').empty(); for ( const record of domain.records ?? [] ) { @@ -382,9 +389,12 @@ async function UIWindowAttachCustomDomain (options) { `); + const type = domain.isApex && record.purpose === 'routing' + ? text('custom_domain_record_type_apex') + : record.type; $tr.find('.custom-domain-record-type').text(record.required - ? record.type - : `${record.type} ${i18n('custom_domain_optional', [], false)}`); + ? type + : `${type} ${text('custom_domain_optional')}`); const [$name, $value] = $tr.find('code').toArray().map((el) => $(el)); $name.text(record.name); $value.text(record.value); @@ -405,8 +415,17 @@ async function UIWindowAttachCustomDomain (options) { return; } // A domain that is no longer pending comes back with only its - // routing record; keep the full set shown when it was added. - chosen = { ...res.data, records: res.data.records?.length > 1 ? res.data.records : chosen.records }; + // routing record, and a GET never repeats the provider or the + // redirect; keep what was shown when it was added. + const redirect = chosen.records.filter((r) => r.purpose === 'redirect'); + const fresh = (res.data.records?.length > 1 ? res.data.records : chosen.records) + .filter((r) => r.purpose !== 'redirect'); + chosen = { + ...res.data, + dnsProvider: chosen.dnsProvider, + redirectFrom: chosen.redirectFrom, + records: [...fresh.slice(0, 1), ...redirect, ...fresh.slice(1)], + }; renderRecords(chosen); } catch ( e ) { console.error('custom-domain: status check failed', e); @@ -415,6 +434,25 @@ async function UIWindowAttachCustomDomain (options) { setBusy(false); } }); + // Same row, so the month already paid covers the www hostname. + $overlay.find('.custom-domain-use-www').on('click', async () => { + if ( busy || ! chosen ) return; + setBusy(true); + try { + const res = await api('POST', `/${encodeURIComponent(chosen.uuid)}/use-www`); + if ( ! res.ok ) { + showError('records', errorFor(res)); + return; + } + chosen = res.data; + renderRecords(chosen); + } catch ( e ) { + console.error('custom-domain: switch to www failed', e); + showError('records', text('custom_domain_error_generic')); + } finally { + setBusy(false); + } + }); $overlay.find('.custom-domain-done').on('click', () => { if ( ! busy ) close(); }); diff --git a/src/gui/src/css/dashboard.css b/src/gui/src/css/dashboard.css index 22ac6c8c6..c43eb30ab 100644 --- a/src/gui/src/css/dashboard.css +++ b/src/gui/src/css/dashboard.css @@ -2124,6 +2124,16 @@ input.myapps-group-name:focus { border-top: 1px solid var(--afb-border); vertical-align: top; } +.custom-domain-redirect strong { + color: var(--afb-fg); + word-break: break-all; +} +.custom-domain-use-www { + display: block; + width: 100%; + margin-top: 10px; + font-size: 13px; +} .custom-domain-copy { display: block; word-break: break-all; diff --git a/src/gui/src/i18n/translations/en.js b/src/gui/src/i18n/translations/en.js index 6ad4ec1fe..95f1bdd58 100644 --- a/src/gui/src/i18n/translations/en.js +++ b/src/gui/src/i18n/translations/en.js @@ -160,7 +160,7 @@ const en = { current_password: 'Current Password', custom_domain_add: 'Add Domain', custom_domain_add_new: 'Add a New Domain', - custom_domain_apex_hint: 'This is a root domain. Your DNS provider needs to support CNAME flattening, ALIAS, or ANAME records for it. If it doesn\'t, connect www instead and forward the root domain to it.', + custom_domain_apex_hint: 'This is a root domain, so your DNS provider needs to support a CNAME, ALIAS or ANAME record at the root (sometimes called CNAME flattening).', custom_domain_claim_c2a: 'Someone else added this domain but never connected it. If it\'s yours, add this record at your DNS provider, then verify. The domain moves to your account.', custom_domain_claim_not_found: 'The record isn\'t visible yet. DNS changes can take a few minutes; try again shortly.', custom_domain_check_status: 'Check Status', @@ -181,7 +181,10 @@ const en = { custom_domain_none_yet: 'You don\'t have any custom domains yet.', custom_domain_optional: '(optional)', custom_domain_price_note: 'Each domain costs %% per month. example.com and www.example.com count as two domains.', + custom_domain_no_root_alias: 'My DNS provider doesn\'t support CNAME, ALIAS or ANAME at the root', custom_domain_record_type: 'Type', + custom_domain_record_type_apex: 'CNAME / ALIAS / ANAME', + custom_domain_redirect_note: 'Your website is served on %strong%. The A record forwards visitors from %strong% to it.', custom_domain_record_value: 'Value', custom_domain_records_c2a: 'Add these records at your DNS provider. The domain starts working a few minutes after they\'re in place.', custom_domain_requested_by: 'Requested by %strong%', diff --git a/src/puter-js/src/modules/hosting/types.js b/src/puter-js/src/modules/hosting/types.js index d6f36729d..7fdacb101 100644 --- a/src/puter-js/src/modules/hosting/types.js +++ b/src/puter-js/src/modules/hosting/types.js @@ -15,10 +15,11 @@ * A DNS record the user sets at their DNS provider for a custom domain. * * @typedef {Object} CustomDomainRecord - * @property {'CNAME' | 'TXT'} type Record type. + * @property {'CNAME' | 'TXT' | 'A'} type Record type. * @property {string} name Record name (host). * @property {string} value Record value. - * @property {'routing' | 'ownership' | 'certificate'} purpose What the record is for. + * @property {'routing' | 'ownership' | 'certificate' | 'redirect'} purpose What the record is + * for. `redirect` is the A record that forwards a root domain to its `www.` hostname. * @property {boolean} required `true` for records the user has to set (routing and * ownership); certificate records only speed issuance up. */ @@ -38,6 +39,10 @@ * @property {CustomDomainRecord[]} records DNS records to set. * @property {number} createdAt When the domain was added, in milliseconds since the epoch. * @property {number | null} activatedAt When the domain went active, if it has. + * @property {'route53' | 'godaddy' | 'other'} [dnsProvider] Who hosts the DNS of a root + * domain, when Puter looked it up while adding it. + * @property {string} [redirectFrom] The root domain forwarded to this `www.` hostname, when + * the root domain couldn't be served directly. */ export {};