From 6e89fd070e7dc4e88da1eafc3fa22555b865f13f Mon Sep 17 00:00:00 2001 From: NebuloDev Date: Thu, 1 Oct 2026 15:12:37 +0200 Subject: [PATCH 1/9] Fix subdomain allocation and record coexistence Only require valid allocation IPs for A and AAAA records, and allow compatible DNS record types to coexist while preserving CNAME conflicts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- subdomains/README.md | 12 ++++++---- .../011_allow_compatible_record_types.php | 24 +++++++++++++++++++ subdomains/src/Enums/RecordType.php | 4 ++-- .../SubdomainRelationManager.php | 8 ++++++- .../Subdomains/SubdomainResource.php | 8 ++++++- subdomains/src/Models/Subdomain.php | 12 ++++++++-- 6 files changed, 58 insertions(+), 10 deletions(-) create mode 100644 subdomains/database/migrations/011_allow_compatible_record_types.php diff --git a/subdomains/README.md b/subdomains/README.md index dcc7cd46..514b3fd4 100644 --- a/subdomains/README.md +++ b/subdomains/README.md @@ -1,6 +1,6 @@ # Subdomains (by Boy132 & HarlequinSin) -Allows users to create and manage custom subdomains (A/AAAA or SRV) for their game servers using Cloudflare DNS. +Allows users to create and manage custom subdomains (A/AAAA, CNAME, or SRV) for their game servers using Cloudflare DNS. ## Setup @@ -32,11 +32,11 @@ If a DNS Record type is not available, check whether it is enabled on the domain ### Server primary allocations -A and AAAA Subdomains will use the IP of the server's primary allocation as their target. SRV records will use the primary allocation's port as part of their target. +A and AAAA Subdomains will use the IP of the server's primary allocation as their target. SRV records require a primary allocation and use its port as part of their target. CNAME records use the node's configured subdomain target and do not use the primary allocation. -IPs such as `0.0.0.0` and `::` are considered invalid for the purposes of creating subdomains. They should be changed to proper IP addresses on which your servers can be reached. +For A and AAAA records, IPs such as `0.0.0.0` and `::` are considered invalid. They should be changed to proper IP addresses on which your servers can be reached. -**IMPORTANT: In order to create subdomains for a server, that server's primary allocation MUST have a valid IP address.** This also applies for CNAME and SRV Subdomains. +**IMPORTANT: In order to create A or AAAA subdomains for a server, that server's primary allocation MUST have a valid IP address.** SRV subdomains require a primary allocation because they use its port, but do not use its IP address. CNAME subdomains do not require an allocation. ### Subdomain targets @@ -44,6 +44,10 @@ CNAME and SRV Subdomains must point to a specific Subdomain target. These can be Note: According to [RFC2782](https://www.rfc-editor.org/info/rfc2782/), SRV records must always point to either an A or AAAA record. While some applications may handle SRV records pointing to CNAME records correctly, this can lead to undefined behavior. +### Record coexistence + +SRV records can coexist with A, AAAA, and CNAME records because they are created at a separate service-specific name. A and AAAA records can also coexist at the same name. CNAME records cannot coexist with any other record at the same name. + ### Use Allocation Alias You can specify whether servers on a specific node should use the allocation Alias instead of the allocation IP when creating A and AAAA records. diff --git a/subdomains/database/migrations/011_allow_compatible_record_types.php b/subdomains/database/migrations/011_allow_compatible_record_types.php new file mode 100644 index 00000000..48820d64 --- /dev/null +++ b/subdomains/database/migrations/011_allow_compatible_record_types.php @@ -0,0 +1,24 @@ +dropUnique(['name', 'domain_id']); + $table->unique(['name', 'domain_id', 'record_type']); + }); + } + + public function down(): void + { + Schema::table('subdomains', function (Blueprint $table) { + $table->dropUnique(['name', 'domain_id', 'record_type']); + $table->unique(['name', 'domain_id']); + }); + } +}; diff --git a/subdomains/src/Enums/RecordType.php b/subdomains/src/Enums/RecordType.php index aa936352..0323fc4a 100644 --- a/subdomains/src/Enums/RecordType.php +++ b/subdomains/src/Enums/RecordType.php @@ -35,7 +35,7 @@ public function canBeUsedErrors(Server $server, CloudflareDomain $domain): Colle $targetAddress = ''; if ($allocation) { - if (in_array($allocation->ip, ['0.0.0.0', '::'])) { + if (in_array($this, [self::A, self::AAAA]) && in_array($allocation->ip, ['0.0.0.0', '::'])) { $errors->add('Allocation ip is invalid (0.0.0.0 or ::)'); } @@ -63,7 +63,7 @@ public function canBeUsedErrors(Server $server, CloudflareDomain $domain): Colle $errors->add('Server has no allocation'); } - if (in_array($targetAddress, ['0.0.0.0', '::'])) { + if (in_array($this, [self::A, self::AAAA]) && in_array($targetAddress, ['0.0.0.0', '::'])) { $errors->add('Allocation target address is invalid (0.0.0.0 or ::)'); } diff --git a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php index 303b7576..0e329e03 100644 --- a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php +++ b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php @@ -22,6 +22,7 @@ use Filament\Support\Exceptions\Halt; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; +use Illuminate\Validation\Rules\Unique; /** * @method Server getOwnerRecord() @@ -112,7 +113,12 @@ public function form(Schema $schema): Schema TextInput::make('name') ->label(trans('subdomains::strings.name')) ->required() - ->unique() + ->unique( + ignoreRecord: true, + modifyRuleUsing: fn (Unique $rule, Get $get) => $rule + ->where('domain_id', $get('domain_id')) + ->where('record_type', $get('record_type')), + ) ->alphaDash() ->rule(new NotOnBlacklist()) ->columnSpanFull() diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index e8fb25b5..d647e667 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -26,6 +26,7 @@ use Filament\Support\Exceptions\Halt; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; +use Illuminate\Validation\Rules\Unique; class SubdomainResource extends Resource { @@ -147,7 +148,12 @@ public static function form(Schema $schema): Schema TextInput::make('name') ->label(trans('subdomains::strings.name')) ->required() - ->unique() + ->unique( + ignoreRecord: true, + modifyRuleUsing: fn (Unique $rule, Get $get) => $rule + ->where('domain_id', $get('domain_id')) + ->where('record_type', $get('record_type')), + ) ->alphaDash() ->rule(new NotOnBlacklist()) ->columnSpanFull() diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 0eab019e..3c04c8ba 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -129,7 +129,6 @@ public function upsertOnCloudflare(): void // @phpstan-ignore staticMethod.notFound $searchResponse = Http::cloudflare()->get("zones/{$this->domain->cloudflare_id}/dns_records", [ 'name' => $searchName, - 'type' => $this->record_type, ])->json(); if ($searchResponse['success']) { @@ -137,7 +136,16 @@ public function upsertOnCloudflare(): void foreach ($results as $record) { if ($record['id'] !== $this->cloudflare_id) { - throw new Exception('A subdomain with that name already exists'); + $recordType = $record['type']; + $isConflict = match ($this->record_type) { + RecordType::CNAME => true, + RecordType::A, RecordType::AAAA => $recordType === 'CNAME', + RecordType::SRV => in_array($recordType, ['CNAME', 'SRV']), + }; + + if ($isConflict) { + throw new Exception("A $recordType record already exists for this subdomain"); + } } } } else { From eb3e18e06ddad62bfd644be082cbd6c62830b75a Mon Sep 17 00:00:00 2001 From: NebuloDev Date: Thu, 1 Oct 2026 15:17:39 +0200 Subject: [PATCH 2/9] Allow SRV records for separate servers Scope local duplicate validation by server so SRV records with different service-specific names are not incorrectly blocked. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../database/migrations/011_allow_compatible_record_types.php | 4 ++-- .../Servers/RelationManagers/SubdomainRelationManager.php | 3 ++- .../Server/Resources/Subdomains/SubdomainResource.php | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/subdomains/database/migrations/011_allow_compatible_record_types.php b/subdomains/database/migrations/011_allow_compatible_record_types.php index 48820d64..50b8129e 100644 --- a/subdomains/database/migrations/011_allow_compatible_record_types.php +++ b/subdomains/database/migrations/011_allow_compatible_record_types.php @@ -10,14 +10,14 @@ public function up(): void { Schema::table('subdomains', function (Blueprint $table) { $table->dropUnique(['name', 'domain_id']); - $table->unique(['name', 'domain_id', 'record_type']); + $table->unique(['name', 'domain_id', 'record_type', 'server_id']); }); } public function down(): void { Schema::table('subdomains', function (Blueprint $table) { - $table->dropUnique(['name', 'domain_id', 'record_type']); + $table->dropUnique(['name', 'domain_id', 'record_type', 'server_id']); $table->unique(['name', 'domain_id']); }); } diff --git a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php index 0e329e03..91e3952e 100644 --- a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php +++ b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php @@ -117,7 +117,8 @@ public function form(Schema $schema): Schema ignoreRecord: true, modifyRuleUsing: fn (Unique $rule, Get $get) => $rule ->where('domain_id', $get('domain_id')) - ->where('record_type', $get('record_type')), + ->where('record_type', $get('record_type')) + ->where('server_id', $this->getOwnerRecord()->id), ) ->alphaDash() ->rule(new NotOnBlacklist()) diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index d647e667..a7d12b07 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -152,7 +152,8 @@ public static function form(Schema $schema): Schema ignoreRecord: true, modifyRuleUsing: fn (Unique $rule, Get $get) => $rule ->where('domain_id', $get('domain_id')) - ->where('record_type', $get('record_type')), + ->where('record_type', $get('record_type')) + ->where('server_id', Filament::getTenant()->id), ) ->alphaDash() ->rule(new NotOnBlacklist()) From a92b62672b7194649aa54a906cff0ea0a7e4dd4e Mon Sep 17 00:00:00 2001 From: NebuloDev Date: Thu, 1 Oct 2026 15:19:03 +0200 Subject: [PATCH 3/9] Guard subdomain migration rollback Fail clearly before restoring the old unique constraint when compatible duplicate records still exist. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../011_allow_compatible_record_types.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/subdomains/database/migrations/011_allow_compatible_record_types.php b/subdomains/database/migrations/011_allow_compatible_record_types.php index 50b8129e..6b07a9cb 100644 --- a/subdomains/database/migrations/011_allow_compatible_record_types.php +++ b/subdomains/database/migrations/011_allow_compatible_record_types.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 @@ -16,6 +17,18 @@ public function up(): void public function down(): void { + $hasDuplicateSubdomains = DB::table('subdomains') + ->select('name', 'domain_id') + ->groupBy('name', 'domain_id') + ->havingRaw('COUNT(*) > 1') + ->exists(); + + if ($hasDuplicateSubdomains) { + throw new RuntimeException( + 'Cannot roll back subdomain coexistence while multiple record types share the same name and domain. Remove the duplicate records first.', + ); + } + Schema::table('subdomains', function (Blueprint $table) { $table->dropUnique(['name', 'domain_id', 'record_type', 'server_id']); $table->unique(['name', 'domain_id']); From fbd5efec24634ce26267a4888b93214d4de046e0 Mon Sep 17 00:00:00 2001 From: NebuloDev Date: Thu, 1 Oct 2026 15:19:43 +0200 Subject: [PATCH 4/9] Fix tenant type in subdomain validation Use the existing typed server tenant when scoping the uniqueness rule. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../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 a7d12b07..d1327657 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -153,7 +153,7 @@ public static function form(Schema $schema): Schema modifyRuleUsing: fn (Unique $rule, Get $get) => $rule ->where('domain_id', $get('domain_id')) ->where('record_type', $get('record_type')) - ->where('server_id', Filament::getTenant()->id), + ->where('server_id', $server->id), ) ->alphaDash() ->rule(new NotOnBlacklist()) From a2154bce9b10cf2556a20f96660e620d2b04d03d Mon Sep 17 00:00:00 2001 From: NebuloDev Date: Thu, 1 Oct 2026 15:22:53 +0200 Subject: [PATCH 5/9] Prevent conflicting CNAME renames Reject local CNAME and A/AAAA name conflicts and restore edited subdomains when Cloudflare synchronization fails. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SubdomainRelationManager.php | 20 +++++++++++++++---- .../Subdomains/SubdomainResource.php | 20 +++++++++++++++---- subdomains/src/Services/SubdomainService.php | 5 +++++ 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php index 91e3952e..da453e09 100644 --- a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php +++ b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php @@ -3,6 +3,7 @@ namespace Boy132\Subdomains\Filament\Admin\Resources\Servers\RelationManagers; use App\Models\Server; +use Boy132\Subdomains\Enums\RecordType; use Boy132\Subdomains\Models\CloudflareDomain; use Boy132\Subdomains\Models\Subdomain; use Boy132\Subdomains\Rules\NotOnBlacklist; @@ -115,10 +116,21 @@ public function form(Schema $schema): Schema ->required() ->unique( ignoreRecord: true, - modifyRuleUsing: fn (Unique $rule, Get $get) => $rule - ->where('domain_id', $get('domain_id')) - ->where('record_type', $get('record_type')) - ->where('server_id', $this->getOwnerRecord()->id), + modifyRuleUsing: function (Unique $rule, Get $get): Unique { + $rule->where('domain_id', $get('domain_id')); + + if (in_array($get('record_type'), [RecordType::A->value, RecordType::AAAA->value, RecordType::CNAME->value])) { + $rule->whereIn('record_type', match ($get('record_type')) { + RecordType::CNAME->value => [RecordType::A->value, RecordType::AAAA->value, RecordType::CNAME->value], + default => [$get('record_type'), RecordType::CNAME->value], + }); + } else { + $rule->where('record_type', $get('record_type')) + ->where('server_id', $this->getOwnerRecord()->id); + } + + return $rule; + }, ) ->alphaDash() ->rule(new NotOnBlacklist()) diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index d1327657..c209423f 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -5,6 +5,7 @@ use App\Models\Server; use App\Traits\Filament\BlockAccessInConflict; use App\Traits\Filament\HasLimitBadge; +use Boy132\Subdomains\Enums\RecordType; use Boy132\Subdomains\Filament\Server\Resources\Subdomains\Pages\ListSubdomains; use Boy132\Subdomains\Models\CloudflareDomain; use Boy132\Subdomains\Models\Subdomain; @@ -150,10 +151,21 @@ public static function form(Schema $schema): Schema ->required() ->unique( ignoreRecord: true, - modifyRuleUsing: fn (Unique $rule, Get $get) => $rule - ->where('domain_id', $get('domain_id')) - ->where('record_type', $get('record_type')) - ->where('server_id', $server->id), + modifyRuleUsing: function (Unique $rule, Get $get) use ($server): Unique { + $rule->where('domain_id', $get('domain_id')); + + if (in_array($get('record_type'), [RecordType::A->value, RecordType::AAAA->value, RecordType::CNAME->value])) { + $rule->whereIn('record_type', match ($get('record_type')) { + RecordType::CNAME->value => [RecordType::A->value, RecordType::AAAA->value, RecordType::CNAME->value], + default => [$get('record_type'), RecordType::CNAME->value], + }); + } else { + $rule->where('record_type', $get('record_type')) + ->where('server_id', $server->id); + } + + return $rule; + }, ) ->alphaDash() ->rule(new NotOnBlacklist()) diff --git a/subdomains/src/Services/SubdomainService.php b/subdomains/src/Services/SubdomainService.php index 3592993b..d561a81c 100644 --- a/subdomains/src/Services/SubdomainService.php +++ b/subdomains/src/Services/SubdomainService.php @@ -15,10 +15,12 @@ class SubdomainService public function handle(array $data, ?Subdomain $subdomain = null): Subdomain { $newSubdomain = true; + $originalAttributes = []; if (is_null($subdomain)) { $subdomain = Subdomain::create($data); } else { + $originalAttributes = $subdomain->getRawOriginal(); $subdomain->update($data); $newSubdomain = false; } @@ -30,6 +32,9 @@ public function handle(array $data, ?Subdomain $subdomain = null): Subdomain } catch (Exception $exception) { if ($newSubdomain) { $subdomain->delete(); + } else { + $subdomain->setRawAttributes($originalAttributes); + $subdomain->saveQuietly(); } throw $exception; From 3f64e8d344795cd85a51551b4553bff60c34d779 Mon Sep 17 00:00:00 2001 From: NebuloDev Date: Thu, 1 Oct 2026 15:25:18 +0200 Subject: [PATCH 6/9] Enforce subdomain record uniqueness globally Remove server_id from the compatible record type key and matching validation rules. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../migrations/011_allow_compatible_record_types.php | 4 ++-- .../Servers/RelationManagers/SubdomainRelationManager.php | 3 +-- .../Server/Resources/Subdomains/SubdomainResource.php | 5 ++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/subdomains/database/migrations/011_allow_compatible_record_types.php b/subdomains/database/migrations/011_allow_compatible_record_types.php index 6b07a9cb..7b86852a 100644 --- a/subdomains/database/migrations/011_allow_compatible_record_types.php +++ b/subdomains/database/migrations/011_allow_compatible_record_types.php @@ -11,7 +11,7 @@ public function up(): void { Schema::table('subdomains', function (Blueprint $table) { $table->dropUnique(['name', 'domain_id']); - $table->unique(['name', 'domain_id', 'record_type', 'server_id']); + $table->unique(['name', 'domain_id', 'record_type']); }); } @@ -30,7 +30,7 @@ public function down(): void } Schema::table('subdomains', function (Blueprint $table) { - $table->dropUnique(['name', 'domain_id', 'record_type', 'server_id']); + $table->dropUnique(['name', 'domain_id', 'record_type']); $table->unique(['name', 'domain_id']); }); } diff --git a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php index da453e09..f704d7d8 100644 --- a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php +++ b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php @@ -125,8 +125,7 @@ public function form(Schema $schema): Schema default => [$get('record_type'), RecordType::CNAME->value], }); } else { - $rule->where('record_type', $get('record_type')) - ->where('server_id', $this->getOwnerRecord()->id); + $rule->where('record_type', $get('record_type')); } return $rule; diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index c209423f..c7001fba 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -151,7 +151,7 @@ public static function form(Schema $schema): Schema ->required() ->unique( ignoreRecord: true, - modifyRuleUsing: function (Unique $rule, Get $get) use ($server): Unique { + modifyRuleUsing: function (Unique $rule, Get $get): Unique { $rule->where('domain_id', $get('domain_id')); if (in_array($get('record_type'), [RecordType::A->value, RecordType::AAAA->value, RecordType::CNAME->value])) { @@ -160,8 +160,7 @@ public static function form(Schema $schema): Schema default => [$get('record_type'), RecordType::CNAME->value], }); } else { - $rule->where('record_type', $get('record_type')) - ->where('server_id', $server->id); + $rule->where('record_type', $get('record_type')); } return $rule; From 025daa4ff1420d89d15965d059ae69b56b64de14 Mon Sep 17 00:00:00 2001 From: NebuloDev Date: Thu, 1 Oct 2026 16:12:56 +0200 Subject: [PATCH 7/9] Allow distinct SRV service records Use the effective SRV service name for local uniqueness and reject unmanaged same-type A or AAAA records during Cloudflare checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../011_allow_compatible_record_types.php | 14 ++++++++++++-- subdomains/src/Enums/RecordType.php | 9 +++++++++ .../RelationManagers/SubdomainRelationManager.php | 2 +- .../Resources/Subdomains/SubdomainResource.php | 2 +- subdomains/src/Models/Subdomain.php | 7 ++++++- 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/subdomains/database/migrations/011_allow_compatible_record_types.php b/subdomains/database/migrations/011_allow_compatible_record_types.php index 7b86852a..5c4d8e91 100644 --- a/subdomains/database/migrations/011_allow_compatible_record_types.php +++ b/subdomains/database/migrations/011_allow_compatible_record_types.php @@ -11,7 +11,16 @@ public function up(): void { Schema::table('subdomains', function (Blueprint $table) { $table->dropUnique(['name', 'domain_id']); - $table->unique(['name', 'domain_id', 'record_type']); + $table->string('record_identifier')->nullable()->after('record_type'); + }); + + \Boy132\Subdomains\Models\Subdomain::query()->with('server')->each(function ($subdomain) { + $subdomain->saveQuietly(); + }); + + Schema::table('subdomains', function (Blueprint $table) { + $table->string('record_identifier')->nullable(false)->change(); + $table->unique(['name', 'domain_id', 'record_identifier']); }); } @@ -30,8 +39,9 @@ public function down(): void } Schema::table('subdomains', function (Blueprint $table) { - $table->dropUnique(['name', 'domain_id', 'record_type']); + $table->dropUnique(['name', 'domain_id', 'record_identifier']); $table->unique(['name', 'domain_id']); + $table->dropColumn('record_identifier'); }); } }; diff --git a/subdomains/src/Enums/RecordType.php b/subdomains/src/Enums/RecordType.php index 0323fc4a..d096fd1d 100644 --- a/subdomains/src/Enums/RecordType.php +++ b/subdomains/src/Enums/RecordType.php @@ -20,6 +20,15 @@ public function getLabel(): string return $this->name; } + public function uniqueIdentifier(Server $server): string + { + if ($this !== self::SRV) { + return $this->value; + } + + return SRVServiceType::fromServer($server)?->value ?? 'SRV'; + } + /** * Returns errors that prevent this record type from being used with the provided server and domain. * If empty, then this record type is allowed to be used. diff --git a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php index f704d7d8..8a9b9e56 100644 --- a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php +++ b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php @@ -125,7 +125,7 @@ public function form(Schema $schema): Schema default => [$get('record_type'), RecordType::CNAME->value], }); } else { - $rule->where('record_type', $get('record_type')); + $rule->where('record_identifier', RecordType::SRV->uniqueIdentifier($this->getOwnerRecord())); } return $rule; diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index c7001fba..1d87ff70 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -160,7 +160,7 @@ public static function form(Schema $schema): Schema default => [$get('record_type'), RecordType::CNAME->value], }); } else { - $rule->where('record_type', $get('record_type')); + $rule->where('record_identifier', RecordType::SRV->uniqueIdentifier($server)); } return $rule; diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 3c04c8ba..dc8035c7 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -17,6 +17,7 @@ * @property string $name * @property RecordType $record_type * @property ?string $cloudflare_id + * @property string $record_identifier * @property int $domain_id * @property CloudflareDomain $domain * @property int $server_id @@ -39,6 +40,10 @@ protected static function boot(): void static::deleted(function (self $model) { $model->deleteOnCloudflare(); }); + + static::saving(function (self $model) { + $model->record_identifier = $model->record_type->uniqueIdentifier($model->server); + }); } protected function casts(): array @@ -139,7 +144,7 @@ public function upsertOnCloudflare(): void $recordType = $record['type']; $isConflict = match ($this->record_type) { RecordType::CNAME => true, - RecordType::A, RecordType::AAAA => $recordType === 'CNAME', + RecordType::A, RecordType::AAAA => in_array($recordType, [$this->record_type->value, 'CNAME'], true), RecordType::SRV => in_array($recordType, ['CNAME', 'SRV']), }; From c307aa3aa8c6971116d7b546245731fc532f3ed9 Mon Sep 17 00:00:00 2001 From: NebuloDev Date: Thu, 1 Oct 2026 16:14:30 +0200 Subject: [PATCH 8/9] Fix lint issues in subdomain validation Correct PHPStan null handling and tenant capture, and format the coexistence migration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../database/migrations/011_allow_compatible_record_types.php | 3 ++- subdomains/src/Enums/RecordType.php | 2 +- .../Filament/Server/Resources/Subdomains/SubdomainResource.php | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/subdomains/database/migrations/011_allow_compatible_record_types.php b/subdomains/database/migrations/011_allow_compatible_record_types.php index 5c4d8e91..fbf73515 100644 --- a/subdomains/database/migrations/011_allow_compatible_record_types.php +++ b/subdomains/database/migrations/011_allow_compatible_record_types.php @@ -1,5 +1,6 @@ string('record_identifier')->nullable()->after('record_type'); }); - \Boy132\Subdomains\Models\Subdomain::query()->with('server')->each(function ($subdomain) { + Subdomain::query()->with('server')->each(function (Subdomain $subdomain) { $subdomain->saveQuietly(); }); diff --git a/subdomains/src/Enums/RecordType.php b/subdomains/src/Enums/RecordType.php index d096fd1d..1eda3b00 100644 --- a/subdomains/src/Enums/RecordType.php +++ b/subdomains/src/Enums/RecordType.php @@ -26,7 +26,7 @@ public function uniqueIdentifier(Server $server): string return $this->value; } - return SRVServiceType::fromServer($server)?->value ?? 'SRV'; + return SRVServiceType::fromServer($server)->value ?? 'SRV'; } /** diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index 1d87ff70..8988b098 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -151,7 +151,7 @@ public static function form(Schema $schema): Schema ->required() ->unique( ignoreRecord: true, - modifyRuleUsing: function (Unique $rule, Get $get): Unique { + modifyRuleUsing: function (Unique $rule, Get $get) use ($server): Unique { $rule->where('domain_id', $get('domain_id')); if (in_array($get('record_type'), [RecordType::A->value, RecordType::AAAA->value, RecordType::CNAME->value])) { From 23eb9e4db2289289596e91f08226175c9d486e10 Mon Sep 17 00:00:00 2001 From: NebuloDev Date: Thu, 1 Oct 2026 16:23:52 +0200 Subject: [PATCH 9/9] Populate record identifiers during migration Backfill existing subdomains before enforcing the non-null effective record identifier and guard missing SRV service types. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../database/migrations/011_allow_compatible_record_types.php | 1 + subdomains/src/Enums/RecordType.php | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/subdomains/database/migrations/011_allow_compatible_record_types.php b/subdomains/database/migrations/011_allow_compatible_record_types.php index fbf73515..88c2ea63 100644 --- a/subdomains/database/migrations/011_allow_compatible_record_types.php +++ b/subdomains/database/migrations/011_allow_compatible_record_types.php @@ -16,6 +16,7 @@ public function up(): void }); Subdomain::query()->with('server')->each(function (Subdomain $subdomain) { + $subdomain->record_identifier = $subdomain->record_type->uniqueIdentifier($subdomain->server); $subdomain->saveQuietly(); }); diff --git a/subdomains/src/Enums/RecordType.php b/subdomains/src/Enums/RecordType.php index 1eda3b00..418f0d9f 100644 --- a/subdomains/src/Enums/RecordType.php +++ b/subdomains/src/Enums/RecordType.php @@ -26,7 +26,9 @@ public function uniqueIdentifier(Server $server): string return $this->value; } - return SRVServiceType::fromServer($server)->value ?? 'SRV'; + $serviceType = SRVServiceType::fromServer($server); + + return $serviceType ? $serviceType->value : 'SRV'; } /**