From e6033538be9f78f39ab4432b6e9e015a5874279e Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 13:03:13 +1100 Subject: [PATCH 01/79] 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 00000000..3cbe2a1a --- /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/79] 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 e1cd0ea6..511bd809 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/79] 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 571166fd..71087682 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/79] 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 45de8545..537fa057 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 ec372b37..992fedc9 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/79] 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 14cd3238..a85f92fe 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/79] 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 511bd809..b81a42a8 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 992fedc9..b7f43135 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 a85f92fe..b0ffe383 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/79] 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 b81a42a8..3bc86252 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 b0ffe383..05a5ff9e 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/79] 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 00000000..6b73b24b --- /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/79] 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 6b73b24b..15b0307d 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/79] 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 05a5ff9e..514dc0cc 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/79] 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 238e6fe3..7a0dbf4f 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 c5109599..134e4179 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 15b0307d..0bfa7d99 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/79] 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 514dc0cc..01aaf4ee 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 0bfa7d99..9fb0b994 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/79] 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 01aaf4ee..9b1342a8 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 9fb0b994..f6837144 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/79] 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 9b1342a8..39803899 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/79] - 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 39803899..a78125f0 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/79] 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 537fa057..ab5c5702 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/79] - 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 b7f43135..bc3ba9c9 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/79] - 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 134e4179..46027459 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 f6837144..6832418b 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/79] 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 a78125f0..e598a455 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 6832418b..dcbd5dc3 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/79] 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 3bc86252..796ef63b 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 bc3ba9c9..3b8c25f2 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 46027459..1dbd87c4 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 e598a455..53f21d67 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 17e71641..655a4652 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/79] 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 53f21d67..9dd6d93b 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/79] 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 dcbd5dc3..b2cd73bf 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/79] 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 3b8c25f2..b44ffbd1 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 9dd6d93b..b2432552 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 b2cd73bf..11f57dc7 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/79] 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 11f57dc7..b0525e52 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/79] 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 7ff7cbcd..23e3ee61 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/79] 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 796ef63b..462d7670 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 b2432552..78e1c0cf 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/79] 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 462d7670..5ae1ac46 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/79] 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 b44ffbd1..c2b65ee0 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/79] 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 5ae1ac46..494b760e 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 78e1c0cf..f6617fac 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/79] 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 c2b65ee0..1d9fe3b5 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/79] 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 494b760e..3b8f3872 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/79] 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 f6617fac..72884295 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']) { From 6b316869ed0a690532ed0759d856f5fd441a9f4b Mon Sep 17 00:00:00 2001 From: HarlequinSin <73203025+HarlequinSin@users.noreply.github.com> Date: Thu, 1 Jan 2026 19:49:34 +1100 Subject: [PATCH 33/79] migration to move srv_target from cloudflare domains to nodes --- .../005_add_srv_target_to_nodes.php | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 subdomains/database/migrations/005_add_srv_target_to_nodes.php diff --git a/subdomains/database/migrations/005_add_srv_target_to_nodes.php b/subdomains/database/migrations/005_add_srv_target_to_nodes.php new file mode 100644 index 00000000..cab88da6 --- /dev/null +++ b/subdomains/database/migrations/005_add_srv_target_to_nodes.php @@ -0,0 +1,30 @@ +dropColumn('srv_target'); + }); + + Schema::table('nodes', function (Blueprint $table) { + $table->string('srv_target')->nullable()->after('fqdn'); + }); + } + + public function down(): void + { + Schema::table('cloudflare_domains', function (Blueprint $table) { + $table->string('srv_target')->nullable()->after('cloudflare_id'); + }); + + Schema::table('nodes', function (Blueprint $table) { + $table->dropColumn('srv_target'); + }); + } +}; From d2c0e18c4af160c9d008c6278a3b2b9448aa1f7f Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Thu, 1 Jan 2026 20:02:33 +1100 Subject: [PATCH 34/79] Changing srv_target from domains to node --- .../Filament/Admin/Nodes/Pages/EditNode.php | 792 ++++++++++++++++++ .../CloudflareDomainResource.php | 7 - .../Subdomains/SubdomainResource.php | 14 +- subdomains/src/Models/CloudflareDomain.php | 1 - .../Providers/SubdomainsPluginProvider.php | 4 +- 5 files changed, 802 insertions(+), 16 deletions(-) create mode 100644 subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php diff --git a/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php b/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php new file mode 100644 index 00000000..c819888b --- /dev/null +++ b/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php @@ -0,0 +1,792 @@ +components([ + Tabs::make('Tabs') + ->columns([ + 'default' => 2, + 'sm' => 3, + 'md' => 3, + 'lg' => 4, + ]) + ->persistTabInQueryString() + ->columnSpanFull() + ->tabs([ + Tab::make('overview') + ->label(trans('admin/node.tabs.overview')) + ->icon('tabler-chart-area-line-filled') + ->columns([ + 'default' => 4, + 'sm' => 2, + 'md' => 4, + 'lg' => 4, + ]) + ->schema([ + Fieldset::make() + ->label(trans('admin/node.node_info')) + ->columns(4) + ->columnSpanFull() + ->schema([ + TextEntry::make('wings_version') + ->label(trans('admin/node.wings_version')) + ->state(fn (Node $node, SoftwareVersionService $versionService) => ($node->systemInformation()['version'] ?? trans('admin/node.unknown')) . ' ' . trans('admin/node.latest', ['version' => $versionService->latestWingsVersion()])), + TextEntry::make('cpu_threads') + ->label(trans('admin/node.cpu_threads')) + ->state(fn (Node $node) => $node->systemInformation()['cpu_count'] ?? 0), + TextEntry::make('architecture') + ->label(trans('admin/node.architecture')) + ->state(fn (Node $node) => $node->systemInformation()['architecture'] ?? trans('admin/node.unknown')), + TextEntry::make('kernel') + ->label(trans('admin/node.kernel')) + ->state(fn (Node $node) => $node->systemInformation()['kernel_version'] ?? trans('admin/node.unknown')), + ]), + View::make('filament.components.node-cpu-chart') + ->columnSpan([ + 'default' => 4, + 'sm' => 1, + 'md' => 2, + 'lg' => 2, + ]), + View::make('filament.components.node-memory-chart') + ->columnSpan([ + 'default' => 4, + 'sm' => 1, + 'md' => 2, + 'lg' => 2, + ]), + View::make('filament.components.node-storage-chart') + ->columnSpanFull(), + ]), + Tab::make('basic_settings') + ->label(trans('admin/node.tabs.basic_settings')) + ->icon('tabler-server') + ->schema([ + TextInput::make('fqdn') + ->columnSpan(2) + ->required() + ->autofocus() + ->live(debounce: 1500) + ->rules(Node::getRulesForField('fqdn')) + ->prohibited(fn ($state) => is_ip($state) && request()->isSecure()) + ->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain')) + ->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com') + ->helperText(function ($state) { + if (is_ip($state)) { + if (request()->isSecure()) { + return trans('admin/node.fqdn_help'); + } + + return ''; + } + + return trans('admin/node.error'); + }) + ->hintColor('danger') + ->hint(function ($state) { + if (is_ip($state) && request()->isSecure()) { + return trans('admin/node.ssl_ip'); + } + + return ''; + }) + ->afterStateUpdated(function (Set $set, ?string $state) { + $set('dns', null); + $set('ip', null); + + [$subdomain] = str($state)->explode('.', 2); + if (!is_numeric($subdomain)) { + $set('name', $subdomain); + } + + if (!$state || is_ip($state)) { + $set('dns', null); + + return; + } + + $ip = get_ip_from_hostname($state); + if ($ip) { + $set('dns', true); + + $set('ip', $ip); + } else { + $set('dns', false); + } + }) + ->maxLength(255), + TextInput::make('ip') + ->disabled() + ->hidden(), + ToggleButtons::make('dns') + ->label(trans('admin/node.dns')) + ->helperText(trans('admin/node.dns_help')) + ->disabled() + ->inline() + ->default(null) + ->hint(fn (Get $get) => $get('ip')) + ->hintColor('success') + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 1 => trans('admin/node.valid'), + 0 => trans('admin/node.invalid'), + ]) + ->colors([ + 1 => 'success', + 0 => 'danger', + ]) + ->columnSpan(1), + TextInput::make('daemon_connect') + ->columnSpan(1) + ->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port')) + ->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help')) + ->minValue(1) + ->maxValue(65535) + ->default(8080) + ->required() + ->integer(), + TextInput::make('name') + ->label(trans('admin/node.display_name')) + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 2, + ]) + ->required() + ->maxLength(100), + Hidden::make('scheme'), + Hidden::make('behind_proxy'), + ToggleButtons::make('connection') + ->label(trans('admin/node.ssl')) + ->columnSpan(1) + ->inline() + ->helperText(function (Get $get) { + if (request()->isSecure()) { + return new HtmlString(trans('admin/node.panel_on_ssl')); + } + + if (is_ip($get('fqdn'))) { + return trans('admin/node.ssl_help'); + } + + return ''; + }) + ->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure()) + ->options([ + 'http' => 'HTTP', + 'https' => 'HTTPS (SSL)', + 'https_proxy' => 'HTTPS with (reverse) proxy', + ]) + ->colors([ + 'http' => 'warning', + 'https' => 'success', + 'https_proxy' => 'success', + ]) + ->icons([ + 'http' => 'tabler-lock-open-off', + 'https' => 'tabler-lock', + 'https_proxy' => 'tabler-shield-lock', + ]) + ->formatStateUsing(fn (Get $get) => $get('scheme') === 'http' ? 'http' : ($get('behind_proxy') ? 'https_proxy' : 'https')) + ->live() + ->dehydrated(false) + ->afterStateUpdated(function ($state, Set $set) { + $set('scheme', $state === 'http' ? 'http' : 'https'); + $set('behind_proxy', $state === 'https_proxy'); + + $set('daemon_connect', $state === 'https_proxy' ? 443 : 8080); + $set('daemon_listen', 8080); + }), + TextInput::make('daemon_listen') + ->columnSpan(1) + ->label(trans('admin/node.listen_port')) + ->helperText(trans('admin/node.listen_port_help')) + ->minValue(1) + ->maxValue(65535) + ->default(8080) + ->required() + ->integer() + ->visible(fn (Get $get) => $get('connection') === 'https_proxy'), + ]), + Tab::make('advanced_settings') + ->label(trans('admin/node.tabs.advanced_settings')) + ->columns([ + 'default' => 1, + 'sm' => 1, + 'md' => 4, + 'lg' => 6, + ]) + ->icon('tabler-server-cog') + ->schema([ + TextInput::make('id') + ->label(trans('admin/node.node_id')) + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 2, + 'lg' => 1, + ]) + ->disabled(), + TextInput::make('uuid') + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 2, + 'lg' => 2, + ]) + ->label(trans('admin/node.node_uuid')) + ->hintCopy() + ->disabled(), + TagsInput::make('tags') + ->label(trans('admin/node.tags')) + ->placeholder('') + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 2, + 'lg' => 2, + ]), + TextInput::make('upload_size') + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 2, + 'lg' => 1, + ]) + ->label(trans('admin/node.upload_limit')) + ->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help.0') . trans('admin/node.upload_limit_help.1')) + ->numeric()->required() + ->minValue(1) + ->maxValue(1024) + ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'), + TextInput::make('daemon_sftp') + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 3, + ]) + ->label(trans('admin/node.sftp_port')) + ->minValue(1) + ->maxValue(65535) + ->default(2022) + ->required() + ->integer(), + TextInput::make('daemon_sftp_alias') + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 3, + ]) + ->label(trans('admin/node.sftp_alias')) + ->helperText(trans('admin/node.sftp_alias_help')), + ToggleButtons::make('public') + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 3, + ]) + ->label(trans('admin/node.use_for_deploy')) + ->inline() + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 1 => trans('admin/node.yes'), + 0 => trans('admin/node.no'), + ]) + ->colors([ + 1 => 'success', + 0 => 'danger', + ]), + ToggleButtons::make('maintenance_mode') + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 3, + ]) + ->label(trans('admin/node.maintenance_mode')) + ->inline() + ->hintIcon('tabler-question-mark', trans('admin/node.maintenance_mode_help')) + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 1 => trans('admin/node.enabled'), + 0 => trans('admin/node.disabled'), + ]) + ->colors([ + 1 => 'danger', + 0 => 'success', + ]), + // Subdomains plugin field + TextInput::make('srv_target') + ->label(trans('subdomains::strings.srv_target')) + ->placeholder('play.example.com') + ->helperText(trans('subdomains::strings.srv_target_help')) + ->rules(['nullable', 'string', 'regex:/^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(?columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 3, + ]) + ->dehydrateStateUsing(function ($state) { + $this->record->forceFill(['srv_target' => $state])->save(); + + Notification::make() + ->title(trans('subdomains::strings.notifications.srv_target_updated_title')) + ->body(trans('subdomains::strings.notifications.srv_target_updated')) + ->warning() + ->send(); + + return $state; + }), + Grid::make() + ->columns([ + 'default' => 1, + 'sm' => 1, + 'md' => 3, + 'lg' => 6, + ]) + ->columnSpanFull() + ->schema([ + ToggleButtons::make('unlimited_mem') + ->dehydrated() + ->label(trans('admin/node.memory'))->inlineLabel()->inline() + ->afterStateUpdated(fn (Set $set) => $set('memory', 0)) + ->afterStateUpdated(fn (Set $set) => $set('memory_overallocate', 0)) + ->formatStateUsing(fn (Get $get) => $get('memory') == 0) + ->live() + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 1 => trans('admin/node.unlimited'), + 0 => trans('admin/node.limited'), + ]) + ->colors([ + 1 => 'primary', + 0 => 'warning', + ]) + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 2, + ]), + TextInput::make('memory') + ->dehydratedWhenHidden() + ->hidden(fn (Get $get) => $get('unlimited_mem')) + ->label(trans('admin/node.memory_limit'))->inlineLabel() + ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB') + ->required() + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 2, + ]) + ->numeric() + ->minValue(0), + TextInput::make('memory_overallocate') + ->dehydratedWhenHidden() + ->label(trans('admin/node.overallocate'))->inlineLabel() + ->required() + ->hidden(fn (Get $get) => $get('unlimited_mem')) + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 2, + ]) + ->numeric() + ->minValue(-1) + ->maxValue(100) + ->suffix('%'), + ]), + Grid::make() + ->columnSpanFull() + ->columns([ + 'default' => 1, + 'sm' => 1, + 'md' => 3, + 'lg' => 6, + ]) + ->schema([ + ToggleButtons::make('unlimited_disk') + ->dehydrated() + ->label(trans('admin/node.disk'))->inlineLabel()->inline() + ->live() + ->afterStateUpdated(fn (Set $set) => $set('disk', 0)) + ->afterStateUpdated(fn (Set $set) => $set('disk_overallocate', 0)) + ->formatStateUsing(fn (Get $get) => $get('disk') == 0) + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 1 => trans('admin/node.unlimited'), + 0 => trans('admin/node.limited'), + ]) + ->colors([ + 1 => 'primary', + 0 => 'warning', + ]) + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 2, + ]), + TextInput::make('disk') + ->dehydratedWhenHidden() + ->hidden(fn (Get $get) => $get('unlimited_disk')) + ->label(trans('admin/node.disk_limit'))->inlineLabel() + ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB') + ->required() + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 2, + ]) + ->numeric() + ->minValue(0), + TextInput::make('disk_overallocate') + ->dehydratedWhenHidden() + ->hidden(fn (Get $get) => $get('unlimited_disk')) + ->label(trans('admin/node.overallocate'))->inlineLabel() + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 2, + ]) + ->required() + ->numeric() + ->minValue(-1) + ->maxValue(100) + ->suffix('%'), + ]), + Grid::make() + ->columns(6) + ->columnSpanFull() + ->schema([ + ToggleButtons::make('unlimited_cpu') + ->dehydrated() + ->label(trans('admin/node.cpu'))->inlineLabel()->inline() + ->live() + ->afterStateUpdated(fn (Set $set) => $set('cpu', 0)) + ->afterStateUpdated(fn (Set $set) => $set('cpu_overallocate', 0)) + ->formatStateUsing(fn (Get $get) => $get('cpu') == 0) + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 1 => trans('admin/node.unlimited'), + 0 => trans('admin/node.limited'), + ]) + ->colors([ + 1 => 'primary', + 0 => 'warning', + ]) + ->columnSpan(2), + TextInput::make('cpu') + ->dehydratedWhenHidden() + ->hidden(fn (Get $get) => $get('unlimited_cpu')) + ->label(trans('admin/node.cpu_limit'))->inlineLabel() + ->suffix('%') + ->required() + ->columnSpan(2) + ->numeric() + ->minValue(0), + TextInput::make('cpu_overallocate') + ->dehydratedWhenHidden() + ->hidden(fn (Get $get) => $get('unlimited_cpu')) + ->label(trans('admin/node.overallocate'))->inlineLabel() + ->columnSpan(2) + ->required() + ->numeric() + ->minValue(-1) + ->maxValue(100) + ->suffix('%'), + ]), + ]), + Tab::make('config_file') + ->label(trans('admin/node.tabs.config_file')) + ->icon('tabler-code') + ->schema([ + TextEntry::make('instructions') + ->label(trans('admin/node.instructions')) + ->columnSpanFull() + ->state(new HtmlString(trans('admin/node.instructions_help'))), + CodeEntry::make('config') + ->label('/etc/pelican/config.yml') + ->grammar(Grammar::Yaml) + ->state(fn (Node $node) => $node->getYamlConfiguration()) + ->copyable() + ->disabled() + ->columnSpanFull(), + Grid::make() + ->columns() + ->columnSpanFull() + ->schema([ + Actions::make([ + Action::make('autoDeploy') + ->label(trans('admin/node.auto_deploy')) + ->color('primary') + ->modalHeading(trans('admin/node.auto_deploy')) + ->icon('tabler-rocket') + ->modalSubmitAction(false) + ->modalCancelAction(false) + ->modalFooterActionsAlignment(Alignment::Center) + ->schema([ + ToggleButtons::make('docker') + ->label(trans('admin/node.auto_label')) + ->live() + ->helperText(trans('admin/node.auto_question')) + ->inline() + ->default(false) + ->afterStateUpdated(fn (bool $state, NodeAutoDeployService $service, Node $node, Set $set) => $set('generatedToken', $service->handle(request(), $node, $state))) + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 0 => trans('admin/node.standalone'), + 1 => trans('admin/node.docker'), + ]) + ->colors([ + 0 => 'primary', + 1 => 'success', + ]) + ->columnSpan(1), + Textarea::make('generatedToken') + ->label(trans('admin/node.auto_command')) + ->readOnly() + ->autosize() + ->hintCopy() + ->formatStateUsing(fn (NodeAutoDeployService $service, Node $node, Set $set, Get $get) => $set('generatedToken', $service->handle(request(), $node, $get('docker')))), + ]) + ->mountUsing(function (Schema $schema) { + $schema->fill(); + }), + ])->fullWidth(), + Actions::make([ + Action::make('resetKey') + ->label(trans('admin/node.reset_token')) + ->color('danger') + ->requiresConfirmation() + ->modalHeading(trans('admin/node.reset_token')) + ->modalDescription(trans('admin/node.reset_help')) + ->action(function (Node $node) { + try { + $this->nodeUpdateService->handle($node, [], true); + } catch (Exception) { + Notification::make() + ->title(trans('admin/node.error_connecting', ['node' => $node->name])) + ->body(trans('admin/node.error_connecting_description')) + ->color('warning') + ->icon('tabler-database') + ->warning() + ->send(); + + } + Notification::make()->success()->title(trans('admin/node.token_reset'))->send(); + $this->fillForm(); + }), + ])->fullWidth(), + ]), + ]), + Tab::make('diagnostics') + ->label(trans('admin/node.tabs.diagnostics')) + ->icon('tabler-heart-search') + ->schema([ + Section::make('diag') + ->heading(trans('admin/node.tabs.diagnostics')) + ->columnSpanFull() + ->columns(4) + ->disabled(fn (Get $get) => $get('pulled')) + ->headerActions([ + Action::make('pull') + ->label(trans('admin/node.diagnostics.pull')) + ->icon('tabler-cloud-download')->iconButton()->iconSize(IconSize::ExtraLarge) + ->hidden(fn (Get $get) => $get('pulled')) + ->action(function (Get $get, Set $set, Node $node) { + $includeEndpoints = $get('include_endpoints') ?? true; + $includeLogs = $get('include_logs') ?? true; + $logLines = $get('log_lines') ?? 200; + + try { + $response = $this->daemonSystemRepository->setNode($node)->getDiagnostics($logLines, $includeEndpoints, $includeLogs); + + if ($response->status() === 404) { + Notification::make() + ->title(trans('admin/node.diagnostics.404')) + ->warning() + ->send(); + + return; + } + + $set('pulled', true); + $set('uploaded', false); + $set('log', $response->body()); + + Notification::make() + ->title(trans('admin/node.diagnostics.logs_pulled')) + ->success() + ->send(); + } catch (ConnectionException $e) { + Notification::make() + ->title(trans('admin/node.error_connecting', ['node' => $node->name])) + ->body($e->getMessage()) + ->danger() + ->send(); + + } + }), + Action::make('upload') + ->label(trans('admin/node.diagnostics.upload')) + ->visible(fn (Get $get) => $get('pulled') ?? false) + ->icon('tabler-cloud-upload')->iconButton()->iconSize(IconSize::ExtraLarge) + ->action(function (Get $get, Set $set) { + try { + $response = Http::asMultipart() + ->attach('c', $get('log')) + ->attach('e', '14d') + ->post('https://logs.pelican.dev'); + + if ($response->failed()) { + Notification::make() + ->title(trans('admin/node.diagnostics.upload_failed')) + ->body(fn () => $response->status() . ' - ' . $response->body()) + ->danger() + ->send(); + + return; + } + + $data = $response->json(); + $url = $data['url']; + + Notification::make() + ->title(trans('admin/node.diagnostics.logs_uploaded')) + ->body("{$url}") + ->success() + ->actions([ + Action::make('viewLogs') + ->label(trans('admin/node.diagnostics.view_logs')) + ->url($url) + ->openUrlInNewTab(true), + ]) + ->persistent() + ->send(); + $set('log', $url); + $set('pulled', false); + $set('uploaded', true); + + } catch (\Exception $e) { + Notification::make() + ->title(trans('admin/node.diagnostics.upload_failed')) + ->body($e->getMessage()) + ->danger() + ->send(); + } + }), + Action::make('clear') + ->label(trans('admin/node.diagnostics.clear')) + ->visible(fn (Get $get) => $get('pulled') ?? false) + ->icon('tabler-trash')->iconButton()->iconSize(IconSize::ExtraLarge)->color('danger') + ->action(function (Get $get, Set $set) { + $set('pulled', false); + $set('uploaded', false); + $set('log', null); + $this->refresh(); + } + ), + ]) + ->schema([ + ToggleButtons::make('include_endpoints') + ->hintIcon('tabler-question-mark')->inline() + ->hintIconTooltip(trans('admin/node.diagnostics.include_endpoints_hint')) + ->formatStateUsing(fn () => 1) + ->boolean(), + ToggleButtons::make('include_logs') + ->live() + ->hintIcon('tabler-question-mark')->inline() + ->hintIconTooltip(trans('admin/node.diagnostics.include_logs_hint')) + ->formatStateUsing(fn () => 1) + ->boolean(), + Slider::make('log_lines') + ->columnSpan(2) + ->hiddenLabel() + ->live() + ->tooltips(RawJs::make(<<<'JS' + `${$value} lines` + JS)) + ->visible(fn (Get $get) => $get('include_logs')) + ->range(minValue: 100, maxValue: 500) + ->pips(PipsMode::Steps, density: 10) + ->step(50) + ->formatStateUsing(fn () => 200) + ->fillTrack(), + Hidden::make('pulled'), + Hidden::make('uploaded'), + ]), + Textarea::make('log') + ->hiddenLabel() + ->columnSpanFull() + ->rows(35) + ->visible(fn (Get $get) => ($get('pulled') ?? false) || ($get('uploaded') ?? false)), + ]), + ]), + ]); + } +} \ No newline at end of file diff --git a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php index 71087682..571166fd 100644 --- a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php +++ b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php @@ -53,8 +53,6 @@ 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'), @@ -79,11 +77,6 @@ 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}(?label(trans('subdomains::strings.name')) ->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.srv_target_missing')) - ->disabled(fn (Subdomain $record) => !$record->domain || empty($record->domain->srv_target)), + TextColumn::make('record_type') + ->label(trans('subdomains::strings.record_type')) + ->icon(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server?->node?->srv_target) ? 'tabler-alert-triangle' : null) + ->color(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server?->node?->srv_target) ? 'danger' : null) + ->helperText(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server?->node?->srv_target) ? trans('subdomains::strings.srv_target_missing') : null), ]) ->recordActions([ EditAction::make() @@ -122,10 +123,9 @@ public static function form(Schema $schema): Schema ->searchable(), Toggle::make('srv_record') ->label(trans('subdomains::strings.srv_record')) - ->helperText(trans('subdomains::strings.srv_record_help')) + ->helperText(fn () => Filament::getTenant()->node->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.srv_target_missing')) ->reactive() - ->disabled(fn (callable $get) => (empty($get('domain_id')) || empty(CloudflareDomain::find($get('domain_id'))?->srv_target))) - ->default(false), + ->disabled(fn () => empty(Filament::getTenant()->node->srv_target)), ]); } diff --git a/subdomains/src/Models/CloudflareDomain.php b/subdomains/src/Models/CloudflareDomain.php index 1dbd87c4..d98bad90 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -18,7 +18,6 @@ class CloudflareDomain extends Model protected $fillable = [ 'name', 'cloudflare_id', - 'srv_target', ]; protected static function boot(): void diff --git a/subdomains/src/Providers/SubdomainsPluginProvider.php b/subdomains/src/Providers/SubdomainsPluginProvider.php index 6ce1afaa..4bd21988 100644 --- a/subdomains/src/Providers/SubdomainsPluginProvider.php +++ b/subdomains/src/Providers/SubdomainsPluginProvider.php @@ -2,10 +2,11 @@ namespace Boy132\Subdomains\Providers; +use App\Filament\Admin\Resources\Nodes\NodeResource; use App\Filament\Admin\Resources\Servers\ServerResource; use App\Models\Role; use App\Models\Server; -use Boy132\Subdomains\Filament\Admin\Resources\Users\RelationManagers\SubdomainRelationManager; +use Boy132\Subdomains\Filament\Admin\Resources\Nodes\Pages\EditNode; use Boy132\Subdomains\Models\Subdomain; use Illuminate\Support\Facades\Http; use Illuminate\Support\ServiceProvider; @@ -14,6 +15,7 @@ class SubdomainsPluginProvider extends ServiceProvider { public function register(): void { + NodeResource::registerCustomPages(['edit' => EditNode::route('/{record}/edit')]); ServerResource::registerCustomRelations(SubdomainRelationManager::class); Role::registerCustomDefaultPermissions('cloudflare_domain'); From c476a60cb3cbca9474759ba8ed928164454ccc8a Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Thu, 1 Jan 2026 20:02:53 +1100 Subject: [PATCH 35/79] lang updates for new notifications --- subdomains/lang/en/strings.php | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index 3b8f3872..0d42305b 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -3,39 +3,44 @@ return [ 'no_domains' => 'No Domains', 'domain' => 'Domain|Domains', - 'no_subdomains' => 'No Subdomains', 'subdomain' => 'Subdomain|Subdomains', + 'name' => 'Name', + 'limit' => 'Limit', 'change_limit' => 'Change Limit', 'limit_changed' => 'Limit changed', 'limit_reached' => 'Subdomain Limit Reached', - 'create_subdomain' => 'Create Subdomain', - 'name' => 'Name', + 'create_subdomain' => 'Create Subdomain', + 'subdomain_change_limit' => "Change Subdomain Limit", + 'subdomain_limit' => "Subdomain Limit", + 'record_type' => 'Record Type', 'srv_record' => 'SRV Record', 'srv_record_help' => 'Enable this option to create a SRV record instead of an A or AAAA record.', + 'set_srv_target' => 'Set SRV Target', '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.', - ], + 'srv_target_missing' => 'SRV record is missing from node configuration.', + 'srv_target_confirmation' => 'Changing the SRV target will require all existing subdomains using SRV records to be updated. Are you sure you want to continue?', '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' => [ + 'srv_target_updated_title' => 'SRV target changed successfully.', + 'srv_target_updated' => 'Existing subdomains using SRV records will need to be updated to use the new target.', + 'cloudflare_missing_zone_title' => 'Cloudflare: Missing Zone ID', '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 save SRV record.', + 'cloudflare_missing_srv_port' => 'SRV port is missing for :server.', 'cloudflare_missing_srv_target_title' => 'Cloudflare: Missing SRV Target', - 'cloudflare_missing_srv_target' => 'SRV target is missing for :subdomain. Cannot save SRV record.', + 'cloudflare_missing_srv_target' => 'SRV target is missing from :node. ', 'cloudflare_record_updated_title' => 'Cloudflare: Record Updated', 'cloudflare_record_updated' => 'Successfully updated :subdomain record to :record_type', @@ -55,6 +60,9 @@ 'cloudflare_zone_fetch_failed' => 'Failed to fetch Cloudflare Zone ID for domain: :domain', 'cloudflare_domain_saved' => 'Successfully saved domain: :domain', - 'settings_saved' => 'Settings saved', + 'cloudflare_invalid_service_record_type_title' => 'Cloudflare: Unable to determine service type', + 'cloudflare_invalid_service_record_type' => 'Unable to determine service type for SRV record of :subdomain. Please check egg is correctly tagged', ], + + 'settings_saved' => 'Settings saved', ]; From 51252636a54376b51416e14d3a0b6cbf7ca2b232 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Thu, 1 Jan 2026 20:07:10 +1100 Subject: [PATCH 36/79] Changing SRV record service from hard-coded for minecraft to based on egg tags (Only minecraft at this time) --- subdomains/src/Enums/ServiceRecordType.php | 58 +++++++++++++++++++ subdomains/src/Models/Subdomain.php | 32 ++++++---- subdomains/src/Services/CloudflareService.php | 2 +- 3 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 subdomains/src/Enums/ServiceRecordType.php diff --git a/subdomains/src/Enums/ServiceRecordType.php b/subdomains/src/Enums/ServiceRecordType.php new file mode 100644 index 00000000..f3a2130a --- /dev/null +++ b/subdomains/src/Enums/ServiceRecordType.php @@ -0,0 +1,58 @@ +name)->title(); + } + + public static function isSupported(Server $server): bool + { + $tags = $server->egg->tags ?? []; + foreach (self::cases() as $type) { + if (in_array($type->name, $tags)) { + return true; + } + } + return false; + } + + public static function fromServer(Server $server): ?self + { + $tags = $server->egg->tags ?? []; + return self::fromTags($tags); + } + + /** @param string[] $tags */ + public static function fromTags(array $tags): ?self + { + foreach (self::cases() as $type) { + if (in_array($type->name, $tags)) { + return $type; + } + } + + return null; + } + + public function service(): string + { + $parts = explode('.', $this->value); + return $parts[0]; + } + + public function protocol(): string + { + $parts = explode('.', $this->value); + return $parts[1]; + } +} diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 72884295..96b0282d 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -91,7 +91,7 @@ public function setRecordType($value): void protected function upsertOnCloudflare(): void { - $service = app(CloudflareService::class); + $registrar = app(CloudflareService::class); $zoneId = $this->domain->cloudflare_id; if (empty($zoneId)) { @@ -106,13 +106,12 @@ protected function upsertOnCloudflare(): void return; } - // SRV: target comes from domain, port from server allocation + // SRV: target comes from node, port from server allocation if ($this->record_type === 'SRV') { $port = $this->server && $this->server->allocation ? ($this->server->allocation->port ?? null) : null; if (empty($port)) { Log::warning('Server missing allocation with port', $this->toArray()); - Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_missing_srv_port_title')) @@ -122,19 +121,32 @@ 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]); + $serviceRecordType = ServiceRecordType::fromServer($this->server); + if (!$serviceRecordType) { + Log::warning('Unable to determine service record type for SRV record', ['server_id' => $this->server->id, 'server' => $this->server->name]); + Notification::make() + ->danger() + ->title(trans('subdomains::strings.notifications.cloudflare_invalid_service_record_type_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_invalid_service_record_type', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) + ->send(); + + return; + } + if (!$this->server || !$this->server->node || empty($this->server->node->srv_target)) { + Log::warning('Node missing SRV target for SRV record', ['node_id' => $this->server->node?->id ?? null]); 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')])) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_target', ['node' => $this->server->node->name])) ->send(); return; } - $result = $service->upsertDnsRecord($zoneId, $this->name, 'SRV', $this->domain->srv_target, $this->cloudflare_id, $port); + $recordName = sprintf('%s.%s.%s', $serviceRecordType->service(), $serviceRecordType->protocol(), $this->name); + + $result = $registrar->upsertDnsRecord($zoneId, $recordName, 'SRV', $this->server->node->srv_target, $this->cloudflare_id, $port, $serviceRecordType); if ($result['success'] && !empty($result['id'])) { if ($this->cloudflare_id !== $result['id']) { @@ -172,7 +184,7 @@ protected function upsertOnCloudflare(): void return; } - $result = $service->upsertDnsRecord($zoneId, $this->name, $this->record_type, $this->server->allocation->ip, $this->cloudflare_id, null); + $result = $registrar->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']) { @@ -201,9 +213,9 @@ protected function upsertOnCloudflare(): void protected function deleteOnCloudflare(): void { if ($this->cloudflare_id && $this->domain && $this->domain->cloudflare_id) { - $service = app(CloudflareService::class); + $registrar = app(CloudflareService::class); - $result = $service->deleteDnsRecord($this->domain->cloudflare_id, $this->cloudflare_id); + $result = $registrar->deleteDnsRecord($this->domain->cloudflare_id, $this->cloudflare_id); if (!empty($result['success'])) { Notification::make() diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index b0525e52..ab19184a 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -64,7 +64,7 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType } $payload = [ - 'name' => sprintf('_minecraft._tcp.%s', $name), + 'name' => $name, 'ttl' => $ttl, 'type' => 'SRV', 'comment' => $comment, From de62b89ce85bf8812c2c3393dcfff48954052749 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Thu, 1 Jan 2026 20:26:22 +1100 Subject: [PATCH 37/79] cleaning up record_type/srv_record code --- subdomains/src/Models/Subdomain.php | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 96b0282d..eaf8bca3 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -36,17 +36,14 @@ class Subdomain extends Model implements HasLabel 'srv_record' => 'boolean', ]; + protected $appends = [ + 'srv_record', + ]; + protected static function boot(): void { parent::boot(); - static::saving(function (self $model) { - if (array_key_exists('srv_record', $model->attributes)) { - $model->setRecordType($model->attributes['srv_record']); - unset($model->attributes['srv_record']); - } - }); - static::saved(function (self $model) { $model->upsertOnCloudflare(); }); @@ -76,12 +73,13 @@ public function getSrvRecordAttribute(): bool return $this->record_type === 'SRV'; } - public function setRecordType($value): void + public function setSrvRecordAttribute($isSrvRecord): void { - if ($value) { + if ($isSrvRecord) { $this->attributes['record_type'] = 'SRV'; } else { - if ($this->server && $this->server->allocation && is_ipv6($this->server->allocation->ip)) { + $ip = $this->server?->allocation?->ip ?? null; + if (!empty($ip) && filter_var($ip, FILTER_FLAG_IPV6)) { $this->attributes['record_type'] = 'AAAA'; } else { $this->attributes['record_type'] = 'A'; From 8b7df3e32aa3ff45b4e3c1d92c8d78b90a372e30 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Thu, 1 Jan 2026 20:27:27 +1100 Subject: [PATCH 38/79] Fixed undefined class --- subdomains/src/Models/Subdomain.php | 1 + 1 file changed, 1 insertion(+) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index eaf8bca3..e3560a99 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -3,6 +3,7 @@ namespace Boy132\Subdomains\Models; use App\Models\Server; +use Boy132\Subdomains\Enums\ServiceRecordType; use Boy132\Subdomains\Services\CloudflareService; use Filament\Notifications\Notification; use Filament\Support\Contracts\HasLabel; From 194243ca973dfe33241d265a80b1831984037681 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Thu, 1 Jan 2026 20:29:14 +1100 Subject: [PATCH 39/79] cx and pint formatting --- subdomains/lang/en/strings.php | 4 ++-- subdomains/src/Enums/ServiceRecordType.php | 4 ++++ .../Server/Resources/Subdomains/SubdomainResource.php | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index 0d42305b..6ad3ff72 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -13,8 +13,8 @@ 'limit_reached' => 'Subdomain Limit Reached', 'create_subdomain' => 'Create Subdomain', - 'subdomain_change_limit' => "Change Subdomain Limit", - 'subdomain_limit' => "Subdomain Limit", + 'subdomain_change_limit' => 'Change Subdomain Limit', + 'subdomain_limit' => 'Subdomain Limit', 'record_type' => 'Record Type', 'srv_record' => 'SRV Record', diff --git a/subdomains/src/Enums/ServiceRecordType.php b/subdomains/src/Enums/ServiceRecordType.php index f3a2130a..05fa224b 100644 --- a/subdomains/src/Enums/ServiceRecordType.php +++ b/subdomains/src/Enums/ServiceRecordType.php @@ -23,12 +23,14 @@ public static function isSupported(Server $server): bool return true; } } + return false; } public static function fromServer(Server $server): ?self { $tags = $server->egg->tags ?? []; + return self::fromTags($tags); } @@ -47,12 +49,14 @@ public static function fromTags(array $tags): ?self public function service(): string { $parts = explode('.', $this->value); + return $parts[0]; } public function protocol(): string { $parts = explode('.', $this->value); + return $parts[1]; } } diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index e017dc6c..9d2e40fd 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -19,7 +19,6 @@ 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 @@ -112,6 +111,7 @@ public static function form(Schema $schema): Schema TextInput::make('name') ->label(trans('subdomains::strings.name')) ->required() + ->suffix(fn (callable $get) => CloudflareDomain::find($get('domain_id'))?->name) ->unique(), Select::make('domain_id') ->label(trans_choice('subdomains::strings.domain', 1)) From 079563f613b231714fbaba0441bd73547b00ad6e Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Thu, 1 Jan 2026 20:44:39 +1100 Subject: [PATCH 40/79] pint style fixes --- .../005_add_srv_target_to_nodes.php | 60 +++++++++---------- .../Filament/Admin/Nodes/Pages/EditNode.php | 22 ++++--- 2 files changed, 40 insertions(+), 42 deletions(-) diff --git a/subdomains/database/migrations/005_add_srv_target_to_nodes.php b/subdomains/database/migrations/005_add_srv_target_to_nodes.php index cab88da6..dc30a4b8 100644 --- a/subdomains/database/migrations/005_add_srv_target_to_nodes.php +++ b/subdomains/database/migrations/005_add_srv_target_to_nodes.php @@ -1,30 +1,30 @@ -dropColumn('srv_target'); - }); - - Schema::table('nodes', function (Blueprint $table) { - $table->string('srv_target')->nullable()->after('fqdn'); - }); - } - - public function down(): void - { - Schema::table('cloudflare_domains', function (Blueprint $table) { - $table->string('srv_target')->nullable()->after('cloudflare_id'); - }); - - Schema::table('nodes', function (Blueprint $table) { - $table->dropColumn('srv_target'); - }); - } -}; +dropColumn('srv_target'); + }); + + Schema::table('nodes', function (Blueprint $table) { + $table->string('srv_target')->nullable()->after('fqdn'); + }); + } + + public function down(): void + { + Schema::table('cloudflare_domains', function (Blueprint $table) { + $table->string('srv_target')->nullable()->after('cloudflare_id'); + }); + + Schema::table('nodes', function (Blueprint $table) { + $table->dropColumn('srv_target'); + }); + } +}; diff --git a/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php b/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php index c819888b..cc5cd7c0 100644 --- a/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php +++ b/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php @@ -2,19 +2,12 @@ namespace Boy132\Subdomains\Filament\Admin\Resources\Nodes\Pages; -use App\Filament\Admin\Resources\Nodes\NodeResource; use App\Filament\Admin\Resources\Nodes\Pages\EditNode as BaseEditNode; use App\Models\Node; -use App\Repositories\Daemon\DaemonSystemRepository; use App\Services\Helpers\SoftwareVersionService; use App\Services\Nodes\NodeAutoDeployService; -use App\Services\Nodes\NodeUpdateService; -use App\Traits\Filament\CanCustomizeHeaderActions; -use App\Traits\Filament\CanCustomizeHeaderWidgets; -use Boy132\Subdomains\Models\Subdomain; use Exception; use Filament\Actions\Action; -use Filament\Actions\DeleteAction; use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Slider; use Filament\Forms\Components\Slider\Enums\PipsMode; @@ -25,7 +18,6 @@ use Filament\Infolists\Components\CodeEntry; use Filament\Infolists\Components\TextEntry; use Filament\Notifications\Notification; -use Filament\Resources\Pages\EditRecord; use Filament\Schemas\Components\Actions; use Filament\Schemas\Components\Fieldset; use Filament\Schemas\Components\Grid; @@ -42,11 +34,9 @@ use Filament\Support\RawJs; use Illuminate\Http\Client\ConnectionException; use Illuminate\Support\Facades\Http; -use Illuminate\Support\HtmlString; use Illuminate\Support\Facades\Log; +use Illuminate\Support\HtmlString; use Phiki\Grammar\Grammar; -use Boy132\Subdomains\Jobs\UpdateServerSubdomains; -use App\Models\Server; class EditNode extends BaseEditNode { @@ -395,6 +385,14 @@ public function form(Schema $schema): Schema ->warning() ->send(); + // Update all subdomains on servers linked to this node + $serverIds = Server::where('node_id', $this->record->id)->pluck('id'); + Log::info('Updating subdomains for Node ID ', ['node_id' => $this->record->id, 'serverIds' => $serverIds]); + + foreach ($serverIds as $serverId) { + UpdateServerSubdomains::dispatch($serverId, $state); + } + return $state; }), Grid::make() @@ -789,4 +787,4 @@ public function form(Schema $schema): Schema ]), ]); } -} \ No newline at end of file +} From 12db61c90ce6eff7c48dd3c5bd3d951970c03c02 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Thu, 1 Jan 2026 21:00:54 +1100 Subject: [PATCH 41/79] moving to different branch --- subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php b/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php index cc5cd7c0..cfe48d4d 100644 --- a/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php +++ b/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php @@ -385,14 +385,6 @@ public function form(Schema $schema): Schema ->warning() ->send(); - // Update all subdomains on servers linked to this node - $serverIds = Server::where('node_id', $this->record->id)->pluck('id'); - Log::info('Updating subdomains for Node ID ', ['node_id' => $this->record->id, 'serverIds' => $serverIds]); - - foreach ($serverIds as $serverId) { - UpdateServerSubdomains::dispatch($serverId, $state); - } - return $state; }), Grid::make() From f2ba928534551b687b4805d7efb260be437c8e50 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Thu, 1 Jan 2026 21:01:09 +1100 Subject: [PATCH 42/79] shortened/simplified all npe checks --- subdomains/src/Models/Subdomain.php | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index e3560a99..5da74ea3 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -92,14 +92,15 @@ protected function upsertOnCloudflare(): void { $registrar = app(CloudflareService::class); - $zoneId = $this->domain->cloudflare_id; + // add null check for domain + $zoneId = $this->domain?->cloudflare_id ?? null; if (empty($zoneId)) { Log::warning('Cloudflare zone id missing for domain', ['domain_id' => $this->domain_id]); Notification::make() ->danger() ->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')])) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_zone', ['domain' => $this->domain?->name ?? 'unknown', 'subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown')])) ->send(); return; @@ -107,14 +108,14 @@ protected function upsertOnCloudflare(): void // SRV: target comes from node, port from server allocation if ($this->record_type === 'SRV') { - $port = $this->server && $this->server->allocation ? ($this->server->allocation->port ?? null) : null; + $port = $this->server?->allocation?->port ?? null; if (empty($port)) { Log::warning('Server missing allocation with port', $this->toArray()); Notification::make() ->danger() ->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')])) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_port', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown')])) ->send(); return; @@ -122,22 +123,22 @@ protected function upsertOnCloudflare(): void $serviceRecordType = ServiceRecordType::fromServer($this->server); if (!$serviceRecordType) { - Log::warning('Unable to determine service record type for SRV record', ['server_id' => $this->server->id, 'server' => $this->server->name]); + Log::warning('Unable to determine service record type for SRV record', ['server_id' => $this->server?->id, 'server' => $this->server?->name]); Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_invalid_service_record_type_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_invalid_service_record_type', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) + ->body(trans('subdomains::strings.notifications.cloudflare_invalid_service_record_type', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown')])) ->send(); return; } - if (!$this->server || !$this->server->node || empty($this->server->node->srv_target)) { + if (empty($this->server?->node?->srv_target)) { Log::warning('Node missing SRV target for SRV record', ['node_id' => $this->server->node?->id ?? null]); Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_missing_srv_target_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_target', ['node' => $this->server->node->name])) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_target', ['node' => $this->server->node?->name ?? 'unknown'])) ->send(); return; @@ -171,7 +172,8 @@ protected function upsertOnCloudflare(): void } // A/AAAA - if (!$this->server || !$this->server->allocation || $this->server->allocation->ip === '0.0.0.0' || $this->server->allocation->ip === '::') { + $ip = $this->server?->allocation?->ip; + if (empty($ip) || $ip === '0.0.0.0' || $ip === '::') { Log::warning('Server allocation missing or invalid IP', ['server_id' => $this->server_id]); Notification::make() @@ -183,7 +185,7 @@ protected function upsertOnCloudflare(): void return; } - $result = $registrar->upsertDnsRecord($zoneId, $this->name, $this->record_type, $this->server->allocation->ip, $this->cloudflare_id, null); + $result = $registrar->upsertDnsRecord($zoneId, $this->name, $this->record_type, $ip, $this->cloudflare_id, null); if ($result['success'] && !empty($result['id'])) { if ($this->cloudflare_id !== $result['id']) { From ccaa94eaef417eed5b4b1b599136a8334a83f2c2 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Thu, 1 Jan 2026 21:11:21 +1100 Subject: [PATCH 43/79] more simplified npe checks --- subdomains/src/Models/Subdomain.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 5da74ea3..4b1463f5 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -80,7 +80,7 @@ public function setSrvRecordAttribute($isSrvRecord): void $this->attributes['record_type'] = 'SRV'; } else { $ip = $this->server?->allocation?->ip ?? null; - if (!empty($ip) && filter_var($ip, FILTER_FLAG_IPV6)) { + if (!empty($ip) && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $this->attributes['record_type'] = 'AAAA'; } else { $this->attributes['record_type'] = 'A'; @@ -156,7 +156,7 @@ protected function upsertOnCloudflare(): void Notification::make() ->success() ->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])) + ->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]); @@ -164,7 +164,7 @@ protected function upsertOnCloudflare(): void Notification::make() ->danger() ->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'] ?? [])])) + ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) ->send(); } @@ -172,14 +172,14 @@ protected function upsertOnCloudflare(): void } // A/AAAA - $ip = $this->server?->allocation?->ip; + $ip = $this->server?->allocation?->ip ?? null; if (empty($ip) || $ip === '0.0.0.0' || $ip === '::') { Log::warning('Server allocation missing or invalid IP', ['server_id' => $this->server_id]); Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_missing_ip_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_missing_ip', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_ip', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown')])) ->send(); return; @@ -195,12 +195,12 @@ protected function upsertOnCloudflare(): void Notification::make() ->success() ->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])) + ->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]); - $domainName = $this->domain->name ?? 'unknown'; + $domainName = $this->domain?->name ?? 'unknown'; $sub = sprintf('%s.%s', $this->name, $domainName); Notification::make() @@ -222,13 +222,13 @@ protected function deleteOnCloudflare(): void Notification::make() ->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')])) + ->body(trans('subdomains::strings.notifications.cloudflare_delete_success', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown')])) ->send(); } 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'] ?? [])])) + ->body(trans('subdomains::strings.notifications.cloudflare_delete_failed', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) ->send(); } From f4757fdb94d14059e83eecccf12fd69a6f0aa3b1 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Thu, 1 Jan 2026 21:15:17 +1100 Subject: [PATCH 44/79] npe and pint style issues --- subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php | 1 - subdomains/src/Services/CloudflareService.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php b/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php index cfe48d4d..250fff9e 100644 --- a/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php +++ b/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php @@ -34,7 +34,6 @@ use Filament\Support\RawJs; use Illuminate\Http\Client\ConnectionException; use Illuminate\Support\Facades\Http; -use Illuminate\Support\Facades\Log; use Illuminate\Support\HtmlString; use Phiki\Grammar\Grammar; diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index ab19184a..17813787 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() && ($body['success'] === true || count($body['result']) > 0); + $success = $response->successful() && ($body['success'] === true || (is_array($body['result']) && count($body['result']) > 0)); return [ 'success' => $success, From 060dd6e99d71a11837b04702708b17dbe3bcc3f4 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Fri, 2 Jan 2026 07:43:29 +1100 Subject: [PATCH 45/79] Added field updated check for srv_target --- subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php b/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php index 250fff9e..4f6f9c31 100644 --- a/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php +++ b/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php @@ -36,6 +36,7 @@ use Illuminate\Support\Facades\Http; use Illuminate\Support\HtmlString; use Phiki\Grammar\Grammar; +use Throwable; class EditNode extends BaseEditNode { @@ -376,8 +377,11 @@ public function form(Schema $schema): Schema 'lg' => 3, ]) ->dehydrateStateUsing(function ($state) { - $this->record->forceFill(['srv_target' => $state])->save(); + if ($this->record->srv_target === $state) { + return $state; + } + $this->record->forceFill(['srv_target' => $state])->save(); Notification::make() ->title(trans('subdomains::strings.notifications.srv_target_updated_title')) ->body(trans('subdomains::strings.notifications.srv_target_updated')) @@ -386,6 +390,7 @@ public function form(Schema $schema): Schema return $state; }), + // Subdomains plugin field Grid::make() ->columns([ 'default' => 1, From fa0e3a6446a7d21180c92d549f3be66ecbea50e1 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Fri, 2 Jan 2026 15:01:04 +1100 Subject: [PATCH 46/79] creating set srv_target button --- .../Components/Actions/SetSrvTargetAction.php | 48 +++++++++++++++++++ .../Providers/SubdomainsPluginProvider.php | 4 +- 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php diff --git a/subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php b/subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php new file mode 100644 index 00000000..ef1feb7a --- /dev/null +++ b/subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php @@ -0,0 +1,48 @@ +label(fn () => trans('subdomains::strings.set_srv_target')); + + $this->icon('tabler-world-www'); + + $this->schema(function (?Node $node) { + return [ + TextInput::make('srv_target') + ->label(fn () => trans('subdomains::strings.srv_target')) + ->default(fn () => $node?->srv_target) + ->placeholder('play.example.com OR IPv4/IPv6 address') + ->helperText(trans('subdomains::strings.srv_target_confirmation')) + - requireConfirmation() + ->rules(['nullable', 'string', 'regex:/^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(?action(function (Node $node, array $data) { + // ForceFill so we don't need to overwrite on Node::$fillable + $node->forceFill(['srv_target' => $data['srv_target']])->save(); + + Notification::make() + ->title(trans('subdomains::strings.notifications.srv_target_updated_title')) + ->body(trans('subdomains::strings.notifications.srv_target_updated')) + ->warning() + ->send(); + })->requiresConfirmation()->modalIconColor('danger')->modalDescription(trans('subdomains::strings.srv_target_confirmation')->toString()); + } +} diff --git a/subdomains/src/Providers/SubdomainsPluginProvider.php b/subdomains/src/Providers/SubdomainsPluginProvider.php index 4bd21988..d1cb0b76 100644 --- a/subdomains/src/Providers/SubdomainsPluginProvider.php +++ b/subdomains/src/Providers/SubdomainsPluginProvider.php @@ -2,11 +2,13 @@ namespace Boy132\Subdomains\Providers; +use App\Enums\HeaderActionPosition; use App\Filament\Admin\Resources\Nodes\NodeResource; use App\Filament\Admin\Resources\Servers\ServerResource; use App\Models\Role; use App\Models\Server; use Boy132\Subdomains\Filament\Admin\Resources\Nodes\Pages\EditNode; +use Boy132\Subdomains\Filament\Components\Actions\SetSrvTargetAction; use Boy132\Subdomains\Models\Subdomain; use Illuminate\Support\Facades\Http; use Illuminate\Support\ServiceProvider; @@ -15,8 +17,8 @@ class SubdomainsPluginProvider extends ServiceProvider { public function register(): void { - NodeResource::registerCustomPages(['edit' => EditNode::route('/{record}/edit')]); ServerResource::registerCustomRelations(SubdomainRelationManager::class); + EditNode::registerCustomHeaderActions(HeaderActionPosition::Before, SetSrvTargetAction::make()); Role::registerCustomDefaultPermissions('cloudflare_domain'); Role::registerCustomModelIcon('cloudflare_domain', 'tabler-world-www'); From 565adf6085f1613a7097b245bc09bd8401599fed Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Fri, 2 Jan 2026 15:04:10 +1100 Subject: [PATCH 47/79] changing subdomain model logic to: Update registrar, if success, update DB --- subdomains/src/Models/Subdomain.php | 145 +++++++++++++++++----------- 1 file changed, 86 insertions(+), 59 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 4b1463f5..b24a4c25 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -5,6 +5,7 @@ use App\Models\Server; use Boy132\Subdomains\Enums\ServiceRecordType; use Boy132\Subdomains\Services\CloudflareService; +use Filament\Facades\Filament; use Filament\Notifications\Notification; use Filament\Support\Contracts\HasLabel; use Illuminate\Contracts\Support\Htmlable; @@ -45,12 +46,52 @@ protected static function boot(): void { parent::boot(); - static::saved(function (self $model) { - $model->upsertOnCloudflare(); + static::creating(function (self $model) { + // Relation does not exist yet, so we need to set it manually. + $model->setRelation('server', Filament::getTenant()); + + $registrarUpdated = $model->upsertOnCloudflare(); + if (!$registrarUpdated) { + return false; + } + + Notification::make() + ->success() + ->title(trans('subdomains::strings.notifications.cloudflare_record_created_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_record_created', ['subdomain' => $model->name . '.' . ($model->domain?->name ?? 'unknown'), 'record_type' => $model->record_type])) + ->send(); + + return true; }); - static::deleted(function (self $model) { - $model->deleteOnCloudflare(); + static::updating(function (self $model) { + $registrarUpdated = $model->upsertOnCloudflare(); + if (!$registrarUpdated) { + return false; + } + + Notification::make() + ->success() + ->title(trans('subdomains::strings.notifications.cloudflare_record_updated_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_record_updated', ['subdomain' => $model->name . '.' . ($model->domain?->name ?? 'unknown'), 'record_type' => $model->record_type])) + ->send(); + + return true; + }); + + static::deleting(function (self $model) { + $registrarUpdated = $model->deleteOnCloudflare(); + if (!$registrarUpdated) { + return false; + } + + Notification::make() + ->success() + ->title(trans('subdomains::strings.notifications.cloudflare_record_deleted_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_record_deleted', ['subdomain' => $model->name . '.' . ($model->domain?->name ?? 'unknown')])) + ->send(); + + return true; }); } @@ -88,11 +129,10 @@ public function setSrvRecordAttribute($isSrvRecord): void } } - protected function upsertOnCloudflare(): void + protected function upsertOnCloudflare(): bool { $registrar = app(CloudflareService::class); - // add null check for domain $zoneId = $this->domain?->cloudflare_id ?? null; if (empty($zoneId)) { Log::warning('Cloudflare zone id missing for domain', ['domain_id' => $this->domain_id]); @@ -103,22 +143,21 @@ protected function upsertOnCloudflare(): void ->body(trans('subdomains::strings.notifications.cloudflare_missing_zone', ['domain' => $this->domain?->name ?? 'unknown', 'subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown')])) ->send(); - return; + return false; } // SRV: target comes from node, port from server allocation if ($this->record_type === 'SRV') { - $port = $this->server?->allocation?->port ?? null; - + $port = $this->server->allocation?->port ?? null; if (empty($port)) { Log::warning('Server missing allocation with port', $this->toArray()); Notification::make() ->danger() ->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')])) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_port', ['server' => $this->server?->name ?? 'unassigned'])) ->send(); - return; + return false; } $serviceRecordType = ServiceRecordType::fromServer($this->server); @@ -130,7 +169,7 @@ protected function upsertOnCloudflare(): void ->body(trans('subdomains::strings.notifications.cloudflare_invalid_service_record_type', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown')])) ->send(); - return; + return false; } if (empty($this->server?->node?->srv_target)) { @@ -141,48 +180,41 @@ protected function upsertOnCloudflare(): void ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_target', ['node' => $this->server->node?->name ?? 'unknown'])) ->send(); - return; + return false; } $recordName = sprintf('%s.%s.%s', $serviceRecordType->service(), $serviceRecordType->protocol(), $this->name); - $result = $registrar->upsertDnsRecord($zoneId, $recordName, 'SRV', $this->server->node->srv_target, $this->cloudflare_id, $port, $serviceRecordType); + $result = $registrar->upsertDnsRecord($zoneId, $recordName, 'SRV', $this->server->node->srv_target, $this->cloudflare_id, $port); if ($result['success'] && !empty($result['id'])) { if ($this->cloudflare_id !== $result['id']) { $this->updateQuietly(['cloudflare_id' => $result['id']]); } - Notification::make() - ->success() - ->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(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(); + return true; } + Log::error('Failed to upsert SRV record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); + Notification::make() + ->danger() + ->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(); - return; + return false; } // A/AAAA $ip = $this->server?->allocation?->ip ?? null; if (empty($ip) || $ip === '0.0.0.0' || $ip === '::') { Log::warning('Server allocation missing or invalid IP', ['server_id' => $this->server_id]); - Notification::make() ->danger() ->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; + return false; } $result = $registrar->upsertDnsRecord($zoneId, $this->name, $this->record_type, $ip, $this->cloudflare_id, null); @@ -192,47 +224,42 @@ protected function upsertOnCloudflare(): void $this->updateQuietly(['cloudflare_id' => $result['id']]); } - Notification::make() - ->success() - ->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]); + return true; + } - $domainName = $this->domain?->name ?? 'unknown'; - $sub = sprintf('%s.%s', $this->name, $domainName); + $domainName = $this->domain?->name ?? 'unknown'; + $subdomain = sprintf('%s.%s', $this->name, $domainName); - Notification::make() - ->danger() - ->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(); - } + Log::error('Failed to upsert record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); + Notification::make() + ->danger() + ->title(trans('subdomains::strings.notifications.cloudflare_upsert_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $subdomain, 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) + ->send(); + + return false; } - protected function deleteOnCloudflare(): void + protected function deleteOnCloudflare(): bool { if ($this->cloudflare_id && $this->domain && $this->domain->cloudflare_id) { $registrar = app(CloudflareService::class); $result = $registrar->deleteDnsRecord($this->domain->cloudflare_id, $this->cloudflare_id); - if (!empty($result['success'])) { - Notification::make() - ->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(); - } 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(); + if (!empty($result['success']) || $result['status'] === 404) { + return true; } - return; + 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(); + + return false; } + + return true; } } From 912984c1c99dd71eb69921a86b84f24b5e1d75fb Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Fri, 2 Jan 2026 15:04:52 +1100 Subject: [PATCH 48/79] fixing notification issues --- subdomains/lang/en/strings.php | 14 +++--- subdomains/src/Services/CloudflareService.php | 47 +++++++++---------- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index 6ad3ff72..701cbe55 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -24,7 +24,7 @@ 'srv_target' => 'SRV Target', 'srv_target_help' => 'The hostname that SRV records point to (for example: play.example.com).', 'srv_target_missing' => 'SRV record is missing from node configuration.', - 'srv_target_confirmation' => 'Changing the SRV target will require all existing subdomains using SRV records to be updated. Are you sure you want to continue?', + 'srv_target_confirmation' => 'Changing the SRV target will require all existing subdomains using SRV records to be updated to reflect the new target.', '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".', @@ -42,21 +42,21 @@ 'cloudflare_missing_srv_target_title' => 'Cloudflare: Missing SRV Target', 'cloudflare_missing_srv_target' => 'SRV target is missing from :node. ', + 'cloudflare_record_created_title' => 'Cloudflare: Record Created', + 'cloudflare_record_created' => 'Successfully created :subdomain record of type :record', + 'cloudflare_record_updated_title' => 'Cloudflare: Record Updated', 'cloudflare_record_updated' => 'Successfully updated :subdomain record to :record_type', + 'cloudflare_record_deleted_title' => 'Cloudflare: Record Deleted', + 'cloudflare_record_deleted' => 'Successfully deleted Cloudflare record for :subdomain.', + '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' => '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.', - - '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', diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index 17813787..af8cc9a5 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -23,10 +23,9 @@ public function getZoneId(string $domainName): ?string } catch (\Throwable $e) { Log::error('Cloudflare getZoneId request failed: ' . $e->getMessage(), ['domain' => $domainName]); - return null; + return ['success' => false, 'errors' => ['exception' => $e->getMessage()], 'status' => $e->getCode(), 'body' => $e->getTraceAsString()]; } - $status = $response->status(); $body = $response->json() ?? []; if ($response->successful() && !empty($body['result']) && count($body['result']) > 0) { @@ -34,10 +33,10 @@ public function getZoneId(string $domainName): ?string } if (!empty($body['errors'])) { - Log::warning('Cloudflare getZoneId returned errors', ['domain' => $domainName, 'status' => $status, 'errors' => $body['errors']]); + Log::warning('Cloudflare getZoneId returned errors', ['domain' => $domainName, 'status' => $response->status(), 'errors' => $body['errors']]); } - return null; + return ['success' => false, 'errors' => [], 'status' => $response->status(), 'body' => $body]; } public function upsertDnsRecord(string $zoneId, string $name, string $recordType, string $target, ?string $recordId = null, ?int $port = null): array @@ -113,28 +112,12 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType return $parsed; } catch (\Throwable $e) { - Log::error('Cloudflare upsert exception: ' . $e->getMessage(), ['zone' => $zoneId, 'payload' => $payload]); + Log::error('Cloudflare upsert exception: ' . $e->getMessage(), ['zone' => $zoneId, 'payload' => $payload, 'status' => $e->getCode()]); - return ['success' => false, 'id' => null, 'errors' => ['exception' => $e->getMessage()], 'status' => 0, 'body' => null]; + return ['success' => false, 'errors' => ['exception' => $e->getMessage()], 'status' => $e->getCode(), 'body' => $e->getTraceAsString()]; } } - protected function parseCloudflareHttpResponse(Response $response): array - { - $status = $response->status(); - $body = $response->json() ?? []; - - $success = $response->successful() && ($body['success'] === true || (is_array($body['result']) && count($body['result']) > 0)); - - return [ - 'success' => $success, - 'id' => $body['result']['id'] ?? null, - 'errors' => $body['errors'] ?? [], - 'status' => $status, - 'body' => $body, - ]; - } - public function deleteDnsRecord(string $zoneId, string $recordId): array { if (empty($zoneId) || empty($recordId)) { @@ -154,9 +137,25 @@ public function deleteDnsRecord(string $zoneId, string $recordId): array return $parsed; } catch (\Throwable $e) { - Log::error('Cloudflare delete exception: ' . $e->getMessage(), ['zone' => $zoneId, 'id' => $recordId]); + Log::error('Cloudflare delete exception: ' . $e->getMessage(), ['zone' => $zoneId, 'id' => $recordId, 'payload' => $payload, 'status' => $e->getCode()]); - return ['success' => false, 'errors' => ['exception' => $e->getMessage()], 'status' => 0, 'body' => null]; + return ['success' => false, 'errors' => ['exception' => $e->getMessage()], 'status' => $e->getCode(), 'body' => $e->getTraceAsString()]; } } + + protected function parseCloudflareHttpResponse(Response $response): array + { + $status = $response->status(); + $body = $response->json() ?? []; + + $success = $response->successful() && ($body['success'] === true || (is_array($body['result']) && count($body['result']) > 0)); + + return [ + 'success' => $success, + 'id' => $body['result']['id'] ?? null, + 'errors' => $body['errors'] ?? [], + 'status' => $status, + 'body' => $body, + ]; + } } From c771faa9bbe2c3e0d43471a7349ccabc20cffef0 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Fri, 2 Jan 2026 15:39:27 +1100 Subject: [PATCH 49/79] moved to button --- .../Filament/Admin/Nodes/Pages/EditNode.php | 786 ------------------ 1 file changed, 786 deletions(-) delete mode 100644 subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php diff --git a/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php b/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php deleted file mode 100644 index 4f6f9c31..00000000 --- a/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php +++ /dev/null @@ -1,786 +0,0 @@ -components([ - Tabs::make('Tabs') - ->columns([ - 'default' => 2, - 'sm' => 3, - 'md' => 3, - 'lg' => 4, - ]) - ->persistTabInQueryString() - ->columnSpanFull() - ->tabs([ - Tab::make('overview') - ->label(trans('admin/node.tabs.overview')) - ->icon('tabler-chart-area-line-filled') - ->columns([ - 'default' => 4, - 'sm' => 2, - 'md' => 4, - 'lg' => 4, - ]) - ->schema([ - Fieldset::make() - ->label(trans('admin/node.node_info')) - ->columns(4) - ->columnSpanFull() - ->schema([ - TextEntry::make('wings_version') - ->label(trans('admin/node.wings_version')) - ->state(fn (Node $node, SoftwareVersionService $versionService) => ($node->systemInformation()['version'] ?? trans('admin/node.unknown')) . ' ' . trans('admin/node.latest', ['version' => $versionService->latestWingsVersion()])), - TextEntry::make('cpu_threads') - ->label(trans('admin/node.cpu_threads')) - ->state(fn (Node $node) => $node->systemInformation()['cpu_count'] ?? 0), - TextEntry::make('architecture') - ->label(trans('admin/node.architecture')) - ->state(fn (Node $node) => $node->systemInformation()['architecture'] ?? trans('admin/node.unknown')), - TextEntry::make('kernel') - ->label(trans('admin/node.kernel')) - ->state(fn (Node $node) => $node->systemInformation()['kernel_version'] ?? trans('admin/node.unknown')), - ]), - View::make('filament.components.node-cpu-chart') - ->columnSpan([ - 'default' => 4, - 'sm' => 1, - 'md' => 2, - 'lg' => 2, - ]), - View::make('filament.components.node-memory-chart') - ->columnSpan([ - 'default' => 4, - 'sm' => 1, - 'md' => 2, - 'lg' => 2, - ]), - View::make('filament.components.node-storage-chart') - ->columnSpanFull(), - ]), - Tab::make('basic_settings') - ->label(trans('admin/node.tabs.basic_settings')) - ->icon('tabler-server') - ->schema([ - TextInput::make('fqdn') - ->columnSpan(2) - ->required() - ->autofocus() - ->live(debounce: 1500) - ->rules(Node::getRulesForField('fqdn')) - ->prohibited(fn ($state) => is_ip($state) && request()->isSecure()) - ->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain')) - ->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com') - ->helperText(function ($state) { - if (is_ip($state)) { - if (request()->isSecure()) { - return trans('admin/node.fqdn_help'); - } - - return ''; - } - - return trans('admin/node.error'); - }) - ->hintColor('danger') - ->hint(function ($state) { - if (is_ip($state) && request()->isSecure()) { - return trans('admin/node.ssl_ip'); - } - - return ''; - }) - ->afterStateUpdated(function (Set $set, ?string $state) { - $set('dns', null); - $set('ip', null); - - [$subdomain] = str($state)->explode('.', 2); - if (!is_numeric($subdomain)) { - $set('name', $subdomain); - } - - if (!$state || is_ip($state)) { - $set('dns', null); - - return; - } - - $ip = get_ip_from_hostname($state); - if ($ip) { - $set('dns', true); - - $set('ip', $ip); - } else { - $set('dns', false); - } - }) - ->maxLength(255), - TextInput::make('ip') - ->disabled() - ->hidden(), - ToggleButtons::make('dns') - ->label(trans('admin/node.dns')) - ->helperText(trans('admin/node.dns_help')) - ->disabled() - ->inline() - ->default(null) - ->hint(fn (Get $get) => $get('ip')) - ->hintColor('success') - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 1 => trans('admin/node.valid'), - 0 => trans('admin/node.invalid'), - ]) - ->colors([ - 1 => 'success', - 0 => 'danger', - ]) - ->columnSpan(1), - TextInput::make('daemon_connect') - ->columnSpan(1) - ->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port')) - ->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help')) - ->minValue(1) - ->maxValue(65535) - ->default(8080) - ->required() - ->integer(), - TextInput::make('name') - ->label(trans('admin/node.display_name')) - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 2, - ]) - ->required() - ->maxLength(100), - Hidden::make('scheme'), - Hidden::make('behind_proxy'), - ToggleButtons::make('connection') - ->label(trans('admin/node.ssl')) - ->columnSpan(1) - ->inline() - ->helperText(function (Get $get) { - if (request()->isSecure()) { - return new HtmlString(trans('admin/node.panel_on_ssl')); - } - - if (is_ip($get('fqdn'))) { - return trans('admin/node.ssl_help'); - } - - return ''; - }) - ->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure()) - ->options([ - 'http' => 'HTTP', - 'https' => 'HTTPS (SSL)', - 'https_proxy' => 'HTTPS with (reverse) proxy', - ]) - ->colors([ - 'http' => 'warning', - 'https' => 'success', - 'https_proxy' => 'success', - ]) - ->icons([ - 'http' => 'tabler-lock-open-off', - 'https' => 'tabler-lock', - 'https_proxy' => 'tabler-shield-lock', - ]) - ->formatStateUsing(fn (Get $get) => $get('scheme') === 'http' ? 'http' : ($get('behind_proxy') ? 'https_proxy' : 'https')) - ->live() - ->dehydrated(false) - ->afterStateUpdated(function ($state, Set $set) { - $set('scheme', $state === 'http' ? 'http' : 'https'); - $set('behind_proxy', $state === 'https_proxy'); - - $set('daemon_connect', $state === 'https_proxy' ? 443 : 8080); - $set('daemon_listen', 8080); - }), - TextInput::make('daemon_listen') - ->columnSpan(1) - ->label(trans('admin/node.listen_port')) - ->helperText(trans('admin/node.listen_port_help')) - ->minValue(1) - ->maxValue(65535) - ->default(8080) - ->required() - ->integer() - ->visible(fn (Get $get) => $get('connection') === 'https_proxy'), - ]), - Tab::make('advanced_settings') - ->label(trans('admin/node.tabs.advanced_settings')) - ->columns([ - 'default' => 1, - 'sm' => 1, - 'md' => 4, - 'lg' => 6, - ]) - ->icon('tabler-server-cog') - ->schema([ - TextInput::make('id') - ->label(trans('admin/node.node_id')) - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 2, - 'lg' => 1, - ]) - ->disabled(), - TextInput::make('uuid') - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 2, - 'lg' => 2, - ]) - ->label(trans('admin/node.node_uuid')) - ->hintCopy() - ->disabled(), - TagsInput::make('tags') - ->label(trans('admin/node.tags')) - ->placeholder('') - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 2, - 'lg' => 2, - ]), - TextInput::make('upload_size') - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 2, - 'lg' => 1, - ]) - ->label(trans('admin/node.upload_limit')) - ->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help.0') . trans('admin/node.upload_limit_help.1')) - ->numeric()->required() - ->minValue(1) - ->maxValue(1024) - ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'), - TextInput::make('daemon_sftp') - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 3, - ]) - ->label(trans('admin/node.sftp_port')) - ->minValue(1) - ->maxValue(65535) - ->default(2022) - ->required() - ->integer(), - TextInput::make('daemon_sftp_alias') - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 3, - ]) - ->label(trans('admin/node.sftp_alias')) - ->helperText(trans('admin/node.sftp_alias_help')), - ToggleButtons::make('public') - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 3, - ]) - ->label(trans('admin/node.use_for_deploy')) - ->inline() - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 1 => trans('admin/node.yes'), - 0 => trans('admin/node.no'), - ]) - ->colors([ - 1 => 'success', - 0 => 'danger', - ]), - ToggleButtons::make('maintenance_mode') - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 3, - ]) - ->label(trans('admin/node.maintenance_mode')) - ->inline() - ->hintIcon('tabler-question-mark', trans('admin/node.maintenance_mode_help')) - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 1 => trans('admin/node.enabled'), - 0 => trans('admin/node.disabled'), - ]) - ->colors([ - 1 => 'danger', - 0 => 'success', - ]), - // Subdomains plugin field - TextInput::make('srv_target') - ->label(trans('subdomains::strings.srv_target')) - ->placeholder('play.example.com') - ->helperText(trans('subdomains::strings.srv_target_help')) - ->rules(['nullable', 'string', 'regex:/^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(?columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 3, - ]) - ->dehydrateStateUsing(function ($state) { - if ($this->record->srv_target === $state) { - return $state; - } - - $this->record->forceFill(['srv_target' => $state])->save(); - Notification::make() - ->title(trans('subdomains::strings.notifications.srv_target_updated_title')) - ->body(trans('subdomains::strings.notifications.srv_target_updated')) - ->warning() - ->send(); - - return $state; - }), - // Subdomains plugin field - Grid::make() - ->columns([ - 'default' => 1, - 'sm' => 1, - 'md' => 3, - 'lg' => 6, - ]) - ->columnSpanFull() - ->schema([ - ToggleButtons::make('unlimited_mem') - ->dehydrated() - ->label(trans('admin/node.memory'))->inlineLabel()->inline() - ->afterStateUpdated(fn (Set $set) => $set('memory', 0)) - ->afterStateUpdated(fn (Set $set) => $set('memory_overallocate', 0)) - ->formatStateUsing(fn (Get $get) => $get('memory') == 0) - ->live() - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 1 => trans('admin/node.unlimited'), - 0 => trans('admin/node.limited'), - ]) - ->colors([ - 1 => 'primary', - 0 => 'warning', - ]) - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 2, - ]), - TextInput::make('memory') - ->dehydratedWhenHidden() - ->hidden(fn (Get $get) => $get('unlimited_mem')) - ->label(trans('admin/node.memory_limit'))->inlineLabel() - ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB') - ->required() - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 2, - ]) - ->numeric() - ->minValue(0), - TextInput::make('memory_overallocate') - ->dehydratedWhenHidden() - ->label(trans('admin/node.overallocate'))->inlineLabel() - ->required() - ->hidden(fn (Get $get) => $get('unlimited_mem')) - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 2, - ]) - ->numeric() - ->minValue(-1) - ->maxValue(100) - ->suffix('%'), - ]), - Grid::make() - ->columnSpanFull() - ->columns([ - 'default' => 1, - 'sm' => 1, - 'md' => 3, - 'lg' => 6, - ]) - ->schema([ - ToggleButtons::make('unlimited_disk') - ->dehydrated() - ->label(trans('admin/node.disk'))->inlineLabel()->inline() - ->live() - ->afterStateUpdated(fn (Set $set) => $set('disk', 0)) - ->afterStateUpdated(fn (Set $set) => $set('disk_overallocate', 0)) - ->formatStateUsing(fn (Get $get) => $get('disk') == 0) - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 1 => trans('admin/node.unlimited'), - 0 => trans('admin/node.limited'), - ]) - ->colors([ - 1 => 'primary', - 0 => 'warning', - ]) - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 2, - ]), - TextInput::make('disk') - ->dehydratedWhenHidden() - ->hidden(fn (Get $get) => $get('unlimited_disk')) - ->label(trans('admin/node.disk_limit'))->inlineLabel() - ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB') - ->required() - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 2, - ]) - ->numeric() - ->minValue(0), - TextInput::make('disk_overallocate') - ->dehydratedWhenHidden() - ->hidden(fn (Get $get) => $get('unlimited_disk')) - ->label(trans('admin/node.overallocate'))->inlineLabel() - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 2, - ]) - ->required() - ->numeric() - ->minValue(-1) - ->maxValue(100) - ->suffix('%'), - ]), - Grid::make() - ->columns(6) - ->columnSpanFull() - ->schema([ - ToggleButtons::make('unlimited_cpu') - ->dehydrated() - ->label(trans('admin/node.cpu'))->inlineLabel()->inline() - ->live() - ->afterStateUpdated(fn (Set $set) => $set('cpu', 0)) - ->afterStateUpdated(fn (Set $set) => $set('cpu_overallocate', 0)) - ->formatStateUsing(fn (Get $get) => $get('cpu') == 0) - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 1 => trans('admin/node.unlimited'), - 0 => trans('admin/node.limited'), - ]) - ->colors([ - 1 => 'primary', - 0 => 'warning', - ]) - ->columnSpan(2), - TextInput::make('cpu') - ->dehydratedWhenHidden() - ->hidden(fn (Get $get) => $get('unlimited_cpu')) - ->label(trans('admin/node.cpu_limit'))->inlineLabel() - ->suffix('%') - ->required() - ->columnSpan(2) - ->numeric() - ->minValue(0), - TextInput::make('cpu_overallocate') - ->dehydratedWhenHidden() - ->hidden(fn (Get $get) => $get('unlimited_cpu')) - ->label(trans('admin/node.overallocate'))->inlineLabel() - ->columnSpan(2) - ->required() - ->numeric() - ->minValue(-1) - ->maxValue(100) - ->suffix('%'), - ]), - ]), - Tab::make('config_file') - ->label(trans('admin/node.tabs.config_file')) - ->icon('tabler-code') - ->schema([ - TextEntry::make('instructions') - ->label(trans('admin/node.instructions')) - ->columnSpanFull() - ->state(new HtmlString(trans('admin/node.instructions_help'))), - CodeEntry::make('config') - ->label('/etc/pelican/config.yml') - ->grammar(Grammar::Yaml) - ->state(fn (Node $node) => $node->getYamlConfiguration()) - ->copyable() - ->disabled() - ->columnSpanFull(), - Grid::make() - ->columns() - ->columnSpanFull() - ->schema([ - Actions::make([ - Action::make('autoDeploy') - ->label(trans('admin/node.auto_deploy')) - ->color('primary') - ->modalHeading(trans('admin/node.auto_deploy')) - ->icon('tabler-rocket') - ->modalSubmitAction(false) - ->modalCancelAction(false) - ->modalFooterActionsAlignment(Alignment::Center) - ->schema([ - ToggleButtons::make('docker') - ->label(trans('admin/node.auto_label')) - ->live() - ->helperText(trans('admin/node.auto_question')) - ->inline() - ->default(false) - ->afterStateUpdated(fn (bool $state, NodeAutoDeployService $service, Node $node, Set $set) => $set('generatedToken', $service->handle(request(), $node, $state))) - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 0 => trans('admin/node.standalone'), - 1 => trans('admin/node.docker'), - ]) - ->colors([ - 0 => 'primary', - 1 => 'success', - ]) - ->columnSpan(1), - Textarea::make('generatedToken') - ->label(trans('admin/node.auto_command')) - ->readOnly() - ->autosize() - ->hintCopy() - ->formatStateUsing(fn (NodeAutoDeployService $service, Node $node, Set $set, Get $get) => $set('generatedToken', $service->handle(request(), $node, $get('docker')))), - ]) - ->mountUsing(function (Schema $schema) { - $schema->fill(); - }), - ])->fullWidth(), - Actions::make([ - Action::make('resetKey') - ->label(trans('admin/node.reset_token')) - ->color('danger') - ->requiresConfirmation() - ->modalHeading(trans('admin/node.reset_token')) - ->modalDescription(trans('admin/node.reset_help')) - ->action(function (Node $node) { - try { - $this->nodeUpdateService->handle($node, [], true); - } catch (Exception) { - Notification::make() - ->title(trans('admin/node.error_connecting', ['node' => $node->name])) - ->body(trans('admin/node.error_connecting_description')) - ->color('warning') - ->icon('tabler-database') - ->warning() - ->send(); - - } - Notification::make()->success()->title(trans('admin/node.token_reset'))->send(); - $this->fillForm(); - }), - ])->fullWidth(), - ]), - ]), - Tab::make('diagnostics') - ->label(trans('admin/node.tabs.diagnostics')) - ->icon('tabler-heart-search') - ->schema([ - Section::make('diag') - ->heading(trans('admin/node.tabs.diagnostics')) - ->columnSpanFull() - ->columns(4) - ->disabled(fn (Get $get) => $get('pulled')) - ->headerActions([ - Action::make('pull') - ->label(trans('admin/node.diagnostics.pull')) - ->icon('tabler-cloud-download')->iconButton()->iconSize(IconSize::ExtraLarge) - ->hidden(fn (Get $get) => $get('pulled')) - ->action(function (Get $get, Set $set, Node $node) { - $includeEndpoints = $get('include_endpoints') ?? true; - $includeLogs = $get('include_logs') ?? true; - $logLines = $get('log_lines') ?? 200; - - try { - $response = $this->daemonSystemRepository->setNode($node)->getDiagnostics($logLines, $includeEndpoints, $includeLogs); - - if ($response->status() === 404) { - Notification::make() - ->title(trans('admin/node.diagnostics.404')) - ->warning() - ->send(); - - return; - } - - $set('pulled', true); - $set('uploaded', false); - $set('log', $response->body()); - - Notification::make() - ->title(trans('admin/node.diagnostics.logs_pulled')) - ->success() - ->send(); - } catch (ConnectionException $e) { - Notification::make() - ->title(trans('admin/node.error_connecting', ['node' => $node->name])) - ->body($e->getMessage()) - ->danger() - ->send(); - - } - }), - Action::make('upload') - ->label(trans('admin/node.diagnostics.upload')) - ->visible(fn (Get $get) => $get('pulled') ?? false) - ->icon('tabler-cloud-upload')->iconButton()->iconSize(IconSize::ExtraLarge) - ->action(function (Get $get, Set $set) { - try { - $response = Http::asMultipart() - ->attach('c', $get('log')) - ->attach('e', '14d') - ->post('https://logs.pelican.dev'); - - if ($response->failed()) { - Notification::make() - ->title(trans('admin/node.diagnostics.upload_failed')) - ->body(fn () => $response->status() . ' - ' . $response->body()) - ->danger() - ->send(); - - return; - } - - $data = $response->json(); - $url = $data['url']; - - Notification::make() - ->title(trans('admin/node.diagnostics.logs_uploaded')) - ->body("{$url}") - ->success() - ->actions([ - Action::make('viewLogs') - ->label(trans('admin/node.diagnostics.view_logs')) - ->url($url) - ->openUrlInNewTab(true), - ]) - ->persistent() - ->send(); - $set('log', $url); - $set('pulled', false); - $set('uploaded', true); - - } catch (\Exception $e) { - Notification::make() - ->title(trans('admin/node.diagnostics.upload_failed')) - ->body($e->getMessage()) - ->danger() - ->send(); - } - }), - Action::make('clear') - ->label(trans('admin/node.diagnostics.clear')) - ->visible(fn (Get $get) => $get('pulled') ?? false) - ->icon('tabler-trash')->iconButton()->iconSize(IconSize::ExtraLarge)->color('danger') - ->action(function (Get $get, Set $set) { - $set('pulled', false); - $set('uploaded', false); - $set('log', null); - $this->refresh(); - } - ), - ]) - ->schema([ - ToggleButtons::make('include_endpoints') - ->hintIcon('tabler-question-mark')->inline() - ->hintIconTooltip(trans('admin/node.diagnostics.include_endpoints_hint')) - ->formatStateUsing(fn () => 1) - ->boolean(), - ToggleButtons::make('include_logs') - ->live() - ->hintIcon('tabler-question-mark')->inline() - ->hintIconTooltip(trans('admin/node.diagnostics.include_logs_hint')) - ->formatStateUsing(fn () => 1) - ->boolean(), - Slider::make('log_lines') - ->columnSpan(2) - ->hiddenLabel() - ->live() - ->tooltips(RawJs::make(<<<'JS' - `${$value} lines` - JS)) - ->visible(fn (Get $get) => $get('include_logs')) - ->range(minValue: 100, maxValue: 500) - ->pips(PipsMode::Steps, density: 10) - ->step(50) - ->formatStateUsing(fn () => 200) - ->fillTrack(), - Hidden::make('pulled'), - Hidden::make('uploaded'), - ]), - Textarea::make('log') - ->hiddenLabel() - ->columnSpanFull() - ->rows(35) - ->visible(fn (Get $get) => ($get('pulled') ?? false) || ($get('uploaded') ?? false)), - ]), - ]), - ]); - } -} From 8b6c3b0816dbaa422aa78c1f035daf0437c9a128 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Fri, 2 Jan 2026 15:47:24 +1100 Subject: [PATCH 50/79] unused imports --- subdomains/src/Providers/SubdomainsPluginProvider.php | 1 - 1 file changed, 1 deletion(-) diff --git a/subdomains/src/Providers/SubdomainsPluginProvider.php b/subdomains/src/Providers/SubdomainsPluginProvider.php index d1cb0b76..948edb8e 100644 --- a/subdomains/src/Providers/SubdomainsPluginProvider.php +++ b/subdomains/src/Providers/SubdomainsPluginProvider.php @@ -3,7 +3,6 @@ namespace Boy132\Subdomains\Providers; use App\Enums\HeaderActionPosition; -use App\Filament\Admin\Resources\Nodes\NodeResource; use App\Filament\Admin\Resources\Servers\ServerResource; use App\Models\Role; use App\Models\Server; From 2b30342d88c6e56499706c401fe9dcd1f34cf5a3 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Fri, 2 Jan 2026 16:06:06 +1100 Subject: [PATCH 51/79] phpstan issues --- subdomains/src/Models/Subdomain.php | 12 ++++++------ subdomains/src/Services/CloudflareService.php | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index b24a4c25..32695371 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -115,12 +115,12 @@ public function getSrvRecordAttribute(): bool return $this->record_type === 'SRV'; } - public function setSrvRecordAttribute($isSrvRecord): void + public function setSrvRecordAttribute(bool $isSrvRecord): void { if ($isSrvRecord) { $this->attributes['record_type'] = 'SRV'; } else { - $ip = $this->server?->allocation?->ip ?? null; + $ip = $this->server?->allocation?->ip; if (!empty($ip) && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $this->attributes['record_type'] = 'AAAA'; } else { @@ -133,7 +133,7 @@ protected function upsertOnCloudflare(): bool { $registrar = app(CloudflareService::class); - $zoneId = $this->domain?->cloudflare_id ?? null; + $zoneId = $this->domain?->cloudflare_id; if (empty($zoneId)) { Log::warning('Cloudflare zone id missing for domain', ['domain_id' => $this->domain_id]); @@ -148,7 +148,7 @@ protected function upsertOnCloudflare(): bool // SRV: target comes from node, port from server allocation if ($this->record_type === 'SRV') { - $port = $this->server->allocation?->port ?? null; + $port = $this->server->allocation?->port; if (empty($port)) { Log::warning('Server missing allocation with port', $this->toArray()); Notification::make() @@ -173,7 +173,7 @@ protected function upsertOnCloudflare(): bool } if (empty($this->server?->node?->srv_target)) { - Log::warning('Node missing SRV target for SRV record', ['node_id' => $this->server->node?->id ?? null]); + Log::warning('Node missing SRV target for SRV record', ['node_id' => $this->server->node?->id]); Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_missing_srv_target_title')) @@ -205,7 +205,7 @@ protected function upsertOnCloudflare(): bool } // A/AAAA - $ip = $this->server?->allocation?->ip ?? null; + $ip = $this->server?->allocation?->ip; if (empty($ip) || $ip === '0.0.0.0' || $ip === '::') { Log::warning('Server allocation missing or invalid IP', ['server_id' => $this->server_id]); Notification::make() diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index af8cc9a5..8e1d3762 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -26,7 +26,7 @@ public function getZoneId(string $domainName): ?string return ['success' => false, 'errors' => ['exception' => $e->getMessage()], 'status' => $e->getCode(), 'body' => $e->getTraceAsString()]; } - $body = $response->json() ?? []; + $body = $response->json(); if ($response->successful() && !empty($body['result']) && count($body['result']) > 0) { return $body['result'][0]['id'] ?? null; @@ -146,7 +146,7 @@ public function deleteDnsRecord(string $zoneId, string $recordId): array protected function parseCloudflareHttpResponse(Response $response): array { $status = $response->status(); - $body = $response->json() ?? []; + $body = $response->json(); $success = $response->successful() && ($body['success'] === true || (is_array($body['result']) && count($body['result']) > 0)); From 109a2c65865390980d5045e690020b64e3d50a4d Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 09:22:12 +1100 Subject: [PATCH 52/79] fixing phpstan issues --- .../Components/Actions/SetSrvTargetAction.php | 5 ++-- .../Subdomains/SubdomainResource.php | 10 +++---- subdomains/src/Models/Subdomain.php | 20 ++++++------- .../Providers/SubdomainsPluginProvider.php | 3 +- subdomains/src/Services/CloudflareService.php | 29 +++++++++++++++++-- 5 files changed, 45 insertions(+), 22 deletions(-) diff --git a/subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php b/subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php index ef1feb7a..4af1bb72 100644 --- a/subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php +++ b/subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php @@ -29,8 +29,7 @@ protected function setUp(): void ->default(fn () => $node?->srv_target) ->placeholder('play.example.com OR IPv4/IPv6 address') ->helperText(trans('subdomains::strings.srv_target_confirmation')) - - requireConfirmation() - ->rules(['nullable', 'string', 'regex:/^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(?rules(['nullable', 'string', 'regex:/^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(?body(trans('subdomains::strings.notifications.srv_target_updated')) ->warning() ->send(); - })->requiresConfirmation()->modalIconColor('danger')->modalDescription(trans('subdomains::strings.srv_target_confirmation')->toString()); + })->requiresConfirmation()->modalIconColor('danger'); } } diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index 5f0f790a..89276bed 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -80,9 +80,9 @@ public static function table(Table $table): Table ->state(fn (Subdomain $subdomain) => $subdomain->getLabel()), TextColumn::make('record_type') ->label(trans('subdomains::strings.record_type')) - ->icon(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server?->node?->srv_target) ? 'tabler-alert-triangle' : null) - ->color(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server?->node?->srv_target) ? 'danger' : null) - ->helperText(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server?->node?->srv_target) ? trans('subdomains::strings.srv_target_missing') : null), + ->icon(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server?->node->srv_target) ? 'tabler-alert-triangle' : null) + ->color(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server?->node->srv_target) ? 'danger' : null) + ->tooltip(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server?->node->srv_target) ? trans('subdomains::strings.srv_target_missing') : null), ]) ->recordActions([ EditAction::make() @@ -111,7 +111,7 @@ public static function form(Schema $schema): Schema TextInput::make('name') ->label(trans('subdomains::strings.name')) ->required() - ->suffix(fn (callable $get) => CloudflareDomain::find($get('domain_id'))?->name) + ->suffix(fn (callable $get) => CloudflareDomain::find($get('domain_id'))->name) ->unique(), Select::make('domain_id') ->label(trans_choice('subdomains::strings.domain', 1)) @@ -119,7 +119,7 @@ public static function form(Schema $schema): Schema ->required() ->relationship('domain', 'name') ->preload() - ->default(fn () => CloudflareDomain::first()?->id ?? null) + ->default(fn () => CloudflareDomain::first()->id ?? null) ->searchable(), Toggle::make('srv_record') ->label(trans('subdomains::strings.srv_record')) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 32695371..f1ec9872 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -58,7 +58,7 @@ protected static function boot(): void Notification::make() ->success() ->title(trans('subdomains::strings.notifications.cloudflare_record_created_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_record_created', ['subdomain' => $model->name . '.' . ($model->domain?->name ?? 'unknown'), 'record_type' => $model->record_type])) + ->body(trans('subdomains::strings.notifications.cloudflare_record_created', ['subdomain' => $model->name . '.' . ($model->domain->name ?? 'unknown'), 'record_type' => $model->record_type])) ->send(); return true; @@ -73,7 +73,7 @@ protected static function boot(): void Notification::make() ->success() ->title(trans('subdomains::strings.notifications.cloudflare_record_updated_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_record_updated', ['subdomain' => $model->name . '.' . ($model->domain?->name ?? 'unknown'), 'record_type' => $model->record_type])) + ->body(trans('subdomains::strings.notifications.cloudflare_record_updated', ['subdomain' => $model->name . '.' . ($model->domain->name ?? 'unknown'), 'record_type' => $model->record_type])) ->send(); return true; @@ -88,7 +88,7 @@ protected static function boot(): void Notification::make() ->success() ->title(trans('subdomains::strings.notifications.cloudflare_record_deleted_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_record_deleted', ['subdomain' => $model->name . '.' . ($model->domain?->name ?? 'unknown')])) + ->body(trans('subdomains::strings.notifications.cloudflare_record_deleted', ['subdomain' => $model->name . '.' . ($model->domain->name ?? 'unknown')])) ->send(); return true; @@ -140,7 +140,7 @@ protected function upsertOnCloudflare(): bool Notification::make() ->danger() ->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')])) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_zone', ['domain' => $this->domain->name ?? 'unknown', 'subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) ->send(); return false; @@ -154,7 +154,7 @@ protected function upsertOnCloudflare(): bool Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_missing_srv_port_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_port', ['server' => $this->server?->name ?? 'unassigned'])) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_port', ['server' => $this->server->name ?? 'unassigned'])) ->send(); return false; @@ -162,22 +162,22 @@ protected function upsertOnCloudflare(): bool $serviceRecordType = ServiceRecordType::fromServer($this->server); if (!$serviceRecordType) { - Log::warning('Unable to determine service record type for SRV record', ['server_id' => $this->server?->id, 'server' => $this->server?->name]); + Log::warning('Unable to determine service record type for SRV record', ['server_id' => $this->server->id ?? 'unknown', 'server' => $this->server->name ?? 'unknown']); Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_invalid_service_record_type_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_invalid_service_record_type', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown')])) + ->body(trans('subdomains::strings.notifications.cloudflare_invalid_service_record_type', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) ->send(); return false; } - if (empty($this->server?->node?->srv_target)) { + if (empty($this->server?->node->srv_target)) { Log::warning('Node missing SRV target for SRV record', ['node_id' => $this->server->node?->id]); Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_missing_srv_target_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_target', ['node' => $this->server->node?->name ?? 'unknown'])) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_target', ['node' => $this->server->node->name ?? 'unknown'])) ->send(); return false; @@ -198,7 +198,7 @@ protected function upsertOnCloudflare(): bool Notification::make() ->danger() ->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'] ?? [])])) + ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) ->send(); return false; diff --git a/subdomains/src/Providers/SubdomainsPluginProvider.php b/subdomains/src/Providers/SubdomainsPluginProvider.php index 948edb8e..0e295ddd 100644 --- a/subdomains/src/Providers/SubdomainsPluginProvider.php +++ b/subdomains/src/Providers/SubdomainsPluginProvider.php @@ -6,8 +6,9 @@ use App\Filament\Admin\Resources\Servers\ServerResource; use App\Models\Role; use App\Models\Server; -use Boy132\Subdomains\Filament\Admin\Resources\Nodes\Pages\EditNode; +use App\Filament\Admin\Resources\Nodes\Pages\EditNode; use Boy132\Subdomains\Filament\Components\Actions\SetSrvTargetAction; +use Boy132\Subdomains\Filament\Admin\Resources\Servers\RelationManagers\SubdomainRelationManager; use Boy132\Subdomains\Models\Subdomain; use Illuminate\Support\Facades\Http; use Illuminate\Support\ServiceProvider; diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index 8e1d3762..af361363 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -17,13 +17,14 @@ public function getZoneId(string $domainName): ?string } try { + // @phpstan-ignore staticMethod.notFound $response = Http::cloudflare()->get('zones', [ 'name' => $domainName, ]); } catch (\Throwable $e) { Log::error('Cloudflare getZoneId request failed: ' . $e->getMessage(), ['domain' => $domainName]); - return ['success' => false, 'errors' => ['exception' => $e->getMessage()], 'status' => $e->getCode(), 'body' => $e->getTraceAsString()]; + return null; } $body = $response->json(); @@ -36,9 +37,19 @@ public function getZoneId(string $domainName): ?string Log::warning('Cloudflare getZoneId returned errors', ['domain' => $domainName, 'status' => $response->status(), 'errors' => $body['errors']]); } - return ['success' => false, 'errors' => [], 'status' => $response->status(), 'body' => $body]; + return null; } + /** + * @param string $zoneId + * @param string $name + * @param string $recordType + * @param string $target + * @param string|null $recordId + * @param int|null $port + * + * @return array{success: bool, id: string|null, errors: array, status: int, body: mixed|null} + */ 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)) { @@ -89,6 +100,7 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType try { if ($recordId) { + // @phpstan-ignore staticMethod.notFound $response = Http::cloudflare()->put("zones/{$zoneId}/dns_records/{$recordId}", $payload); $parsed = $this->parseCloudflareHttpResponse($response); @@ -101,6 +113,7 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType return $parsed; } + // @phpstan-ignore staticMethod.notFound $response = Http::cloudflare()->post("zones/{$zoneId}/dns_records", $payload); $parsed = $this->parseCloudflareHttpResponse($response); @@ -118,6 +131,11 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType } } + /** + * @param string $zoneId + * @param string $recordId + * @return array{success: bool, id: string|null, errors: array, status: int, body: mixed|null} + */ public function deleteDnsRecord(string $zoneId, string $recordId): array { if (empty($zoneId) || empty($recordId)) { @@ -125,6 +143,7 @@ public function deleteDnsRecord(string $zoneId, string $recordId): array } try { + // @phpstan-ignore staticMethod.notFound $response = Http::cloudflare()->delete("zones/{$zoneId}/dns_records/{$recordId}"); $parsed = $this->parseCloudflareHttpResponse($response); @@ -137,12 +156,16 @@ public function deleteDnsRecord(string $zoneId, string $recordId): array return $parsed; } catch (\Throwable $e) { - Log::error('Cloudflare delete exception: ' . $e->getMessage(), ['zone' => $zoneId, 'id' => $recordId, 'payload' => $payload, 'status' => $e->getCode()]); + Log::error('Cloudflare delete exception: ' . $e->getMessage(), ['zone' => $zoneId, 'id' => $recordId, 'status' => $e->getCode()]); return ['success' => false, 'errors' => ['exception' => $e->getMessage()], 'status' => $e->getCode(), 'body' => $e->getTraceAsString()]; } } + /** + * @param Response $response + * @return array{success: bool, id: string|null, errors: array, status: int, body: mixed|null} + */ protected function parseCloudflareHttpResponse(Response $response): array { $status = $response->status(); From f16ed6703b9283ed66ba4a6318577bf5859f8309 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 10:05:14 +1100 Subject: [PATCH 53/79] pint/phpstan issues --- .../Components/Actions/SetSrvTargetAction.php | 4 ++-- .../Subdomains/SubdomainResource.php | 10 ++++----- subdomains/src/Models/CloudflareDomain.php | 1 + subdomains/src/Models/Subdomain.php | 21 ++++++++++--------- .../Providers/SubdomainsPluginProvider.php | 4 ++-- subdomains/src/Services/CloudflareService.php | 10 --------- 6 files changed, 21 insertions(+), 29 deletions(-) diff --git a/subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php b/subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php index 4af1bb72..00149e85 100644 --- a/subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php +++ b/subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php @@ -22,11 +22,11 @@ protected function setUp(): void $this->icon('tabler-world-www'); - $this->schema(function (?Node $node) { + $this->schema(function (Node $node) { return [ TextInput::make('srv_target') ->label(fn () => trans('subdomains::strings.srv_target')) - ->default(fn () => $node?->srv_target) + ->default(fn () => $node?->srv_target) // @phpstan-ignore variable.undefined ->placeholder('play.example.com OR IPv4/IPv6 address') ->helperText(trans('subdomains::strings.srv_target_confirmation')) ->rules(['nullable', 'string', 'regex:/^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(?state(fn (Subdomain $subdomain) => $subdomain->getLabel()), TextColumn::make('record_type') ->label(trans('subdomains::strings.record_type')) - ->icon(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server?->node->srv_target) ? 'tabler-alert-triangle' : null) - ->color(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server?->node->srv_target) ? 'danger' : null) - ->tooltip(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server?->node->srv_target) ? trans('subdomains::strings.srv_target_missing') : null), + ->icon(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server->node->srv_target) ? 'tabler-alert-triangle' : null) // @phpstan-ignore variable.undefined + ->color(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server->node->srv_target) ? 'danger' : null) // @phpstan-ignore variable.undefined + ->tooltip(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server->node->srv_target) ? trans('subdomains::strings.srv_target_missing') : null), // @phpstan-ignore variable.undefined ]) ->recordActions([ EditAction::make() @@ -123,9 +123,9 @@ public static function form(Schema $schema): Schema ->searchable(), Toggle::make('srv_record') ->label(trans('subdomains::strings.srv_record')) - ->helperText(fn () => Filament::getTenant()->node->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.srv_target_missing')) + ->helperText(fn () => Filament::getTenant()->node->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.srv_target_missing')) // @phpstan-ignore variable.undefined ->reactive() - ->disabled(fn () => empty(Filament::getTenant()->node->srv_target)), + ->disabled(fn () => empty(Filament::getTenant()->node->srv_target)), // @phpstan-ignore variable.undefined ]); } diff --git a/subdomains/src/Models/CloudflareDomain.php b/subdomains/src/Models/CloudflareDomain.php index db4c1efb..4f0f18e6 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -6,6 +6,7 @@ use Filament\Notifications\Notification; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Facades\Http; /** * @property int $id diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index f1ec9872..2c088f0b 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -120,7 +120,7 @@ public function setSrvRecordAttribute(bool $isSrvRecord): void if ($isSrvRecord) { $this->attributes['record_type'] = 'SRV'; } else { - $ip = $this->server?->allocation?->ip; + $ip = $this->server->allocation?->ip; if (!empty($ip) && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $this->attributes['record_type'] = 'AAAA'; } else { @@ -133,7 +133,7 @@ protected function upsertOnCloudflare(): bool { $registrar = app(CloudflareService::class); - $zoneId = $this->domain?->cloudflare_id; + $zoneId = $this->domain->cloudflare_id; if (empty($zoneId)) { Log::warning('Cloudflare zone id missing for domain', ['domain_id' => $this->domain_id]); @@ -172,7 +172,8 @@ protected function upsertOnCloudflare(): bool return false; } - if (empty($this->server?->node->srv_target)) { + // @phpstan-ignore variable.undefined + if (empty($this->server->node->srv_target)) { Log::warning('Node missing SRV target for SRV record', ['node_id' => $this->server->node?->id]); Notification::make() ->danger() @@ -184,7 +185,7 @@ protected function upsertOnCloudflare(): bool } $recordName = sprintf('%s.%s.%s', $serviceRecordType->service(), $serviceRecordType->protocol(), $this->name); - + // @phpstan-ignore variable.undefined $result = $registrar->upsertDnsRecord($zoneId, $recordName, 'SRV', $this->server->node->srv_target, $this->cloudflare_id, $port); if ($result['success'] && !empty($result['id'])) { @@ -198,20 +199,20 @@ protected function upsertOnCloudflare(): bool Notification::make() ->danger() ->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'] ?? [])])) + ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'])])) ->send(); return false; } // A/AAAA - $ip = $this->server?->allocation?->ip; + $ip = $this->server->allocation?->ip; if (empty($ip) || $ip === '0.0.0.0' || $ip === '::') { Log::warning('Server allocation missing or invalid IP', ['server_id' => $this->server_id]); Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_missing_ip_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_missing_ip', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown')])) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_ip', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) ->send(); return false; @@ -227,14 +228,14 @@ protected function upsertOnCloudflare(): bool return true; } - $domainName = $this->domain?->name ?? 'unknown'; + $domainName = $this->domain->name ?? 'unknown'; $subdomain = sprintf('%s.%s', $this->name, $domainName); Log::error('Failed to upsert record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_upsert_failed_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $subdomain, 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) + ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $subdomain, 'errors' => json_encode($result['errors'])])) ->send(); return false; @@ -254,7 +255,7 @@ protected function deleteOnCloudflare(): bool 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'] ?? [])])) + ->body(trans('subdomains::strings.notifications.cloudflare_delete_failed', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown'), 'errors' => json_encode($result['errors'])])) ->send(); return false; diff --git a/subdomains/src/Providers/SubdomainsPluginProvider.php b/subdomains/src/Providers/SubdomainsPluginProvider.php index 0e295ddd..67747e65 100644 --- a/subdomains/src/Providers/SubdomainsPluginProvider.php +++ b/subdomains/src/Providers/SubdomainsPluginProvider.php @@ -3,12 +3,12 @@ namespace Boy132\Subdomains\Providers; use App\Enums\HeaderActionPosition; +use App\Filament\Admin\Resources\Nodes\Pages\EditNode; use App\Filament\Admin\Resources\Servers\ServerResource; use App\Models\Role; use App\Models\Server; -use App\Filament\Admin\Resources\Nodes\Pages\EditNode; -use Boy132\Subdomains\Filament\Components\Actions\SetSrvTargetAction; use Boy132\Subdomains\Filament\Admin\Resources\Servers\RelationManagers\SubdomainRelationManager; +use Boy132\Subdomains\Filament\Components\Actions\SetSrvTargetAction; use Boy132\Subdomains\Models\Subdomain; use Illuminate\Support\Facades\Http; use Illuminate\Support\ServiceProvider; diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index af361363..9559c136 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -41,13 +41,6 @@ public function getZoneId(string $domainName): ?string } /** - * @param string $zoneId - * @param string $name - * @param string $recordType - * @param string $target - * @param string|null $recordId - * @param int|null $port - * * @return array{success: bool, id: string|null, errors: array, status: int, body: mixed|null} */ public function upsertDnsRecord(string $zoneId, string $name, string $recordType, string $target, ?string $recordId = null, ?int $port = null): array @@ -132,8 +125,6 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType } /** - * @param string $zoneId - * @param string $recordId * @return array{success: bool, id: string|null, errors: array, status: int, body: mixed|null} */ public function deleteDnsRecord(string $zoneId, string $recordId): array @@ -163,7 +154,6 @@ public function deleteDnsRecord(string $zoneId, string $recordId): array } /** - * @param Response $response * @return array{success: bool, id: string|null, errors: array, status: int, body: mixed|null} */ protected function parseCloudflareHttpResponse(Response $response): array From 0d8b080b25ee75b86f02e636ad38a5bef46fae1d Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 13:22:45 +1100 Subject: [PATCH 54/79] fixing more pint/phpstan issues --- .../SubdomainRelationManager.php | 2 +- .../Components/Actions/SetSrvTargetAction.php | 2 +- .../Resources/Subdomains/SubdomainResource.php | 17 +++++++++++------ subdomains/src/Models/Subdomain.php | 8 ++++---- subdomains/src/Services/CloudflareService.php | 6 +++--- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php index ab5c5702..aded83e1 100644 --- a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php +++ b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php @@ -1,6 +1,6 @@ label(fn () => trans('subdomains::strings.srv_target')) - ->default(fn () => $node?->srv_target) // @phpstan-ignore variable.undefined + ->default(fn () => $node->srv_target) // @phpstan-ignore property.undefined ->placeholder('play.example.com OR IPv4/IPv6 address') ->helperText(trans('subdomains::strings.srv_target_confirmation')) ->rules(['nullable', 'string', 'regex:/^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(?allocation?->ip ?? null; - return parent::canAccess() && $server->allocation && $server->allocation->ip !== '0.0.0.0' && $server->allocation->ip !== '::' && CloudflareDomain::count() > 0; + return parent::canAccess() + && $ip !== null + && $ip !== '0.0.0.0' + && $ip !== '::' + && CloudflareDomain::count() > 0; } public static function getNavigationLabel(): string @@ -80,9 +85,9 @@ public static function table(Table $table): Table ->state(fn (Subdomain $subdomain) => $subdomain->getLabel()), TextColumn::make('record_type') ->label(trans('subdomains::strings.record_type')) - ->icon(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server->node->srv_target) ? 'tabler-alert-triangle' : null) // @phpstan-ignore variable.undefined - ->color(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server->node->srv_target) ? 'danger' : null) // @phpstan-ignore variable.undefined - ->tooltip(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server->node->srv_target) ? trans('subdomains::strings.srv_target_missing') : null), // @phpstan-ignore variable.undefined + ->icon(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server->node->srv_target) ? 'tabler-alert-triangle' : null) // @phpstan-ignore property.undefined + ->color(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server->node->srv_target) ? 'danger' : null) // @phpstan-ignore property.undefined + ->tooltip(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server->node->srv_target) ? trans('subdomains::strings.srv_target_missing') : null), // @phpstan-ignore property.undefined ]) ->recordActions([ EditAction::make() @@ -123,9 +128,9 @@ public static function form(Schema $schema): Schema ->searchable(), Toggle::make('srv_record') ->label(trans('subdomains::strings.srv_record')) - ->helperText(fn () => Filament::getTenant()->node->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.srv_target_missing')) // @phpstan-ignore variable.undefined + ->helperText(fn () => Filament::getTenant()->node->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.srv_target_missing')) // @phpstan-ignore property.undefined ->reactive() - ->disabled(fn () => empty(Filament::getTenant()->node->srv_target)), // @phpstan-ignore variable.undefined + ->disabled(fn () => empty(Filament::getTenant()->node->srv_target)), // @phpstan-ignore property.undefined ]); } diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 2c088f0b..5a4b1bf6 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -172,9 +172,9 @@ protected function upsertOnCloudflare(): bool return false; } - // @phpstan-ignore variable.undefined + // @phpstan-ignore property.undefined if (empty($this->server->node->srv_target)) { - Log::warning('Node missing SRV target for SRV record', ['node_id' => $this->server->node?->id]); + Log::warning('Node missing SRV target for SRV record', ['node_id' => $this->server->node->id]); Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_missing_srv_target_title')) @@ -185,7 +185,7 @@ protected function upsertOnCloudflare(): bool } $recordName = sprintf('%s.%s.%s', $serviceRecordType->service(), $serviceRecordType->protocol(), $this->name); - // @phpstan-ignore variable.undefined + // @phpstan-ignore property.undefined $result = $registrar->upsertDnsRecord($zoneId, $recordName, 'SRV', $this->server->node->srv_target, $this->cloudflare_id, $port); if ($result['success'] && !empty($result['id'])) { @@ -255,7 +255,7 @@ protected function deleteOnCloudflare(): bool 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'])])) + ->body(trans('subdomains::strings.notifications.cloudflare_delete_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'])])) ->send(); return false; diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index 9559c136..fb04795a 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -120,7 +120,7 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType } catch (\Throwable $e) { Log::error('Cloudflare upsert exception: ' . $e->getMessage(), ['zone' => $zoneId, 'payload' => $payload, 'status' => $e->getCode()]); - return ['success' => false, 'errors' => ['exception' => $e->getMessage()], 'status' => $e->getCode(), 'body' => $e->getTraceAsString()]; + return ['success' => false, 'id' => null, 'errors' => ['exception' => $e->getMessage()], 'status' => $e->getCode(), 'body' => $e->getTraceAsString()]; } } @@ -130,7 +130,7 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType public function deleteDnsRecord(string $zoneId, string $recordId): array { if (empty($zoneId) || empty($recordId)) { - return ['success' => false, 'errors' => ['missing_parameters' => true], 'status' => 0, 'body' => null]; + return ['success' => false, 'id' => null, 'errors' => ['missing_parameters' => true], 'status' => 0, 'body' => null]; } try { @@ -149,7 +149,7 @@ public function deleteDnsRecord(string $zoneId, string $recordId): array } catch (\Throwable $e) { Log::error('Cloudflare delete exception: ' . $e->getMessage(), ['zone' => $zoneId, 'id' => $recordId, 'status' => $e->getCode()]); - return ['success' => false, 'errors' => ['exception' => $e->getMessage()], 'status' => $e->getCode(), 'body' => $e->getTraceAsString()]; + return ['success' => false, 'id' => null, 'errors' => ['exception' => $e->getMessage()], 'status' => $e->getCode(), 'body' => $e->getTraceAsString()]; } } From 5e4f7395940dbcffedc397fbb3e6ac613cc50b7d Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 14:12:03 +1100 Subject: [PATCH 55/79] phpstan issues --- .../Filament/Components/Actions/SetSrvTargetAction.php | 2 +- .../Server/Resources/Subdomains/SubdomainResource.php | 10 +++++----- subdomains/src/Models/Subdomain.php | 1 - 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php b/subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php index ef8867b0..f85d537f 100644 --- a/subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php +++ b/subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php @@ -26,7 +26,7 @@ protected function setUp(): void return [ TextInput::make('srv_target') ->label(fn () => trans('subdomains::strings.srv_target')) - ->default(fn () => $node->srv_target) // @phpstan-ignore property.undefined + ->default(fn () => $node->srv_target) // @phpstan-ignore property.notFound ->placeholder('play.example.com OR IPv4/IPv6 address') ->helperText(trans('subdomains::strings.srv_target_confirmation')) ->rules(['nullable', 'string', 'regex:/^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(?allocation?->ip ?? null; + $ip = $server->allocation->ip ?? null; return parent::canAccess() && $ip !== null @@ -85,9 +85,9 @@ public static function table(Table $table): Table ->state(fn (Subdomain $subdomain) => $subdomain->getLabel()), TextColumn::make('record_type') ->label(trans('subdomains::strings.record_type')) - ->icon(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server->node->srv_target) ? 'tabler-alert-triangle' : null) // @phpstan-ignore property.undefined - ->color(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server->node->srv_target) ? 'danger' : null) // @phpstan-ignore property.undefined - ->tooltip(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server->node->srv_target) ? trans('subdomains::strings.srv_target_missing') : null), // @phpstan-ignore property.undefined + ->icon(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server->node->srv_target) ? 'tabler-alert-triangle' : null) + ->color(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server->node->srv_target) ? 'danger' : null) + ->tooltip(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server->node->srv_target) ? trans('subdomains::strings.srv_target_missing') : null), ]) ->recordActions([ EditAction::make() @@ -130,7 +130,7 @@ public static function form(Schema $schema): Schema ->label(trans('subdomains::strings.srv_record')) ->helperText(fn () => Filament::getTenant()->node->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.srv_target_missing')) // @phpstan-ignore property.undefined ->reactive() - ->disabled(fn () => empty(Filament::getTenant()->node->srv_target)), // @phpstan-ignore property.undefined + ->disabled(fn () => empty(Filament::getTenant()->node->srv_target)), ]); } diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 5a4b1bf6..b84c05e4 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -172,7 +172,6 @@ protected function upsertOnCloudflare(): bool return false; } - // @phpstan-ignore property.undefined if (empty($this->server->node->srv_target)) { Log::warning('Node missing SRV target for SRV record', ['node_id' => $this->server->node->id]); Notification::make() From c54cbebae7904000be67fc5cec9851ce89c98cbc Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 14:17:03 +1100 Subject: [PATCH 56/79] phpstan issues, again --- .../Filament/Server/Resources/Subdomains/SubdomainResource.php | 2 +- subdomains/src/Models/Subdomain.php | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index 39c3cf89..02c4a335 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -128,7 +128,7 @@ public static function form(Schema $schema): Schema ->searchable(), Toggle::make('srv_record') ->label(trans('subdomains::strings.srv_record')) - ->helperText(fn () => Filament::getTenant()->node->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.srv_target_missing')) // @phpstan-ignore property.undefined + ->helperText(fn () => Filament::getTenant()->node->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.srv_target_missing')) // @phpstan-ignore property.notFound ->reactive() ->disabled(fn () => empty(Filament::getTenant()->node->srv_target)), ]); diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index b84c05e4..18537098 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -184,7 +184,6 @@ protected function upsertOnCloudflare(): bool } $recordName = sprintf('%s.%s.%s', $serviceRecordType->service(), $serviceRecordType->protocol(), $this->name); - // @phpstan-ignore property.undefined $result = $registrar->upsertDnsRecord($zoneId, $recordName, 'SRV', $this->server->node->srv_target, $this->cloudflare_id, $port); if ($result['success'] && !empty($result['id'])) { @@ -242,7 +241,7 @@ protected function upsertOnCloudflare(): bool protected function deleteOnCloudflare(): bool { - if ($this->cloudflare_id && $this->domain && $this->domain->cloudflare_id) { + if ($this->cloudflare_id && $this->domain->cloudflare_id) { $registrar = app(CloudflareService::class); $result = $registrar->deleteDnsRecord($this->domain->cloudflare_id, $this->cloudflare_id); From 0fe6e8fa8b61a554f89e0f764f9b3e53ec57d1fc Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 19:18:06 +1100 Subject: [PATCH 57/79] change domains to require cloudflare_id to be set --- .../migrations/005_add_srv_target_to_nodes.php | 14 ++++++++++++++ subdomains/src/Models/CloudflareDomain.php | 3 +-- subdomains/src/Models/Subdomain.php | 11 ++++++++--- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/subdomains/database/migrations/005_add_srv_target_to_nodes.php b/subdomains/database/migrations/005_add_srv_target_to_nodes.php index dc30a4b8..5a7f330e 100644 --- a/subdomains/database/migrations/005_add_srv_target_to_nodes.php +++ b/subdomains/database/migrations/005_add_srv_target_to_nodes.php @@ -8,8 +8,17 @@ { public function up(): void { + // Can safely delete any Cloudflare domains without IDs as they're useless anyway + DB::table('cloudflare_domains')->whereNull('cloudflare_id')->delete(); + DB::table('subdomains')->whereNull('cloudflare_domain_id')->update(['cloudflare_domain_id' => null]); + Schema::table('cloudflare_domains', function (Blueprint $table) { $table->dropColumn('srv_target'); + $table->string('cloudflare_id')->nullable(false)->change(); + }); + + Schema::table('subdomains', function (Blueprint $table) { + $table->string('cloudflare_id')->nullable(false)->change(); }); Schema::table('nodes', function (Blueprint $table) { @@ -21,6 +30,11 @@ public function down(): void { Schema::table('cloudflare_domains', function (Blueprint $table) { $table->string('srv_target')->nullable()->after('cloudflare_id'); + $table->string('cloudflare_id')->nullable()->change(); + }); + + Schema::table('subdomains', function (Blueprint $table) { + $table->string('cloudflare_id')->nullable()->change(); }); Schema::table('nodes', function (Blueprint $table) { diff --git a/subdomains/src/Models/CloudflareDomain.php b/subdomains/src/Models/CloudflareDomain.php index 4f0f18e6..c7018401 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -11,8 +11,7 @@ /** * @property int $id * @property string $name - * @property ?string $cloudflare_id - * @property ?string $srv_target + * @property string $cloudflare_id */ class CloudflareDomain extends Model { diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 18537098..4ec02f02 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -17,7 +17,7 @@ * @property int $id * @property string $name * @property string $record_type - * @property ?string $cloudflare_id + * @property string $cloudflare_id * @property int $domain_id * @property CloudflareDomain $domain * @property int $server_id @@ -241,8 +241,13 @@ protected function upsertOnCloudflare(): bool protected function deleteOnCloudflare(): bool { - if ($this->cloudflare_id && $this->domain->cloudflare_id) { - $registrar = app(CloudflareService::class); + if (empty($this->cloudflare_id)) { + Log::warning('Subdomain deleteOnCloudflare called but no cloudflare_id set', ['subdomain_id' => $this->id]); + + return true; + } + + $registrar = app(CloudflareService::class); $result = $registrar->deleteDnsRecord($this->domain->cloudflare_id, $this->cloudflare_id); From 03a351d15b3be9244943e8c4447c7e5fd714fce9 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 19:24:13 +1100 Subject: [PATCH 58/79] - Added check for existing record before updating/deleting --- subdomains/lang/en/strings.php | 5 +- subdomains/src/Models/Subdomain.php | 66 +++++++++---------- subdomains/src/Services/CloudflareService.php | 49 ++++++++++++-- 3 files changed, 81 insertions(+), 39 deletions(-) diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index 701cbe55..1557db34 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -43,7 +43,7 @@ 'cloudflare_missing_srv_target' => 'SRV target is missing from :node. ', 'cloudflare_record_created_title' => 'Cloudflare: Record Created', - 'cloudflare_record_created' => 'Successfully created :subdomain record of type :record', + 'cloudflare_record_created' => 'Successfully created :subdomain record', 'cloudflare_record_updated_title' => 'Cloudflare: Record Updated', 'cloudflare_record_updated' => 'Successfully updated :subdomain record to :record_type', @@ -51,6 +51,9 @@ 'cloudflare_record_deleted_title' => 'Cloudflare: Record Deleted', 'cloudflare_record_deleted' => 'Successfully deleted Cloudflare record for :subdomain.', + 'cloudflare_delete_failed_title' => 'Cloudflare: Delete Failed', + 'cloudflare_delete_failed' => 'Failed to delete Cloudflare record for :subdomain.', + 'cloudflare_missing_ip_title' => 'Cloudflare: Missing IP', 'cloudflare_missing_ip' => 'Server allocation IP is missing or invalid for :subdomain. Cannot save A/AAAA record.', diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 4ec02f02..64802a0e 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -58,7 +58,7 @@ protected static function boot(): void Notification::make() ->success() ->title(trans('subdomains::strings.notifications.cloudflare_record_created_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_record_created', ['subdomain' => $model->name . '.' . ($model->domain->name ?? 'unknown'), 'record_type' => $model->record_type])) + ->body(trans('subdomains::strings.notifications.cloudflare_record_created', ['subdomain' => $model->getLabel(), 'record_type' => $model->record_type])) ->send(); return true; @@ -120,7 +120,7 @@ public function setSrvRecordAttribute(bool $isSrvRecord): void if ($isSrvRecord) { $this->attributes['record_type'] = 'SRV'; } else { - $ip = $this->server->allocation?->ip; + $ip = $this->server?->allocation->ip; if (!empty($ip) && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $this->attributes['record_type'] = 'AAAA'; } else { @@ -134,13 +134,15 @@ protected function upsertOnCloudflare(): bool $registrar = app(CloudflareService::class); $zoneId = $this->domain->cloudflare_id; - if (empty($zoneId)) { - Log::warning('Cloudflare zone id missing for domain', ['domain_id' => $this->domain_id]); + $domainName = $this->domain->name; + + if (empty($zoneId) || empty($domainName)) { + Log::warning('Cloudflare zone id or name missing for domain', ['domain_id' => $this->domain_id]); Notification::make() ->danger() ->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')])) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_zone', ['domain' => $domainName ?? 'unknown', 'subdomain' => $this->getLabel() ?? 'unknown'])) ->send(); return false; @@ -166,7 +168,7 @@ protected function upsertOnCloudflare(): bool Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_invalid_service_record_type_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_invalid_service_record_type', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) + ->body(trans('subdomains::strings.notifications.cloudflare_invalid_service_record_type', ['subdomain' => $this->getLabel() ?? 'unknown'])) ->send(); return false; @@ -183,21 +185,25 @@ protected function upsertOnCloudflare(): bool return false; } - $recordName = sprintf('%s.%s.%s', $serviceRecordType->service(), $serviceRecordType->protocol(), $this->name); - $result = $registrar->upsertDnsRecord($zoneId, $recordName, 'SRV', $this->server->node->srv_target, $this->cloudflare_id, $port); + $result = $registrar->upsertDnsRecord($zoneId, $domainName, $this->name, 'SRV', $this->server->node->srv_target, $this->cloudflare_id, $port, $serviceRecordType); if ($result['success'] && !empty($result['id'])) { if ($this->cloudflare_id !== $result['id']) { - $this->updateQuietly(['cloudflare_id' => $result['id']]); + if ($this->exists) { + $this->updateQuietly(['cloudflare_id' => $result['id']]); + } else { + // Model is being created, set attribute so it gets persisted with the insert + $this->cloudflare_id = $result['id']; + } } return true; } - Log::error('Failed to upsert SRV record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); + Notification::make() ->danger() ->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'])])) + ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $this->getLabel() ?? 'unknown', 'errors' => json_encode($result['errors'])])) ->send(); return false; @@ -210,30 +216,24 @@ protected function upsertOnCloudflare(): bool Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_missing_ip_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_missing_ip', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_ip', ['subdomain' => $this->getLabel() ?? 'unknown'])) ->send(); return false; } - $result = $registrar->upsertDnsRecord($zoneId, $this->name, $this->record_type, $ip, $this->cloudflare_id, null); + $result = $registrar->upsertDnsRecord($zoneId, $domainName, $this->name, $this->record_type, $ip, $this->cloudflare_id, null, null); if ($result['success'] && !empty($result['id'])) { - if ($this->cloudflare_id !== $result['id']) { - $this->updateQuietly(['cloudflare_id' => $result['id']]); - } + $this->cloudflare_id = $result['id']; return true; } - $domainName = $this->domain->name ?? 'unknown'; - $subdomain = sprintf('%s.%s', $this->name, $domainName); - - Log::error('Failed to upsert record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_upsert_failed_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $subdomain, 'errors' => json_encode($result['errors'])])) + ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $this->getLabel() ?? 'unknown', 'errors' => json_encode($result['errors'])])) ->send(); return false; @@ -241,6 +241,7 @@ protected function upsertOnCloudflare(): bool protected function deleteOnCloudflare(): bool { + // No Cloudflare record to delete, consider it successful if (empty($this->cloudflare_id)) { Log::warning('Subdomain deleteOnCloudflare called but no cloudflare_id set', ['subdomain_id' => $this->id]); @@ -249,21 +250,18 @@ protected function deleteOnCloudflare(): bool $registrar = app(CloudflareService::class); - $result = $registrar->deleteDnsRecord($this->domain->cloudflare_id, $this->cloudflare_id); - - if (!empty($result['success']) || $result['status'] === 404) { - return true; - } - - 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'])])) - ->send(); + $result = $registrar->deleteDnsRecord($this->domain->cloudflare_id, $this->cloudflare_id); - return false; + if (!empty($result['success']) || $result['status'] === 404) { + return true; } - return true; + Notification::make() + ->danger() + ->title(trans('subdomains::strings.notifications.cloudflare_delete_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_delete_failed', ['subdomain' => $this->getLabel() ?? 'unknown', 'errors' => json_encode($result['errors'])])) + ->send(); + + return false; } } diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index fb04795a..bf75f3fb 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -2,12 +2,46 @@ namespace Boy132\Subdomains\Services; +use Boy132\Subdomains\Enums\ServiceRecordType; use Illuminate\Http\Client\Response; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class CloudflareService { + /** + * @return array{success: bool, id: string|null, errors: array, status: int, body: mixed|null} + */ + public function getRecord(string $zoneId, string $name, string $recordType): ?array + { + if (empty($zoneId) || empty($name) || empty($recordType)) { + Log::error('Cloudflare getRecord called with missing parameters', ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); + + return null; + } + + try { + // @phpstan-ignore staticMethod.notFound + $response = Http::cloudflare()->get("zones/{$zoneId}/dns_records?type={$recordType}&name={$name}"); + } catch (\Throwable $e) { + Log::error('Cloudflare getRecord request failed: ' . $e->getMessage(), ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); + + return null; + } + + $parsed = $this->parseCloudflareHttpResponse($response); + + if ($parsed['success']) { + return $parsed; + } + + if (!empty($parsed['errors'])) { + Log::warning('Cloudflare getRecord returned errors', ['zone' => $zoneId, 'name' => $name, 'type' => $recordType, 'status' => $response->status(), 'errors' => $parsed['errors']]); + } + + return null; + } + public function getZoneId(string $domainName): ?string { if (empty($domainName)) { @@ -43,7 +77,7 @@ public function getZoneId(string $domainName): ?string /** * @return array{success: bool, id: string|null, errors: array, status: int, body: mixed|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 $domainName, string $name, string $recordType, string $target, ?string $recordId, ?int $port, ?ServiceRecordType $serviceRecordType): array { if (empty($zoneId) || empty($name) || empty($recordType)) { Log::error('Cloudflare upsertDnsRecord missing required parameters', ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); @@ -60,14 +94,15 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType // 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]); + if (empty($port) || empty($target) || empty($serviceRecordType)) { + Log::error('Cloudflare upsert missing SRV target, port or service record information', ['zone' => $zoneId, 'name' => $name, 'type' => $recordType, 'port' => $port, 'target' => $target, 'serviceRecordType' => $serviceRecordType]); return ['success' => false, 'id' => null, 'errors' => ['missing_srv_target_or_port' => true], 'status' => 0, 'body' => null]; } + $fqdn = sprintf('%s.%s.%s.%s', $serviceRecordType->service(), $serviceRecordType->protocol(), $name, $domainName); $payload = [ - 'name' => $name, + 'name' => $fqdn, 'ttl' => $ttl, 'type' => 'SRV', 'comment' => $comment, @@ -81,6 +116,7 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType ], ]; } else { + $fqdn = sprintf('%s.%s', $name, $domainName); $payload = [ 'name' => $name, 'ttl' => $ttl, @@ -91,6 +127,11 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType ]; } + $response = $this->getRecord($zoneId, $fqdn, $recordType); + if ($response && !empty($response['body']['result']) && count($response['body']['result']) > 0) { + return ['success' => false, 'id' => $response['body']['result'][0]['id'], 'errors' => ['record_exists' => true], 'status' => 409, 'body' => $response['body']]; + } + try { if ($recordId) { // @phpstan-ignore staticMethod.notFound From 5c2b23f03701d0e7a248dc097c4af630eb865655 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 19:28:22 +1100 Subject: [PATCH 59/79] php stan fixes --- 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 64802a0e..d6879e84 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -120,7 +120,7 @@ public function setSrvRecordAttribute(bool $isSrvRecord): void if ($isSrvRecord) { $this->attributes['record_type'] = 'SRV'; } else { - $ip = $this->server?->allocation->ip; + $ip = $this->server->allocation?->ip; if (!empty($ip) && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $this->attributes['record_type'] = 'AAAA'; } else { @@ -142,7 +142,7 @@ protected function upsertOnCloudflare(): bool Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_missing_zone_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_missing_zone', ['domain' => $domainName ?? 'unknown', 'subdomain' => $this->getLabel() ?? 'unknown'])) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_zone', ['domain' => $domainName, 'subdomain' => $this->getLabel() ?? 'unknown'])) ->send(); return false; From dc91cca1f1553f0973bfee7ed7b41d0dc815e251 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 19:47:08 +1100 Subject: [PATCH 60/79] Updated subdomains information --- subdomains/README.md | 38 +++++++++++++++++++++++++++++++++++++- subdomains/plugin.json | 2 +- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/subdomains/README.md b/subdomains/README.md index d9cf0b1f..0b05c230 100644 --- a/subdomains/README.md +++ b/subdomains/README.md @@ -5,6 +5,42 @@ Allows users to create and manage custom subdomains for their game servers using ## Features - Create custom subdomains for game servers -- Cloudflare DNS integration for automatic record management +- Cloudflare DNS integration for automatic record management (A/AAAA and SRV) - Admin management of Cloudflare domains - Per-server subdomain limits + +### Supported games & required tags + +| Game | Required Egg tag | +|------|------------------| +| Minecraft | `minecraft` | + +### SRV requirements + +SRV records require a few things to be configured before they can be created: + +1. **Node SRV target** — the node must have an `SRV Target` defined (this points SRV records to the correct host). +2. **Server allocation port** — the server must have an allocation with a port (SRV records include the port number). +3. **Egg tag** — the server's Egg must include a supported tag (e.g., `minecraft`) so the plugin knows the service and protocol to use (for example `_minecraft._tcp`). + +### Troubleshooting + +- **Cloudflare zone fetch failed / missing zone ID:** ensure the API token has access to the zone, or paste the Zone ID manually in **Admin > Domains**. +- **Missing IP for A/AAAA:** ensure the server has an allocation with a valid IP address. +- **Missing SRV port:** ensure the server allocation includes a port. +- **Missing SRV target:** configure the node's SRV target. +- **Unsupported SRV service:** add the appropriate tag to the Egg (e.g., `minecraft`). + +### Examples + +#### A/AAAA Record + +1. Admin > Domains > Create > `example.com` +2. Server > Subdomains > Create > Name: `play`, Select: `A` > Save + +#### SRV Record + +1. Admin > Domains > Create > `example.com` +2. Node > Set SRV Target > `play.example.com` - This is the public hostname/ip that SRV records will point to. +3. Ensure the server egg has the been tagged correctly. Refer to the Supported games & required tags table above. +4. Server > Subdomains > Create > Name: `play`, Select: `SRV` > Save \ No newline at end of file diff --git a/subdomains/plugin.json b/subdomains/plugin.json index 23e3ee61..595b575f 100644 --- a/subdomains/plugin.json +++ b/subdomains/plugin.json @@ -2,7 +2,7 @@ "id": "subdomains", "name": "Subdomains", "author": "Boy132", - "version": "1.1.0", + "version": "1.2.0", "description": "Allows users to create subdomains for their servers", "category": "plugin", "url": "https://github.com/pelican-dev/plugins/tree/main/subdomains", From 3b2dd884dd5ff0118068c28187ced417bd9bbc11 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 19:52:08 +1100 Subject: [PATCH 61/79] fixed invalid column issue --- subdomains/database/migrations/005_add_srv_target_to_nodes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subdomains/database/migrations/005_add_srv_target_to_nodes.php b/subdomains/database/migrations/005_add_srv_target_to_nodes.php index 5a7f330e..83d1a23e 100644 --- a/subdomains/database/migrations/005_add_srv_target_to_nodes.php +++ b/subdomains/database/migrations/005_add_srv_target_to_nodes.php @@ -10,7 +10,7 @@ public function up(): void { // Can safely delete any Cloudflare domains without IDs as they're useless anyway DB::table('cloudflare_domains')->whereNull('cloudflare_id')->delete(); - DB::table('subdomains')->whereNull('cloudflare_domain_id')->update(['cloudflare_domain_id' => null]); + DB::table('subdomains')->whereNull('cloudflare_id')->update(['cloudflare_id' => null]); Schema::table('cloudflare_domains', function (Blueprint $table) { $table->dropColumn('srv_target'); From c09e0502e2526943e00113035259bed6e38783b1 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 19:54:44 +1100 Subject: [PATCH 62/79] Remove null cloudflare_id entries from subdomains table --- subdomains/database/migrations/005_add_srv_target_to_nodes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subdomains/database/migrations/005_add_srv_target_to_nodes.php b/subdomains/database/migrations/005_add_srv_target_to_nodes.php index 83d1a23e..b74d179b 100644 --- a/subdomains/database/migrations/005_add_srv_target_to_nodes.php +++ b/subdomains/database/migrations/005_add_srv_target_to_nodes.php @@ -10,7 +10,7 @@ public function up(): void { // Can safely delete any Cloudflare domains without IDs as they're useless anyway DB::table('cloudflare_domains')->whereNull('cloudflare_id')->delete(); - DB::table('subdomains')->whereNull('cloudflare_id')->update(['cloudflare_id' => null]); + DB::table('subdomains')->whereNull('cloudflare_id')->delete(); Schema::table('cloudflare_domains', function (Blueprint $table) { $table->dropColumn('srv_target'); From 660b0b636cdc083bd118716c64f298526127c1f1 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 20:28:44 +1100 Subject: [PATCH 63/79] fixing various null issues --- .../005_add_srv_target_to_nodes.php | 1 + subdomains/lang/en/strings.php | 2 +- .../Components/Actions/SetSrvTargetAction.php | 3 +- .../Subdomains/SubdomainResource.php | 2 +- subdomains/src/Models/Subdomain.php | 34 ++++++++++++++++--- 5 files changed, 34 insertions(+), 8 deletions(-) diff --git a/subdomains/database/migrations/005_add_srv_target_to_nodes.php b/subdomains/database/migrations/005_add_srv_target_to_nodes.php index b74d179b..05961620 100644 --- a/subdomains/database/migrations/005_add_srv_target_to_nodes.php +++ b/subdomains/database/migrations/005_add_srv_target_to_nodes.php @@ -2,6 +2,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; return new class extends Migration diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index 1557db34..46add357 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -40,7 +40,7 @@ 'cloudflare_missing_srv_port' => 'SRV port is missing for :server.', 'cloudflare_missing_srv_target_title' => 'Cloudflare: Missing SRV Target', - 'cloudflare_missing_srv_target' => 'SRV target is missing from :node. ', + 'cloudflare_missing_srv_target' => 'SRV target is missing from :node.', 'cloudflare_record_created_title' => 'Cloudflare: Record Created', 'cloudflare_record_created' => 'Successfully created :subdomain record', diff --git a/subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php b/subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php index f85d537f..5cbbae3d 100644 --- a/subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php +++ b/subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php @@ -28,8 +28,7 @@ protected function setUp(): void ->label(fn () => trans('subdomains::strings.srv_target')) ->default(fn () => $node->srv_target) // @phpstan-ignore property.notFound ->placeholder('play.example.com OR IPv4/IPv6 address') - ->helperText(trans('subdomains::strings.srv_target_confirmation')) - ->rules(['nullable', 'string', 'regex:/^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(?helperText(trans('subdomains::strings.srv_target_confirmation')), ]; }); diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index 02c4a335..a02d3089 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -116,7 +116,7 @@ public static function form(Schema $schema): Schema TextInput::make('name') ->label(trans('subdomains::strings.name')) ->required() - ->suffix(fn (callable $get) => CloudflareDomain::find($get('domain_id'))->name) + ->suffix(fn (callable $get) => CloudflareDomain::find($get('domain_id'))?->name) ->unique(), Select::make('domain_id') ->label(trans_choice('subdomains::strings.domain', 1)) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index d6879e84..88c32b44 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -120,7 +120,7 @@ public function setSrvRecordAttribute(bool $isSrvRecord): void if ($isSrvRecord) { $this->attributes['record_type'] = 'SRV'; } else { - $ip = $this->server->allocation?->ip; + $ip = $this->server?->allocation?->ip; if (!empty($ip) && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $this->attributes['record_type'] = 'AAAA'; } else { @@ -136,9 +136,19 @@ protected function upsertOnCloudflare(): bool $zoneId = $this->domain->cloudflare_id; $domainName = $this->domain->name; + if ($this->server || $this->server->node) { + Log::warning('Subdomain server/node relation is null', ['subdomain_id' => $this->id]); + Notification::make() + ->danger() + ->title(trans('subdomains::strings.notifications.cloudflare_upsert_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $this->getLabel() ?? 'unknown', 'errors' => 'Server/node relation is null'])) + ->send(); + + return false; + } + if (empty($zoneId) || empty($domainName)) { Log::warning('Cloudflare zone id or name missing for domain', ['domain_id' => $this->domain_id]); - Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_missing_zone_title')) @@ -192,7 +202,6 @@ protected function upsertOnCloudflare(): bool if ($this->exists) { $this->updateQuietly(['cloudflare_id' => $result['id']]); } else { - // Model is being created, set attribute so it gets persisted with the insert $this->cloudflare_id = $result['id']; } } @@ -225,7 +234,13 @@ protected function upsertOnCloudflare(): bool $result = $registrar->upsertDnsRecord($zoneId, $domainName, $this->name, $this->record_type, $ip, $this->cloudflare_id, null, null); if ($result['success'] && !empty($result['id'])) { - $this->cloudflare_id = $result['id']; + if ($this->cloudflare_id !== $result['id']) { + if ($this->exists) { + $this->updateQuietly(['cloudflare_id' => $result['id']]); + } else { + $this->cloudflare_id = $result['id']; + } + } return true; } @@ -248,6 +263,17 @@ protected function deleteOnCloudflare(): bool return true; } + if ($this->domain || empty($this->domain->cloudflare_id)) { + Log::warning('Cloudflare zone missing for subdomain during subdomain delete', ['domain_id' => $this->domain_id]); + Notification::make() + ->danger() + ->title(trans('subdomains::strings.notifications.cloudflare_delete_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_delete_failed', ['errors' => 'Cloudflare zone missing for domain'])) + ->send(); + + return false; + } + $registrar = app(CloudflareService::class); $result = $registrar->deleteDnsRecord($this->domain->cloudflare_id, $this->cloudflare_id); From 52b158921ae63c647a1dd546eddc1fad2c0dfbb7 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 20:35:30 +1100 Subject: [PATCH 64/79] phpstan issues --- subdomains/src/Models/Subdomain.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 88c32b44..213003b0 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -120,7 +120,7 @@ public function setSrvRecordAttribute(bool $isSrvRecord): void if ($isSrvRecord) { $this->attributes['record_type'] = 'SRV'; } else { - $ip = $this->server?->allocation?->ip; + $ip = $this->server->allocation?->ip; if (!empty($ip) && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $this->attributes['record_type'] = 'AAAA'; } else { @@ -136,7 +136,7 @@ protected function upsertOnCloudflare(): bool $zoneId = $this->domain->cloudflare_id; $domainName = $this->domain->name; - if ($this->server || $this->server->node) { + if (empty($this->server) || empty($this->server->node)) { Log::warning('Subdomain server/node relation is null', ['subdomain_id' => $this->id]); Notification::make() ->danger() @@ -263,7 +263,7 @@ protected function deleteOnCloudflare(): bool return true; } - if ($this->domain || empty($this->domain->cloudflare_id)) { + if (empty($this->domain->cloudflare_id)) { Log::warning('Cloudflare zone missing for subdomain during subdomain delete', ['domain_id' => $this->domain_id]); Notification::make() ->danger() From 75c17da44cd1bcd4935a818e745b98c513649e3b Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 20:40:18 +1100 Subject: [PATCH 65/79] more npes --- .../Server/Resources/Subdomains/SubdomainResource.php | 10 +++++----- subdomains/src/Models/Subdomain.php | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index a02d3089..1d9e0fb1 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -85,9 +85,9 @@ public static function table(Table $table): Table ->state(fn (Subdomain $subdomain) => $subdomain->getLabel()), TextColumn::make('record_type') ->label(trans('subdomains::strings.record_type')) - ->icon(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server->node->srv_target) ? 'tabler-alert-triangle' : null) - ->color(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server->node->srv_target) ? 'danger' : null) - ->tooltip(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server->node->srv_target) ? trans('subdomains::strings.srv_target_missing') : null), + ->icon(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server?->node?->srv_target) ? 'tabler-alert-triangle' : null) + ->color(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server?->node?->srv_target) ? 'danger' : null) + ->tooltip(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server?->node?->srv_target) ? trans('subdomains::strings.srv_target_missing') : null), ]) ->recordActions([ EditAction::make() @@ -128,9 +128,9 @@ public static function form(Schema $schema): Schema ->searchable(), Toggle::make('srv_record') ->label(trans('subdomains::strings.srv_record')) - ->helperText(fn () => Filament::getTenant()->node->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.srv_target_missing')) // @phpstan-ignore property.notFound + ->helperText(fn () => Filament::getTenant()->node?->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.srv_target_missing')) // @phpstan-ignore property.notFound ->reactive() - ->disabled(fn () => empty(Filament::getTenant()->node->srv_target)), + ->disabled(fn () => empty(Filament::getTenant()->node?->srv_target)), ]); } diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 213003b0..79a8c97b 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -219,7 +219,7 @@ protected function upsertOnCloudflare(): bool } // A/AAAA - $ip = $this->server->allocation?->ip; + $ip = $this->server?->allocation?->ip; if (empty($ip) || $ip === '0.0.0.0' || $ip === '::') { Log::warning('Server allocation missing or invalid IP', ['server_id' => $this->server_id]); Notification::make() From 0173530b3f75dce3359ded51c723020d05cb01ea Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 20:47:52 +1100 Subject: [PATCH 66/79] more null checks --- .../Resources/Subdomains/SubdomainResource.php | 8 ++++---- subdomains/src/Models/Subdomain.php | 13 ++++++++++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index 1d9e0fb1..b627b28b 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -85,9 +85,9 @@ public static function table(Table $table): Table ->state(fn (Subdomain $subdomain) => $subdomain->getLabel()), TextColumn::make('record_type') ->label(trans('subdomains::strings.record_type')) - ->icon(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server?->node?->srv_target) ? 'tabler-alert-triangle' : null) - ->color(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server?->node?->srv_target) ? 'danger' : null) - ->tooltip(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server?->node?->srv_target) ? trans('subdomains::strings.srv_target_missing') : null), + ->icon(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? 'tabler-alert-triangle' : null) + ->color(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? 'danger' : null) + ->tooltip(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? trans('subdomains::strings.srv_target_missing') : null), ]) ->recordActions([ EditAction::make() @@ -130,7 +130,7 @@ public static function form(Schema $schema): Schema ->label(trans('subdomains::strings.srv_record')) ->helperText(fn () => Filament::getTenant()->node?->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.srv_target_missing')) // @phpstan-ignore property.notFound ->reactive() - ->disabled(fn () => empty(Filament::getTenant()->node?->srv_target)), + ->disabled(fn () => !Filament::getTenant()->node?->srv_target), ]); } diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 79a8c97b..719d6236 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -147,6 +147,17 @@ protected function upsertOnCloudflare(): bool return false; } + if (empty($this->server->allocation)) { + Log::warning('Subdomain server allocation is null', ['subdomain_id' => $this->id, 'server_id' => $this->server->id]); + Notification::make() + ->danger() + ->title(trans('subdomains::strings.notifications.cloudflare_upsert_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $this->getLabel() ?? 'unknown', 'errors' => 'Server allocation is null'])) + ->send(); + + return false; + } + if (empty($zoneId) || empty($domainName)) { Log::warning('Cloudflare zone id or name missing for domain', ['domain_id' => $this->domain_id]); Notification::make() @@ -219,7 +230,7 @@ protected function upsertOnCloudflare(): bool } // A/AAAA - $ip = $this->server?->allocation?->ip; + $ip = $this->server->allocation?->ip; if (empty($ip) || $ip === '0.0.0.0' || $ip === '::') { Log::warning('Server allocation missing or invalid IP', ['server_id' => $this->server_id]); Notification::make() From a746d35ec58b3c430f9a59cbd6b1b512b4f81612 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 20:52:22 +1100 Subject: [PATCH 67/79] phpstan ignore errors --- .../Server/Resources/Subdomains/SubdomainResource.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index b627b28b..aedf3271 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -85,9 +85,9 @@ public static function table(Table $table): Table ->state(fn (Subdomain $subdomain) => $subdomain->getLabel()), TextColumn::make('record_type') ->label(trans('subdomains::strings.record_type')) - ->icon(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? 'tabler-alert-triangle' : null) - ->color(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? 'danger' : null) - ->tooltip(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? trans('subdomains::strings.srv_target_missing') : null), + ->icon(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? 'tabler-alert-triangle' : null) // phpstan-ignore nullsafe.neverNull, property.notFound + ->color(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? 'danger' : null) // phpstan-ignore nullsafe.neverNull, property.notFound + ->tooltip(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? trans('subdomains::strings.srv_target_missing') : null), // phpstan-ignore nullsafe.neverNull, property.notFound ]) ->recordActions([ EditAction::make() @@ -130,7 +130,7 @@ public static function form(Schema $schema): Schema ->label(trans('subdomains::strings.srv_record')) ->helperText(fn () => Filament::getTenant()->node?->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.srv_target_missing')) // @phpstan-ignore property.notFound ->reactive() - ->disabled(fn () => !Filament::getTenant()->node?->srv_target), + ->disabled(fn () => !Filament::getTenant()->node?->srv_target), // @phpstan-ignore property.notFound ]); } From 53e6dab4bf1cc80913b07820df98643c5d0cf91e Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 21:42:24 +1100 Subject: [PATCH 68/79] phpstan ignores --- .../Server/Resources/Subdomains/SubdomainResource.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index aedf3271..272e3519 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -85,9 +85,9 @@ public static function table(Table $table): Table ->state(fn (Subdomain $subdomain) => $subdomain->getLabel()), TextColumn::make('record_type') ->label(trans('subdomains::strings.record_type')) - ->icon(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? 'tabler-alert-triangle' : null) // phpstan-ignore nullsafe.neverNull, property.notFound - ->color(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? 'danger' : null) // phpstan-ignore nullsafe.neverNull, property.notFound - ->tooltip(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? trans('subdomains::strings.srv_target_missing') : null), // phpstan-ignore nullsafe.neverNull, property.notFound + ->icon(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? 'tabler-alert-triangle' : null) // @phpstan-ignore nullsafe.neverNull, property.notFound + ->color(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? 'danger' : null) // @phpstan-ignore nullsafe.neverNull, property.notFound + ->tooltip(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? trans('subdomains::strings.srv_target_missing') : null), // @phpstan-ignore nullsafe.neverNull, property.notFound ]) ->recordActions([ EditAction::make() From 87d4de8a1b1e9402c799aed64a0edd90e51bff96 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 21:48:20 +1100 Subject: [PATCH 69/79] more phpstan ignores... --- .../Server/Resources/Subdomains/SubdomainResource.php | 6 +++--- subdomains/src/Models/Subdomain.php | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index 272e3519..395c385c 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -85,9 +85,9 @@ public static function table(Table $table): Table ->state(fn (Subdomain $subdomain) => $subdomain->getLabel()), TextColumn::make('record_type') ->label(trans('subdomains::strings.record_type')) - ->icon(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? 'tabler-alert-triangle' : null) // @phpstan-ignore nullsafe.neverNull, property.notFound - ->color(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? 'danger' : null) // @phpstan-ignore nullsafe.neverNull, property.notFound - ->tooltip(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? trans('subdomains::strings.srv_target_missing') : null), // @phpstan-ignore nullsafe.neverNull, property.notFound + ->icon(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? 'tabler-alert-triangle' : null) // @phpstan-ignore nullsafe.neverNull, nullsafe.neverNull, property.notFound + ->color(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? 'danger' : null) // @phpstan-ignore nullsafe.neverNull, nullsafe.neverNull, property.notFound + ->tooltip(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? trans('subdomains::strings.srv_target_missing') : null), // @phpstan-ignore nullsafe.neverNull, nullsafe.neverNull, property.notFound ]) ->recordActions([ EditAction::make() diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 719d6236..3694f6f3 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -171,7 +171,7 @@ protected function upsertOnCloudflare(): bool // SRV: target comes from node, port from server allocation if ($this->record_type === 'SRV') { - $port = $this->server->allocation?->port; + $port = $this->server->allocation?->port; // @phpstan-ignore nullsafe.neverNull if (empty($port)) { Log::warning('Server missing allocation with port', $this->toArray()); Notification::make() @@ -230,7 +230,7 @@ protected function upsertOnCloudflare(): bool } // A/AAAA - $ip = $this->server->allocation?->ip; + $ip = $this->server->allocation?->ip; // @phpstan-ignore nullsafe.neverNull if (empty($ip) || $ip === '0.0.0.0' || $ip === '::') { Log::warning('Server allocation missing or invalid IP', ['server_id' => $this->server_id]); Notification::make() From 732e9256be4d896f9561e5859ff6046d77ed9b59 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 21:56:11 +1100 Subject: [PATCH 70/79] npe fixes --- .../Server/Resources/Subdomains/SubdomainResource.php | 4 ++-- subdomains/src/Models/Subdomain.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index 395c385c..7ec76847 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -36,7 +36,7 @@ public static function canAccess(): bool { /** @var Server $server */ $server = Filament::getTenant(); - $ip = $server->allocation->ip ?? null; + $ip = $server->allocation?->ip ?? null; return parent::canAccess() && $ip !== null @@ -124,7 +124,7 @@ public static function form(Schema $schema): Schema ->required() ->relationship('domain', 'name') ->preload() - ->default(fn () => CloudflareDomain::first()->id ?? null) + ->default(fn () => CloudflareDomain::first()?->id ?? null) ->searchable(), Toggle::make('srv_record') ->label(trans('subdomains::strings.srv_record')) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 3694f6f3..028d5297 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -120,7 +120,7 @@ public function setSrvRecordAttribute(bool $isSrvRecord): void if ($isSrvRecord) { $this->attributes['record_type'] = 'SRV'; } else { - $ip = $this->server->allocation?->ip; + $ip = $this->server?->allocation?->ip; // @phpstan-ignore nullsafe.neverNull if (!empty($ip) && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $this->attributes['record_type'] = 'AAAA'; } else { @@ -274,7 +274,7 @@ protected function deleteOnCloudflare(): bool return true; } - if (empty($this->domain->cloudflare_id)) { + if (!$this->domain?->cloudflare_id) { Log::warning('Cloudflare zone missing for subdomain during subdomain delete', ['domain_id' => $this->domain_id]); Notification::make() ->danger() From a288d46ce3a3e758bec453e31080038610254cf9 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 21:59:02 +1100 Subject: [PATCH 71/79] phpstan ignores... again --- .../Server/Resources/Subdomains/SubdomainResource.php | 4 ++-- subdomains/src/Models/Subdomain.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index 7ec76847..86089b05 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -36,7 +36,7 @@ public static function canAccess(): bool { /** @var Server $server */ $server = Filament::getTenant(); - $ip = $server->allocation?->ip ?? null; + $ip = $server->allocation?->ip ?? null; // @phpstan-ignore nullsafe.neverNull return parent::canAccess() && $ip !== null @@ -124,7 +124,7 @@ public static function form(Schema $schema): Schema ->required() ->relationship('domain', 'name') ->preload() - ->default(fn () => CloudflareDomain::first()?->id ?? null) + ->default(fn () => CloudflareDomain::first()?->id ?? null) // @phpstan-ignore nullsafe.neverNull ->searchable(), Toggle::make('srv_record') ->label(trans('subdomains::strings.srv_record')) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 028d5297..2de60faa 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -274,7 +274,7 @@ protected function deleteOnCloudflare(): bool return true; } - if (!$this->domain?->cloudflare_id) { + if (!$this->domain?->cloudflare_id) { // @phpstan-ignore nullsafe.neverNull Log::warning('Cloudflare zone missing for subdomain during subdomain delete', ['domain_id' => $this->domain_id]); Notification::make() ->danger() From 54cc9eea708d9bd46456335b6a5212d25d68c9d4 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 22:25:45 +1100 Subject: [PATCH 72/79] improve server relation handling during creation --- subdomains/src/Models/Subdomain.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 2de60faa..3f07ff0e 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -48,7 +48,10 @@ protected static function boot(): void static::creating(function (self $model) { // Relation does not exist yet, so we need to set it manually. - $model->setRelation('server', Filament::getTenant()); + if (!$model->relationLoaded('server') && $model->server_id) { + $model->loadMissing('server.allocation'); + $model->setRelation('server', $model->server); + } $registrarUpdated = $model->upsertOnCloudflare(); if (!$registrarUpdated) { From 0c75e5a840be2b0bc855030e15881fe8be242734 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 22:27:28 +1100 Subject: [PATCH 73/79] no_unused_imports --- subdomains/src/Models/Subdomain.php | 1 - 1 file changed, 1 deletion(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 3f07ff0e..6ef7c7a5 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -5,7 +5,6 @@ use App\Models\Server; use Boy132\Subdomains\Enums\ServiceRecordType; use Boy132\Subdomains\Services\CloudflareService; -use Filament\Facades\Filament; use Filament\Notifications\Notification; use Filament\Support\Contracts\HasLabel; use Illuminate\Contracts\Support\Htmlable; From a82334ae1762c8494c22ade48ac26507e704870c Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 22:38:31 +1100 Subject: [PATCH 74/79] simplify server relation handling during creation --- subdomains/src/Models/Subdomain.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 6ef7c7a5..3f425bb4 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -47,9 +47,8 @@ protected static function boot(): void static::creating(function (self $model) { // Relation does not exist yet, so we need to set it manually. - if (!$model->relationLoaded('server') && $model->server_id) { + if ($model->server_id) { $model->loadMissing('server.allocation'); - $model->setRelation('server', $model->server); } $registrarUpdated = $model->upsertOnCloudflare(); From de10ede41fc1051cfa2266e3b2d04d829792c8c2 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 22:51:00 +1100 Subject: [PATCH 75/79] nullcheck for domainName & zoneId --- 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 3f425bb4..1f3b1fb4 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -134,8 +134,8 @@ protected function upsertOnCloudflare(): bool { $registrar = app(CloudflareService::class); - $zoneId = $this->domain->cloudflare_id; - $domainName = $this->domain->name; + $zoneId = $this->domain?->cloudflare_id; + $domainName = $this->domain?->name; if (empty($this->server) || empty($this->server->node)) { Log::warning('Subdomain server/node relation is null', ['subdomain_id' => $this->id]); From 0ba3dc62ab5cd3a57a49865ca1458126edc32f59 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 4 Jan 2026 22:54:08 +1100 Subject: [PATCH 76/79] phpstan ignores --- 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 1f3b1fb4..b6e5b11c 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -134,8 +134,8 @@ protected function upsertOnCloudflare(): bool { $registrar = app(CloudflareService::class); - $zoneId = $this->domain?->cloudflare_id; - $domainName = $this->domain?->name; + $zoneId = $this->domain?->cloudflare_id; // @phpstan-ignore nullsafe.neverNull + $domainName = $this->domain?->name; // @phpstan-ignore nullsafe.neverNull if (empty($this->server) || empty($this->server->node)) { Log::warning('Subdomain server/node relation is null', ['subdomain_id' => $this->id]); From 113db001e14090e396a1625737180afa1c6994ba Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 5 Jan 2026 07:39:54 +1100 Subject: [PATCH 77/79] Change requests for: https://github.com/pelican-dev/plugins/pull/67#pullrequestreview-3625058495 --- ...4_add_srv_target_to_cloudflare_domains.php | 22 ------------- ...es.php => 004_add_srv_target_to_nodes.php} | 1 - subdomains/plugin.json | 2 +- subdomains/src/Enums/ServiceRecordType.php | 12 ------- .../Subdomains/SubdomainResource.php | 19 +++++------- subdomains/src/Models/Subdomain.php | 31 ++++++------------- 6 files changed, 18 insertions(+), 69 deletions(-) delete mode 100644 subdomains/database/migrations/004_add_srv_target_to_cloudflare_domains.php rename subdomains/database/migrations/{005_add_srv_target_to_nodes.php => 004_add_srv_target_to_nodes.php} (97%) 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 deleted file mode 100644 index 3cbe2a1a..00000000 --- a/subdomains/database/migrations/004_add_srv_target_to_cloudflare_domains.php +++ /dev/null @@ -1,22 +0,0 @@ -string('srv_target')->nullable()->after('cloudflare_id'); - }); - } - - public function down(): void - { - Schema::table('cloudflare_domains', function (Blueprint $table) { - $table->dropColumn('srv_target'); - }); - } -}; diff --git a/subdomains/database/migrations/005_add_srv_target_to_nodes.php b/subdomains/database/migrations/004_add_srv_target_to_nodes.php similarity index 97% rename from subdomains/database/migrations/005_add_srv_target_to_nodes.php rename to subdomains/database/migrations/004_add_srv_target_to_nodes.php index 05961620..b74d179b 100644 --- a/subdomains/database/migrations/005_add_srv_target_to_nodes.php +++ b/subdomains/database/migrations/004_add_srv_target_to_nodes.php @@ -2,7 +2,6 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; return new class extends Migration diff --git a/subdomains/plugin.json b/subdomains/plugin.json index 595b575f..7ff7cbcd 100644 --- a/subdomains/plugin.json +++ b/subdomains/plugin.json @@ -2,7 +2,7 @@ "id": "subdomains", "name": "Subdomains", "author": "Boy132", - "version": "1.2.0", + "version": "1.0.0", "description": "Allows users to create subdomains for their servers", "category": "plugin", "url": "https://github.com/pelican-dev/plugins/tree/main/subdomains", diff --git a/subdomains/src/Enums/ServiceRecordType.php b/subdomains/src/Enums/ServiceRecordType.php index 05fa224b..0ab69eb9 100644 --- a/subdomains/src/Enums/ServiceRecordType.php +++ b/subdomains/src/Enums/ServiceRecordType.php @@ -15,18 +15,6 @@ public function getLabel(): string return str($this->name)->title(); } - public static function isSupported(Server $server): bool - { - $tags = $server->egg->tags ?? []; - foreach (self::cases() as $type) { - if (in_array($type->name, $tags)) { - return true; - } - } - - return false; - } - public static function fromServer(Server $server): ?self { $tags = $server->egg->tags ?? []; diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index 86089b05..d570a567 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -36,13 +36,8 @@ public static function canAccess(): bool { /** @var Server $server */ $server = Filament::getTenant(); - $ip = $server->allocation?->ip ?? null; // @phpstan-ignore nullsafe.neverNull - return parent::canAccess() - && $ip !== null - && $ip !== '0.0.0.0' - && $ip !== '::' - && CloudflareDomain::count() > 0; + return parent::canAccess() && $server->allocation && $server->allocation->ip !== '0.0.0.0' && $server->allocation->ip !== '::' && CloudflareDomain::count() > 0; } public static function getNavigationLabel(): string @@ -85,9 +80,9 @@ public static function table(Table $table): Table ->state(fn (Subdomain $subdomain) => $subdomain->getLabel()), TextColumn::make('record_type') ->label(trans('subdomains::strings.record_type')) - ->icon(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? 'tabler-alert-triangle' : null) // @phpstan-ignore nullsafe.neverNull, nullsafe.neverNull, property.notFound - ->color(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? 'danger' : null) // @phpstan-ignore nullsafe.neverNull, nullsafe.neverNull, property.notFound - ->tooltip(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server?->node?->srv_target ? trans('subdomains::strings.srv_target_missing') : null), // @phpstan-ignore nullsafe.neverNull, nullsafe.neverNull, property.notFound + ->icon(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server->node->srv_target ? 'tabler-alert-triangle' : null) // @phpstan-ignore property.notFound + ->color(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server->node->srv_target ? 'danger' : null) // @phpstan-ignore property.notFound + ->tooltip(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server->node->srv_target ? trans('subdomains::strings.srv_target_missing') : null), // @phpstan-ignore property.notFound ]) ->recordActions([ EditAction::make() @@ -124,13 +119,13 @@ public static function form(Schema $schema): Schema ->required() ->relationship('domain', 'name') ->preload() - ->default(fn () => CloudflareDomain::first()?->id ?? null) // @phpstan-ignore nullsafe.neverNull + ->default(fn () => CloudflareDomain::first()?->id) ->searchable(), Toggle::make('srv_record') ->label(trans('subdomains::strings.srv_record')) - ->helperText(fn () => Filament::getTenant()->node?->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.srv_target_missing')) // @phpstan-ignore property.notFound + ->helperText(fn () => Filament::getTenant()->node->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.srv_target_missing')) // @phpstan-ignore property.notFound ->reactive() - ->disabled(fn () => !Filament::getTenant()->node?->srv_target), // @phpstan-ignore property.notFound + ->disabled(fn () => !Filament::getTenant()->node->srv_target), // @phpstan-ignore property.notFound ]); } diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index b6e5b11c..668b8f25 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -21,6 +21,7 @@ * @property CloudflareDomain $domain * @property int $server_id * @property Server $server + * @property bool $srv_record */ class Subdomain extends Model implements HasLabel { @@ -48,7 +49,7 @@ protected static function boot(): void static::creating(function (self $model) { // Relation does not exist yet, so we need to set it manually. if ($model->server_id) { - $model->loadMissing('server.allocation'); + $model->loadMissing('server.allocation', 'server.node', 'domain'); } $registrarUpdated = $model->upsertOnCloudflare(); @@ -121,7 +122,7 @@ public function setSrvRecordAttribute(bool $isSrvRecord): void if ($isSrvRecord) { $this->attributes['record_type'] = 'SRV'; } else { - $ip = $this->server?->allocation?->ip; // @phpstan-ignore nullsafe.neverNull + $ip = $this->server->allocation->ip; if (!empty($ip) && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $this->attributes['record_type'] = 'AAAA'; } else { @@ -134,8 +135,8 @@ protected function upsertOnCloudflare(): bool { $registrar = app(CloudflareService::class); - $zoneId = $this->domain?->cloudflare_id; // @phpstan-ignore nullsafe.neverNull - $domainName = $this->domain?->name; // @phpstan-ignore nullsafe.neverNull + $zoneId = $this->domain->cloudflare_id; + $domainName = $this->domain->name; if (empty($this->server) || empty($this->server->node)) { Log::warning('Subdomain server/node relation is null', ['subdomain_id' => $this->id]); @@ -159,8 +160,8 @@ protected function upsertOnCloudflare(): bool return false; } - if (empty($zoneId) || empty($domainName)) { - Log::warning('Cloudflare zone id or name missing for domain', ['domain_id' => $this->domain_id]); + if (empty($zoneId)) { + Log::warning('Cloudflare zone id missing for domain', ['domain_id' => $this->domain_id]); Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_missing_zone_title')) @@ -172,18 +173,6 @@ protected function upsertOnCloudflare(): bool // SRV: target comes from node, port from server allocation if ($this->record_type === 'SRV') { - $port = $this->server->allocation?->port; // @phpstan-ignore nullsafe.neverNull - if (empty($port)) { - Log::warning('Server missing allocation with port', $this->toArray()); - Notification::make() - ->danger() - ->title(trans('subdomains::strings.notifications.cloudflare_missing_srv_port_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_port', ['server' => $this->server->name ?? 'unassigned'])) - ->send(); - - return false; - } - $serviceRecordType = ServiceRecordType::fromServer($this->server); if (!$serviceRecordType) { Log::warning('Unable to determine service record type for SRV record', ['server_id' => $this->server->id ?? 'unknown', 'server' => $this->server->name ?? 'unknown']); @@ -207,7 +196,7 @@ protected function upsertOnCloudflare(): bool return false; } - $result = $registrar->upsertDnsRecord($zoneId, $domainName, $this->name, 'SRV', $this->server->node->srv_target, $this->cloudflare_id, $port, $serviceRecordType); + $result = $registrar->upsertDnsRecord($zoneId, $domainName, $this->name, 'SRV', $this->server->node->srv_target, $this->cloudflare_id, $this->server->allocation->port, $serviceRecordType); if ($result['success'] && !empty($result['id'])) { if ($this->cloudflare_id !== $result['id']) { @@ -231,7 +220,7 @@ protected function upsertOnCloudflare(): bool } // A/AAAA - $ip = $this->server->allocation?->ip; // @phpstan-ignore nullsafe.neverNull + $ip = $this->server->allocation->ip; if (empty($ip) || $ip === '0.0.0.0' || $ip === '::') { Log::warning('Server allocation missing or invalid IP', ['server_id' => $this->server_id]); Notification::make() @@ -275,7 +264,7 @@ protected function deleteOnCloudflare(): bool return true; } - if (!$this->domain?->cloudflare_id) { // @phpstan-ignore nullsafe.neverNull + if (!$this->domain->cloudflare_id) { Log::warning('Cloudflare zone missing for subdomain during subdomain delete', ['domain_id' => $this->domain_id]); Notification::make() ->danger() From 465e8b0dec33de643b6f3fe336c51d097b860822 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 5 Jan 2026 07:42:06 +1100 Subject: [PATCH 78/79] pint and missing use reference --- subdomains/database/migrations/004_add_srv_target_to_nodes.php | 1 + 1 file changed, 1 insertion(+) diff --git a/subdomains/database/migrations/004_add_srv_target_to_nodes.php b/subdomains/database/migrations/004_add_srv_target_to_nodes.php index b74d179b..05961620 100644 --- a/subdomains/database/migrations/004_add_srv_target_to_nodes.php +++ b/subdomains/database/migrations/004_add_srv_target_to_nodes.php @@ -2,6 +2,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; return new class extends Migration From 543393fe84b26609cc6254454228f75d6dc31100 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 5 Jan 2026 14:32:56 +1100 Subject: [PATCH 79/79] Added some additional egg tag types for SRV records --- subdomains/README.md | 5 +++++ subdomains/src/Enums/ServiceRecordType.php | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/subdomains/README.md b/subdomains/README.md index 0b05c230..f53a4ac3 100644 --- a/subdomains/README.md +++ b/subdomains/README.md @@ -13,7 +13,12 @@ Allows users to create and manage custom subdomains for their game servers using | Game | Required Egg tag | |------|------------------| +| Factorio | `factorio` | | Minecraft | `minecraft` | +| Mumble | `mumble` | +| Rust | `rust` | +| SCP: Secret Laboratory | `scpsl` | +| TeamSpeak 3 | `teamspeak` | ### SRV requirements diff --git a/subdomains/src/Enums/ServiceRecordType.php b/subdomains/src/Enums/ServiceRecordType.php index 0ab69eb9..cc6d072a 100644 --- a/subdomains/src/Enums/ServiceRecordType.php +++ b/subdomains/src/Enums/ServiceRecordType.php @@ -8,7 +8,12 @@ enum ServiceRecordType: string implements HasLabel { // Service record types + case factorio = '_factorio._udp'; case minecraft = '_minecraft._tcp'; + case mumble = '_mumble._tcp'; + case rust = '_rust._udp'; + case scpsl = '_scpsl._udp'; + case teamspeak = '_ts3._udp'; public function getLabel(): string {