Deploy Ladill Hosting / deploy (push) Successful in 47s
Treat ns_auto domains as managed DNS, publish subdomain A records via PowerDNS without failing the UX when the local DNS registry is missing, and promote real linked domains over pending-setup.local placeholders. Co-authored-by: Cursor <cursoragent@cursor.com>
195 lines
6.2 KiB
PHP
195 lines
6.2 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Hosting;
|
|
|
|
use App\Models\HostedSite;
|
|
use App\Models\HostingAccount;
|
|
use App\Services\Hosting\Providers\SharedNodeProvider;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* Sales Centre used to stamp pending-setup.local as primary_domain (and sometimes a
|
|
* primary HostedSite). Link Domain historically only created addons, so the
|
|
* placeholder kept hijacking the account label. This service promotes a real
|
|
* linked domain and retires placeholder primaries.
|
|
*/
|
|
class PlaceholderPrimaryDomainService
|
|
{
|
|
public function __construct(
|
|
private SharedNodeProvider $provider,
|
|
) {}
|
|
|
|
public static function isPlaceholderDomain(?string $domain): bool
|
|
{
|
|
$domain = strtolower(trim((string) $domain));
|
|
|
|
if ($domain === '' || $domain === 'pending-setup.local') {
|
|
return true;
|
|
}
|
|
|
|
// Temporary nginx host used when an account is provisioned without a domain.
|
|
return str_ends_with($domain, '.ladill.com');
|
|
}
|
|
|
|
public function accountNeedsPromotion(HostingAccount $account): bool
|
|
{
|
|
$account->loadMissing('sites');
|
|
|
|
$primarySites = $account->sites->where('type', 'primary')->values();
|
|
$hasPlaceholderPrimarySite = $primarySites->contains(
|
|
fn (HostedSite $site): bool => static::isPlaceholderDomain($site->domain)
|
|
);
|
|
$hasRealPrimarySite = $primarySites->contains(
|
|
fn (HostedSite $site): bool => ! static::isPlaceholderDomain($site->domain)
|
|
);
|
|
|
|
// Never steal primary from an account that already has a real primary site.
|
|
if ($hasRealPrimarySite) {
|
|
return false;
|
|
}
|
|
|
|
// Placeholder primary_domain (incl. null/empty) or only placeholder primary sites.
|
|
if (static::isPlaceholderDomain($account->primary_domain) || $hasPlaceholderPrimarySite) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Whether a newly linked host should be stored as the account primary site.
|
|
*/
|
|
public function shouldBecomePrimary(HostingAccount $account, string $domainHost): bool
|
|
{
|
|
if ($this->accountNeedsPromotion($account)) {
|
|
return true;
|
|
}
|
|
|
|
$primary = strtolower(trim((string) $account->primary_domain));
|
|
|
|
return $primary !== ''
|
|
&& $primary === strtolower(trim($domainHost))
|
|
&& ! $account->sites->contains(
|
|
fn (HostedSite $site): bool => $site->type === 'primary'
|
|
&& ! static::isPlaceholderDomain($site->domain)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Prefer a real hosted site over a placeholder primary_domain for UI labels.
|
|
*/
|
|
public function displayLabel(HostingAccount $account): string
|
|
{
|
|
$account->loadMissing('sites');
|
|
|
|
$real = $account->sites
|
|
->filter(fn (HostedSite $site): bool => ! static::isPlaceholderDomain($site->domain))
|
|
->sortBy(fn (HostedSite $site): array => [$site->type !== 'primary', $site->domain])
|
|
->values();
|
|
|
|
if ($real->isNotEmpty()) {
|
|
if ($real->count() === 1) {
|
|
return (string) $real->first()->domain;
|
|
}
|
|
|
|
$preview = $real->take(2)->pluck('domain')->implode(', ');
|
|
|
|
return $real->count() > 2
|
|
? "{$preview} +".($real->count() - 2)
|
|
: $preview;
|
|
}
|
|
|
|
if (! static::isPlaceholderDomain($account->primary_domain)) {
|
|
return (string) $account->primary_domain;
|
|
}
|
|
|
|
return (string) $account->username;
|
|
}
|
|
|
|
public function promoteSite(HostingAccount $account, HostedSite $site): void
|
|
{
|
|
if (static::isPlaceholderDomain($site->domain)) {
|
|
return;
|
|
}
|
|
|
|
$account->loadMissing(['sites', 'node']);
|
|
|
|
$site->update(['type' => 'primary']);
|
|
|
|
$account->update([
|
|
'primary_domain' => $site->domain,
|
|
]);
|
|
|
|
foreach ($account->sites as $other) {
|
|
if ((int) $other->id === (int) $site->id) {
|
|
continue;
|
|
}
|
|
|
|
if ($other->type === 'primary' && static::isPlaceholderDomain($other->domain)) {
|
|
$this->retirePlaceholderSite($account, $other);
|
|
} elseif ($other->type === 'primary') {
|
|
$other->update(['type' => 'addon']);
|
|
}
|
|
}
|
|
|
|
$account->unsetRelation('sites');
|
|
}
|
|
|
|
/**
|
|
* Heal accounts that already have a real linked domain but still carry a
|
|
* placeholder primary_domain / primary HostedSite.
|
|
*/
|
|
public function healAccount(HostingAccount $account): bool
|
|
{
|
|
if (! $this->accountNeedsPromotion($account)) {
|
|
return false;
|
|
}
|
|
|
|
$account->loadMissing(['sites', 'node']);
|
|
|
|
$candidate = $account->sites
|
|
->filter(fn (HostedSite $site): bool => ! static::isPlaceholderDomain($site->domain)
|
|
&& in_array($site->type, ['primary', 'addon'], true))
|
|
->sortBy(fn (HostedSite $site): array => [$site->type !== 'primary', $site->id])
|
|
->first();
|
|
|
|
if (! $candidate) {
|
|
// No real site yet — just clear a stale placeholder label.
|
|
if (static::isPlaceholderDomain($account->primary_domain) && filled($account->primary_domain)) {
|
|
$account->update(['primary_domain' => null]);
|
|
|
|
foreach ($account->sites->where('type', 'primary') as $placeholder) {
|
|
if (static::isPlaceholderDomain($placeholder->domain)) {
|
|
$this->retirePlaceholderSite($account, $placeholder);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
$this->promoteSite($account, $candidate);
|
|
|
|
return true;
|
|
}
|
|
|
|
protected function retirePlaceholderSite(HostingAccount $account, HostedSite $site): void
|
|
{
|
|
try {
|
|
if ($account->node) {
|
|
$this->provider->removeSite($site);
|
|
}
|
|
} catch (\Throwable $e) {
|
|
Log::info('PlaceholderPrimaryDomainService: best-effort nginx cleanup failed', [
|
|
'site_id' => $site->id,
|
|
'domain' => $site->domain,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
|
|
$site->delete();
|
|
}
|
|
}
|