allowsCustomer($this->customer()), 403); $this->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; } // Whether anything is being SERVED under the old domain right now. A // change here can only ever take a verified domain away — the new one // starts unproven — so this is the one question that decides whether // the proxy and Nextcloud have to be told. $wasVerified = $instance->domainIsVerified(); // 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_cert_ok' => false, 'domain_checked_at' => null, 'domain_error' => null, 'domain_failures' => 0, ])->save(); $this->stopOrStartServing($instance, $wasVerified); $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_cert_ok' => false, 'domain_checked_at' => null, 'domain_error' => null, 'domain_failures' => 0, ])->save(); $this->stopOrStartServing($instance, $wasVerified); } $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; } $wasVerified = $instance->domainIsVerified(); $wasServed = $instance->domainIsServed(); $present = $verifier->proofPresent($instance); $instance->forceFill([ 'domain_checked_at' => now(), 'domain_verified_at' => $present ? ($instance->domain_verified_at ?? now()) : null, 'domain_cert_ok' => $present ? $instance->domain_cert_ok : false, 'domain_error' => $present ? null : 'missing_txt', 'domain_failures' => $present ? 0 : $instance->domain_failures + 1, ])->save(); // This button flips verification both ways, so it is a place the // address changes and has to be re-applied like any other. It also // re-applies a domain that is proven but still answering nothing — // which is what somebody is doing here when they add the A record // after the TXT one and press it again, and it is the only way they // have of asking us to look now rather than at the next change. if ($present !== $wasVerified || ($present && ! $wasServed)) { app(ReapplyInstanceAddress::class)($instance); } $this->justChecked = true; $this->dispatch('notify', message: __($present ? 'domain.verified' : 'domain.not_found')); } /** * Tell the proxy and Nextcloud that the address has moved. * * Only when something WAS being served: an unproven domain never reached a * router or a trusted_domains entry, so replacing one with another changes * nothing that has to be un-done, and starting a run for it would be remote * work on a live machine for no reason at all. */ private function stopOrStartServing(Instance $instance, bool $wasVerified): void { if ($wasVerified) { app(ReapplyInstanceAddress::class)($instance); } } 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, ]); } }