Fix subdomain DNS messaging for Ladill nameservers and retire pending-setup primaries.
Deploy Ladill Hosting / deploy (push) Successful in 47s
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>
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user