domain = (string) ($this->instance()?->custom_domain ?? ''); } private function instance(): ?Instance { // The shared resolver, not `auth()->user()->customer`: accounts created // before the explicit link exists are matched by address, and this page // must not be the one place that forgets that. return $this->customer()?->instances() ->whereIn('status', ['active', 'provisioning', 'cancellation_scheduled']) ->latest('id') ->first(); } /** * Save the domain and mint a fresh token. * * A NEW token every time the domain changes. Reusing the old one would let * a customer who once verified example.com claim any other domain later * without touching its DNS — the record for the first domain would still be * sitting there, and the value is all that is compared. */ public function save(DomainVerifier $verifier): void { $this->validate(); $instance = $this->instance(); if ($instance === null) { return; } $domain = Str::lower(trim($this->domain)); $domain = preg_replace('#^https?://#', '', $domain); $domain = rtrim((string) $domain, '/.'); // A hostname, not a URL and not a path. Loose on purpose — the DNS // lookup is the real test, and a pattern strict enough to be a // specification would reject somebody's perfectly valid domain. if ($domain !== '' && ! preg_match('/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/', $domain)) { $this->addError('domain', __('domain.invalid')); return; } // Clearing the field takes the domain away entirely, including its // verification — the instance falls back to its platform address. if ($domain === '') { $instance->forceFill([ 'custom_domain' => null, 'domain_token' => null, 'domain_verified_at' => null, 'domain_checked_at' => null, 'domain_error' => null, 'domain_failures' => 0, ])->save(); $this->domain = ''; $this->dispatch('notify', message: __('domain.removed')); return; } if ($domain !== $instance->custom_domain) { $instance->forceFill([ 'custom_domain' => $domain, 'domain_token' => $verifier->newToken(), 'domain_verified_at' => null, 'domain_checked_at' => null, 'domain_error' => null, 'domain_failures' => 0, ])->save(); } $this->domain = $domain; $this->dispatch('notify', message: __('domain.saved')); } /** * Look now, rather than waiting for tonight. * * The nightly run is what keeps a domain honest; this button is only so * that somebody who has just created the record does not have to wait until * the morning to find out whether they typed it correctly. */ public function checkNow(DomainVerifier $verifier): void { $instance = $this->instance(); if ($instance === null || blank($instance->custom_domain)) { return; } $present = $verifier->proofPresent($instance); $instance->forceFill([ 'domain_checked_at' => now(), 'domain_verified_at' => $present ? ($instance->domain_verified_at ?? now()) : null, 'domain_error' => $present ? null : 'missing_txt', 'domain_failures' => $present ? 0 : $instance->domain_failures + 1, ])->save(); $this->justChecked = true; $this->dispatch('notify', message: __($present ? 'domain.verified' : 'domain.not_found')); } public function render() { $instance = $this->instance(); $verifier = app(DomainVerifier::class); return view('livewire.custom-domain', [ 'instance' => $instance, 'platformAddress' => $instance?->subdomain ? $instance->subdomain.'.'.ProvisioningSettings::dnsZone() : null, 'recordName' => $instance && filled($instance->custom_domain) ? $verifier->recordName($instance->custom_domain) : null, 'recordValue' => $instance && filled($instance->domain_token) ? $verifier->recordValue($instance) : null, ]); } }