<?php
 namespace App\Modules\Dns\Cloudflare; use App\Core\Dns\AbstractDnsDriver; use Illuminate\Support\Facades\Http; use RuntimeException; class Cloudflare extends AbstractDnsDriver { private const BASE_URL = 'https://api.cloudflare.com/client/v4'; private const TIMEOUT = 30; public function getName(): string { return 'cloudflare'; } public function getLabel(): string { return 'Cloudflare'; } public function adminFields(): array { return [ 'api_token' => [ 'type' => 'password', 'label' => 'Cloudflare API Token', 'required' => true, 'description' => 'Create token at dash.cloudflare.com/profile/api-tokens with Zone:DNS:Edit permission.', ], 'account_id' => [ 'type' => 'text', 'label' => 'Account ID', 'required' => true, 'description' => 'Found on the Cloudflare dashboard right sidebar (any domain → API section).', ], ]; } public function createZone(string $domain): string { $resp = $this->call('POST', '/zones', [ 'name' => $domain, 'account' => ['id' => $this->getConfig('account_id')], 'type' => 'full', ]); $zoneId = $resp['result']['id'] ?? null; if (! $zoneId) { throw new RuntimeException('Cloudflare createZone returned no zone id'); } return (string) $zoneId; } public function deleteZone(string $zoneId): void { try { $this->call('DELETE', "/zones/{$zoneId}"); } catch (\Throwable $e) { if (! str_contains($e->getMessage(), 'HTTP 404')) { throw $e; } } } public function listRecords(string $zoneId): array { $resp = $this->call('GET', "/zones/{$zoneId}/dns_records"); $out = []; foreach (($resp['result'] ?? []) as $r) { $out[] = [ 'id' => (string) $r['id'], 'type' => (string) $r['type'], 'name' => (string) $r['name'], 'content' => (string) $r['content'], 'ttl' => (int) ($r['ttl'] ?? 0), 'priority' => isset($r['priority']) ? (int) $r['priority'] : null, ]; } return $out; } public function createRecord(string $zoneId, array $record): string { $body = [ 'type' => $record['type'], 'name' => $record['name'], 'content' => $record['content'], 'ttl' => $record['ttl'] ?? 1, ]; if (isset($record['priority'])) { $body['priority'] = $record['priority']; } if (isset($record['proxied'])) { $body['proxied'] = (bool) $record['proxied']; } $resp = $this->call('POST', "/zones/{$zoneId}/dns_records", $body); $recordId = $resp['result']['id'] ?? null; if (! $recordId) { throw new RuntimeException('Cloudflare createRecord returned no id'); } return (string) $recordId; } public function updateRecord(string $zoneId, string $recordId, array $record): void { $body = [ 'type' => $record['type'], 'name' => $record['name'], 'content' => $record['content'], 'ttl' => $record['ttl'] ?? 1, ]; if (isset($record['priority'])) { $body['priority'] = $record['priority']; } if (isset($record['proxied'])) { $body['proxied'] = (bool) $record['proxied']; } $this->call('PUT', "/zones/{$zoneId}/dns_records/{$recordId}", $body); } public function deleteRecord(string $zoneId, string $recordId): void { try { $this->call('DELETE', "/zones/{$zoneId}/dns_records/{$recordId}"); } catch (\Throwable $e) { if (! str_contains($e->getMessage(), 'HTTP 404')) { throw $e; } } } public function testConnection(): array { $token = $this->getConfig('api_token'); if (! $token) { return ['success' => false, 'message' => 'api_token is required']; } try { $resp = Http::withToken($token) ->acceptJson() ->timeout(15) ->get(self::BASE_URL.'/user/tokens/verify'); if (! $resp->successful()) { return ['success' => false, 'message' => 'Cloudflare HTTP '.$resp->status()]; } $body = $resp->json(); $status = $body['result']['status'] ?? 'unknown'; $isValid = ($body['success'] ?? false) === true && $status === 'active'; return $isValid ? ['success' => true, 'message' => 'Cloudflare token verified (active)'] : ['success' => false, 'message' => "Token status: {$status}"]; } catch (\Throwable $e) { return ['success' => false, 'message' => $e->getMessage()]; } } protected function call(string $method, string $path, array $body = []): array { $token = $this->getConfig('api_token'); if (! $token) { throw new RuntimeException('Cloudflare api_token missing'); } $req = Http::withToken($token) ->acceptJson() ->timeout(self::TIMEOUT); $url = self::BASE_URL.$path; $resp = match ($method) { 'GET' => $req->get($url, $body), 'POST' => $req->post($url, $body), 'PUT' => $req->put($url, $body), 'DELETE' => $req->delete($url), }; if (! $resp->successful()) { $errMsg = 'HTTP '.$resp->status(); $j = $resp->json(); if (is_array($j) && ! empty($j['errors'])) { $errMsg .= ': '.json_encode($j['errors']); } throw new RuntimeException("Cloudflare {$errMsg}"); } return $resp->json() ?? []; } } 