159 lines
5.5 KiB
PHP
159 lines
5.5 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire;
|
|
|
|
use App\Livewire\Concerns\ResolvesCustomer;
|
|
use App\Models\Instance;
|
|
use App\Services\Domains\DomainVerifier;
|
|
use App\Support\ProvisioningSettings;
|
|
use Illuminate\Support\Str;
|
|
use Livewire\Attributes\Validate;
|
|
use Livewire\Component;
|
|
|
|
/**
|
|
* The customer's own domain, and the proof that it is theirs.
|
|
*
|
|
* Two things happen here and they are deliberately separate: pointing the
|
|
* domain at us (an A record, which we cannot check for them because it may be
|
|
* behind a CDN) and proving they own it (a TXT record, which we can). Only the
|
|
* second one gates anything.
|
|
*
|
|
* A domain can be changed at any time. It was suggested that it should be
|
|
* fixed once set — it should not: adding or moving a domain is a proxy entry, a
|
|
* certificate and one line in trusted_domains, and a customer who wants to move
|
|
* their address after three months is a customer who is still here.
|
|
*/
|
|
class CustomDomain extends Component
|
|
{
|
|
use ResolvesCustomer;
|
|
|
|
#[Validate('nullable|string|max:253')]
|
|
public string $domain = '';
|
|
|
|
public bool $justChecked = false;
|
|
|
|
public function mount(): void
|
|
{
|
|
$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;
|
|
}
|
|
|
|
// 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,
|
|
]);
|
|
}
|
|
}
|