> * * @throws \RuntimeException */ public function list(string $domainId): array { $response = $this->client->get('zones/records/get', [ 'domain' => $domainId, 'listZone' => 'true', ]); if (! $this->client->isSuccessful($response)) { Log::error('Failed to fetch Technitium DNS records', [ 'domainId' => $domainId, 'response' => $response->json(), ]); throw new \RuntimeException( 'Failed to fetch DNS records: ' .($response->json('errorMessage') ?? 'Unknown error') ); } $records = $this->client->responseData($response, 'records') ?? []; return collect($records) // Skip SOA records — they are managed by Technitium itself ->reject(fn (array $r) => strtoupper($r['type'] ?? '') === 'SOA') ->map(function (array $record) { $type = $record['type'] ?? ''; $rData = $record['rData'] ?? []; $domain = $record['name'] ?? ''; return [ 'id' => RecordMapper::encodeRecordId($domain, $type, $rData), 'type' => $type, 'name' => $domain, 'content' => RecordMapper::rDataToContent($type, $rData), 'ttl' => $record['ttl'] ?? self::DEFAULT_TTL, 'proxied' => false, 'created_on' => null, 'modified_on' => null, ]; }) ->values() ->toArray(); } /** * Create a new DNS record. * * @return array * * @throws ValidationException */ public function create(string $domainId, array $input): array { try { $type = $input['type']; $name = $input['name']; $content = $input['content']; $ttl = $input['ttl'] ?? self::DEFAULT_TTL; $priority = $input['prio'] ?? null; $params = array_merge( [ 'domain' => $name, 'zone' => $domainId, 'type' => $type, 'ttl' => $ttl, ], RecordMapper::contentToApiParams($type, $content, $priority), ); $response = $this->client->get('zones/records/add', $params); $this->ensureSuccessful($response, 'create', [ 'domainId' => $domainId, 'input' => $input, ]); // Build the rData from the content to create a valid record ID $rData = RecordMapper::contentToApiParams($type, $content, $priority); return [ 'id' => RecordMapper::encodeRecordId($name, $type, $rData), 'type' => $type, 'name' => $name, 'content' => $content, 'ttl' => $ttl, 'proxied' => false, ]; } catch (ValidationException $e) { throw $e; } catch (Throwable $e) { Log::error('Technitium createRecord exception', ['error' => $e->getMessage()]); throw ValidationException::withMessages(['record' => 'Failed to create DNS record: '.$e->getMessage()]); } } /** * Update an existing DNS record. * * Technitium requires specifying the OLD record values to identify the record, * plus the NEW values. The composite record ID contains the old values. * * @return array * * @throws ValidationException */ public function update(string $domainId, string $recordId, array $input): array { try { $old = RecordMapper::decodeRecordId($recordId); $type = $input['type']; $name = $input['name']; $content = $input['content']; $ttl = $input['ttl'] ?? self::DEFAULT_TTL; $priority = $input['prio'] ?? null; $params = array_merge( [ 'domain' => $old['domain'], 'zone' => $domainId, 'type' => $old['type'], 'ttl' => $ttl, // New domain name if it changed 'newDomain' => $name, ], // Old record values for identification RecordMapper::oldRecordParams($old['type'], $old['rData']), // New record values RecordMapper::contentToApiParams($type, $content, $priority), ); $response = $this->client->get('zones/records/update', $params); $this->ensureSuccessful($response, 'update', [ 'domainId' => $domainId, 'recordId' => $recordId, 'input' => $input, ]); $newRData = RecordMapper::contentToApiParams($type, $content, $priority); return [ 'id' => RecordMapper::encodeRecordId($name, $type, $newRData), 'type' => $type, 'name' => $name, 'content' => $content, 'ttl' => $ttl, 'proxied' => false, ]; } catch (ValidationException $e) { throw $e; } catch (Throwable $e) { Log::error('Technitium updateRecord exception', ['error' => $e->getMessage()]); throw ValidationException::withMessages(['record' => 'Failed to update DNS record: '.$e->getMessage()]); } } /** * Delete a DNS record. */ public function delete(string $domainId, string $recordId): bool { try { $old = RecordMapper::decodeRecordId($recordId); $params = array_merge( [ 'domain' => $old['domain'], 'zone' => $domainId, 'type' => $old['type'], ], RecordMapper::deleteParams($old['type'], $old['rData']), ); $response = $this->client->get('zones/records/delete', $params); if (! $this->client->isSuccessful($response)) { Log::error('Failed to delete Technitium DNS record', [ 'domainId' => $domainId, 'recordId' => $recordId, 'response' => $response->json(), ]); return false; } return true; } catch (Throwable $e) { Log::error('Technitium deleteRecord exception', ['error' => $e->getMessage()]); return false; } } /** * @throws ValidationException */ private function ensureSuccessful(Response $response, string $operation, array $context): void { if (! $this->client->isSuccessful($response)) { Log::error("Failed to {$operation} Technitium DNS record", array_merge($context, [ 'response' => $response->json(), ])); throw ValidationException::withMessages([ 'record' => "Failed to {$operation} DNS record: ".($response->json('errorMessage') ?? 'Unknown error'), ]); } } }