VitoTechnitiumDns/Actions/ManageRecords.php
Corentin BARNICHON 0d7b62539d first commit
2026-04-16 18:13:36 +02:00

236 lines
7.8 KiB
PHP

<?php
namespace App\Vito\Plugins\LiittleCookie\VitoTechnitiumDns\Actions;
use App\Vito\Plugins\LiittleCookie\VitoTechnitiumDns\Services\RecordMapper;
use App\Vito\Plugins\LiittleCookie\VitoTechnitiumDns\Services\TechnitiumClient;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
use Throwable;
class ManageRecords
{
private const int DEFAULT_TTL = 3600;
public function __construct(private readonly TechnitiumClient $client) {}
/**
* List all records for a zone.
*
* @return array<int, array<string, mixed>>
*
* @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<string, mixed>
*
* @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<string, mixed>
*
* @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'),
]);
}
}
}