From e6033538be9f78f39ab4432b6e9e015a5874279e Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 13:03:13 +1100 Subject: [PATCH 01/32] add srv_target column to cloudflare_domains migration --- ...4_add_srv_target_to_cloudflare_domains.php | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 subdomains/database/migrations/004_add_srv_target_to_cloudflare_domains.php diff --git a/subdomains/database/migrations/004_add_srv_target_to_cloudflare_domains.php b/subdomains/database/migrations/004_add_srv_target_to_cloudflare_domains.php new file mode 100644 index 0000000..3cbe2a1 --- /dev/null +++ b/subdomains/database/migrations/004_add_srv_target_to_cloudflare_domains.php @@ -0,0 +1,22 @@ +string('srv_target')->nullable()->after('cloudflare_id'); + }); + } + + public function down(): void + { + Schema::table('cloudflare_domains', function (Blueprint $table) { + $table->dropColumn('srv_target'); + }); + } +}; From 5948e9c4b5994b29169fa5ab633fcae4de9ca1f8 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 13:03:34 +1100 Subject: [PATCH 02/32] add SRV record and target strings --- subdomains/lang/en/strings.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index e1cd0ea..511bd80 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -14,6 +14,12 @@ 'name' => 'Name', + 'srv_record' => 'SRV Record', + 'srv_record_help' => 'Enable this option to create a SRV record instead of an A or AAAA record.', + + 'srv_target' => 'SRV Target', + 'srv_target_help' => 'The hostname that SRV records point to (for example: play.example.com).', + 'api_token' => 'Cloudflare API Token', 'api_token_help' => 'The token needs to have read permissions for Zone.Zone and write for Zone.Dns. For better security you can also set the "Zone Resources" to exclude certain domains and add the panel ip to the "Client IP Adress Filtering".', ]; From cc7665723a954b3c3eb82760ce89317f035174db Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 13:04:14 +1100 Subject: [PATCH 03/32] add srv_target field to CloudflareDomain model and resource --- .../CloudflareDomains/CloudflareDomainResource.php | 7 +++++++ subdomains/src/Models/CloudflareDomain.php | 2 ++ 2 files changed, 9 insertions(+) diff --git a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php index 571166f..7108768 100644 --- a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php +++ b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php @@ -53,6 +53,8 @@ public static function table(Table $table): Table ->columns([ TextColumn::make('name') ->label(trans('subdomains::strings.name')), + TextColumn::make('srv_target') + ->label(trans('subdomains::strings.srv_target')), TextColumn::make('subdomains_count') ->label(trans_choice('subdomains::strings.subdomain', 2)) ->counts('subdomains'), @@ -77,6 +79,11 @@ public static function form(Schema $schema): Schema ->label(trans('subdomains::strings.name')) ->required() ->unique(), + TextInput::make('srv_target') + ->label(trans('subdomains::strings.srv_target')) + ->helperText(trans('subdomains::strings.srv_target_help')) + ->placeholder('play.example.com') + ->rules(['nullable', 'string', 'regex:/^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(? Date: Sun, 28 Dec 2025 13:04:33 +1100 Subject: [PATCH 04/32] add srv_record toggle to SubdomainRelationManager and SubdomainResource --- .../SubdomainRelationManager.php | 7 +++++-- .../Resources/Subdomains/SubdomainResource.php | 16 +++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php index 45de854..537fa05 100644 --- a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php +++ b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php @@ -12,6 +12,7 @@ use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\Toggle; use Filament\Notifications\Notification; use Filament\Resources\RelationManagers\RelationManager; use Filament\Schemas\Schema; @@ -83,8 +84,10 @@ public function form(Schema $schema): Schema ->relationship('domain', 'name') ->preload() ->searchable(), - Hidden::make('record_type') - ->default(fn () => is_ipv6($this->getOwnerRecord()->allocation->ip) ? 'AAAA' : 'A'), + Toggle::make('srv_record') + ->label(trans('subdomains::strings.srv_record')) + ->helperText(trans('subdomains::strings.srv_record_help')) + ->default(false), ]); } } diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index ec372b3..992fedc 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -15,10 +15,12 @@ use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\Toggle; use Filament\Resources\Resource; use Filament\Schemas\Schema; use Filament\Support\Enums\IconSize; use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Columns\ToggleColumn; use Filament\Tables\Table; class SubdomainResource extends Resource @@ -78,6 +80,9 @@ public static function table(Table $table): Table TextColumn::make('label') ->label(trans('subdomains::strings.name')) ->state(fn (Subdomain $subdomain) => $subdomain->getLabel()), + ToggleColumn::make('srv_record') + ->label(trans('subdomains::strings.srv_record')) + ->tooltip(trans('subdomains::strings.srv_record_help')), ]) ->recordActions([ EditAction::make(), @@ -111,13 +116,10 @@ public static function form(Schema $schema): Schema ->relationship('domain', 'name') ->preload() ->searchable(), - Hidden::make('record_type') - ->default(function () { - /** @var Server $server */ - $server = Filament::getTenant(); - - return is_ipv6($server->allocation->ip) ? 'AAAA' : 'A'; - }), + Toggle::make('srv_record') + ->label(trans('subdomains::strings.srv_record')) + ->helperText(trans('subdomains::strings.srv_record_help')) + ->default(false), ]); } From 2e7246634d4fc1f449a3b47d34c132fb0862ba39 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 13:06:09 +1100 Subject: [PATCH 05/32] Adding logic for srv records --- subdomains/src/Models/Subdomain.php | 158 ++++++++++++++++++++++++++-- 1 file changed, 151 insertions(+), 7 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 14cd323..a85f92f 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -27,12 +27,45 @@ class Subdomain extends Model implements HasLabel 'cloudflare_id', 'domain_id', 'server_id', + 'srv_record', + ]; + + protected $casts = [ + 'srv_record' => 'boolean', ]; protected static function boot(): void { parent::boot(); + static::saving(function (self $model) { + // If srv_record provided in the payload, ensure record_type follows it and then remove it + if (array_key_exists('srv_record', $model->attributes)) { + $srv = (bool) $model->attributes['srv_record']; + + if ($srv) { + $model->attributes['record_type'] = 'SRV'; + } else { + if ($model->server && $model->server->allocation && is_ipv6($model->server->allocation->ip)) { + $model->attributes['record_type'] = 'AAAA'; + } else { + $model->attributes['record_type'] = 'A'; + } + } + + unset($model->attributes['srv_record']); + } + + // If no record_type is present, set a sensible default based on server allocation + if (!isset($model->attributes['record_type'])) { + if ($model->server && $model->server->allocation && is_ipv6($model->server->allocation->ip)) { + $model->attributes['record_type'] = 'AAAA'; + } else { + $model->attributes['record_type'] = 'A'; + } + } + }); + static::created(function (self $model) { $model->createOnCloudflare(); }); @@ -61,21 +94,95 @@ public function getLabel(): string|Htmlable|null return $this->name . '.' . $this->domain->name; } + public function getSrvRecordAttribute(): bool + { + return $this->record_type === 'SRV'; + } + + public function setSrvRecordAttribute($value): void + { + if ($value) { + $this->attributes['record_type'] = 'SRV'; + } else { + if ($this->server && $this->server->allocation && is_ipv6($this->server->allocation->ip)) { + $this->attributes['record_type'] = 'AAAA'; + } else { + $this->attributes['record_type'] = 'A'; + } + } + } + + protected function buildSrvPayload(): ?array + { + $target = $this->domain->srv_target ?? null; + $port = $this->server && $this->server->allocation ? ($this->server->allocation->port ?? null) : null; + + if (empty($target) || empty($port)) { + return null; + } + + $priority = (int) ($this->srv_priority ?? 0); + $weight = (int) ($this->srv_weight ?? 0); + $port = (int) $port; + + // Need to build the name to include the services and protocol parts for SRV records, this may vary based on game(egg tag)/server type + // Temporrary placeholder for service and protocol = '_minecraft._tcp.' + + return [ + 'name' => sprintf('_minecraft._tcp.%s', $this->name), + 'ttl' => 1, + 'type' => 'SRV', + 'comment' => 'Created by Pelican Subdomains plugin', + 'content' => sprintf('%d %d %d %s', $priority, $weight, $port, $target), + 'proxied' => false, + 'data' => [ + 'priority' => $priority, + 'weight' => $weight, + 'port' => $port, + 'target' => $target, + ], + ]; + } + protected function createOnCloudflare(): void { + if ($this->record_type === 'SRV') { + $payload = $this->buildSrvPayload(); + + if ($payload === null) { + return; + } + + if (!$this->cloudflare_id) { + $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $payload)->json(); + + if (!empty($response['success'])) { + $dnsRecord = $response['result']; + + $this->updateQuietly([ + 'cloudflare_id' => $dnsRecord['id'], + ]); + } + } + + return; + } + if (!$this->server->allocation || $this->server->allocation->ip === '0.0.0.0' || $this->server->allocation->ip === '::') { return; } if (!$this->cloudflare_id) { - $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", [ + $body = [ 'name' => $this->name, - 'ttl' => 120, + 'ttl' => 1, 'type' => $this->record_type, 'comment' => 'Created by Pelican Subdomains plugin', 'content' => $this->server->allocation->ip, 'proxied' => false, - ])->json(); + ]; + + $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $body)->json(); if ($response['success']) { $dnsRecord = $response['result']; @@ -93,16 +200,53 @@ protected function updateOnCloudflare(): void return; } - if ($this->cloudflare_id) { - Http::cloudflare()->patch("zones/{$this->domain->cloudflare_id}/dns_records/{$this->cloudflare_id}", [ + if ($this->record_type === 'SRV') { + $payload = $this->buildSrvPayload(); + + if ($payload === null) { + return; + } + + if ($this->cloudflare_id) { + Http::cloudflare()->put("zones/{$this->domain->cloudflare_id}/dns_records/{$this->cloudflare_id}", $payload); + } else { + $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $payload)->json(); + + if (!empty($response['success'])) { + $dnsRecord = $response['result']; + + $this->updateQuietly([ + 'cloudflare_id' => $dnsRecord['id'], + ]); + } + } + + return; + } else { + $body = [ 'name' => $this->name, - 'ttl' => 120, + 'ttl' => 1, 'type' => $this->record_type, 'comment' => 'Created by Pelican Subdomains plugin', 'content' => $this->server->allocation->ip, 'proxied' => false, - ]); + ]; + + if ($this->cloudflare_id) { + Http::cloudflare()->put("zones/{$this->domain->cloudflare_id}/dns_records/{$this->cloudflare_id}", $body); + } else { + $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $body)->json(); + + if (!empty($response['success'])) { + $dnsRecord = $response['result']; + + $this->updateQuietly([ + 'cloudflare_id' => $dnsRecord['id'], + ]); + } + } } + } protected function deleteOnCloudflare(): void From f20e58eb3ac8162632c097637fc8a75229a16734 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 13:53:09 +1100 Subject: [PATCH 06/32] add error messages for missing SRV target and enhance tooltip logic in SubdomainResource --- subdomains/lang/en/strings.php | 11 +++++++++++ .../Server/Resources/Subdomains/SubdomainResource.php | 4 +++- subdomains/src/Models/Subdomain.php | 2 ++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index 511bd80..b81a42a 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -20,6 +20,17 @@ 'srv_target' => 'SRV Target', 'srv_target_help' => 'The hostname that SRV records point to (for example: play.example.com).', + 'errors' => [ + 'srv_target_missing' => 'Cannot enable SRV record because the selected domain does not have an SRV target set.', + ], + 'api_token' => 'Cloudflare API Token', 'api_token_help' => 'The token needs to have read permissions for Zone.Zone and write for Zone.Dns. For better security you can also set the "Zone Resources" to exclude certain domains and add the panel ip to the "Client IP Adress Filtering".', + + 'notifications' => [ + 'dns_created' => 'DNS record created on Cloudflare', + 'dns_updated' => 'DNS record updated on Cloudflare', + 'dns_deleted' => 'DNS record deleted from Cloudflare', + 'dns_action_failed' => 'Cloudflare DNS action failed', + ], ]; diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index 992fedc..b7f4313 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -82,7 +82,8 @@ public static function table(Table $table): Table ->state(fn (Subdomain $subdomain) => $subdomain->getLabel()), ToggleColumn::make('srv_record') ->label(trans('subdomains::strings.srv_record')) - ->tooltip(trans('subdomains::strings.srv_record_help')), + ->tooltip(fn (Subdomain $record) => $record->domain && $record->domain->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.errors.srv_target_missing')) + ->disabled(fn (Subdomain $record) => !$record->domain || empty($record->domain->srv_target)), ]) ->recordActions([ EditAction::make(), @@ -119,6 +120,7 @@ public static function form(Schema $schema): Schema Toggle::make('srv_record') ->label(trans('subdomains::strings.srv_record')) ->helperText(trans('subdomains::strings.srv_record_help')) + ->disabled(fn (callable $get) => !$get('domain_id') || empty(CloudflareDomain::find($get('domain_id'))->srv_target ?? null)) ->default(false), ]); } diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index a85f92f..b0ffe38 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -118,6 +118,8 @@ protected function buildSrvPayload(): ?array $port = $this->server && $this->server->allocation ? ($this->server->allocation->port ?? null) : null; if (empty($target) || empty($port)) { + // Output to log for debugging + Log::error('SRV record target or port is missing for Subdomain ID ' . $this->id . '. Target: ' . ($target ?? 'null') . ', Port: ' . ($port ?? 'null')); return null; } From 58de843308a8051078b6045aacf77fd610b77488 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 20:08:49 +1100 Subject: [PATCH 07/32] add zone request status messages and improve logging for missing SRV record target or port --- subdomains/lang/en/strings.php | 2 ++ subdomains/src/Models/Subdomain.php | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index b81a42a..3bc8625 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -32,5 +32,7 @@ 'dns_updated' => 'DNS record updated on Cloudflare', 'dns_deleted' => 'DNS record deleted from Cloudflare', 'dns_action_failed' => 'Cloudflare DNS action failed', + 'zone_request_failed' => 'Cloudflare zone request failed', + 'zone_request_succeeded' => 'Cloudflare zone request succeeded', ], ]; diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index b0ffe38..05a5ff9 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -118,7 +118,6 @@ protected function buildSrvPayload(): ?array $port = $this->server && $this->server->allocation ? ($this->server->allocation->port ?? null) : null; if (empty($target) || empty($port)) { - // Output to log for debugging Log::error('SRV record target or port is missing for Subdomain ID ' . $this->id . '. Target: ' . ($target ?? 'null') . ', Port: ' . ($port ?? 'null')); return null; } From 2ff7a9956ac1d7e2bfe9d9705eaf088681bb7dc9 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 22:39:59 +1100 Subject: [PATCH 08/32] add CloudflareService class for managing DNS records with upsert and delete functionality --- subdomains/src/Services/CloudflareService.php | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 subdomains/src/Services/CloudflareService.php diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php new file mode 100644 index 0000000..6b73b24 --- /dev/null +++ b/subdomains/src/Services/CloudflareService.php @@ -0,0 +1,187 @@ +get("zones/{$zoneId}/dns_records", [ + 'name' => $name, + 'type' => $type, + 'per_page' => 1, + ]); + } catch (\Throwable $e) { + Log::error('Cloudflare findDnsRecordId request failed: ' . $e->getMessage(), ['zone' => $zoneId, 'name' => $name, 'type' => $type]); + return null; + } + + $status = $response->status(); + $body = $response->json() ?? []; + + if ($response->successful() && !empty($body['result']) && count($body['result']) > 0) { + return $body['result'][0]['id'] ?? null; + } + + if (!empty($body['errors'])) { + Log::warning('Cloudflare findDnsRecordId returned errors', ['zone' => $zoneId, 'name' => $name, 'type' => $type, 'status' => $status, 'errors' => $body['errors']]); + } + + return null; + } + + public function upsertDnsRecord( + string $zoneId, + string $name, + string $recordType, + string $target = null, + ?int $port = null, + ): array + { + if (empty($zoneId) || empty($name) || empty($recordType)) { + Log::error('Cloudflare upsertDnsRecord missing required parameters', ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); + return ['success' => false, 'id' => null, 'errors' => ['missing_parameters' => true], 'status' => 0, 'body' => null]; + } + + // Hardcoded/derived defaults + $priority = 0; + $weight = 0; + $ttl = 1; + $comment = 'Created by Pelican Subdomains plugin'; + $proxied = false; + + // Build payload based on type + if ($recordType === 'SRV') { + if (empty($port) || empty($target)) { + Log::error('Cloudflare upsert missing SRV target or port', ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); + return ['success' => false, 'id' => null, 'errors' => ['missing_srv_target_or_port' => true], 'status' => 0, 'body' => null]; + } + + $payload = [ + 'name' => sprintf('_minecraft._tcp.%s', $name), + 'ttl' => $ttl, + 'type' => 'SRV', + 'comment' => $comment, + 'content' => sprintf('%d %d %d %s', $priority, $weight, $port, $target), + 'proxied' => $proxied, + 'data' => [ + 'priority' => $priority, + 'weight' => $weight, + 'port' => (int) $port, + 'target' => $target, + ], + ]; + } else { + $ip = $content ?? $target; + + if (empty($ip)) { + Log::error('Cloudflare upsert missing IP/content for record type ' . $recordType, ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); + return ['success' => false, 'id' => null, 'errors' => ['missing_ip' => true], 'status' => 0, 'body' => null]; + } + + $payload = [ + 'name' => $name, + 'ttl' => $ttl, + 'type' => $recordType, + 'comment' => $comment, + 'content' => $target, + 'proxied' => $proxied, + ]; + } + + $existingId = $this->findDnsRecordId($zoneId, $payload['name'], $payload['type']); + + try { + if ($existingId) { + $response = Http::cloudflare()->put("zones/{$zoneId}/dns_records/{$existingId}", $payload); + $parsed = $this->parseCloudflareHttpResponse($response); + + if ($parsed['success']) { + return $parsed; + } + + Log::error('Cloudflare update failed', ['zone' => $zoneId, 'id' => $existingId, 'response' => $parsed]); + return $parsed; + } + + $response = Http::cloudflare()->post("zones/{$zoneId}/dns_records", $payload); + $parsed = $this->parseCloudflareHttpResponse($response); + + if ($parsed['success'] && !empty($parsed['id'])) { + return $parsed; + } + + Log::error('Cloudflare create failed', ['zone' => $zoneId, 'payload' => $payload, 'response' => $parsed]); + return $parsed; + } catch (\Throwable $e) { + Log::error('Cloudflare upsert exception: ' . $e->getMessage(), ['zone' => $zoneId, 'payload' => $payload]); + return ['success' => false, 'id' => null, 'errors' => ['exception' => $e->getMessage()], 'status' => 0, 'body' => null]; + } + } + + + protected function parseCloudflareResponse(array $response): array + { + return [ + 'success' => !empty($response['success']), + 'id' => $response['result']['id'] ?? null, + 'result' => $response['result'] ?? null, + 'errors' => $response['errors'] ?? [], + ]; + } + + /** + * Parse a Response object into a normalized structure with HTTP status and body. + */ + protected function parseCloudflareHttpResponse(Response $response): array + { + $status = $response->status(); + $body = $response->json() ?? []; + + $success = $response->successful() && (!empty($body['success']) || !empty($body['result'])); + + return [ + 'success' => $success, + 'id' => $body['result']['id'] ?? null, + 'errors' => $body['errors'] ?? [], + 'status' => $status, + 'body' => $body, + ]; + } + + + /** + * Delete a DNS record by id. Returns a detailed response array. + */ + public function deleteDnsRecord(string $zoneId, string $recordId): array + { + if (empty($zoneId) || empty($recordId)) { + return ['success' => false, 'errors' => ['missing_parameters' => true], 'status' => 0, 'body' => null]; + } + + try { + $response = Http::cloudflare()->delete("zones/{$zoneId}/dns_records/{$recordId}"); + + $parsed = $this->parseCloudflareHttpResponse($response); + + if ($parsed['success']) { + return $parsed; + } + + Log::error('Cloudflare delete failed', ['zone' => $zoneId, 'id' => $recordId, 'response' => $parsed]); + return $parsed; + } catch (\Throwable $e) { + Log::error('Cloudflare delete exception: ' . $e->getMessage(), ['zone' => $zoneId, 'id' => $recordId]); + return ['success' => false, 'errors' => ['exception' => $e->getMessage()], 'status' => 0, 'body' => null]; + } + } +} From 15721872d7477e2bb85de50d94ca30417a43887c Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 22:49:38 +1100 Subject: [PATCH 09/32] cleanup --- subdomains/src/Services/CloudflareService.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index 6b73b24..15b0307 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -43,7 +43,7 @@ public function upsertDnsRecord( string $zoneId, string $name, string $recordType, - string $target = null, + string $target, ?int $port = null, ): array { @@ -139,9 +139,6 @@ protected function parseCloudflareResponse(array $response): array ]; } - /** - * Parse a Response object into a normalized structure with HTTP status and body. - */ protected function parseCloudflareHttpResponse(Response $response): array { $status = $response->status(); @@ -159,9 +156,6 @@ protected function parseCloudflareHttpResponse(Response $response): array } - /** - * Delete a DNS record by id. Returns a detailed response array. - */ public function deleteDnsRecord(string $zoneId, string $recordId): array { if (empty($zoneId) || empty($recordId)) { From eb1a0ebbb28b5faf6b34793e99350a08eedb7be7 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 22:50:18 +1100 Subject: [PATCH 10/32] Remade script to utilize upsert with better error handling --- subdomains/src/Models/Subdomain.php | 196 ++++++++++++---------------- 1 file changed, 80 insertions(+), 116 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 05a5ff9..514dc0c 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -8,6 +8,9 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Log; +use Filament\Notifications\Notification; +use Boy132\Subdomains\Services\CloudflareService; /** * @property int $id @@ -66,12 +69,8 @@ protected static function boot(): void } }); - static::created(function (self $model) { - $model->createOnCloudflare(); - }); - - static::updated(function (self $model) { - $model->updateOnCloudflare(); + static::saved(function (self $model) { + $model->upsertOnCloudflare(); }); static::deleted(function (self $model) { @@ -99,6 +98,8 @@ public function getSrvRecordAttribute(): bool return $this->record_type === 'SRV'; } + + public function setSrvRecordAttribute($value): void { if ($value) { @@ -112,148 +113,111 @@ public function setSrvRecordAttribute($value): void } } - protected function buildSrvPayload(): ?array + + + protected function upsertOnCloudflare(): void { - $target = $this->domain->srv_target ?? null; - $port = $this->server && $this->server->allocation ? ($this->server->allocation->port ?? null) : null; + $service = app(CloudflareService::class); - if (empty($target) || empty($port)) { - Log::error('SRV record target or port is missing for Subdomain ID ' . $this->id . '. Target: ' . ($target ?? 'null') . ', Port: ' . ($port ?? 'null')); - return null; - } + $zoneId = $this->domain->cloudflare_id; + if (empty($zoneId)) { + Log::warning('Cloudflare zone id missing for domain', ['domain_id' => $this->domain_id]); - $priority = (int) ($this->srv_priority ?? 0); - $weight = (int) ($this->srv_weight ?? 0); - $port = (int) $port; - - // Need to build the name to include the services and protocol parts for SRV records, this may vary based on game(egg tag)/server type - // Temporrary placeholder for service and protocol = '_minecraft._tcp.' - - return [ - 'name' => sprintf('_minecraft._tcp.%s', $this->name), - 'ttl' => 1, - 'type' => 'SRV', - 'comment' => 'Created by Pelican Subdomains plugin', - 'content' => sprintf('%d %d %d %s', $priority, $weight, $port, $target), - 'proxied' => false, - 'data' => [ - 'priority' => $priority, - 'weight' => $weight, - 'port' => $port, - 'target' => $target, - ], - ]; - } + Notification::make() + ->danger() + ->title('Cloudflare: Missing Zone ID') + ->body(sprintf('Cloudflare zone ID is not configured for %s. Cannot upsert DNS record for %s.%s.', $this->domain->name ?? 'unknown', $this->name, $this->domain->name ?? 'unknown')) + ->send(); - protected function createOnCloudflare(): void - { + return; + } + + // SRV: target comes from domain, port from server allocation if ($this->record_type === 'SRV') { - $payload = $this->buildSrvPayload(); + $port = $this->server && $this->server->allocation ? ($this->server->allocation->port ?? null) : null; + + if (empty($port)) { + Log::warning('Server missing allocation with port', ['server_id' => $this->server_id]); + + Notification::make() + ->danger() + ->title('Cloudflare: Missing SRV Port') + ->body(sprintf('SRV target or port is missing for %s.%s. Cannot upsert SRV record.', $this->name, $this->domain->name ?? 'unknown')) + ->send(); - if ($payload === null) { return; } - if (!$this->cloudflare_id) { - $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $payload)->json(); - - if (!empty($response['success'])) { - $dnsRecord = $response['result']; + $result = $service->upsertDnsRecord($zoneId, $this->name, 'SRV', $this->domain->srv_target, $port); - $this->updateQuietly([ - 'cloudflare_id' => $dnsRecord['id'], - ]); + if ($result['success'] && !empty($result['id'])) { + if ($this->cloudflare_id !== $result['id']) { + $this->updateQuietly(['cloudflare_id' => $result['id']]); } + } else { + Log::error('Failed to upsert SRV record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); + + Notification::make() + ->danger() + ->title('Cloudflare: SRV upsert failed') + ->body('Failed to upsert SRV record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->send(); } return; } + // A/AAAA if (!$this->server->allocation || $this->server->allocation->ip === '0.0.0.0' || $this->server->allocation->ip === '::') { return; } - if (!$this->cloudflare_id) { - $body = [ - 'name' => $this->name, - 'ttl' => 1, - 'type' => $this->record_type, - 'comment' => 'Created by Pelican Subdomains plugin', - 'content' => $this->server->allocation->ip, - 'proxied' => false, - ]; + $result = $service->upsertDnsRecord($zoneId, $this->name, $this->record_type, $ip, null, null, $this->domain->name); - $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $body)->json(); + if ($result['success'] && !empty($result['id'])) { + if ($this->cloudflare_id !== $result['id']) { + $this->updateQuietly(['cloudflare_id' => $result['id']]); + } + } else { + Log::error('Failed to upsert record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); - if ($response['success']) { - $dnsRecord = $response['result']; + $domainName = $this->domain->name ?? 'unknown'; + $sub = sprintf('%s.%s', $this->name, $domainName); - $this->updateQuietly([ - 'cloudflare_id' => $dnsRecord['id'], - ]); - } + Notification::make() + ->danger() + ->title('Cloudflare: Upsert failed') + ->body('Failed to upsert record for ' . $sub . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->send(); } } - protected function updateOnCloudflare(): void - { - if (!$this->server->allocation || $this->server->allocation->ip === '0.0.0.0' || $this->server->allocation->ip === '::') { - return; - } - - if ($this->record_type === 'SRV') { - $payload = $this->buildSrvPayload(); - if ($payload === null) { - return; - } - if ($this->cloudflare_id) { - Http::cloudflare()->put("zones/{$this->domain->cloudflare_id}/dns_records/{$this->cloudflare_id}", $payload); - } else { - $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $payload)->json(); - - if (!empty($response['success'])) { - $dnsRecord = $response['result']; + protected function deleteOnCloudflare(): void + { + if ($this->cloudflare_id && $this->domain && $this->domain->cloudflare_id) { + $service = app(CloudflareService::class); - $this->updateQuietly([ - 'cloudflare_id' => $dnsRecord['id'], - ]); - } - } + $result = $service->deleteDnsRecord($this->domain->cloudflare_id, $this->cloudflare_id); - return; - } else { - $body = [ - 'name' => $this->name, - 'ttl' => 1, - 'type' => $this->record_type, - 'comment' => 'Created by Pelican Subdomains plugin', - 'content' => $this->server->allocation->ip, - 'proxied' => false, - ]; - - if ($this->cloudflare_id) { - Http::cloudflare()->put("zones/{$this->domain->cloudflare_id}/dns_records/{$this->cloudflare_id}", $body); + if (!empty($result['success'])) { + Log::info('Deleted Cloudflare record for Subdomain ID ' . $this->id, ['result' => $result]); } else { - $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $body)->json(); - - if (!empty($response['success'])) { - $dnsRecord = $response['result']; - - $this->updateQuietly([ - 'cloudflare_id' => $dnsRecord['id'], - ]); - } + Log::warning('Failed to delete Cloudflare record for Subdomain ID ' . $this->id, ['result' => $result]); + + Notification::make() + ->danger() + ->title('Cloudflare: Delete failed') + ->body('Failed to delete Cloudflare record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->send(); + + Notification::make() + ->danger() + ->title('Cloudflare: Upsert failed') + ->body('Failed to upsert record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->send(); } } - - } - - protected function deleteOnCloudflare(): void - { - if ($this->cloudflare_id) { - Http::cloudflare()->delete("zones/{$this->domain->cloudflare_id}/dns_records/{$this->cloudflare_id}"); - } } } From 6951bc70c44ce21109d11bf73143f7a6661eb5e1 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 08:11:36 +1100 Subject: [PATCH 11/32] Update to CloudflareDomain model to fetch Cloudflare Zone ID with notifications on success or failure --- .../Pages/ManageCloudflareDomains.php | 3 +- subdomains/src/Models/CloudflareDomain.php | 38 +++++++-------- subdomains/src/Services/CloudflareService.php | 46 ++++++++++++++++--- 3 files changed, 62 insertions(+), 25 deletions(-) diff --git a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/Pages/ManageCloudflareDomains.php b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/Pages/ManageCloudflareDomains.php index 238e6fe..7a0dbf4 100644 --- a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/Pages/ManageCloudflareDomains.php +++ b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/Pages/ManageCloudflareDomains.php @@ -14,7 +14,8 @@ protected function getHeaderActions(): array { return [ CreateAction::make() - ->createAnother(false), + ->createAnother(false) + ->successNotification(null), ]; } } diff --git a/subdomains/src/Models/CloudflareDomain.php b/subdomains/src/Models/CloudflareDomain.php index c510959..134e417 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -5,6 +5,8 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Facades\Http; +use Boy132\Subdomains\Services\CloudflareService; +use Filament\Notifications\Notification; /** * @property int $id @@ -25,7 +27,24 @@ protected static function boot(): void parent::boot(); static::created(function (self $model) { - $model->fetchCloudflareId(); + $service = new CloudflareService(); + + $zoneId = $service->getZoneId($model->name); + if (!$zoneId) { + Notification::make() + ->title('Failed to fetch Cloudflare Zone ID for domain: ' . $model->name) + ->danger() + ->send(); + } + + Notification::make() + ->title('Successfully saved domain: ' . $model->name) + ->success() + ->send(); + + $model->update([ + 'cloudflare_id' => $zoneId, + ]); }); } @@ -33,21 +52,4 @@ public function subdomains(): HasMany { return $this->hasMany(Subdomain::class, 'domain_id'); } - - public function fetchCloudflareId(): void - { - $response = Http::cloudflare()->get('zones', [ - 'name' => $this->name, - ])->json(); - - if ($response['success']) { - $zones = $response['result']; - - if (count($zones) > 0) { - $this->update([ - 'cloudflare_id' => $zones[0]['id'], - ]); - } - } - } } diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index 15b0307..0bfa7d9 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -8,23 +8,57 @@ class CloudflareService { - public function findDnsRecordId(string $zoneId, string $name, string $type): ?string + public function getZoneId(string $domainName): ?string { - if (empty($zoneId)) { + if (empty($domainName)) { + Log::error('Cloudflare getZoneId called with empty domain name', ['domain' => $domainName]); return null; } try { - $response = Http::cloudflare()->get("zones/{$zoneId}/dns_records", [ + $response = Http::cloudflare()->get('zones', [ + 'name' => $domainName, + ]); + } catch (\Throwable $e) { + Log::error('Cloudflare getZoneId request failed: ' . $e->getMessage(), ['domain' => $domainName]); + return null; + } + + $status = $response->status(); + $body = $response->json() ?? []; + + if ($response->successful() && !empty($body['result']) && count($body['result']) > 0) { + return $body['result'][0]['id'] ?? null; + } + + if (!empty($body['errors'])) { + Log::warning('Cloudflare getZoneId returned errors', ['domain' => $domainName, 'status' => $status, 'errors' => $body['errors']]); + } + + return null; + } + + public function getDnsRecordId(string $zoneId, string $name, string $type): ?string + { + if (empty($zoneId) || empty($name)) { + return null; + } + + try { + $queryParams = [ 'name' => $name, 'type' => $type, 'per_page' => 1, - ]); + ]; + + $response = Http::cloudflare()->get("zones/{$zoneId}/dns_records"); } catch (\Throwable $e) { - Log::error('Cloudflare findDnsRecordId request failed: ' . $e->getMessage(), ['zone' => $zoneId, 'name' => $name, 'type' => $type]); + Log::error('Cloudflare getDnsRecordId request failed: ' . $e->getMessage(), ['zone' => $zoneId, 'name' => $name, 'type' => $type]); return null; } + log::warning('record response: '.$response); // !remove + log::warning($payload); // !remove $status = $response->status(); $body = $response->json() ?? []; @@ -33,7 +67,7 @@ public function findDnsRecordId(string $zoneId, string $name, string $type): ?st } if (!empty($body['errors'])) { - Log::warning('Cloudflare findDnsRecordId returned errors', ['zone' => $zoneId, 'name' => $name, 'type' => $type, 'status' => $status, 'errors' => $body['errors']]); + Log::warning('Cloudflare getDnsRecordId returned errors', ['zone' => $zoneId, 'name' => $name, 'type' => $type, 'status' => $status, 'errors' => $body['errors']]); } return null; From f18101a755d2068dc2f82ba4ae7268e153118a7f Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 08:40:41 +1100 Subject: [PATCH 12/32] Changed upsert to require a recordId to be known (prevents changing records created outside plugin) --- subdomains/src/Models/Subdomain.php | 4 +- subdomains/src/Services/CloudflareService.php | 44 ++----------------- 2 files changed, 6 insertions(+), 42 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 514dc0c..01aaf4e 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -148,7 +148,7 @@ protected function upsertOnCloudflare(): void return; } - $result = $service->upsertDnsRecord($zoneId, $this->name, 'SRV', $this->domain->srv_target, $port); + $result = $service->upsertDnsRecord($zoneId, $this->name, 'SRV', $this->domain->srv_target, $this->cloudflare_id ?? null, $port); if ($result['success'] && !empty($result['id'])) { if ($this->cloudflare_id !== $result['id']) { @@ -172,7 +172,7 @@ protected function upsertOnCloudflare(): void return; } - $result = $service->upsertDnsRecord($zoneId, $this->name, $this->record_type, $ip, null, null, $this->domain->name); + $result = $service->upsertDnsRecord($zoneId, $this->name, $this->record_type, $this->server->allocation->ip, $this->cloudflare_id ?? null, null); if ($result['success'] && !empty($result['id'])) { if ($this->cloudflare_id !== $result['id']) { diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index 0bfa7d9..9fb0b99 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -38,46 +38,12 @@ public function getZoneId(string $domainName): ?string return null; } - public function getDnsRecordId(string $zoneId, string $name, string $type): ?string - { - if (empty($zoneId) || empty($name)) { - return null; - } - - try { - $queryParams = [ - 'name' => $name, - 'type' => $type, - 'per_page' => 1, - ]; - - $response = Http::cloudflare()->get("zones/{$zoneId}/dns_records"); - } catch (\Throwable $e) { - Log::error('Cloudflare getDnsRecordId request failed: ' . $e->getMessage(), ['zone' => $zoneId, 'name' => $name, 'type' => $type]); - return null; - } - - log::warning('record response: '.$response); // !remove - log::warning($payload); // !remove - $status = $response->status(); - $body = $response->json() ?? []; - - if ($response->successful() && !empty($body['result']) && count($body['result']) > 0) { - return $body['result'][0]['id'] ?? null; - } - - if (!empty($body['errors'])) { - Log::warning('Cloudflare getDnsRecordId returned errors', ['zone' => $zoneId, 'name' => $name, 'type' => $type, 'status' => $status, 'errors' => $body['errors']]); - } - - return null; - } - public function upsertDnsRecord( string $zoneId, string $name, string $recordType, string $target, + ?string $recordId = null, ?int $port = null, ): array { @@ -132,18 +98,16 @@ public function upsertDnsRecord( ]; } - $existingId = $this->findDnsRecordId($zoneId, $payload['name'], $payload['type']); - try { - if ($existingId) { - $response = Http::cloudflare()->put("zones/{$zoneId}/dns_records/{$existingId}", $payload); + if ($recordId) { + $response = Http::cloudflare()->put("zones/{$zoneId}/dns_records/{$recordId}", $payload); $parsed = $this->parseCloudflareHttpResponse($response); if ($parsed['success']) { return $parsed; } - Log::error('Cloudflare update failed', ['zone' => $zoneId, 'id' => $existingId, 'response' => $parsed]); + Log::error('Cloudflare update failed', ['zone' => $zoneId, 'recordId' => $recordId, 'response' => $parsed]); return $parsed; } From 58f73e170f629d206d582f0a33961dd20ba618ad Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 08:41:19 +1100 Subject: [PATCH 13/32] Cleanup --- subdomains/src/Models/Subdomain.php | 31 ++----------------- subdomains/src/Services/CloudflareService.php | 18 ----------- 2 files changed, 3 insertions(+), 46 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 01aaf4e..9b1342a 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -42,38 +42,15 @@ protected static function boot(): void parent::boot(); static::saving(function (self $model) { - // If srv_record provided in the payload, ensure record_type follows it and then remove it if (array_key_exists('srv_record', $model->attributes)) { - $srv = (bool) $model->attributes['srv_record']; - - if ($srv) { - $model->attributes['record_type'] = 'SRV'; - } else { - if ($model->server && $model->server->allocation && is_ipv6($model->server->allocation->ip)) { - $model->attributes['record_type'] = 'AAAA'; - } else { - $model->attributes['record_type'] = 'A'; - } - } - + $model->setRecordType($model->attributes['srv_record']); unset($model->attributes['srv_record']); } - // If no record_type is present, set a sensible default based on server allocation - if (!isset($model->attributes['record_type'])) { - if ($model->server && $model->server->allocation && is_ipv6($model->server->allocation->ip)) { - $model->attributes['record_type'] = 'AAAA'; - } else { - $model->attributes['record_type'] = 'A'; - } - } - }); - - static::saved(function (self $model) { $model->upsertOnCloudflare(); }); - static::deleted(function (self $model) { + static::deleting(function (self $model) { $model->deleteOnCloudflare(); }); } @@ -98,9 +75,7 @@ public function getSrvRecordAttribute(): bool return $this->record_type === 'SRV'; } - - - public function setSrvRecordAttribute($value): void + public function setRecordType($value): void { if ($value) { $this->attributes['record_type'] = 'SRV'; diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index 9fb0b99..f683714 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -81,13 +81,6 @@ public function upsertDnsRecord( ], ]; } else { - $ip = $content ?? $target; - - if (empty($ip)) { - Log::error('Cloudflare upsert missing IP/content for record type ' . $recordType, ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); - return ['success' => false, 'id' => null, 'errors' => ['missing_ip' => true], 'status' => 0, 'body' => null]; - } - $payload = [ 'name' => $name, 'ttl' => $ttl, @@ -126,17 +119,6 @@ public function upsertDnsRecord( } } - - protected function parseCloudflareResponse(array $response): array - { - return [ - 'success' => !empty($response['success']), - 'id' => $response['result']['id'] ?? null, - 'result' => $response['result'] ?? null, - 'errors' => $response['errors'] ?? [], - ]; - } - protected function parseCloudflareHttpResponse(Response $response): array { $status = $response->status(); From 89f45b8338ce9a6e48de17d8c1074d2d1410b7dd Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 08:41:32 +1100 Subject: [PATCH 14/32] Fixing notifications --- subdomains/src/Models/Subdomain.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 9b1342a..3980389 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -129,6 +129,12 @@ protected function upsertOnCloudflare(): void if ($this->cloudflare_id !== $result['id']) { $this->updateQuietly(['cloudflare_id' => $result['id']]); } + + Notification::make() + ->success() + ->title('Cloudflare: Record updated') + ->body('Successfully updated ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . ' to '. $this->record_type) + ->send(); } else { Log::error('Failed to upsert SRV record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); @@ -144,6 +150,14 @@ protected function upsertOnCloudflare(): void // A/AAAA if (!$this->server->allocation || $this->server->allocation->ip === '0.0.0.0' || $this->server->allocation->ip === '::') { + Log::warning('Server allocation missing or invalid IP', ['server_id' => $this->server_id]); + + Notification::make() + ->danger() + ->title('Cloudflare: Missing IP') + ->body(sprintf('Server allocation IP is missing or invalid for %s.%s. Cannot upsert A/AAAA record.', $this->name, $this->domain->name ?? 'unknown')) + ->send(); + return; } @@ -153,6 +167,12 @@ protected function upsertOnCloudflare(): void if ($this->cloudflare_id !== $result['id']) { $this->updateQuietly(['cloudflare_id' => $result['id']]); } + + Notification::make() + ->success() + ->title('Cloudflare: Record updated') + ->body('Successfully updated ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . ' to '. $this->record_type) + ->send(); } else { Log::error('Failed to upsert record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); From d369cc3039027f649507bdfcc2f42caac2507949 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 09:57:36 +1100 Subject: [PATCH 15/32] - Fixed issue with create record as SRV - Fixed potential potential NPE - Corrected some notifications --- subdomains/src/Models/Subdomain.php | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 3980389..a78125f 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -46,11 +46,13 @@ protected static function boot(): void $model->setRecordType($model->attributes['srv_record']); unset($model->attributes['srv_record']); } + }); + static::saved(function (self $model) { $model->upsertOnCloudflare(); }); - static::deleting(function (self $model) { + static::deleted(function (self $model) { $model->deleteOnCloudflare(); }); } @@ -88,8 +90,6 @@ public function setRecordType($value): void } } - - protected function upsertOnCloudflare(): void { $service = app(CloudflareService::class); @@ -112,7 +112,7 @@ protected function upsertOnCloudflare(): void $port = $this->server && $this->server->allocation ? ($this->server->allocation->port ?? null) : null; if (empty($port)) { - Log::warning('Server missing allocation with port', ['server_id' => $this->server_id]); + Log::warning('Server missing allocation with port', $this->toArray()); Notification::make() ->danger() @@ -149,7 +149,7 @@ protected function upsertOnCloudflare(): void } // A/AAAA - if (!$this->server->allocation || $this->server->allocation->ip === '0.0.0.0' || $this->server->allocation->ip === '::') { + if (!$this->server || !$this->server->allocation || $this->server->allocation->ip === '0.0.0.0' || $this->server->allocation->ip === '::') { Log::warning('Server allocation missing or invalid IP', ['server_id' => $this->server_id]); Notification::make() @@ -187,8 +187,6 @@ protected function upsertOnCloudflare(): void } } - - protected function deleteOnCloudflare(): void { if ($this->cloudflare_id && $this->domain && $this->domain->cloudflare_id) { @@ -197,22 +195,18 @@ protected function deleteOnCloudflare(): void $result = $service->deleteDnsRecord($this->domain->cloudflare_id, $this->cloudflare_id); if (!empty($result['success'])) { - Log::info('Deleted Cloudflare record for Subdomain ID ' . $this->id, ['result' => $result]); - } else { - Log::warning('Failed to delete Cloudflare record for Subdomain ID ' . $this->id, ['result' => $result]); - Notification::make() ->danger() - ->title('Cloudflare: Delete failed') - ->body('Failed to delete Cloudflare record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->title('Cloudflare: Delete successed') + ->body('Successfully deleted Cloudflare record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '.') ->send(); + } Notification::make() ->danger() - ->title('Cloudflare: Upsert failed') - ->body('Failed to upsert record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->title('Cloudflare: Delete failed') + ->body('Failed to delete Cloudflare record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) ->send(); - } } } } From 7d1b33a62916c2a9c248129d4e1caec697064d71 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 09:57:55 +1100 Subject: [PATCH 16/32] Remove unused Hidden component --- .../Servers/RelationManagers/SubdomainRelationManager.php | 1 - 1 file changed, 1 deletion(-) diff --git a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php index 537fa05..ab5c570 100644 --- a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php +++ b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php @@ -9,7 +9,6 @@ use Filament\Actions\CreateAction; use Filament\Actions\DeleteAction; use Filament\Actions\EditAction; -use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; From 910b812cd1bc86d08275042503a79d2ed3c777b7 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 09:59:31 +1100 Subject: [PATCH 17/32] - Added default for subdomain selection - Removed inbuilt notifications in favor of service notifications --- .../Server/Resources/Subdomains/SubdomainResource.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index b7f4313..bc3ba9c 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -12,7 +12,6 @@ use Filament\Actions\DeleteAction; use Filament\Actions\EditAction; use Filament\Facades\Filament; -use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; @@ -87,7 +86,8 @@ public static function table(Table $table): Table ]) ->recordActions([ EditAction::make(), - DeleteAction::make(), + DeleteAction::make() + ->successNotification(null), ]) ->toolbarActions([ CreateAction::make() @@ -98,6 +98,7 @@ public static function table(Table $table): Table ->createAnother(false) ->hiddenLabel() ->iconButton() + ->successNotification(null) ->iconSize(IconSize::ExtraLarge), ]); } @@ -116,11 +117,12 @@ public static function form(Schema $schema): Schema ->required() ->relationship('domain', 'name') ->preload() + ->default(fn () => CloudflareDomain::first()?->id ?? null) ->searchable(), Toggle::make('srv_record') ->label(trans('subdomains::strings.srv_record')) ->helperText(trans('subdomains::strings.srv_record_help')) - ->disabled(fn (callable $get) => !$get('domain_id') || empty(CloudflareDomain::find($get('domain_id'))->srv_target ?? null)) + ->disabled(false) // TODO: Dynamically disable if domain missing srv_target ->default(false), ]); } From b42370d97398c0152ccb81ef2b55d0e7706d2bc6 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:00:34 +1100 Subject: [PATCH 18/32] - Fixed erroneous success logic - Fixed Style/unused class issues --- subdomains/src/Models/CloudflareDomain.php | 23 +++++++++---------- subdomains/src/Services/CloudflareService.php | 1 - 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/subdomains/src/Models/CloudflareDomain.php b/subdomains/src/Models/CloudflareDomain.php index 134e417..4602745 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -2,11 +2,10 @@ namespace Boy132\Subdomains\Models; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Support\Facades\Http; use Boy132\Subdomains\Services\CloudflareService; use Filament\Notifications\Notification; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; /** * @property int $id @@ -35,16 +34,16 @@ protected static function boot(): void ->title('Failed to fetch Cloudflare Zone ID for domain: ' . $model->name) ->danger() ->send(); - } - - Notification::make() - ->title('Successfully saved domain: ' . $model->name) - ->success() - ->send(); + } else { + Notification::make() + ->title('Successfully saved domain: ' . $model->name) + ->success() + ->send(); - $model->update([ - 'cloudflare_id' => $zoneId, - ]); + $model->update([ + 'cloudflare_id' => $zoneId, + ]); + } }); } diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index f683714..6832418 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -135,7 +135,6 @@ protected function parseCloudflareHttpResponse(Response $response): array ]; } - public function deleteDnsRecord(string $zoneId, string $recordId): array { if (empty($zoneId) || empty($recordId)) { From ea4e6023d91f9a9675971c4b2c7f5c4798cf258f Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:08:43 +1100 Subject: [PATCH 19/32] Removed unused classes --- subdomains/src/Models/Subdomain.php | 5 ++--- subdomains/src/Services/CloudflareService.php | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index a78125f..e598a45 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -3,14 +3,13 @@ namespace Boy132\Subdomains\Models; use App\Models\Server; +use Boy132\Subdomains\Services\CloudflareService; +use Filament\Notifications\Notification; use Filament\Support\Contracts\HasLabel; use Illuminate\Contracts\Support\Htmlable; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; -use Filament\Notifications\Notification; -use Boy132\Subdomains\Services\CloudflareService; /** * @property int $id diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index 6832418..dcbd5dc 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -2,9 +2,9 @@ namespace Boy132\Subdomains\Services; +use Illuminate\Http\Client\Response; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; -use Illuminate\Http\Client\Response; class CloudflareService { From 66d4384507275664b1060f9631adfae683bc237b Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:22:24 +1100 Subject: [PATCH 20/32] lang support --- subdomains/lang/en/strings.php | 28 +++++++++++++ .../Subdomains/SubdomainResource.php | 3 +- subdomains/src/Models/CloudflareDomain.php | 4 +- subdomains/src/Models/Subdomain.php | 39 ++++++++++--------- subdomains/src/SubdomainsPlugin.php | 2 +- 5 files changed, 53 insertions(+), 23 deletions(-) diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index 3bc8625..796ef63 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -34,5 +34,33 @@ 'dns_action_failed' => 'Cloudflare DNS action failed', 'zone_request_failed' => 'Cloudflare zone request failed', 'zone_request_succeeded' => 'Cloudflare zone request succeeded', + + 'cloudflare_missing_zone_title' => 'Cloudflare: Missing Zone ID', + 'cloudflare_missing_zone' => 'Cloudflare zone ID is not configured for :domain. Cannot upsert DNS record for :subdomain.', + + 'cloudflare_missing_srv_port_title' => 'Cloudflare: Missing SRV Port', + 'cloudflare_missing_srv_port' => 'SRV target or port is missing for :subdomain. Cannot upsert SRV record.', + + 'cloudflare_record_updated_title' => 'Cloudflare: Record updated', + 'cloudflare_record_updated' => 'Successfully updated :subdomain to :record_type', + + 'cloudflare_srv_upsert_failed_title' => 'Cloudflare: SRV upsert failed', + 'cloudflare_srv_upsert_failed' => 'Failed to upsert SRV record for :subdomain. See logs for details. Errors: :errors', + + 'cloudflare_missing_ip_title' => 'Cloudflare: Missing IP', + 'cloudflare_missing_ip' => 'Server allocation IP is missing or invalid for :subdomain. Cannot upsert A/AAAA record.', + + 'cloudflare_upsert_failed_title' => 'Cloudflare: Upsert failed', + 'cloudflare_upsert_failed' => 'Failed to upsert record for :subdomain. See logs for details. Errors: :errors', + + 'cloudflare_delete_success_title' => 'Cloudflare: Record deleted', + 'cloudflare_delete_success' => 'Successfully deleted Cloudflare record for :subdomain.', + + 'cloudflare_delete_failed_title' => 'Cloudflare: Delete failed', + 'cloudflare_delete_failed' => 'Failed to delete Cloudflare record for :subdomain. See logs for details. Errors: :errors', + + 'cloudflare_zone_fetch_failed' => 'Failed to fetch Cloudflare Zone ID for domain: :domain', + 'cloudflare_domain_saved' => 'Successfully saved domain: :domain', + 'settings_saved' => 'Settings saved', ], ]; diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index bc3ba9c..3b8c25f 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -81,11 +81,12 @@ public static function table(Table $table): Table ->state(fn (Subdomain $subdomain) => $subdomain->getLabel()), ToggleColumn::make('srv_record') ->label(trans('subdomains::strings.srv_record')) - ->tooltip(fn (Subdomain $record) => $record->domain && $record->domain->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.errors.srv_target_missing')) + ->tooltip(fn (Subdomain $record) => $record->domain && $record->domain->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.srv_target_missing')) ->disabled(fn (Subdomain $record) => !$record->domain || empty($record->domain->srv_target)), ]) ->recordActions([ EditAction::make(), + ->successNotification(null), DeleteAction::make() ->successNotification(null), ]) diff --git a/subdomains/src/Models/CloudflareDomain.php b/subdomains/src/Models/CloudflareDomain.php index 4602745..1dbd87c 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -31,12 +31,12 @@ protected static function boot(): void $zoneId = $service->getZoneId($model->name); if (!$zoneId) { Notification::make() - ->title('Failed to fetch Cloudflare Zone ID for domain: ' . $model->name) + ->title(trans('subdomains::strings.notifications.cloudflare_zone_fetch_failed', ['domain' => $model->name])) ->danger() ->send(); } else { Notification::make() - ->title('Successfully saved domain: ' . $model->name) + ->title(trans('subdomains::strings.notifications.cloudflare_domain_saved', ['domain' => $model->name])) ->success() ->send(); diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index e598a45..53f21d6 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -99,8 +99,8 @@ protected function upsertOnCloudflare(): void Notification::make() ->danger() - ->title('Cloudflare: Missing Zone ID') - ->body(sprintf('Cloudflare zone ID is not configured for %s. Cannot upsert DNS record for %s.%s.', $this->domain->name ?? 'unknown', $this->name, $this->domain->name ?? 'unknown')) + ->title(trans('subdomains::strings.notifications.cloudflare_missing_zone_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_zone', ['domain' => $this->domain->name ?? 'unknown', 'subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) ->send(); return; @@ -115,8 +115,8 @@ protected function upsertOnCloudflare(): void Notification::make() ->danger() - ->title('Cloudflare: Missing SRV Port') - ->body(sprintf('SRV target or port is missing for %s.%s. Cannot upsert SRV record.', $this->name, $this->domain->name ?? 'unknown')) + ->title(trans('subdomains::strings.notifications.cloudflare_missing_srv_port_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_port', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) ->send(); return; @@ -131,16 +131,16 @@ protected function upsertOnCloudflare(): void Notification::make() ->success() - ->title('Cloudflare: Record updated') - ->body('Successfully updated ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . ' to '. $this->record_type) + ->title(trans('subdomains::strings.notifications.cloudflare_record_updated_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_record_updated', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'record_type' => $this->record_type])) ->send(); } else { Log::error('Failed to upsert SRV record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); Notification::make() ->danger() - ->title('Cloudflare: SRV upsert failed') - ->body('Failed to upsert SRV record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->title(trans('subdomains::strings.notifications.cloudflare_srv_upsert_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_srv_upsert_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) ->send(); } @@ -153,8 +153,8 @@ protected function upsertOnCloudflare(): void Notification::make() ->danger() - ->title('Cloudflare: Missing IP') - ->body(sprintf('Server allocation IP is missing or invalid for %s.%s. Cannot upsert A/AAAA record.', $this->name, $this->domain->name ?? 'unknown')) + ->title(trans('subdomains::strings.notifications.cloudflare_missing_ip_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_ip', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) ->send(); return; @@ -169,8 +169,8 @@ protected function upsertOnCloudflare(): void Notification::make() ->success() - ->title('Cloudflare: Record updated') - ->body('Successfully updated ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . ' to '. $this->record_type) + ->title(trans('subdomains::strings.notifications.cloudflare_record_updated_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_record_updated', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'record_type' => $this->record_type])) ->send(); } else { Log::error('Failed to upsert record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); @@ -180,8 +180,8 @@ protected function upsertOnCloudflare(): void Notification::make() ->danger() - ->title('Cloudflare: Upsert failed') - ->body('Failed to upsert record for ' . $sub . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->title(trans('subdomains::strings.notifications.cloudflare_upsert_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $sub, 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) ->send(); } } @@ -195,16 +195,17 @@ protected function deleteOnCloudflare(): void if (!empty($result['success'])) { Notification::make() - ->danger() - ->title('Cloudflare: Delete successed') - ->body('Successfully deleted Cloudflare record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '.') + ->success() + ->title(trans('subdomains::strings.notifications.cloudflare_delete_success_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_delete_success', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) ->send(); + return; } Notification::make() ->danger() - ->title('Cloudflare: Delete failed') - ->body('Failed to delete Cloudflare record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->title(trans('subdomains::strings.notifications.cloudflare_delete_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_delete_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) ->send(); } } diff --git a/subdomains/src/SubdomainsPlugin.php b/subdomains/src/SubdomainsPlugin.php index 17e7164..655a465 100644 --- a/subdomains/src/SubdomainsPlugin.php +++ b/subdomains/src/SubdomainsPlugin.php @@ -47,7 +47,7 @@ public function saveSettings(array $data): void ]); Notification::make() - ->title('Settings saved') + ->title(trans('subdomains::strings.notifications.settings_saved')) ->success() ->send(); } From f7b98d3a412e9694fd8091fd6ec4c9058ff55c85 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:27:05 +1100 Subject: [PATCH 21/32] corrected notification log --- subdomains/src/Models/Subdomain.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 53f21d6..9dd6d93 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -200,13 +200,13 @@ protected function deleteOnCloudflare(): void ->body(trans('subdomains::strings.notifications.cloudflare_delete_success', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) ->send(); return; + } else { + Notification::make() + ->danger() + ->title(trans('subdomains::strings.notifications.cloudflare_delete_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_delete_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) + ->send(); } - - Notification::make() - ->danger() - ->title(trans('subdomains::strings.notifications.cloudflare_delete_failed_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_delete_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) - ->send(); } } } From 9804937fc3e8c915c6447dbae252b0d59693b537 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:27:24 +1100 Subject: [PATCH 22/32] Fixed http success logic --- subdomains/src/Services/CloudflareService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index dcbd5dc..b2cd73b 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -124,7 +124,7 @@ protected function parseCloudflareHttpResponse(Response $response): array $status = $response->status(); $body = $response->json() ?? []; - $success = $response->successful() && (!empty($body['success']) || !empty($body['result'])); + $success = $response->successful() && ($body['success'] === true || count($body['result']) > 0); return [ 'success' => $success, From d004ddaf94af9066c401d931a371cb4964814c06 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:32:29 +1100 Subject: [PATCH 23/32] fixed style issues and syntax error --- .../Server/Resources/Subdomains/SubdomainResource.php | 2 +- subdomains/src/Models/Subdomain.php | 5 +++-- subdomains/src/Services/CloudflareService.php | 9 +-------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index 3b8c25f..b44ffbd 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -85,7 +85,7 @@ public static function table(Table $table): Table ->disabled(fn (Subdomain $record) => !$record->domain || empty($record->domain->srv_target)), ]) ->recordActions([ - EditAction::make(), + EditAction::make() ->successNotification(null), DeleteAction::make() ->successNotification(null), diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 9dd6d93..b243255 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -128,7 +128,7 @@ protected function upsertOnCloudflare(): void if ($this->cloudflare_id !== $result['id']) { $this->updateQuietly(['cloudflare_id' => $result['id']]); } - + Notification::make() ->success() ->title(trans('subdomains::strings.notifications.cloudflare_record_updated_title')) @@ -199,7 +199,6 @@ protected function deleteOnCloudflare(): void ->title(trans('subdomains::strings.notifications.cloudflare_delete_success_title')) ->body(trans('subdomains::strings.notifications.cloudflare_delete_success', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) ->send(); - return; } else { Notification::make() ->danger() @@ -207,6 +206,8 @@ protected function deleteOnCloudflare(): void ->body(trans('subdomains::strings.notifications.cloudflare_delete_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) ->send(); } + + return; } } } diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index b2cd73b..11f57dc 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -38,14 +38,7 @@ public function getZoneId(string $domainName): ?string return null; } - public function upsertDnsRecord( - string $zoneId, - string $name, - string $recordType, - string $target, - ?string $recordId = null, - ?int $port = null, - ): array + public function upsertDnsRecord(string $zoneId, string $name, string $recordType, string $target, ?string $recordId = null, ?int $port = null): array { if (empty($zoneId) || empty($name) || empty($recordType)) { Log::error('Cloudflare upsertDnsRecord missing required parameters', ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); From a4d9827007f7619bc6f4f342b154c5ecdd37f745 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:46:58 +1100 Subject: [PATCH 24/32] fixing blank_line_before_statement --- subdomains/src/Services/CloudflareService.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index 11f57dc..b0525e5 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -12,6 +12,7 @@ public function getZoneId(string $domainName): ?string { if (empty($domainName)) { Log::error('Cloudflare getZoneId called with empty domain name', ['domain' => $domainName]); + return null; } @@ -21,6 +22,7 @@ public function getZoneId(string $domainName): ?string ]); } catch (\Throwable $e) { Log::error('Cloudflare getZoneId request failed: ' . $e->getMessage(), ['domain' => $domainName]); + return null; } @@ -42,6 +44,7 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType { if (empty($zoneId) || empty($name) || empty($recordType)) { Log::error('Cloudflare upsertDnsRecord missing required parameters', ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); + return ['success' => false, 'id' => null, 'errors' => ['missing_parameters' => true], 'status' => 0, 'body' => null]; } @@ -56,6 +59,7 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType if ($recordType === 'SRV') { if (empty($port) || empty($target)) { Log::error('Cloudflare upsert missing SRV target or port', ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); + return ['success' => false, 'id' => null, 'errors' => ['missing_srv_target_or_port' => true], 'status' => 0, 'body' => null]; } @@ -94,6 +98,7 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType } Log::error('Cloudflare update failed', ['zone' => $zoneId, 'recordId' => $recordId, 'response' => $parsed]); + return $parsed; } @@ -105,9 +110,11 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType } Log::error('Cloudflare create failed', ['zone' => $zoneId, 'payload' => $payload, 'response' => $parsed]); + return $parsed; } catch (\Throwable $e) { Log::error('Cloudflare upsert exception: ' . $e->getMessage(), ['zone' => $zoneId, 'payload' => $payload]); + return ['success' => false, 'id' => null, 'errors' => ['exception' => $e->getMessage()], 'status' => 0, 'body' => null]; } } @@ -144,9 +151,11 @@ public function deleteDnsRecord(string $zoneId, string $recordId): array } Log::error('Cloudflare delete failed', ['zone' => $zoneId, 'id' => $recordId, 'response' => $parsed]); + return $parsed; } catch (\Throwable $e) { Log::error('Cloudflare delete exception: ' . $e->getMessage(), ['zone' => $zoneId, 'id' => $recordId]); + return ['success' => false, 'errors' => ['exception' => $e->getMessage()], 'status' => 0, 'body' => null]; } } From c4654fd2a273181e86d22dfeff32327d2ac467bd Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:54:41 +1100 Subject: [PATCH 25/32] append version to 1.1.0 --- subdomains/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subdomains/plugin.json b/subdomains/plugin.json index 7ff7cbc..23e3ee6 100644 --- a/subdomains/plugin.json +++ b/subdomains/plugin.json @@ -2,7 +2,7 @@ "id": "subdomains", "name": "Subdomains", "author": "Boy132", - "version": "1.0.0", + "version": "1.1.0", "description": "Allows users to create subdomains for their servers", "category": "plugin", "url": "https://github.com/pelican-dev/plugins/tree/main/subdomains", From 348099d16c2fcce30010e258c3bf526884398630 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 12:53:39 +1100 Subject: [PATCH 26/32] Add notification for missing SRV target in Cloudflare integration --- subdomains/lang/en/strings.php | 3 +++ subdomains/src/Models/Subdomain.php | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index 796ef63..462d767 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -41,6 +41,9 @@ 'cloudflare_missing_srv_port_title' => 'Cloudflare: Missing SRV Port', 'cloudflare_missing_srv_port' => 'SRV target or port is missing for :subdomain. Cannot upsert SRV record.', + 'cloudflare_missing_srv_target_title' => 'Cloudflare: Missing SRV Target', + 'cloudflare_missing_srv_target' => 'SRV target is missing for :subdomain. Cannot upsert SRV record.', + 'cloudflare_record_updated_title' => 'Cloudflare: Record updated', 'cloudflare_record_updated' => 'Successfully updated :subdomain to :record_type', diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index b243255..78e1c0c 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -122,6 +122,18 @@ protected function upsertOnCloudflare(): void return; } + if (empty($this->domain->srv_target)) { + Log::warning('Domain missing SRV target for SRV record', ['domain_id' => $this->domain_id]); + + Notification::make() + ->danger() + ->title(trans('subdomains::strings.notifications.cloudflare_missing_srv_target_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_target', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) + ->send(); + + return; + } + $result = $service->upsertDnsRecord($zoneId, $this->name, 'SRV', $this->domain->srv_target, $this->cloudflare_id ?? null, $port); if ($result['success'] && !empty($result['id'])) { From d64612d694daf2c66591a73950b8fab7bf15faeb Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 13:03:20 +1100 Subject: [PATCH 27/32] minor wording updates --- subdomains/lang/en/strings.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index 462d767..5ae1ac4 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -39,13 +39,13 @@ 'cloudflare_missing_zone' => 'Cloudflare zone ID is not configured for :domain. Cannot upsert DNS record for :subdomain.', 'cloudflare_missing_srv_port_title' => 'Cloudflare: Missing SRV Port', - 'cloudflare_missing_srv_port' => 'SRV target or port is missing for :subdomain. Cannot upsert SRV record.', + 'cloudflare_missing_srv_port' => 'SRV port is missing for :subdomain. Cannot upsert SRV record.', 'cloudflare_missing_srv_target_title' => 'Cloudflare: Missing SRV Target', 'cloudflare_missing_srv_target' => 'SRV target is missing for :subdomain. Cannot upsert SRV record.', 'cloudflare_record_updated_title' => 'Cloudflare: Record updated', - 'cloudflare_record_updated' => 'Successfully updated :subdomain to :record_type', + 'cloudflare_record_updated' => 'Successfully updated :subdomain record to :record_type', 'cloudflare_srv_upsert_failed_title' => 'Cloudflare: SRV upsert failed', 'cloudflare_srv_upsert_failed' => 'Failed to upsert SRV record for :subdomain. See logs for details. Errors: :errors', From 5bfd8dfd25ed1a05f63d6ff5b3a9495b3e236719 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 13:20:39 +1100 Subject: [PATCH 28/32] Dynamically disable SRV record toggle if domain is missing srv_target --- .../Filament/Server/Resources/Subdomains/SubdomainResource.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index b44ffbd..c2b65ee 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -123,7 +123,7 @@ public static function form(Schema $schema): Schema Toggle::make('srv_record') ->label(trans('subdomains::strings.srv_record')) ->helperText(trans('subdomains::strings.srv_record_help')) - ->disabled(false) // TODO: Dynamically disable if domain missing srv_target + ->disabled(fn (callable $get, ?Subdomain $record = null) => (empty($get('domain_id')) || empty(CloudflareDomain::find($get('domain_id'))?->srv_target))) ->default(false), ]); } From e781f6a5bce26777680759e71c9f49f01fe2657b Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 13:29:36 +1100 Subject: [PATCH 29/32] lang cleanup, removed redundent strings --- subdomains/lang/en/strings.php | 23 +++++++---------------- subdomains/src/Models/Subdomain.php | 4 ++-- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index 5ae1ac4..494b760 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -28,33 +28,23 @@ 'api_token_help' => 'The token needs to have read permissions for Zone.Zone and write for Zone.Dns. For better security you can also set the "Zone Resources" to exclude certain domains and add the panel ip to the "Client IP Adress Filtering".', 'notifications' => [ - 'dns_created' => 'DNS record created on Cloudflare', - 'dns_updated' => 'DNS record updated on Cloudflare', - 'dns_deleted' => 'DNS record deleted from Cloudflare', - 'dns_action_failed' => 'Cloudflare DNS action failed', - 'zone_request_failed' => 'Cloudflare zone request failed', - 'zone_request_succeeded' => 'Cloudflare zone request succeeded', - 'cloudflare_missing_zone_title' => 'Cloudflare: Missing Zone ID', - 'cloudflare_missing_zone' => 'Cloudflare zone ID is not configured for :domain. Cannot upsert DNS record for :subdomain.', + 'cloudflare_missing_zone' => 'Cloudflare zone ID is not configured for :domain. Cannot save DNS record for :subdomain.', 'cloudflare_missing_srv_port_title' => 'Cloudflare: Missing SRV Port', - 'cloudflare_missing_srv_port' => 'SRV port is missing for :subdomain. Cannot upsert SRV record.', + 'cloudflare_missing_srv_port' => 'SRV port is missing for :subdomain. Cannot save SRV record.', 'cloudflare_missing_srv_target_title' => 'Cloudflare: Missing SRV Target', - 'cloudflare_missing_srv_target' => 'SRV target is missing for :subdomain. Cannot upsert SRV record.', + 'cloudflare_missing_srv_target' => 'SRV target is missing for :subdomain. Cannot save SRV record.', 'cloudflare_record_updated_title' => 'Cloudflare: Record updated', 'cloudflare_record_updated' => 'Successfully updated :subdomain record to :record_type', - 'cloudflare_srv_upsert_failed_title' => 'Cloudflare: SRV upsert failed', - 'cloudflare_srv_upsert_failed' => 'Failed to upsert SRV record for :subdomain. See logs for details. Errors: :errors', - 'cloudflare_missing_ip_title' => 'Cloudflare: Missing IP', - 'cloudflare_missing_ip' => 'Server allocation IP is missing or invalid for :subdomain. Cannot upsert A/AAAA record.', + 'cloudflare_missing_ip' => 'Server allocation IP is missing or invalid for :subdomain. Cannot save A/AAAA record.', - 'cloudflare_upsert_failed_title' => 'Cloudflare: Upsert failed', - 'cloudflare_upsert_failed' => 'Failed to upsert record for :subdomain. See logs for details. Errors: :errors', + 'cloudflare_upsert_failed_title' => 'Cloudflare: Save failed', + 'cloudflare_upsert_failed' => 'Failed to save record for :subdomain. See logs for details. Errors: :errors', 'cloudflare_delete_success_title' => 'Cloudflare: Record deleted', 'cloudflare_delete_success' => 'Successfully deleted Cloudflare record for :subdomain.', @@ -64,6 +54,7 @@ 'cloudflare_zone_fetch_failed' => 'Failed to fetch Cloudflare Zone ID for domain: :domain', 'cloudflare_domain_saved' => 'Successfully saved domain: :domain', + 'settings_saved' => 'Settings saved', ], ]; diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 78e1c0c..f6617fa 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -151,8 +151,8 @@ protected function upsertOnCloudflare(): void Notification::make() ->danger() - ->title(trans('subdomains::strings.notifications.cloudflare_srv_upsert_failed_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_srv_upsert_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) + ->title(trans('subdomains::strings.notifications.cloudflare_upsert_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) ->send(); } From 8a7e932c204dd83f298f63f5e12da675ec9b538f Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 13:39:52 +1100 Subject: [PATCH 30/32] Make SRV record toggle reactive --- .../Filament/Server/Resources/Subdomains/SubdomainResource.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index c2b65ee..1d9fe3b 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -123,7 +123,8 @@ public static function form(Schema $schema): Schema Toggle::make('srv_record') ->label(trans('subdomains::strings.srv_record')) ->helperText(trans('subdomains::strings.srv_record_help')) - ->disabled(fn (callable $get, ?Subdomain $record = null) => (empty($get('domain_id')) || empty(CloudflareDomain::find($get('domain_id'))?->srv_target))) + ->reactive() + ->disabled(fn (callable $get) => (empty($get('domain_id')) || empty(CloudflareDomain::find($get('domain_id'))?->srv_target))) ->default(false), ]); } From 23c4e399a1a71c5417411c0d3b322fc7015cfcce Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 13:40:14 +1100 Subject: [PATCH 31/32] Fix capitalization in Cloudflare notification titles --- subdomains/lang/en/strings.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index 494b760..3b8f387 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -37,24 +37,24 @@ 'cloudflare_missing_srv_target_title' => 'Cloudflare: Missing SRV Target', 'cloudflare_missing_srv_target' => 'SRV target is missing for :subdomain. Cannot save SRV record.', - 'cloudflare_record_updated_title' => 'Cloudflare: Record updated', + 'cloudflare_record_updated_title' => 'Cloudflare: Record Updated', 'cloudflare_record_updated' => 'Successfully updated :subdomain record to :record_type', 'cloudflare_missing_ip_title' => 'Cloudflare: Missing IP', 'cloudflare_missing_ip' => 'Server allocation IP is missing or invalid for :subdomain. Cannot save A/AAAA record.', - 'cloudflare_upsert_failed_title' => 'Cloudflare: Save failed', + 'cloudflare_upsert_failed_title' => 'Cloudflare: Save Failed', 'cloudflare_upsert_failed' => 'Failed to save record for :subdomain. See logs for details. Errors: :errors', - 'cloudflare_delete_success_title' => 'Cloudflare: Record deleted', + 'cloudflare_delete_success_title' => 'Cloudflare: Record Deleted', 'cloudflare_delete_success' => 'Successfully deleted Cloudflare record for :subdomain.', - 'cloudflare_delete_failed_title' => 'Cloudflare: Delete failed', + 'cloudflare_delete_failed_title' => 'Cloudflare: Delete Failed', 'cloudflare_delete_failed' => 'Failed to delete Cloudflare record for :subdomain. See logs for details. Errors: :errors', 'cloudflare_zone_fetch_failed' => 'Failed to fetch Cloudflare Zone ID for domain: :domain', 'cloudflare_domain_saved' => 'Successfully saved domain: :domain', - + 'settings_saved' => 'Settings saved', ], ]; From f31a101c26a174d7814522114005cca0d38b9749 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 13:46:01 +1100 Subject: [PATCH 32/32] clean up --- subdomains/src/Models/Subdomain.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index f6617fa..7288429 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -134,7 +134,7 @@ protected function upsertOnCloudflare(): void return; } - $result = $service->upsertDnsRecord($zoneId, $this->name, 'SRV', $this->domain->srv_target, $this->cloudflare_id ?? null, $port); + $result = $service->upsertDnsRecord($zoneId, $this->name, 'SRV', $this->domain->srv_target, $this->cloudflare_id, $port); if ($result['success'] && !empty($result['id'])) { if ($this->cloudflare_id !== $result['id']) { @@ -172,7 +172,7 @@ protected function upsertOnCloudflare(): void return; } - $result = $service->upsertDnsRecord($zoneId, $this->name, $this->record_type, $this->server->allocation->ip, $this->cloudflare_id ?? null, null); + $result = $service->upsertDnsRecord($zoneId, $this->name, $this->record_type, $this->server->allocation->ip, $this->cloudflare_id, null); if ($result['success'] && !empty($result['id'])) { if ($this->cloudflare_id !== $result['id']) {