http = Http::baseUrl(rtrim(config('hosting.plesk.url'), '/').'/api/v2') ->withBasicAuth($user, $password) ->acceptJson() ->timeout((int) config('hosting.plesk.timeout', 30)) ->when(! $verify, fn (PendingRequest $request) => $request->withoutVerifying()); } public function createARecord(string $domain, string $subdomain, string $ip): array { $this->validateDomain($domain); $this->validateSubdomain($subdomain); $this->validateIp($ip); $baseDomain = config('hosting.plesk.base_domain'); $zoneDomain = $this->resolveZoneDomain($domain, $baseDomain); $host = $this->resolveHostLabel($domain, $subdomain, $baseDomain); if ($this->findARecord($zoneDomain, $host) !== null) { throw new PleskException( "DNS A record already exists for {$host}.{$zoneDomain}", step: 'plesk_dns_exists', ); } $targetIp = config('hosting.traefik.public_ip') ?: $ip; Log::info('Plesk: creating A record', [ 'zone' => $zoneDomain, 'host' => $host, 'ip' => $targetIp, ]); $response = $this->http->post("/domains/{$zoneDomain}/dns/records", [ 'type' => 'A', 'host' => $host === '@' ? '' : $host, 'value' => $targetIp, ]); if ($response->failed()) { throw new PleskException( 'Plesk create A record failed: '.$response->body(), step: 'plesk_create', context: ['status' => $response->status()], code: $response->status(), ); } return $response->json() ?? []; } public function deleteARecord(string $domain, string $subdomain): void { $baseDomain = config('hosting.plesk.base_domain'); $zoneDomain = $this->resolveZoneDomain($domain, $baseDomain); $host = $this->resolveHostLabel($domain, $subdomain, $baseDomain); $record = $this->findARecord($zoneDomain, $host); if ($record === null) { Log::warning('Plesk: A record not found for deletion', compact('zoneDomain', 'host')); return; } $recordId = $record['id'] ?? null; if ($recordId === null) { throw new PleskException('Plesk record id missing.', step: 'plesk_delete'); } Log::info('Plesk: deleting A record', ['zone' => $zoneDomain, 'id' => $recordId]); $response = $this->http->delete("/domains/{$zoneDomain}/dns/records/{$recordId}"); if ($response->failed() && $response->status() !== 404) { throw new PleskException( 'Plesk delete A record failed: '.$response->body(), step: 'plesk_delete', code: $response->status(), ); } } public function listRecords(string $domain): array { $zoneDomain = $this->resolveZoneDomain($domain, config('hosting.plesk.base_domain')); try { $response = $this->http->get("/domains/{$zoneDomain}/dns/records"); if ($response->failed()) { throw new PleskException( 'Plesk list records failed: '.$response->body(), step: 'plesk_list', code: $response->status(), ); } return $response->json() ?? []; } catch (ConnectionException $e) { throw new PleskException( 'Plesk connection failed: '.$e->getMessage(), step: 'plesk_connection', previous: $e, ); } } public function dnsExists(string $domain, string $subdomain): bool { $zoneDomain = $this->resolveZoneDomain($domain, config('hosting.plesk.base_domain')); $host = $this->resolveHostLabel($domain, $subdomain, config('hosting.plesk.base_domain')); return $this->findARecord($zoneDomain, $host) !== null; } private function findARecord(string $zoneDomain, string $host): ?array { $records = $this->listRecords($zoneDomain); $items = $records['records'] ?? (is_array($records) && array_is_list($records) ? $records : []); foreach ($items as $record) { if (! is_array($record)) { continue; } $type = strtoupper((string) ($record['type'] ?? '')); $recordHost = (string) ($record['host'] ?? $record['name'] ?? ''); if ($type !== 'A') { continue; } $normalizedHost = $recordHost === '' ? '@' : rtrim($recordHost, '.'); if ($normalizedHost === $host || $normalizedHost === "{$host}.{$zoneDomain}") { return $record; } } return null; } private function resolveZoneDomain(string $domain, string $baseDomain): string { if (str_ends_with($domain, '.'.$baseDomain) || $domain === $baseDomain) { return $baseDomain; } return $domain; } private function resolveHostLabel(string $domain, string $subdomain, string $baseDomain): string { if ($subdomain !== '' && $subdomain !== '@') { return $subdomain; } if ($domain === $baseDomain) { return '@'; } if (str_ends_with($domain, '.'.$baseDomain)) { return substr($domain, 0, -(strlen($baseDomain) + 1)); } return explode('.', $domain)[0] ?? $subdomain; } private function validateDomain(string $domain): void { if (! preg_match('/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i', $domain)) { throw new PleskException("Invalid domain: {$domain}", step: 'plesk_validation'); } } private function validateSubdomain(string $subdomain): void { if ($subdomain === '' || $subdomain === '@') { return; } if (! preg_match('/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i', $subdomain)) { throw new PleskException("Invalid subdomain: {$subdomain}", step: 'plesk_validation'); } } private function validateIp(string $ip): void { if (! filter_var($ip, FILTER_VALIDATE_IP)) { throw new PleskException("Invalid IP: {$ip}", step: 'plesk_validation'); } } }