Files
ladill-hosting/app/Services/Hosting/PlaceholderPrimaryDomainService.php
isaaccladandCursor 136cf8b719
Deploy Ladill Hosting / deploy (push) Successful in 47s
Fix subdomain DNS messaging for Ladill nameservers and retire pending-setup primaries.
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>
2026-07-13 16:47:50 +00:00

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();
}
}