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 0000000..3cbe2a1 --- /dev/null +++ b/subdomains/database/migrations/004_add_srv_target_to_cloudflare_domains.php @@ -0,0 +1,22 @@ +string('srv_target')->nullable()->after('cloudflare_id'); + }); + } + + public function down(): void + { + Schema::table('cloudflare_domains', function (Blueprint $table) { + $table->dropColumn('srv_target'); + }); + } +}; From 5948e9c4b5994b29169fa5ab633fcae4de9ca1f8 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 13:03:34 +1100 Subject: [PATCH 02/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 e1cd0ea..511bd80 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -14,6 +14,12 @@ 'name' => 'Name', + 'srv_record' => 'SRV Record', + 'srv_record_help' => 'Enable this option to create a SRV record instead of an A or AAAA record.', + + 'srv_target' => 'SRV Target', + 'srv_target_help' => 'The hostname that SRV records point to (for example: play.example.com).', + 'api_token' => 'Cloudflare API Token', 'api_token_help' => 'The token needs to have read permissions for Zone.Zone and write for Zone.Dns. For better security you can also set the "Zone Resources" to exclude certain domains and add the panel ip to the "Client IP Adress Filtering".', ]; From cc7665723a954b3c3eb82760ce89317f035174db Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 13:04:14 +1100 Subject: [PATCH 03/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 571166f..7108768 100644 --- a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php +++ b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php @@ -53,6 +53,8 @@ public static function table(Table $table): Table ->columns([ TextColumn::make('name') ->label(trans('subdomains::strings.name')), + TextColumn::make('srv_target') + ->label(trans('subdomains::strings.srv_target')), TextColumn::make('subdomains_count') ->label(trans_choice('subdomains::strings.subdomain', 2)) ->counts('subdomains'), @@ -77,6 +79,11 @@ public static function form(Schema $schema): Schema ->label(trans('subdomains::strings.name')) ->required() ->unique(), + TextInput::make('srv_target') + ->label(trans('subdomains::strings.srv_target')) + ->helperText(trans('subdomains::strings.srv_target_help')) + ->placeholder('play.example.com') + ->rules(['nullable', 'string', 'regex:/^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(? Date: Sun, 28 Dec 2025 13:04:33 +1100 Subject: [PATCH 04/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 45de854..537fa05 100644 --- a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php +++ b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php @@ -12,6 +12,7 @@ use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\Toggle; use Filament\Notifications\Notification; use Filament\Resources\RelationManagers\RelationManager; use Filament\Schemas\Schema; @@ -83,8 +84,10 @@ public function form(Schema $schema): Schema ->relationship('domain', 'name') ->preload() ->searchable(), - Hidden::make('record_type') - ->default(fn () => is_ipv6($this->getOwnerRecord()->allocation->ip) ? 'AAAA' : 'A'), + Toggle::make('srv_record') + ->label(trans('subdomains::strings.srv_record')) + ->helperText(trans('subdomains::strings.srv_record_help')) + ->default(false), ]); } } diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index ec372b3..992fedc 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -15,10 +15,12 @@ use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\Toggle; use Filament\Resources\Resource; use Filament\Schemas\Schema; use Filament\Support\Enums\IconSize; use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Columns\ToggleColumn; use Filament\Tables\Table; class SubdomainResource extends Resource @@ -78,6 +80,9 @@ public static function table(Table $table): Table TextColumn::make('label') ->label(trans('subdomains::strings.name')) ->state(fn (Subdomain $subdomain) => $subdomain->getLabel()), + ToggleColumn::make('srv_record') + ->label(trans('subdomains::strings.srv_record')) + ->tooltip(trans('subdomains::strings.srv_record_help')), ]) ->recordActions([ EditAction::make(), @@ -111,13 +116,10 @@ public static function form(Schema $schema): Schema ->relationship('domain', 'name') ->preload() ->searchable(), - Hidden::make('record_type') - ->default(function () { - /** @var Server $server */ - $server = Filament::getTenant(); - - return is_ipv6($server->allocation->ip) ? 'AAAA' : 'A'; - }), + Toggle::make('srv_record') + ->label(trans('subdomains::strings.srv_record')) + ->helperText(trans('subdomains::strings.srv_record_help')) + ->default(false), ]); } From 2e7246634d4fc1f449a3b47d34c132fb0862ba39 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 13:06:09 +1100 Subject: [PATCH 05/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 14cd323..a85f92f 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -27,12 +27,45 @@ class Subdomain extends Model implements HasLabel 'cloudflare_id', 'domain_id', 'server_id', + 'srv_record', + ]; + + protected $casts = [ + 'srv_record' => 'boolean', ]; protected static function boot(): void { parent::boot(); + static::saving(function (self $model) { + // If srv_record provided in the payload, ensure record_type follows it and then remove it + if (array_key_exists('srv_record', $model->attributes)) { + $srv = (bool) $model->attributes['srv_record']; + + if ($srv) { + $model->attributes['record_type'] = 'SRV'; + } else { + if ($model->server && $model->server->allocation && is_ipv6($model->server->allocation->ip)) { + $model->attributes['record_type'] = 'AAAA'; + } else { + $model->attributes['record_type'] = 'A'; + } + } + + unset($model->attributes['srv_record']); + } + + // If no record_type is present, set a sensible default based on server allocation + if (!isset($model->attributes['record_type'])) { + if ($model->server && $model->server->allocation && is_ipv6($model->server->allocation->ip)) { + $model->attributes['record_type'] = 'AAAA'; + } else { + $model->attributes['record_type'] = 'A'; + } + } + }); + static::created(function (self $model) { $model->createOnCloudflare(); }); @@ -61,21 +94,95 @@ public function getLabel(): string|Htmlable|null return $this->name . '.' . $this->domain->name; } + public function getSrvRecordAttribute(): bool + { + return $this->record_type === 'SRV'; + } + + public function setSrvRecordAttribute($value): void + { + if ($value) { + $this->attributes['record_type'] = 'SRV'; + } else { + if ($this->server && $this->server->allocation && is_ipv6($this->server->allocation->ip)) { + $this->attributes['record_type'] = 'AAAA'; + } else { + $this->attributes['record_type'] = 'A'; + } + } + } + + protected function buildSrvPayload(): ?array + { + $target = $this->domain->srv_target ?? null; + $port = $this->server && $this->server->allocation ? ($this->server->allocation->port ?? null) : null; + + if (empty($target) || empty($port)) { + return null; + } + + $priority = (int) ($this->srv_priority ?? 0); + $weight = (int) ($this->srv_weight ?? 0); + $port = (int) $port; + + // Need to build the name to include the services and protocol parts for SRV records, this may vary based on game(egg tag)/server type + // Temporrary placeholder for service and protocol = '_minecraft._tcp.' + + return [ + 'name' => sprintf('_minecraft._tcp.%s', $this->name), + 'ttl' => 1, + 'type' => 'SRV', + 'comment' => 'Created by Pelican Subdomains plugin', + 'content' => sprintf('%d %d %d %s', $priority, $weight, $port, $target), + 'proxied' => false, + 'data' => [ + 'priority' => $priority, + 'weight' => $weight, + 'port' => $port, + 'target' => $target, + ], + ]; + } + protected function createOnCloudflare(): void { + if ($this->record_type === 'SRV') { + $payload = $this->buildSrvPayload(); + + if ($payload === null) { + return; + } + + if (!$this->cloudflare_id) { + $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $payload)->json(); + + if (!empty($response['success'])) { + $dnsRecord = $response['result']; + + $this->updateQuietly([ + 'cloudflare_id' => $dnsRecord['id'], + ]); + } + } + + return; + } + if (!$this->server->allocation || $this->server->allocation->ip === '0.0.0.0' || $this->server->allocation->ip === '::') { return; } if (!$this->cloudflare_id) { - $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", [ + $body = [ 'name' => $this->name, - 'ttl' => 120, + 'ttl' => 1, 'type' => $this->record_type, 'comment' => 'Created by Pelican Subdomains plugin', 'content' => $this->server->allocation->ip, 'proxied' => false, - ])->json(); + ]; + + $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $body)->json(); if ($response['success']) { $dnsRecord = $response['result']; @@ -93,16 +200,53 @@ protected function updateOnCloudflare(): void return; } - if ($this->cloudflare_id) { - Http::cloudflare()->patch("zones/{$this->domain->cloudflare_id}/dns_records/{$this->cloudflare_id}", [ + if ($this->record_type === 'SRV') { + $payload = $this->buildSrvPayload(); + + if ($payload === null) { + return; + } + + if ($this->cloudflare_id) { + Http::cloudflare()->put("zones/{$this->domain->cloudflare_id}/dns_records/{$this->cloudflare_id}", $payload); + } else { + $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $payload)->json(); + + if (!empty($response['success'])) { + $dnsRecord = $response['result']; + + $this->updateQuietly([ + 'cloudflare_id' => $dnsRecord['id'], + ]); + } + } + + return; + } else { + $body = [ 'name' => $this->name, - 'ttl' => 120, + 'ttl' => 1, 'type' => $this->record_type, 'comment' => 'Created by Pelican Subdomains plugin', 'content' => $this->server->allocation->ip, 'proxied' => false, - ]); + ]; + + if ($this->cloudflare_id) { + Http::cloudflare()->put("zones/{$this->domain->cloudflare_id}/dns_records/{$this->cloudflare_id}", $body); + } else { + $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $body)->json(); + + if (!empty($response['success'])) { + $dnsRecord = $response['result']; + + $this->updateQuietly([ + 'cloudflare_id' => $dnsRecord['id'], + ]); + } + } } + } protected function deleteOnCloudflare(): void From f20e58eb3ac8162632c097637fc8a75229a16734 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 13:53:09 +1100 Subject: [PATCH 06/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 511bd80..b81a42a 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -20,6 +20,17 @@ 'srv_target' => 'SRV Target', 'srv_target_help' => 'The hostname that SRV records point to (for example: play.example.com).', + 'errors' => [ + 'srv_target_missing' => 'Cannot enable SRV record because the selected domain does not have an SRV target set.', + ], + 'api_token' => 'Cloudflare API Token', 'api_token_help' => 'The token needs to have read permissions for Zone.Zone and write for Zone.Dns. For better security you can also set the "Zone Resources" to exclude certain domains and add the panel ip to the "Client IP Adress Filtering".', + + 'notifications' => [ + 'dns_created' => 'DNS record created on Cloudflare', + 'dns_updated' => 'DNS record updated on Cloudflare', + 'dns_deleted' => 'DNS record deleted from Cloudflare', + 'dns_action_failed' => 'Cloudflare DNS action failed', + ], ]; diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index 992fedc..b7f4313 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -82,7 +82,8 @@ public static function table(Table $table): Table ->state(fn (Subdomain $subdomain) => $subdomain->getLabel()), ToggleColumn::make('srv_record') ->label(trans('subdomains::strings.srv_record')) - ->tooltip(trans('subdomains::strings.srv_record_help')), + ->tooltip(fn (Subdomain $record) => $record->domain && $record->domain->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.errors.srv_target_missing')) + ->disabled(fn (Subdomain $record) => !$record->domain || empty($record->domain->srv_target)), ]) ->recordActions([ EditAction::make(), @@ -119,6 +120,7 @@ public static function form(Schema $schema): Schema Toggle::make('srv_record') ->label(trans('subdomains::strings.srv_record')) ->helperText(trans('subdomains::strings.srv_record_help')) + ->disabled(fn (callable $get) => !$get('domain_id') || empty(CloudflareDomain::find($get('domain_id'))->srv_target ?? null)) ->default(false), ]); } diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index a85f92f..b0ffe38 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -118,6 +118,8 @@ protected function buildSrvPayload(): ?array $port = $this->server && $this->server->allocation ? ($this->server->allocation->port ?? null) : null; if (empty($target) || empty($port)) { + // Output to log for debugging + Log::error('SRV record target or port is missing for Subdomain ID ' . $this->id . '. Target: ' . ($target ?? 'null') . ', Port: ' . ($port ?? 'null')); return null; } From 58de843308a8051078b6045aacf77fd610b77488 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 20:08:49 +1100 Subject: [PATCH 07/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 b81a42a..3bc8625 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -32,5 +32,7 @@ 'dns_updated' => 'DNS record updated on Cloudflare', 'dns_deleted' => 'DNS record deleted from Cloudflare', 'dns_action_failed' => 'Cloudflare DNS action failed', + 'zone_request_failed' => 'Cloudflare zone request failed', + 'zone_request_succeeded' => 'Cloudflare zone request succeeded', ], ]; diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index b0ffe38..05a5ff9 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -118,7 +118,6 @@ protected function buildSrvPayload(): ?array $port = $this->server && $this->server->allocation ? ($this->server->allocation->port ?? null) : null; if (empty($target) || empty($port)) { - // Output to log for debugging Log::error('SRV record target or port is missing for Subdomain ID ' . $this->id . '. Target: ' . ($target ?? 'null') . ', Port: ' . ($port ?? 'null')); return null; } From 2ff7a9956ac1d7e2bfe9d9705eaf088681bb7dc9 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 22:39:59 +1100 Subject: [PATCH 08/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 0000000..6b73b24 --- /dev/null +++ b/subdomains/src/Services/CloudflareService.php @@ -0,0 +1,187 @@ +get("zones/{$zoneId}/dns_records", [ + 'name' => $name, + 'type' => $type, + 'per_page' => 1, + ]); + } catch (\Throwable $e) { + Log::error('Cloudflare findDnsRecordId request failed: ' . $e->getMessage(), ['zone' => $zoneId, 'name' => $name, 'type' => $type]); + return null; + } + + $status = $response->status(); + $body = $response->json() ?? []; + + if ($response->successful() && !empty($body['result']) && count($body['result']) > 0) { + return $body['result'][0]['id'] ?? null; + } + + if (!empty($body['errors'])) { + Log::warning('Cloudflare findDnsRecordId returned errors', ['zone' => $zoneId, 'name' => $name, 'type' => $type, 'status' => $status, 'errors' => $body['errors']]); + } + + return null; + } + + public function upsertDnsRecord( + string $zoneId, + string $name, + string $recordType, + string $target = null, + ?int $port = null, + ): array + { + if (empty($zoneId) || empty($name) || empty($recordType)) { + Log::error('Cloudflare upsertDnsRecord missing required parameters', ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); + return ['success' => false, 'id' => null, 'errors' => ['missing_parameters' => true], 'status' => 0, 'body' => null]; + } + + // Hardcoded/derived defaults + $priority = 0; + $weight = 0; + $ttl = 1; + $comment = 'Created by Pelican Subdomains plugin'; + $proxied = false; + + // Build payload based on type + if ($recordType === 'SRV') { + if (empty($port) || empty($target)) { + Log::error('Cloudflare upsert missing SRV target or port', ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); + return ['success' => false, 'id' => null, 'errors' => ['missing_srv_target_or_port' => true], 'status' => 0, 'body' => null]; + } + + $payload = [ + 'name' => sprintf('_minecraft._tcp.%s', $name), + 'ttl' => $ttl, + 'type' => 'SRV', + 'comment' => $comment, + 'content' => sprintf('%d %d %d %s', $priority, $weight, $port, $target), + 'proxied' => $proxied, + 'data' => [ + 'priority' => $priority, + 'weight' => $weight, + 'port' => (int) $port, + 'target' => $target, + ], + ]; + } else { + $ip = $content ?? $target; + + if (empty($ip)) { + Log::error('Cloudflare upsert missing IP/content for record type ' . $recordType, ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); + return ['success' => false, 'id' => null, 'errors' => ['missing_ip' => true], 'status' => 0, 'body' => null]; + } + + $payload = [ + 'name' => $name, + 'ttl' => $ttl, + 'type' => $recordType, + 'comment' => $comment, + 'content' => $target, + 'proxied' => $proxied, + ]; + } + + $existingId = $this->findDnsRecordId($zoneId, $payload['name'], $payload['type']); + + try { + if ($existingId) { + $response = Http::cloudflare()->put("zones/{$zoneId}/dns_records/{$existingId}", $payload); + $parsed = $this->parseCloudflareHttpResponse($response); + + if ($parsed['success']) { + return $parsed; + } + + Log::error('Cloudflare update failed', ['zone' => $zoneId, 'id' => $existingId, 'response' => $parsed]); + return $parsed; + } + + $response = Http::cloudflare()->post("zones/{$zoneId}/dns_records", $payload); + $parsed = $this->parseCloudflareHttpResponse($response); + + if ($parsed['success'] && !empty($parsed['id'])) { + return $parsed; + } + + Log::error('Cloudflare create failed', ['zone' => $zoneId, 'payload' => $payload, 'response' => $parsed]); + return $parsed; + } catch (\Throwable $e) { + Log::error('Cloudflare upsert exception: ' . $e->getMessage(), ['zone' => $zoneId, 'payload' => $payload]); + return ['success' => false, 'id' => null, 'errors' => ['exception' => $e->getMessage()], 'status' => 0, 'body' => null]; + } + } + + + protected function parseCloudflareResponse(array $response): array + { + return [ + 'success' => !empty($response['success']), + 'id' => $response['result']['id'] ?? null, + 'result' => $response['result'] ?? null, + 'errors' => $response['errors'] ?? [], + ]; + } + + /** + * Parse a Response object into a normalized structure with HTTP status and body. + */ + protected function parseCloudflareHttpResponse(Response $response): array + { + $status = $response->status(); + $body = $response->json() ?? []; + + $success = $response->successful() && (!empty($body['success']) || !empty($body['result'])); + + return [ + 'success' => $success, + 'id' => $body['result']['id'] ?? null, + 'errors' => $body['errors'] ?? [], + 'status' => $status, + 'body' => $body, + ]; + } + + + /** + * Delete a DNS record by id. Returns a detailed response array. + */ + public function deleteDnsRecord(string $zoneId, string $recordId): array + { + if (empty($zoneId) || empty($recordId)) { + return ['success' => false, 'errors' => ['missing_parameters' => true], 'status' => 0, 'body' => null]; + } + + try { + $response = Http::cloudflare()->delete("zones/{$zoneId}/dns_records/{$recordId}"); + + $parsed = $this->parseCloudflareHttpResponse($response); + + if ($parsed['success']) { + return $parsed; + } + + Log::error('Cloudflare delete failed', ['zone' => $zoneId, 'id' => $recordId, 'response' => $parsed]); + return $parsed; + } catch (\Throwable $e) { + Log::error('Cloudflare delete exception: ' . $e->getMessage(), ['zone' => $zoneId, 'id' => $recordId]); + return ['success' => false, 'errors' => ['exception' => $e->getMessage()], 'status' => 0, 'body' => null]; + } + } +} From 15721872d7477e2bb85de50d94ca30417a43887c Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 22:49:38 +1100 Subject: [PATCH 09/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 6b73b24..15b0307 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -43,7 +43,7 @@ public function upsertDnsRecord( string $zoneId, string $name, string $recordType, - string $target = null, + string $target, ?int $port = null, ): array { @@ -139,9 +139,6 @@ protected function parseCloudflareResponse(array $response): array ]; } - /** - * Parse a Response object into a normalized structure with HTTP status and body. - */ protected function parseCloudflareHttpResponse(Response $response): array { $status = $response->status(); @@ -159,9 +156,6 @@ protected function parseCloudflareHttpResponse(Response $response): array } - /** - * Delete a DNS record by id. Returns a detailed response array. - */ public function deleteDnsRecord(string $zoneId, string $recordId): array { if (empty($zoneId) || empty($recordId)) { From eb1a0ebbb28b5faf6b34793e99350a08eedb7be7 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 22:50:18 +1100 Subject: [PATCH 10/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 05a5ff9..514dc0c 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -8,6 +8,9 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Log; +use Filament\Notifications\Notification; +use Boy132\Subdomains\Services\CloudflareService; /** * @property int $id @@ -66,12 +69,8 @@ protected static function boot(): void } }); - static::created(function (self $model) { - $model->createOnCloudflare(); - }); - - static::updated(function (self $model) { - $model->updateOnCloudflare(); + static::saved(function (self $model) { + $model->upsertOnCloudflare(); }); static::deleted(function (self $model) { @@ -99,6 +98,8 @@ public function getSrvRecordAttribute(): bool return $this->record_type === 'SRV'; } + + public function setSrvRecordAttribute($value): void { if ($value) { @@ -112,148 +113,111 @@ public function setSrvRecordAttribute($value): void } } - protected function buildSrvPayload(): ?array + + + protected function upsertOnCloudflare(): void { - $target = $this->domain->srv_target ?? null; - $port = $this->server && $this->server->allocation ? ($this->server->allocation->port ?? null) : null; + $service = app(CloudflareService::class); - if (empty($target) || empty($port)) { - Log::error('SRV record target or port is missing for Subdomain ID ' . $this->id . '. Target: ' . ($target ?? 'null') . ', Port: ' . ($port ?? 'null')); - return null; - } + $zoneId = $this->domain->cloudflare_id; + if (empty($zoneId)) { + Log::warning('Cloudflare zone id missing for domain', ['domain_id' => $this->domain_id]); - $priority = (int) ($this->srv_priority ?? 0); - $weight = (int) ($this->srv_weight ?? 0); - $port = (int) $port; - - // Need to build the name to include the services and protocol parts for SRV records, this may vary based on game(egg tag)/server type - // Temporrary placeholder for service and protocol = '_minecraft._tcp.' - - return [ - 'name' => sprintf('_minecraft._tcp.%s', $this->name), - 'ttl' => 1, - 'type' => 'SRV', - 'comment' => 'Created by Pelican Subdomains plugin', - 'content' => sprintf('%d %d %d %s', $priority, $weight, $port, $target), - 'proxied' => false, - 'data' => [ - 'priority' => $priority, - 'weight' => $weight, - 'port' => $port, - 'target' => $target, - ], - ]; - } + Notification::make() + ->danger() + ->title('Cloudflare: Missing Zone ID') + ->body(sprintf('Cloudflare zone ID is not configured for %s. Cannot upsert DNS record for %s.%s.', $this->domain->name ?? 'unknown', $this->name, $this->domain->name ?? 'unknown')) + ->send(); - protected function createOnCloudflare(): void - { + return; + } + + // SRV: target comes from domain, port from server allocation if ($this->record_type === 'SRV') { - $payload = $this->buildSrvPayload(); + $port = $this->server && $this->server->allocation ? ($this->server->allocation->port ?? null) : null; + + if (empty($port)) { + Log::warning('Server missing allocation with port', ['server_id' => $this->server_id]); + + Notification::make() + ->danger() + ->title('Cloudflare: Missing SRV Port') + ->body(sprintf('SRV target or port is missing for %s.%s. Cannot upsert SRV record.', $this->name, $this->domain->name ?? 'unknown')) + ->send(); - if ($payload === null) { return; } - if (!$this->cloudflare_id) { - $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $payload)->json(); - - if (!empty($response['success'])) { - $dnsRecord = $response['result']; + $result = $service->upsertDnsRecord($zoneId, $this->name, 'SRV', $this->domain->srv_target, $port); - $this->updateQuietly([ - 'cloudflare_id' => $dnsRecord['id'], - ]); + if ($result['success'] && !empty($result['id'])) { + if ($this->cloudflare_id !== $result['id']) { + $this->updateQuietly(['cloudflare_id' => $result['id']]); } + } else { + Log::error('Failed to upsert SRV record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); + + Notification::make() + ->danger() + ->title('Cloudflare: SRV upsert failed') + ->body('Failed to upsert SRV record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->send(); } return; } + // A/AAAA if (!$this->server->allocation || $this->server->allocation->ip === '0.0.0.0' || $this->server->allocation->ip === '::') { return; } - if (!$this->cloudflare_id) { - $body = [ - 'name' => $this->name, - 'ttl' => 1, - 'type' => $this->record_type, - 'comment' => 'Created by Pelican Subdomains plugin', - 'content' => $this->server->allocation->ip, - 'proxied' => false, - ]; + $result = $service->upsertDnsRecord($zoneId, $this->name, $this->record_type, $ip, null, null, $this->domain->name); - $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $body)->json(); + if ($result['success'] && !empty($result['id'])) { + if ($this->cloudflare_id !== $result['id']) { + $this->updateQuietly(['cloudflare_id' => $result['id']]); + } + } else { + Log::error('Failed to upsert record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); - if ($response['success']) { - $dnsRecord = $response['result']; + $domainName = $this->domain->name ?? 'unknown'; + $sub = sprintf('%s.%s', $this->name, $domainName); - $this->updateQuietly([ - 'cloudflare_id' => $dnsRecord['id'], - ]); - } + Notification::make() + ->danger() + ->title('Cloudflare: Upsert failed') + ->body('Failed to upsert record for ' . $sub . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->send(); } } - protected function updateOnCloudflare(): void - { - if (!$this->server->allocation || $this->server->allocation->ip === '0.0.0.0' || $this->server->allocation->ip === '::') { - return; - } - - if ($this->record_type === 'SRV') { - $payload = $this->buildSrvPayload(); - if ($payload === null) { - return; - } - if ($this->cloudflare_id) { - Http::cloudflare()->put("zones/{$this->domain->cloudflare_id}/dns_records/{$this->cloudflare_id}", $payload); - } else { - $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $payload)->json(); - - if (!empty($response['success'])) { - $dnsRecord = $response['result']; + protected function deleteOnCloudflare(): void + { + if ($this->cloudflare_id && $this->domain && $this->domain->cloudflare_id) { + $service = app(CloudflareService::class); - $this->updateQuietly([ - 'cloudflare_id' => $dnsRecord['id'], - ]); - } - } + $result = $service->deleteDnsRecord($this->domain->cloudflare_id, $this->cloudflare_id); - return; - } else { - $body = [ - 'name' => $this->name, - 'ttl' => 1, - 'type' => $this->record_type, - 'comment' => 'Created by Pelican Subdomains plugin', - 'content' => $this->server->allocation->ip, - 'proxied' => false, - ]; - - if ($this->cloudflare_id) { - Http::cloudflare()->put("zones/{$this->domain->cloudflare_id}/dns_records/{$this->cloudflare_id}", $body); + if (!empty($result['success'])) { + Log::info('Deleted Cloudflare record for Subdomain ID ' . $this->id, ['result' => $result]); } else { - $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $body)->json(); - - if (!empty($response['success'])) { - $dnsRecord = $response['result']; - - $this->updateQuietly([ - 'cloudflare_id' => $dnsRecord['id'], - ]); - } + Log::warning('Failed to delete Cloudflare record for Subdomain ID ' . $this->id, ['result' => $result]); + + Notification::make() + ->danger() + ->title('Cloudflare: Delete failed') + ->body('Failed to delete Cloudflare record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->send(); + + Notification::make() + ->danger() + ->title('Cloudflare: Upsert failed') + ->body('Failed to upsert record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->send(); } } - - } - - protected function deleteOnCloudflare(): void - { - if ($this->cloudflare_id) { - Http::cloudflare()->delete("zones/{$this->domain->cloudflare_id}/dns_records/{$this->cloudflare_id}"); - } } } From 6951bc70c44ce21109d11bf73143f7a6661eb5e1 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 08:11:36 +1100 Subject: [PATCH 11/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 238e6fe..7a0dbf4 100644 --- a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/Pages/ManageCloudflareDomains.php +++ b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/Pages/ManageCloudflareDomains.php @@ -14,7 +14,8 @@ protected function getHeaderActions(): array { return [ CreateAction::make() - ->createAnother(false), + ->createAnother(false) + ->successNotification(null), ]; } } diff --git a/subdomains/src/Models/CloudflareDomain.php b/subdomains/src/Models/CloudflareDomain.php index c510959..134e417 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -5,6 +5,8 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Facades\Http; +use Boy132\Subdomains\Services\CloudflareService; +use Filament\Notifications\Notification; /** * @property int $id @@ -25,7 +27,24 @@ protected static function boot(): void parent::boot(); static::created(function (self $model) { - $model->fetchCloudflareId(); + $service = new CloudflareService(); + + $zoneId = $service->getZoneId($model->name); + if (!$zoneId) { + Notification::make() + ->title('Failed to fetch Cloudflare Zone ID for domain: ' . $model->name) + ->danger() + ->send(); + } + + Notification::make() + ->title('Successfully saved domain: ' . $model->name) + ->success() + ->send(); + + $model->update([ + 'cloudflare_id' => $zoneId, + ]); }); } @@ -33,21 +52,4 @@ public function subdomains(): HasMany { return $this->hasMany(Subdomain::class, 'domain_id'); } - - public function fetchCloudflareId(): void - { - $response = Http::cloudflare()->get('zones', [ - 'name' => $this->name, - ])->json(); - - if ($response['success']) { - $zones = $response['result']; - - if (count($zones) > 0) { - $this->update([ - 'cloudflare_id' => $zones[0]['id'], - ]); - } - } - } } diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index 15b0307..0bfa7d9 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -8,23 +8,57 @@ class CloudflareService { - public function findDnsRecordId(string $zoneId, string $name, string $type): ?string + public function getZoneId(string $domainName): ?string { - if (empty($zoneId)) { + if (empty($domainName)) { + Log::error('Cloudflare getZoneId called with empty domain name', ['domain' => $domainName]); return null; } try { - $response = Http::cloudflare()->get("zones/{$zoneId}/dns_records", [ + $response = Http::cloudflare()->get('zones', [ + 'name' => $domainName, + ]); + } catch (\Throwable $e) { + Log::error('Cloudflare getZoneId request failed: ' . $e->getMessage(), ['domain' => $domainName]); + return null; + } + + $status = $response->status(); + $body = $response->json() ?? []; + + if ($response->successful() && !empty($body['result']) && count($body['result']) > 0) { + return $body['result'][0]['id'] ?? null; + } + + if (!empty($body['errors'])) { + Log::warning('Cloudflare getZoneId returned errors', ['domain' => $domainName, 'status' => $status, 'errors' => $body['errors']]); + } + + return null; + } + + public function getDnsRecordId(string $zoneId, string $name, string $type): ?string + { + if (empty($zoneId) || empty($name)) { + return null; + } + + try { + $queryParams = [ 'name' => $name, 'type' => $type, 'per_page' => 1, - ]); + ]; + + $response = Http::cloudflare()->get("zones/{$zoneId}/dns_records"); } catch (\Throwable $e) { - Log::error('Cloudflare findDnsRecordId request failed: ' . $e->getMessage(), ['zone' => $zoneId, 'name' => $name, 'type' => $type]); + Log::error('Cloudflare getDnsRecordId request failed: ' . $e->getMessage(), ['zone' => $zoneId, 'name' => $name, 'type' => $type]); return null; } + log::warning('record response: '.$response); // !remove + log::warning($payload); // !remove $status = $response->status(); $body = $response->json() ?? []; @@ -33,7 +67,7 @@ public function findDnsRecordId(string $zoneId, string $name, string $type): ?st } if (!empty($body['errors'])) { - Log::warning('Cloudflare findDnsRecordId returned errors', ['zone' => $zoneId, 'name' => $name, 'type' => $type, 'status' => $status, 'errors' => $body['errors']]); + Log::warning('Cloudflare getDnsRecordId returned errors', ['zone' => $zoneId, 'name' => $name, 'type' => $type, 'status' => $status, 'errors' => $body['errors']]); } return null; From f18101a755d2068dc2f82ba4ae7268e153118a7f Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 08:40:41 +1100 Subject: [PATCH 12/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 514dc0c..01aaf4e 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -148,7 +148,7 @@ protected function upsertOnCloudflare(): void return; } - $result = $service->upsertDnsRecord($zoneId, $this->name, 'SRV', $this->domain->srv_target, $port); + $result = $service->upsertDnsRecord($zoneId, $this->name, 'SRV', $this->domain->srv_target, $this->cloudflare_id ?? null, $port); if ($result['success'] && !empty($result['id'])) { if ($this->cloudflare_id !== $result['id']) { @@ -172,7 +172,7 @@ protected function upsertOnCloudflare(): void return; } - $result = $service->upsertDnsRecord($zoneId, $this->name, $this->record_type, $ip, null, null, $this->domain->name); + $result = $service->upsertDnsRecord($zoneId, $this->name, $this->record_type, $this->server->allocation->ip, $this->cloudflare_id ?? null, null); if ($result['success'] && !empty($result['id'])) { if ($this->cloudflare_id !== $result['id']) { diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index 0bfa7d9..9fb0b99 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -38,46 +38,12 @@ public function getZoneId(string $domainName): ?string return null; } - public function getDnsRecordId(string $zoneId, string $name, string $type): ?string - { - if (empty($zoneId) || empty($name)) { - return null; - } - - try { - $queryParams = [ - 'name' => $name, - 'type' => $type, - 'per_page' => 1, - ]; - - $response = Http::cloudflare()->get("zones/{$zoneId}/dns_records"); - } catch (\Throwable $e) { - Log::error('Cloudflare getDnsRecordId request failed: ' . $e->getMessage(), ['zone' => $zoneId, 'name' => $name, 'type' => $type]); - return null; - } - - log::warning('record response: '.$response); // !remove - log::warning($payload); // !remove - $status = $response->status(); - $body = $response->json() ?? []; - - if ($response->successful() && !empty($body['result']) && count($body['result']) > 0) { - return $body['result'][0]['id'] ?? null; - } - - if (!empty($body['errors'])) { - Log::warning('Cloudflare getDnsRecordId returned errors', ['zone' => $zoneId, 'name' => $name, 'type' => $type, 'status' => $status, 'errors' => $body['errors']]); - } - - return null; - } - public function upsertDnsRecord( string $zoneId, string $name, string $recordType, string $target, + ?string $recordId = null, ?int $port = null, ): array { @@ -132,18 +98,16 @@ public function upsertDnsRecord( ]; } - $existingId = $this->findDnsRecordId($zoneId, $payload['name'], $payload['type']); - try { - if ($existingId) { - $response = Http::cloudflare()->put("zones/{$zoneId}/dns_records/{$existingId}", $payload); + if ($recordId) { + $response = Http::cloudflare()->put("zones/{$zoneId}/dns_records/{$recordId}", $payload); $parsed = $this->parseCloudflareHttpResponse($response); if ($parsed['success']) { return $parsed; } - Log::error('Cloudflare update failed', ['zone' => $zoneId, 'id' => $existingId, 'response' => $parsed]); + Log::error('Cloudflare update failed', ['zone' => $zoneId, 'recordId' => $recordId, 'response' => $parsed]); return $parsed; } From 58f73e170f629d206d582f0a33961dd20ba618ad Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 08:41:19 +1100 Subject: [PATCH 13/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 01aaf4e..9b1342a 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -42,38 +42,15 @@ protected static function boot(): void parent::boot(); static::saving(function (self $model) { - // If srv_record provided in the payload, ensure record_type follows it and then remove it if (array_key_exists('srv_record', $model->attributes)) { - $srv = (bool) $model->attributes['srv_record']; - - if ($srv) { - $model->attributes['record_type'] = 'SRV'; - } else { - if ($model->server && $model->server->allocation && is_ipv6($model->server->allocation->ip)) { - $model->attributes['record_type'] = 'AAAA'; - } else { - $model->attributes['record_type'] = 'A'; - } - } - + $model->setRecordType($model->attributes['srv_record']); unset($model->attributes['srv_record']); } - // If no record_type is present, set a sensible default based on server allocation - if (!isset($model->attributes['record_type'])) { - if ($model->server && $model->server->allocation && is_ipv6($model->server->allocation->ip)) { - $model->attributes['record_type'] = 'AAAA'; - } else { - $model->attributes['record_type'] = 'A'; - } - } - }); - - static::saved(function (self $model) { $model->upsertOnCloudflare(); }); - static::deleted(function (self $model) { + static::deleting(function (self $model) { $model->deleteOnCloudflare(); }); } @@ -98,9 +75,7 @@ public function getSrvRecordAttribute(): bool return $this->record_type === 'SRV'; } - - - public function setSrvRecordAttribute($value): void + public function setRecordType($value): void { if ($value) { $this->attributes['record_type'] = 'SRV'; diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index 9fb0b99..f683714 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -81,13 +81,6 @@ public function upsertDnsRecord( ], ]; } else { - $ip = $content ?? $target; - - if (empty($ip)) { - Log::error('Cloudflare upsert missing IP/content for record type ' . $recordType, ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); - return ['success' => false, 'id' => null, 'errors' => ['missing_ip' => true], 'status' => 0, 'body' => null]; - } - $payload = [ 'name' => $name, 'ttl' => $ttl, @@ -126,17 +119,6 @@ public function upsertDnsRecord( } } - - protected function parseCloudflareResponse(array $response): array - { - return [ - 'success' => !empty($response['success']), - 'id' => $response['result']['id'] ?? null, - 'result' => $response['result'] ?? null, - 'errors' => $response['errors'] ?? [], - ]; - } - protected function parseCloudflareHttpResponse(Response $response): array { $status = $response->status(); From 89f45b8338ce9a6e48de17d8c1074d2d1410b7dd Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 08:41:32 +1100 Subject: [PATCH 14/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 9b1342a..3980389 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -129,6 +129,12 @@ protected function upsertOnCloudflare(): void if ($this->cloudflare_id !== $result['id']) { $this->updateQuietly(['cloudflare_id' => $result['id']]); } + + Notification::make() + ->success() + ->title('Cloudflare: Record updated') + ->body('Successfully updated ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . ' to '. $this->record_type) + ->send(); } else { Log::error('Failed to upsert SRV record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); @@ -144,6 +150,14 @@ protected function upsertOnCloudflare(): void // A/AAAA if (!$this->server->allocation || $this->server->allocation->ip === '0.0.0.0' || $this->server->allocation->ip === '::') { + Log::warning('Server allocation missing or invalid IP', ['server_id' => $this->server_id]); + + Notification::make() + ->danger() + ->title('Cloudflare: Missing IP') + ->body(sprintf('Server allocation IP is missing or invalid for %s.%s. Cannot upsert A/AAAA record.', $this->name, $this->domain->name ?? 'unknown')) + ->send(); + return; } @@ -153,6 +167,12 @@ protected function upsertOnCloudflare(): void if ($this->cloudflare_id !== $result['id']) { $this->updateQuietly(['cloudflare_id' => $result['id']]); } + + Notification::make() + ->success() + ->title('Cloudflare: Record updated') + ->body('Successfully updated ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . ' to '. $this->record_type) + ->send(); } else { Log::error('Failed to upsert record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); From d369cc3039027f649507bdfcc2f42caac2507949 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 09:57:36 +1100 Subject: [PATCH 15/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 3980389..a78125f 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -46,11 +46,13 @@ protected static function boot(): void $model->setRecordType($model->attributes['srv_record']); unset($model->attributes['srv_record']); } + }); + static::saved(function (self $model) { $model->upsertOnCloudflare(); }); - static::deleting(function (self $model) { + static::deleted(function (self $model) { $model->deleteOnCloudflare(); }); } @@ -88,8 +90,6 @@ public function setRecordType($value): void } } - - protected function upsertOnCloudflare(): void { $service = app(CloudflareService::class); @@ -112,7 +112,7 @@ protected function upsertOnCloudflare(): void $port = $this->server && $this->server->allocation ? ($this->server->allocation->port ?? null) : null; if (empty($port)) { - Log::warning('Server missing allocation with port', ['server_id' => $this->server_id]); + Log::warning('Server missing allocation with port', $this->toArray()); Notification::make() ->danger() @@ -149,7 +149,7 @@ protected function upsertOnCloudflare(): void } // A/AAAA - if (!$this->server->allocation || $this->server->allocation->ip === '0.0.0.0' || $this->server->allocation->ip === '::') { + if (!$this->server || !$this->server->allocation || $this->server->allocation->ip === '0.0.0.0' || $this->server->allocation->ip === '::') { Log::warning('Server allocation missing or invalid IP', ['server_id' => $this->server_id]); Notification::make() @@ -187,8 +187,6 @@ protected function upsertOnCloudflare(): void } } - - protected function deleteOnCloudflare(): void { if ($this->cloudflare_id && $this->domain && $this->domain->cloudflare_id) { @@ -197,22 +195,18 @@ protected function deleteOnCloudflare(): void $result = $service->deleteDnsRecord($this->domain->cloudflare_id, $this->cloudflare_id); if (!empty($result['success'])) { - Log::info('Deleted Cloudflare record for Subdomain ID ' . $this->id, ['result' => $result]); - } else { - Log::warning('Failed to delete Cloudflare record for Subdomain ID ' . $this->id, ['result' => $result]); - Notification::make() ->danger() - ->title('Cloudflare: Delete failed') - ->body('Failed to delete Cloudflare record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->title('Cloudflare: Delete successed') + ->body('Successfully deleted Cloudflare record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '.') ->send(); + } Notification::make() ->danger() - ->title('Cloudflare: Upsert failed') - ->body('Failed to upsert record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->title('Cloudflare: Delete failed') + ->body('Failed to delete Cloudflare record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) ->send(); - } } } } From 7d1b33a62916c2a9c248129d4e1caec697064d71 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 09:57:55 +1100 Subject: [PATCH 16/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 537fa05..ab5c570 100644 --- a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php +++ b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php @@ -9,7 +9,6 @@ use Filament\Actions\CreateAction; use Filament\Actions\DeleteAction; use Filament\Actions\EditAction; -use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; From 910b812cd1bc86d08275042503a79d2ed3c777b7 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 09:59:31 +1100 Subject: [PATCH 17/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 b7f4313..bc3ba9c 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -12,7 +12,6 @@ use Filament\Actions\DeleteAction; use Filament\Actions\EditAction; use Filament\Facades\Filament; -use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; @@ -87,7 +86,8 @@ public static function table(Table $table): Table ]) ->recordActions([ EditAction::make(), - DeleteAction::make(), + DeleteAction::make() + ->successNotification(null), ]) ->toolbarActions([ CreateAction::make() @@ -98,6 +98,7 @@ public static function table(Table $table): Table ->createAnother(false) ->hiddenLabel() ->iconButton() + ->successNotification(null) ->iconSize(IconSize::ExtraLarge), ]); } @@ -116,11 +117,12 @@ public static function form(Schema $schema): Schema ->required() ->relationship('domain', 'name') ->preload() + ->default(fn () => CloudflareDomain::first()?->id ?? null) ->searchable(), Toggle::make('srv_record') ->label(trans('subdomains::strings.srv_record')) ->helperText(trans('subdomains::strings.srv_record_help')) - ->disabled(fn (callable $get) => !$get('domain_id') || empty(CloudflareDomain::find($get('domain_id'))->srv_target ?? null)) + ->disabled(false) // TODO: Dynamically disable if domain missing srv_target ->default(false), ]); } From b42370d97398c0152ccb81ef2b55d0e7706d2bc6 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:00:34 +1100 Subject: [PATCH 18/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 134e417..4602745 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -2,11 +2,10 @@ namespace Boy132\Subdomains\Models; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Support\Facades\Http; use Boy132\Subdomains\Services\CloudflareService; use Filament\Notifications\Notification; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; /** * @property int $id @@ -35,16 +34,16 @@ protected static function boot(): void ->title('Failed to fetch Cloudflare Zone ID for domain: ' . $model->name) ->danger() ->send(); - } - - Notification::make() - ->title('Successfully saved domain: ' . $model->name) - ->success() - ->send(); + } else { + Notification::make() + ->title('Successfully saved domain: ' . $model->name) + ->success() + ->send(); - $model->update([ - 'cloudflare_id' => $zoneId, - ]); + $model->update([ + 'cloudflare_id' => $zoneId, + ]); + } }); } diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index f683714..6832418 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -135,7 +135,6 @@ protected function parseCloudflareHttpResponse(Response $response): array ]; } - public function deleteDnsRecord(string $zoneId, string $recordId): array { if (empty($zoneId) || empty($recordId)) { From ea4e6023d91f9a9675971c4b2c7f5c4798cf258f Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:08:43 +1100 Subject: [PATCH 19/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 a78125f..e598a45 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -3,14 +3,13 @@ namespace Boy132\Subdomains\Models; use App\Models\Server; +use Boy132\Subdomains\Services\CloudflareService; +use Filament\Notifications\Notification; use Filament\Support\Contracts\HasLabel; use Illuminate\Contracts\Support\Htmlable; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; -use Filament\Notifications\Notification; -use Boy132\Subdomains\Services\CloudflareService; /** * @property int $id diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index 6832418..dcbd5dc 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -2,9 +2,9 @@ namespace Boy132\Subdomains\Services; +use Illuminate\Http\Client\Response; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; -use Illuminate\Http\Client\Response; class CloudflareService { From 66d4384507275664b1060f9631adfae683bc237b Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:22:24 +1100 Subject: [PATCH 20/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 3bc8625..796ef63 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -34,5 +34,33 @@ 'dns_action_failed' => 'Cloudflare DNS action failed', 'zone_request_failed' => 'Cloudflare zone request failed', 'zone_request_succeeded' => 'Cloudflare zone request succeeded', + + 'cloudflare_missing_zone_title' => 'Cloudflare: Missing Zone ID', + 'cloudflare_missing_zone' => 'Cloudflare zone ID is not configured for :domain. Cannot upsert DNS record for :subdomain.', + + 'cloudflare_missing_srv_port_title' => 'Cloudflare: Missing SRV Port', + 'cloudflare_missing_srv_port' => 'SRV target or port is missing for :subdomain. Cannot upsert SRV record.', + + 'cloudflare_record_updated_title' => 'Cloudflare: Record updated', + 'cloudflare_record_updated' => 'Successfully updated :subdomain to :record_type', + + 'cloudflare_srv_upsert_failed_title' => 'Cloudflare: SRV upsert failed', + 'cloudflare_srv_upsert_failed' => 'Failed to upsert SRV record for :subdomain. See logs for details. Errors: :errors', + + 'cloudflare_missing_ip_title' => 'Cloudflare: Missing IP', + 'cloudflare_missing_ip' => 'Server allocation IP is missing or invalid for :subdomain. Cannot upsert A/AAAA record.', + + 'cloudflare_upsert_failed_title' => 'Cloudflare: Upsert failed', + 'cloudflare_upsert_failed' => 'Failed to upsert record for :subdomain. See logs for details. Errors: :errors', + + 'cloudflare_delete_success_title' => 'Cloudflare: Record deleted', + 'cloudflare_delete_success' => 'Successfully deleted Cloudflare record for :subdomain.', + + 'cloudflare_delete_failed_title' => 'Cloudflare: Delete failed', + 'cloudflare_delete_failed' => 'Failed to delete Cloudflare record for :subdomain. See logs for details. Errors: :errors', + + 'cloudflare_zone_fetch_failed' => 'Failed to fetch Cloudflare Zone ID for domain: :domain', + 'cloudflare_domain_saved' => 'Successfully saved domain: :domain', + 'settings_saved' => 'Settings saved', ], ]; diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index bc3ba9c..3b8c25f 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -81,11 +81,12 @@ public static function table(Table $table): Table ->state(fn (Subdomain $subdomain) => $subdomain->getLabel()), ToggleColumn::make('srv_record') ->label(trans('subdomains::strings.srv_record')) - ->tooltip(fn (Subdomain $record) => $record->domain && $record->domain->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.errors.srv_target_missing')) + ->tooltip(fn (Subdomain $record) => $record->domain && $record->domain->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.srv_target_missing')) ->disabled(fn (Subdomain $record) => !$record->domain || empty($record->domain->srv_target)), ]) ->recordActions([ EditAction::make(), + ->successNotification(null), DeleteAction::make() ->successNotification(null), ]) diff --git a/subdomains/src/Models/CloudflareDomain.php b/subdomains/src/Models/CloudflareDomain.php index 4602745..1dbd87c 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -31,12 +31,12 @@ protected static function boot(): void $zoneId = $service->getZoneId($model->name); if (!$zoneId) { Notification::make() - ->title('Failed to fetch Cloudflare Zone ID for domain: ' . $model->name) + ->title(trans('subdomains::strings.notifications.cloudflare_zone_fetch_failed', ['domain' => $model->name])) ->danger() ->send(); } else { Notification::make() - ->title('Successfully saved domain: ' . $model->name) + ->title(trans('subdomains::strings.notifications.cloudflare_domain_saved', ['domain' => $model->name])) ->success() ->send(); diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index e598a45..53f21d6 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -99,8 +99,8 @@ protected function upsertOnCloudflare(): void Notification::make() ->danger() - ->title('Cloudflare: Missing Zone ID') - ->body(sprintf('Cloudflare zone ID is not configured for %s. Cannot upsert DNS record for %s.%s.', $this->domain->name ?? 'unknown', $this->name, $this->domain->name ?? 'unknown')) + ->title(trans('subdomains::strings.notifications.cloudflare_missing_zone_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_zone', ['domain' => $this->domain->name ?? 'unknown', 'subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) ->send(); return; @@ -115,8 +115,8 @@ protected function upsertOnCloudflare(): void Notification::make() ->danger() - ->title('Cloudflare: Missing SRV Port') - ->body(sprintf('SRV target or port is missing for %s.%s. Cannot upsert SRV record.', $this->name, $this->domain->name ?? 'unknown')) + ->title(trans('subdomains::strings.notifications.cloudflare_missing_srv_port_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_port', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) ->send(); return; @@ -131,16 +131,16 @@ protected function upsertOnCloudflare(): void Notification::make() ->success() - ->title('Cloudflare: Record updated') - ->body('Successfully updated ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . ' to '. $this->record_type) + ->title(trans('subdomains::strings.notifications.cloudflare_record_updated_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_record_updated', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'record_type' => $this->record_type])) ->send(); } else { Log::error('Failed to upsert SRV record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); Notification::make() ->danger() - ->title('Cloudflare: SRV upsert failed') - ->body('Failed to upsert SRV record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->title(trans('subdomains::strings.notifications.cloudflare_srv_upsert_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_srv_upsert_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) ->send(); } @@ -153,8 +153,8 @@ protected function upsertOnCloudflare(): void Notification::make() ->danger() - ->title('Cloudflare: Missing IP') - ->body(sprintf('Server allocation IP is missing or invalid for %s.%s. Cannot upsert A/AAAA record.', $this->name, $this->domain->name ?? 'unknown')) + ->title(trans('subdomains::strings.notifications.cloudflare_missing_ip_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_ip', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) ->send(); return; @@ -169,8 +169,8 @@ protected function upsertOnCloudflare(): void Notification::make() ->success() - ->title('Cloudflare: Record updated') - ->body('Successfully updated ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . ' to '. $this->record_type) + ->title(trans('subdomains::strings.notifications.cloudflare_record_updated_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_record_updated', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'record_type' => $this->record_type])) ->send(); } else { Log::error('Failed to upsert record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); @@ -180,8 +180,8 @@ protected function upsertOnCloudflare(): void Notification::make() ->danger() - ->title('Cloudflare: Upsert failed') - ->body('Failed to upsert record for ' . $sub . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->title(trans('subdomains::strings.notifications.cloudflare_upsert_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $sub, 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) ->send(); } } @@ -195,16 +195,17 @@ protected function deleteOnCloudflare(): void if (!empty($result['success'])) { Notification::make() - ->danger() - ->title('Cloudflare: Delete successed') - ->body('Successfully deleted Cloudflare record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '.') + ->success() + ->title(trans('subdomains::strings.notifications.cloudflare_delete_success_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_delete_success', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) ->send(); + return; } Notification::make() ->danger() - ->title('Cloudflare: Delete failed') - ->body('Failed to delete Cloudflare record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->title(trans('subdomains::strings.notifications.cloudflare_delete_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_delete_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) ->send(); } } diff --git a/subdomains/src/SubdomainsPlugin.php b/subdomains/src/SubdomainsPlugin.php index 17e7164..655a465 100644 --- a/subdomains/src/SubdomainsPlugin.php +++ b/subdomains/src/SubdomainsPlugin.php @@ -47,7 +47,7 @@ public function saveSettings(array $data): void ]); Notification::make() - ->title('Settings saved') + ->title(trans('subdomains::strings.notifications.settings_saved')) ->success() ->send(); } From f7b98d3a412e9694fd8091fd6ec4c9058ff55c85 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:27:05 +1100 Subject: [PATCH 21/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 53f21d6..9dd6d93 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -200,13 +200,13 @@ protected function deleteOnCloudflare(): void ->body(trans('subdomains::strings.notifications.cloudflare_delete_success', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) ->send(); return; + } else { + Notification::make() + ->danger() + ->title(trans('subdomains::strings.notifications.cloudflare_delete_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_delete_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) + ->send(); } - - Notification::make() - ->danger() - ->title(trans('subdomains::strings.notifications.cloudflare_delete_failed_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_delete_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) - ->send(); } } } From 9804937fc3e8c915c6447dbae252b0d59693b537 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:27:24 +1100 Subject: [PATCH 22/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 dcbd5dc..b2cd73b 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -124,7 +124,7 @@ protected function parseCloudflareHttpResponse(Response $response): array $status = $response->status(); $body = $response->json() ?? []; - $success = $response->successful() && (!empty($body['success']) || !empty($body['result'])); + $success = $response->successful() && ($body['success'] === true || count($body['result']) > 0); return [ 'success' => $success, From d004ddaf94af9066c401d931a371cb4964814c06 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:32:29 +1100 Subject: [PATCH 23/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 3b8c25f..b44ffbd 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -85,7 +85,7 @@ public static function table(Table $table): Table ->disabled(fn (Subdomain $record) => !$record->domain || empty($record->domain->srv_target)), ]) ->recordActions([ - EditAction::make(), + EditAction::make() ->successNotification(null), DeleteAction::make() ->successNotification(null), diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 9dd6d93..b243255 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -128,7 +128,7 @@ protected function upsertOnCloudflare(): void if ($this->cloudflare_id !== $result['id']) { $this->updateQuietly(['cloudflare_id' => $result['id']]); } - + Notification::make() ->success() ->title(trans('subdomains::strings.notifications.cloudflare_record_updated_title')) @@ -199,7 +199,6 @@ protected function deleteOnCloudflare(): void ->title(trans('subdomains::strings.notifications.cloudflare_delete_success_title')) ->body(trans('subdomains::strings.notifications.cloudflare_delete_success', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) ->send(); - return; } else { Notification::make() ->danger() @@ -207,6 +206,8 @@ protected function deleteOnCloudflare(): void ->body(trans('subdomains::strings.notifications.cloudflare_delete_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) ->send(); } + + return; } } } diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index b2cd73b..11f57dc 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -38,14 +38,7 @@ public function getZoneId(string $domainName): ?string return null; } - public function upsertDnsRecord( - string $zoneId, - string $name, - string $recordType, - string $target, - ?string $recordId = null, - ?int $port = null, - ): array + public function upsertDnsRecord(string $zoneId, string $name, string $recordType, string $target, ?string $recordId = null, ?int $port = null): array { if (empty($zoneId) || empty($name) || empty($recordType)) { Log::error('Cloudflare upsertDnsRecord missing required parameters', ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); From a4d9827007f7619bc6f4f342b154c5ecdd37f745 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:46:58 +1100 Subject: [PATCH 24/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 11f57dc..b0525e5 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -12,6 +12,7 @@ public function getZoneId(string $domainName): ?string { if (empty($domainName)) { Log::error('Cloudflare getZoneId called with empty domain name', ['domain' => $domainName]); + return null; } @@ -21,6 +22,7 @@ public function getZoneId(string $domainName): ?string ]); } catch (\Throwable $e) { Log::error('Cloudflare getZoneId request failed: ' . $e->getMessage(), ['domain' => $domainName]); + return null; } @@ -42,6 +44,7 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType { if (empty($zoneId) || empty($name) || empty($recordType)) { Log::error('Cloudflare upsertDnsRecord missing required parameters', ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); + return ['success' => false, 'id' => null, 'errors' => ['missing_parameters' => true], 'status' => 0, 'body' => null]; } @@ -56,6 +59,7 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType if ($recordType === 'SRV') { if (empty($port) || empty($target)) { Log::error('Cloudflare upsert missing SRV target or port', ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); + return ['success' => false, 'id' => null, 'errors' => ['missing_srv_target_or_port' => true], 'status' => 0, 'body' => null]; } @@ -94,6 +98,7 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType } Log::error('Cloudflare update failed', ['zone' => $zoneId, 'recordId' => $recordId, 'response' => $parsed]); + return $parsed; } @@ -105,9 +110,11 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType } Log::error('Cloudflare create failed', ['zone' => $zoneId, 'payload' => $payload, 'response' => $parsed]); + return $parsed; } catch (\Throwable $e) { Log::error('Cloudflare upsert exception: ' . $e->getMessage(), ['zone' => $zoneId, 'payload' => $payload]); + return ['success' => false, 'id' => null, 'errors' => ['exception' => $e->getMessage()], 'status' => 0, 'body' => null]; } } @@ -144,9 +151,11 @@ public function deleteDnsRecord(string $zoneId, string $recordId): array } Log::error('Cloudflare delete failed', ['zone' => $zoneId, 'id' => $recordId, 'response' => $parsed]); + return $parsed; } catch (\Throwable $e) { Log::error('Cloudflare delete exception: ' . $e->getMessage(), ['zone' => $zoneId, 'id' => $recordId]); + return ['success' => false, 'errors' => ['exception' => $e->getMessage()], 'status' => 0, 'body' => null]; } } From c4654fd2a273181e86d22dfeff32327d2ac467bd Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:54:41 +1100 Subject: [PATCH 25/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 7ff7cbc..23e3ee6 100644 --- a/subdomains/plugin.json +++ b/subdomains/plugin.json @@ -2,7 +2,7 @@ "id": "subdomains", "name": "Subdomains", "author": "Boy132", - "version": "1.0.0", + "version": "1.1.0", "description": "Allows users to create subdomains for their servers", "category": "plugin", "url": "https://github.com/pelican-dev/plugins/tree/main/subdomains", From 348099d16c2fcce30010e258c3bf526884398630 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 12:53:39 +1100 Subject: [PATCH 26/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 796ef63..462d767 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -41,6 +41,9 @@ 'cloudflare_missing_srv_port_title' => 'Cloudflare: Missing SRV Port', 'cloudflare_missing_srv_port' => 'SRV target or port is missing for :subdomain. Cannot upsert SRV record.', + 'cloudflare_missing_srv_target_title' => 'Cloudflare: Missing SRV Target', + 'cloudflare_missing_srv_target' => 'SRV target is missing for :subdomain. Cannot upsert SRV record.', + 'cloudflare_record_updated_title' => 'Cloudflare: Record updated', 'cloudflare_record_updated' => 'Successfully updated :subdomain to :record_type', diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index b243255..78e1c0c 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -122,6 +122,18 @@ protected function upsertOnCloudflare(): void return; } + if (empty($this->domain->srv_target)) { + Log::warning('Domain missing SRV target for SRV record', ['domain_id' => $this->domain_id]); + + Notification::make() + ->danger() + ->title(trans('subdomains::strings.notifications.cloudflare_missing_srv_target_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_target', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) + ->send(); + + return; + } + $result = $service->upsertDnsRecord($zoneId, $this->name, 'SRV', $this->domain->srv_target, $this->cloudflare_id ?? null, $port); if ($result['success'] && !empty($result['id'])) { From d64612d694daf2c66591a73950b8fab7bf15faeb Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 13:03:20 +1100 Subject: [PATCH 27/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 462d767..5ae1ac4 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -39,13 +39,13 @@ 'cloudflare_missing_zone' => 'Cloudflare zone ID is not configured for :domain. Cannot upsert DNS record for :subdomain.', 'cloudflare_missing_srv_port_title' => 'Cloudflare: Missing SRV Port', - 'cloudflare_missing_srv_port' => 'SRV target or port is missing for :subdomain. Cannot upsert SRV record.', + 'cloudflare_missing_srv_port' => 'SRV port is missing for :subdomain. Cannot upsert SRV record.', 'cloudflare_missing_srv_target_title' => 'Cloudflare: Missing SRV Target', 'cloudflare_missing_srv_target' => 'SRV target is missing for :subdomain. Cannot upsert SRV record.', 'cloudflare_record_updated_title' => 'Cloudflare: Record updated', - 'cloudflare_record_updated' => 'Successfully updated :subdomain to :record_type', + 'cloudflare_record_updated' => 'Successfully updated :subdomain record to :record_type', 'cloudflare_srv_upsert_failed_title' => 'Cloudflare: SRV upsert failed', 'cloudflare_srv_upsert_failed' => 'Failed to upsert SRV record for :subdomain. See logs for details. Errors: :errors', From 5bfd8dfd25ed1a05f63d6ff5b3a9495b3e236719 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 13:20:39 +1100 Subject: [PATCH 28/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 b44ffbd..c2b65ee 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -123,7 +123,7 @@ public static function form(Schema $schema): Schema Toggle::make('srv_record') ->label(trans('subdomains::strings.srv_record')) ->helperText(trans('subdomains::strings.srv_record_help')) - ->disabled(false) // TODO: Dynamically disable if domain missing srv_target + ->disabled(fn (callable $get, ?Subdomain $record = null) => (empty($get('domain_id')) || empty(CloudflareDomain::find($get('domain_id'))?->srv_target))) ->default(false), ]); } From e781f6a5bce26777680759e71c9f49f01fe2657b Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 13:29:36 +1100 Subject: [PATCH 29/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 5ae1ac4..494b760 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -28,33 +28,23 @@ 'api_token_help' => 'The token needs to have read permissions for Zone.Zone and write for Zone.Dns. For better security you can also set the "Zone Resources" to exclude certain domains and add the panel ip to the "Client IP Adress Filtering".', 'notifications' => [ - 'dns_created' => 'DNS record created on Cloudflare', - 'dns_updated' => 'DNS record updated on Cloudflare', - 'dns_deleted' => 'DNS record deleted from Cloudflare', - 'dns_action_failed' => 'Cloudflare DNS action failed', - 'zone_request_failed' => 'Cloudflare zone request failed', - 'zone_request_succeeded' => 'Cloudflare zone request succeeded', - 'cloudflare_missing_zone_title' => 'Cloudflare: Missing Zone ID', - 'cloudflare_missing_zone' => 'Cloudflare zone ID is not configured for :domain. Cannot upsert DNS record for :subdomain.', + 'cloudflare_missing_zone' => 'Cloudflare zone ID is not configured for :domain. Cannot save DNS record for :subdomain.', 'cloudflare_missing_srv_port_title' => 'Cloudflare: Missing SRV Port', - 'cloudflare_missing_srv_port' => 'SRV port is missing for :subdomain. Cannot upsert SRV record.', + 'cloudflare_missing_srv_port' => 'SRV port is missing for :subdomain. Cannot save SRV record.', 'cloudflare_missing_srv_target_title' => 'Cloudflare: Missing SRV Target', - 'cloudflare_missing_srv_target' => 'SRV target is missing for :subdomain. Cannot upsert SRV record.', + 'cloudflare_missing_srv_target' => 'SRV target is missing for :subdomain. Cannot save SRV record.', 'cloudflare_record_updated_title' => 'Cloudflare: Record updated', 'cloudflare_record_updated' => 'Successfully updated :subdomain record to :record_type', - 'cloudflare_srv_upsert_failed_title' => 'Cloudflare: SRV upsert failed', - 'cloudflare_srv_upsert_failed' => 'Failed to upsert SRV record for :subdomain. See logs for details. Errors: :errors', - 'cloudflare_missing_ip_title' => 'Cloudflare: Missing IP', - 'cloudflare_missing_ip' => 'Server allocation IP is missing or invalid for :subdomain. Cannot upsert A/AAAA record.', + 'cloudflare_missing_ip' => 'Server allocation IP is missing or invalid for :subdomain. Cannot save A/AAAA record.', - 'cloudflare_upsert_failed_title' => 'Cloudflare: Upsert failed', - 'cloudflare_upsert_failed' => 'Failed to upsert record for :subdomain. See logs for details. Errors: :errors', + 'cloudflare_upsert_failed_title' => 'Cloudflare: Save failed', + 'cloudflare_upsert_failed' => 'Failed to save record for :subdomain. See logs for details. Errors: :errors', 'cloudflare_delete_success_title' => 'Cloudflare: Record deleted', 'cloudflare_delete_success' => 'Successfully deleted Cloudflare record for :subdomain.', @@ -64,6 +54,7 @@ 'cloudflare_zone_fetch_failed' => 'Failed to fetch Cloudflare Zone ID for domain: :domain', 'cloudflare_domain_saved' => 'Successfully saved domain: :domain', + 'settings_saved' => 'Settings saved', ], ]; diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 78e1c0c..f6617fa 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -151,8 +151,8 @@ protected function upsertOnCloudflare(): void Notification::make() ->danger() - ->title(trans('subdomains::strings.notifications.cloudflare_srv_upsert_failed_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_srv_upsert_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) + ->title(trans('subdomains::strings.notifications.cloudflare_upsert_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) ->send(); } From 8a7e932c204dd83f298f63f5e12da675ec9b538f Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 13:39:52 +1100 Subject: [PATCH 30/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 c2b65ee..1d9fe3b 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -123,7 +123,8 @@ public static function form(Schema $schema): Schema Toggle::make('srv_record') ->label(trans('subdomains::strings.srv_record')) ->helperText(trans('subdomains::strings.srv_record_help')) - ->disabled(fn (callable $get, ?Subdomain $record = null) => (empty($get('domain_id')) || empty(CloudflareDomain::find($get('domain_id'))?->srv_target))) + ->reactive() + ->disabled(fn (callable $get) => (empty($get('domain_id')) || empty(CloudflareDomain::find($get('domain_id'))?->srv_target))) ->default(false), ]); } From 23c4e399a1a71c5417411c0d3b322fc7015cfcce Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 13:40:14 +1100 Subject: [PATCH 31/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 494b760..3b8f387 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -37,24 +37,24 @@ 'cloudflare_missing_srv_target_title' => 'Cloudflare: Missing SRV Target', 'cloudflare_missing_srv_target' => 'SRV target is missing for :subdomain. Cannot save SRV record.', - 'cloudflare_record_updated_title' => 'Cloudflare: Record updated', + 'cloudflare_record_updated_title' => 'Cloudflare: Record Updated', 'cloudflare_record_updated' => 'Successfully updated :subdomain record to :record_type', 'cloudflare_missing_ip_title' => 'Cloudflare: Missing IP', 'cloudflare_missing_ip' => 'Server allocation IP is missing or invalid for :subdomain. Cannot save A/AAAA record.', - 'cloudflare_upsert_failed_title' => 'Cloudflare: Save failed', + 'cloudflare_upsert_failed_title' => 'Cloudflare: Save Failed', 'cloudflare_upsert_failed' => 'Failed to save record for :subdomain. See logs for details. Errors: :errors', - 'cloudflare_delete_success_title' => 'Cloudflare: Record deleted', + 'cloudflare_delete_success_title' => 'Cloudflare: Record Deleted', 'cloudflare_delete_success' => 'Successfully deleted Cloudflare record for :subdomain.', - 'cloudflare_delete_failed_title' => 'Cloudflare: Delete failed', + 'cloudflare_delete_failed_title' => 'Cloudflare: Delete Failed', 'cloudflare_delete_failed' => 'Failed to delete Cloudflare record for :subdomain. See logs for details. Errors: :errors', 'cloudflare_zone_fetch_failed' => 'Failed to fetch Cloudflare Zone ID for domain: :domain', 'cloudflare_domain_saved' => 'Successfully saved domain: :domain', - + 'settings_saved' => 'Settings saved', ], ]; From f31a101c26a174d7814522114005cca0d38b9749 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 13:46:01 +1100 Subject: [PATCH 32/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 f6617fa..7288429 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -134,7 +134,7 @@ protected function upsertOnCloudflare(): void return; } - $result = $service->upsertDnsRecord($zoneId, $this->name, 'SRV', $this->domain->srv_target, $this->cloudflare_id ?? null, $port); + $result = $service->upsertDnsRecord($zoneId, $this->name, 'SRV', $this->domain->srv_target, $this->cloudflare_id, $port); if ($result['success'] && !empty($result['id'])) { if ($this->cloudflare_id !== $result['id']) { @@ -172,7 +172,7 @@ protected function upsertOnCloudflare(): void return; } - $result = $service->upsertDnsRecord($zoneId, $this->name, $this->record_type, $this->server->allocation->ip, $this->cloudflare_id ?? null, null); + $result = $service->upsertDnsRecord($zoneId, $this->name, $this->record_type, $this->server->allocation->ip, $this->cloudflare_id, null); if ($result['success'] && !empty($result['id'])) { if ($this->cloudflare_id !== $result['id']) { 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 0000000..cab88da --- /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 0000000..c819888 --- /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 7108768..571166f 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 1dbd87c..d98bad9 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 6ce1afa..4bd2198 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 3b8f387..0d42305 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 0000000..f3a2130 --- /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 7288429..96b0282 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 b0525e5..ab19184 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 96b0282..eaf8bca 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 eaf8bca..e3560a9 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 0d42305..6ad3ff7 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 f3a2130..05fa224 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 e017dc6..9d2e40f 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 cab88da..dc30a4b 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 c819888..cc5cd7c 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 cc5cd7c..cfe48d4 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 e3560a9..5da74ea 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 5da74ea..4b1463f 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 cfe48d4..250fff9 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 ab19184..1781378 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 250fff9..4f6f9c3 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 0000000..ef1feb7 --- /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 4bd2198..d1cb0b7 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 4b1463f..b24a4c2 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 6ad3ff7..701cbe5 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 1781378..af8cc9a 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 4f6f9c3..0000000 --- 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 d1cb0b7..948edb8 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 b24a4c2..3269537 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 af8cc9a..8e1d376 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 ef1feb7..4af1bb7 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 5f0f790..89276be 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 3269537..f1ec987 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 948edb8..0e295dd 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 8e1d376..af36136 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 4af1bb7..00149e8 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 db4c1ef..4f0f18e 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 f1ec987..2c088f0 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 0e295dd..67747e6 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 af36136..9559c13 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 ab5c570..aded83e 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 2c088f0..5a4b1bf 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 9559c13..fb04795 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 ef8867b..f85d537 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 5a4b1bf..b84c05e 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 39c3cf8..02c4a33 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 b84c05e..1853709 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 dc30a4b..5a7f330 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 4f0f18e..c701840 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 1853709..4ec02f0 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 701cbe5..1557db3 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 4ec02f0..64802a0 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 fb04795..bf75f3f 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 64802a0..d6879e8 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 d9cf0b1..0b05c23 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 23e3ee6..595b575 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 5a7f330..83d1a23 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 83d1a23..b74d179 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 b74d179..0596162 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 1557db3..46add35 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 f85d537..5cbbae3 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 02c4a33..a02d308 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 d6879e8..88c32b4 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 88c32b4..213003b 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 a02d308..1d9e0fb 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 213003b..79a8c97 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 1d9e0fb..b627b28 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 79a8c97..719d623 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 b627b28..aedf327 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 aedf327..272e351 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 272e351..395c385 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 719d623..3694f6f 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 395c385..7ec7684 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 3694f6f..028d529 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 7ec7684..86089b0 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 028d529..2de60fa 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 2de60fa..3f07ff0 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 3f07ff0..6ef7c7a 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 6ef7c7a..3f425bb 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 3f425bb..1f3b1fb 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 1f3b1fb..b6e5b11 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 3cbe2a1..0000000 --- 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 0596162..b74d179 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 595b575..7ff7cbc 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 05fa224..0ab69eb 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 86089b0..d570a56 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 b6e5b11..668b8f2 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 b74d179..0596162 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 0b05c23..f53a4ac 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 0ab69eb..cc6d072 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 {