From 8121ab2cc8f59811412d46d326c6abd8b75d0b14 Mon Sep 17 00:00:00 2001 From: gavidroselj Date: Sat, 5 Sep 2026 13:52:06 +0000 Subject: [PATCH 01/16] Add allowed record type config --- .../007_add_allowed_record_types.php | 22 +++++++++++++++++++ subdomains/lang/de/strings.php | 1 + subdomains/lang/en/strings.php | 1 + .../CloudflareDomainResource.php | 18 +++++++++++++-- subdomains/src/Models/CloudflareDomain.php | 16 ++++++++++++++ 5 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 subdomains/database/migrations/007_add_allowed_record_types.php diff --git a/subdomains/database/migrations/007_add_allowed_record_types.php b/subdomains/database/migrations/007_add_allowed_record_types.php new file mode 100644 index 00000000..a058cf97 --- /dev/null +++ b/subdomains/database/migrations/007_add_allowed_record_types.php @@ -0,0 +1,22 @@ +json('allowed_record_types')->after('prefix'); + }); + } + + public function down(): void + { + Schema::table('cloudflare_domains', function (Blueprint $table) { + $table->dropColumn('allowed_record_types'); + }); + } +}; diff --git a/subdomains/lang/de/strings.php b/subdomains/lang/de/strings.php index a3751340..84eae1b0 100644 --- a/subdomains/lang/de/strings.php +++ b/subdomains/lang/de/strings.php @@ -15,6 +15,7 @@ 'name' => 'Name', 'prefix' => 'Präfix', 'record_type' => 'Record Typ', + 'allowed_record_types' => 'Zulässige Recordtypen', 'is_synced' => 'Ist synchronisiert?', 'subdomain_target' => 'Subdomain Ziel', 'no_subdomain_target' => 'Kein Subdomain Ziel', diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index e8956032..0efa11d7 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -15,6 +15,7 @@ 'name' => 'Name', 'prefix' => 'Prefix', 'record_type' => 'Record type', + 'allowed_record_types' => 'Allowed Record types', 'is_synced' => 'Is Synced?', 'subdomain_target' => 'Subdomain target', 'no_subdomain_target' => 'No Subdomain target', diff --git a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php index cf2fc742..cfb0ac93 100644 --- a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php +++ b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php @@ -2,12 +2,15 @@ namespace Boy132\Subdomains\Filament\Admin\Resources\CloudflareDomains; +use Boy132\Subdomains\Enums\RecordType; use Boy132\Subdomains\Filament\Admin\Resources\CloudflareDomains\Pages\ManageCloudflareDomains; use Boy132\Subdomains\Models\CloudflareDomain; use Exception; use Filament\Actions\Action; use Filament\Actions\CreateAction; use Filament\Actions\DeleteAction; +use Filament\Actions\EditAction; +use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Infolists\Components\TextEntry; use Filament\Notifications\Notification; @@ -61,6 +64,9 @@ public static function table(Table $table): Table TextColumn::make('subdomains_count') ->label(trans_choice('subdomains::strings.subdomain', 2)) ->counts('subdomains'), + TextColumn::make('allowed_record_types') + ->label(trans('subdomains::strings.allowed_record_types')) + ->badge(), IconColumn::make('is_synced') ->label(trans('subdomains::strings.is_synced')) ->state(fn (CloudflareDomain $domain) => !is_null($domain->cloudflare_id)) @@ -70,6 +76,7 @@ public static function table(Table $table): Table ->tooltip(fn (CloudflareDomain $domain) => $domain->cloudflare_id), ]) ->recordActions([ + EditAction::make('edit'), Action::make('sync') ->tooltip(trans('subdomains::strings.sync')) ->icon('tabler-refresh') @@ -123,9 +130,16 @@ public static function form(Schema $schema): Schema TextInput::make('name') ->label(trans('subdomains::strings.name')) ->required() - ->unique(), + ->unique() + ->disabledOn('edit'), TextInput::make('prefix') - ->label(trans('subdomains::strings.prefix')), + ->label(trans('subdomains::strings.prefix')) + ->disabledOn('edit'), + Select::make('allowed_record_types') + ->label(trans('subdomains::strings.allowed_record_types')) + ->options(RecordType::class) + ->multiple() + ->default(RecordType::cases()), ]); } diff --git a/subdomains/src/Models/CloudflareDomain.php b/subdomains/src/Models/CloudflareDomain.php index 90a0065f..2c4cb7ab 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -2,9 +2,12 @@ namespace Boy132\Subdomains\Models; +use Boy132\Subdomains\Enums\RecordType; use Exception; +use Illuminate\Database\Eloquent\Casts\AsEnumCollection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Http; /** @@ -12,6 +15,7 @@ * @property string $name * @property ?string $prefix * @property ?string $cloudflare_id + * @property Collection $allowed_record_types */ class CloudflareDomain extends Model { @@ -19,6 +23,7 @@ class CloudflareDomain extends Model 'name', 'prefix', 'cloudflare_id', + 'allowed_record_types', ]; protected static function boot(): void @@ -28,6 +33,17 @@ protected static function boot(): void static::created(function (self $model) { $model->fetchCloudflareId(); }); + + static::saving(function (self $model): void { + $model->allowed_record_types = $model->allowed_record_types->sort(); + }); + } + + protected function casts(): array + { + return [ + 'allowed_record_types' => AsEnumCollection::of(RecordType::class), + ]; } public function subdomains(): HasMany From a4c29509df7f406397b90bb48a50d4798bed0c66 Mon Sep 17 00:00:00 2001 From: gavidroselj Date: Sat, 5 Sep 2026 15:57:39 +0000 Subject: [PATCH 02/16] Enforce record type restrictions --- subdomains/src/Enums/RecordType.php | 33 ------------- .../SubdomainRelationManager.php | 10 ++-- .../Subdomains/SubdomainResource.php | 9 ++-- subdomains/src/Models/CloudflareDomain.php | 46 +++++++++++++++++++ subdomains/src/Models/Subdomain.php | 22 +++++++-- 5 files changed, 71 insertions(+), 49 deletions(-) diff --git a/subdomains/src/Enums/RecordType.php b/subdomains/src/Enums/RecordType.php index c164e63f..118768db 100644 --- a/subdomains/src/Enums/RecordType.php +++ b/subdomains/src/Enums/RecordType.php @@ -2,7 +2,6 @@ namespace Boy132\Subdomains\Enums; -use App\Models\Server; use Filament\Support\Contracts\HasLabel; enum RecordType: string implements HasLabel @@ -16,36 +15,4 @@ public function getLabel(): string { return $this->name; } - - /** - * @return array - */ - public static function availableRecordTypes(Server $server): array - { - // Explicitly forbid ANY record creation when primary allocation is invalid - if ($server->allocation && in_array($server->allocation->ip, ['0.0.0.0', '::'])) { - return []; - } - - $types = []; - - if ($server->allocation) { - if (is_ipv6($server->allocation->ip)) { - $types[self::AAAA->name] = self::AAAA->value; - } else { - $types[self::A->name] = self::A->value; - } - } - - // @phpstan-ignore property.notFound - if ($server->node->subdomain_target) { - $types[self::CNAME->name] = self::CNAME->value; - } - - if ($server->allocation && $server->node->subdomain_target) { - $types[self::SRV->name] = self::SRV->value; - } - - return $types; - } } diff --git a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php index c2ac85ec..7e5fb8b6 100644 --- a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php +++ b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php @@ -3,7 +3,6 @@ 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; @@ -18,6 +17,7 @@ use Filament\Notifications\Notification; use Filament\Resources\RelationManagers\RelationManager; use Filament\Schemas\Components\Utilities\Get; +use Filament\Schemas\Components\Utilities\Set; use Filament\Schemas\Schema; use Filament\Support\Exceptions\Halt; use Filament\Tables\Columns\TextColumn; @@ -84,8 +84,7 @@ public function table(Table $table): Table ->send(); }), CreateAction::make() - ->visible(fn () => CloudflareDomain::count() > 0) - ->disabled(fn () => count(RecordType::availableRecordTypes($this->getOwnerRecord())) <= 0) + ->visible(fn () => count(CloudflareDomain::availableDomains($this->getOwnerRecord())) > 0) ->createAnother(false) ->action(function (array $data, SubdomainService $service) { try { @@ -129,15 +128,14 @@ public function form(Schema $schema): Schema ->relationship('domain', 'name') ->preload() ->searchable() + ->afterStateUpdated(fn (Set $set) => $set('record_type', '')) ->live(), Select::make('record_type') ->label(trans('subdomains::strings.record_type')) ->disabledOn('edit') - ->disabled(fn () => count(RecordType::availableRecordTypes($this->getOwnerRecord())) <= 1) ->required() ->selectablePlaceholder(false) - ->options(RecordType::availableRecordTypes($this->getOwnerRecord())) - ->default(array_first(RecordType::availableRecordTypes($this->getOwnerRecord()))), + ->options(fn (Get $get) => CloudflareDomain::find($get('domain_id'))?->availableRecordTypes($this->getOwnerRecord())), ]); } } diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index 2cd73478..c7bf00f1 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -5,7 +5,6 @@ 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; @@ -21,6 +20,7 @@ use Filament\Notifications\Notification; use Filament\Resources\Resource; use Filament\Schemas\Components\Utilities\Get; +use Filament\Schemas\Components\Utilities\Set; use Filament\Schemas\Schema; use Filament\Support\Enums\IconSize; use Filament\Support\Exceptions\Halt; @@ -43,7 +43,7 @@ public static function canAccess(): bool /** @var Server $server */ $server = Filament::getTenant(); - return parent::canAccess() && CloudflareDomain::count() > 0 && count(RecordType::availableRecordTypes($server)) > 0; + return parent::canAccess() && count(CloudflareDomain::availableDomains($server)) > 0; } public static function getNavigationLabel(): string @@ -163,15 +163,14 @@ public static function form(Schema $schema): Schema ->relationship('domain', 'name') ->preload() ->searchable() + ->afterStateUpdated(fn (Get $get, Set $set) => $set('record_type', '')) ->live(), Select::make('record_type') ->label(trans('subdomains::strings.record_type')) ->disabledOn('edit') - ->disabled(fn () => count(RecordType::availableRecordTypes($server)) <= 1) ->required() ->selectablePlaceholder(false) - ->options(RecordType::availableRecordTypes($server)) - ->default(array_first(RecordType::availableRecordTypes($server))), + ->options(fn (Get $get) => CloudflareDomain::find($get('domain_id'))?->availableRecordTypes($server)), ]); } diff --git a/subdomains/src/Models/CloudflareDomain.php b/subdomains/src/Models/CloudflareDomain.php index 2c4cb7ab..7d8f11d6 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -2,6 +2,7 @@ namespace Boy132\Subdomains\Models; +use App\Models\Server; use Boy132\Subdomains\Enums\RecordType; use Exception; use Illuminate\Database\Eloquent\Casts\AsEnumCollection; @@ -85,4 +86,49 @@ public function fetchCloudflareId(): void } } } + + /** + * @return array + */ + public function availableRecordTypes(Server $server): array + { + $allocation = $server->allocation; + $subdomain_target = $server->node->subdomain_target; // @phpstan-ignore property.notFound + $allowed_record_types = $this->allowed_record_types; + + // Explicitly forbid ANY record creation when primary allocation is invalid + if ($allocation && in_array($allocation->ip, ['0.0.0.0', '::'])) { + return []; + } + + $types = []; + + if ($allowed_record_types->contains(RecordType::A) && $allocation && is_ipv4($allocation->ip)) { + $types[RecordType::A->name] = RecordType::A->value; + } + + if ($allowed_record_types->contains(RecordType::AAAA) && $allocation && is_ipv6($allocation->ip)) { + $types[RecordType::AAAA->name] = RecordType::AAAA->value; + } + + if ($allowed_record_types->contains(RecordType::CNAME) && $subdomain_target) { + $types[RecordType::CNAME->name] = RecordType::CNAME->value; + } + + if ($allowed_record_types->contains(RecordType::SRV) && $allocation && $subdomain_target) { + $types[RecordType::SRV->name] = RecordType::SRV->value; + } + + return $types; + } + + /** + * @return self[] + */ + public static function availableDomains(Server $server): array + { + $domains = self::get(); + + return $domains->all(); + } } diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index e0874309..4c6a33a5 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\RecordType; use Boy132\Subdomains\Enums\SRVServiceType; use Exception; use Filament\Support\Contracts\HasLabel; @@ -14,7 +15,7 @@ /** * @property int $id * @property string $name - * @property string $record_type + * @property RecordType $record_type * @property ?string $cloudflare_id * @property int $domain_id * @property CloudflareDomain $domain @@ -40,6 +41,13 @@ protected static function boot(): void }); } + protected function casts(): array + { + return [ + 'record_type' => RecordType::class, + ]; + } + public function domain(): BelongsTo { return $this->belongsTo(CloudflareDomain::class, 'domain_id'); @@ -65,8 +73,12 @@ public function upsertOnCloudflare(): void $subdomainTarget = $this->server->node->subdomain_target; // @phpstan-ignore property.notFound + if (!$this->domain->allowed_record_types->contains($this->record_type)) { + throw new Exception('Record type ' . $this->record_type->value . ' is not permitted on domain ' . $this->domain->name); + } + switch ($this->record_type) { - case 'SRV': + case RecordType::SRV: if (!$this->server->allocation) { throw new Exception('Server has no allocation'); } @@ -97,7 +109,7 @@ public function upsertOnCloudflare(): void ]; break; - case 'CNAME': + case RecordType::CNAME: if (!$subdomainTarget) { throw new Exception('Node has no Subdomain target'); } @@ -113,8 +125,8 @@ public function upsertOnCloudflare(): void ]; break; - case 'A': - case 'AAAA': + case RecordType::A: + case RecordType::AAAA: if (!$this->server->allocation) { throw new Exception('Server has no allocation'); } From 69aa99453d13b21610c84891461cb255641ffd51 Mon Sep 17 00:00:00 2001 From: gavidroselj Date: Tue, 8 Sep 2026 08:04:10 +0000 Subject: [PATCH 03/16] Compound name prefix uniqueness --- ...make_compound_domain_unique_constraint.php | 25 +++++++++++++++++++ .../CloudflareDomainResource.php | 9 ++++++- subdomains/src/Models/Subdomain.php | 2 +- 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 subdomains/database/migrations/008_make_compound_domain_unique_constraint.php diff --git a/subdomains/database/migrations/008_make_compound_domain_unique_constraint.php b/subdomains/database/migrations/008_make_compound_domain_unique_constraint.php new file mode 100644 index 00000000..8d00852e --- /dev/null +++ b/subdomains/database/migrations/008_make_compound_domain_unique_constraint.php @@ -0,0 +1,25 @@ +dropUnique(['name']); + $table->unique(['name', 'prefix']); + }); + } + + public function down(): void + { + Schema::table('cloudflare_domains', function (Blueprint $table) { + $table->dropUnique(['name', 'prefix']); + + // Unique on 'name' only not restorable since it's a stricter requirement + }); + } +}; diff --git a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php index cfb0ac93..2251790f 100644 --- a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php +++ b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php @@ -15,10 +15,12 @@ use Filament\Infolists\Components\TextEntry; use Filament\Notifications\Notification; use Filament\Resources\Resource; +use Filament\Schemas\Components\Utilities\Get; use Filament\Schemas\Schema; use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; +use Illuminate\Validation\Rules\Unique; class CloudflareDomainResource extends Resource { @@ -130,10 +132,15 @@ 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('name', $get('name')) + ->where('prefix', $get('prefix'))) ->disabledOn('edit'), TextInput::make('prefix') ->label(trans('subdomains::strings.prefix')) + ->unique(ignoreRecord: true, modifyRuleUsing: fn (Unique $rule, Get $get) => $rule + ->where('name', $get('name')) + ->where('prefix', $get('prefix'))) ->disabledOn('edit'), Select::make('allowed_record_types') ->label(trans('subdomains::strings.allowed_record_types')) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 4c6a33a5..c934e28a 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -74,7 +74,7 @@ public function upsertOnCloudflare(): void $subdomainTarget = $this->server->node->subdomain_target; // @phpstan-ignore property.notFound if (!$this->domain->allowed_record_types->contains($this->record_type)) { - throw new Exception('Record type ' . $this->record_type->value . ' is not permitted on domain ' . $this->domain->name); + throw new Exception('Record type ' . $this->record_type->value . ' is not permitted on domain ' . $this->domain->nameWithPrefix()); } switch ($this->record_type) { From 93beff0dc756f32a07fcd4c6f7f4781a3a804df1 Mon Sep 17 00:00:00 2001 From: gavidroselj Date: Tue, 8 Sep 2026 09:50:36 +0000 Subject: [PATCH 04/16] Add domain node restrictions --- .../migrations/009_add_domain_nodes_table.php | 28 +++++++++++++++++++ subdomains/lang/de/strings.php | 1 + subdomains/lang/en/strings.php | 1 + .../CloudflareDomainResource.php | 10 +++++++ .../SubdomainRelationManager.php | 5 ++-- .../Subdomains/SubdomainResource.php | 6 ++-- subdomains/src/Models/CloudflareDomain.php | 16 +++++++---- 7 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 subdomains/database/migrations/009_add_domain_nodes_table.php diff --git a/subdomains/database/migrations/009_add_domain_nodes_table.php b/subdomains/database/migrations/009_add_domain_nodes_table.php new file mode 100644 index 00000000..27fa4ded --- /dev/null +++ b/subdomains/database/migrations/009_add_domain_nodes_table.php @@ -0,0 +1,28 @@ +unsignedInteger('node_id'); + $table->foreign('node_id')->references('id')->on('nodes')->cascadeOnDelete(); + + $table->unsignedInteger('cloudflare_domain_id'); + $table->foreign('cloudflare_domain_id')->references('id')->on('cloudflare_domains')->cascadeOnDelete(); + + $table->timestamps(); + + $table->unique(['node_id', 'cloudflare_domain_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('cloudflare_domain_node'); + } +}; diff --git a/subdomains/lang/de/strings.php b/subdomains/lang/de/strings.php index 84eae1b0..c346f8bb 100644 --- a/subdomains/lang/de/strings.php +++ b/subdomains/lang/de/strings.php @@ -16,6 +16,7 @@ 'prefix' => 'Präfix', 'record_type' => 'Record Typ', 'allowed_record_types' => 'Zulässige Recordtypen', + 'allowed_nodes' => 'Zulässige Nodes', 'is_synced' => 'Ist synchronisiert?', 'subdomain_target' => 'Subdomain Ziel', 'no_subdomain_target' => 'Kein Subdomain Ziel', diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index 0efa11d7..0eeb6694 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -16,6 +16,7 @@ 'prefix' => 'Prefix', 'record_type' => 'Record type', 'allowed_record_types' => 'Allowed Record types', + 'allowed_nodes' => 'Allowed Nodes', 'is_synced' => 'Is Synced?', 'subdomain_target' => 'Subdomain target', 'no_subdomain_target' => 'No Subdomain target', diff --git a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php index 2251790f..a49879bf 100644 --- a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php +++ b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php @@ -20,6 +20,7 @@ use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Validation\Rules\Unique; class CloudflareDomainResource extends Resource @@ -69,6 +70,9 @@ public static function table(Table $table): Table TextColumn::make('allowed_record_types') ->label(trans('subdomains::strings.allowed_record_types')) ->badge(), + TextColumn::make('nodes.name') + ->label(trans('subdomains::strings.allowed_nodes')) + ->badge(), IconColumn::make('is_synced') ->label(trans('subdomains::strings.is_synced')) ->state(fn (CloudflareDomain $domain) => !is_null($domain->cloudflare_id)) @@ -147,6 +151,12 @@ public static function form(Schema $schema): Schema ->options(RecordType::class) ->multiple() ->default(RecordType::cases()), + Select::make('allowed_nodes') + ->label(trans('subdomains::strings.allowed_nodes')) + ->multiple() + ->searchable() + ->preload() + ->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', user()?->accessibleNodes()->pluck('id'))), ]); } diff --git a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php index 7e5fb8b6..ef390543 100644 --- a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php +++ b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php @@ -120,12 +120,13 @@ public function form(Schema $schema): Schema Select::make('domain_id') ->label(trans_choice('subdomains::strings.domain', 1)) ->disabledOn('edit') - ->hidden(fn () => CloudflareDomain::count() <= 1) + ->disabled(fn () => CloudflareDomain::availableDomains($this->getOwnerRecord())->count() <= 1) ->dehydratedWhenHidden() ->required() ->selectablePlaceholder(false) - ->default(fn () => CloudflareDomain::first()?->id) ->relationship('domain', 'name') + ->options(CloudflareDomain::availableDomains($this->getOwnerRecord())->mapWithKeys(fn ($domain) => [$domain->id => $domain->nameWithPrefix()])) + ->default(CloudflareDomain::availableDomains($this->getOwnerRecord())->first()->id) ->preload() ->searchable() ->afterStateUpdated(fn (Set $set) => $set('record_type', '')) diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index c7bf00f1..e3b9db03 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -155,12 +155,12 @@ public static function form(Schema $schema): Schema Select::make('domain_id') ->label(trans_choice('subdomains::strings.domain', 1)) ->disabledOn('edit') - ->hidden(fn () => CloudflareDomain::count() <= 1) + ->disabled(fn () => CloudflareDomain::availableDomains($server)->count() <= 1) ->dehydratedWhenHidden() ->required() ->selectablePlaceholder(false) - ->default(fn () => CloudflareDomain::first()?->id) - ->relationship('domain', 'name') + ->options(CloudflareDomain::availableDomains($server)->mapWithKeys(fn ($domain) => [$domain->id => $domain->nameWithPrefix()])) + ->default(CloudflareDomain::availableDomains($server)->first()->id) ->preload() ->searchable() ->afterStateUpdated(fn (Get $get, Set $set) => $set('record_type', '')) diff --git a/subdomains/src/Models/CloudflareDomain.php b/subdomains/src/Models/CloudflareDomain.php index 7d8f11d6..6fa4c0b3 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -2,11 +2,13 @@ namespace Boy132\Subdomains\Models; +use App\Models\Node; use App\Models\Server; use Boy132\Subdomains\Enums\RecordType; use Exception; use Illuminate\Database\Eloquent\Casts\AsEnumCollection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Http; @@ -16,6 +18,7 @@ * @property string $name * @property ?string $prefix * @property ?string $cloudflare_id + * @property Collection|Node[] $nodes * @property Collection $allowed_record_types */ class CloudflareDomain extends Model @@ -52,6 +55,11 @@ public function subdomains(): HasMany return $this->hasMany(Subdomain::class, 'domain_id'); } + public function nodes(): BelongsToMany + { + return $this->belongsToMany(Node::class); + } + public function nameWithPrefix(): string { return is_null($this->prefix) ? $this->name : "$this->prefix.$this->name"; @@ -123,12 +131,10 @@ public function availableRecordTypes(Server $server): array } /** - * @return self[] + * @return Collection */ - public static function availableDomains(Server $server): array + public static function availableDomains(Server $server): Collection { - $domains = self::get(); - - return $domains->all(); + return $server->node->belongsToMany(self::class)->get(); } } From 1b06b0c17fb8b3fa6db7ea91d7d85e8a4018ff1c Mon Sep 17 00:00:00 2001 From: gavidroselj Date: Tue, 8 Sep 2026 10:05:32 +0000 Subject: [PATCH 05/16] Use collections for RecordType --- .../SubdomainRelationManager.php | 2 +- .../Resources/Subdomains/SubdomainResource.php | 2 +- subdomains/src/Models/CloudflareDomain.php | 17 +++++++++-------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php index ef390543..3d901ff7 100644 --- a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php +++ b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php @@ -136,7 +136,7 @@ public function form(Schema $schema): Schema ->disabledOn('edit') ->required() ->selectablePlaceholder(false) - ->options(fn (Get $get) => CloudflareDomain::find($get('domain_id'))?->availableRecordTypes($this->getOwnerRecord())), + ->options(fn (Get $get) => CloudflareDomain::find($get('domain_id'))?->availableRecordTypes($this->getOwnerRecord())->pluck('name', 'value')), ]); } } diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index e3b9db03..31a49771 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -170,7 +170,7 @@ public static function form(Schema $schema): Schema ->disabledOn('edit') ->required() ->selectablePlaceholder(false) - ->options(fn (Get $get) => CloudflareDomain::find($get('domain_id'))?->availableRecordTypes($server)), + ->options(fn (Get $get) => CloudflareDomain::find($get('domain_id'))?->availableRecordTypes($server)->pluck('name', 'value')), ]); } diff --git a/subdomains/src/Models/CloudflareDomain.php b/subdomains/src/Models/CloudflareDomain.php index 6fa4c0b3..d9d9cbbe 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -96,35 +96,36 @@ public function fetchCloudflareId(): void } /** - * @return array + * @return Collection */ - public function availableRecordTypes(Server $server): array + public function availableRecordTypes(Server $server): Collection { $allocation = $server->allocation; $subdomain_target = $server->node->subdomain_target; // @phpstan-ignore property.notFound $allowed_record_types = $this->allowed_record_types; + $types = new Collection(); + // Explicitly forbid ANY record creation when primary allocation is invalid if ($allocation && in_array($allocation->ip, ['0.0.0.0', '::'])) { - return []; + return $types; } - $types = []; if ($allowed_record_types->contains(RecordType::A) && $allocation && is_ipv4($allocation->ip)) { - $types[RecordType::A->name] = RecordType::A->value; + $types->add(RecordType::A); } if ($allowed_record_types->contains(RecordType::AAAA) && $allocation && is_ipv6($allocation->ip)) { - $types[RecordType::AAAA->name] = RecordType::AAAA->value; + $types->add(RecordType::AAAA); } if ($allowed_record_types->contains(RecordType::CNAME) && $subdomain_target) { - $types[RecordType::CNAME->name] = RecordType::CNAME->value; + $types->add(RecordType::CNAME); } if ($allowed_record_types->contains(RecordType::SRV) && $allocation && $subdomain_target) { - $types[RecordType::SRV->name] = RecordType::SRV->value; + $types->add(RecordType::SRV); } return $types; From 99263871fd9c9d20b2afdc6dff557e8bdaa7cc93 Mon Sep 17 00:00:00 2001 From: gavidroselj Date: Tue, 8 Sep 2026 10:08:52 +0000 Subject: [PATCH 06/16] Fix saving --- .../Servers/RelationManagers/SubdomainRelationManager.php | 4 ++-- .../Server/Resources/Subdomains/SubdomainResource.php | 2 +- subdomains/src/Models/CloudflareDomain.php | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php index 3d901ff7..86b14116 100644 --- a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php +++ b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php @@ -121,7 +121,7 @@ public function form(Schema $schema): Schema ->label(trans_choice('subdomains::strings.domain', 1)) ->disabledOn('edit') ->disabled(fn () => CloudflareDomain::availableDomains($this->getOwnerRecord())->count() <= 1) - ->dehydratedWhenHidden() + ->saved() ->required() ->selectablePlaceholder(false) ->relationship('domain', 'name') @@ -136,7 +136,7 @@ public function form(Schema $schema): Schema ->disabledOn('edit') ->required() ->selectablePlaceholder(false) - ->options(fn (Get $get) => CloudflareDomain::find($get('domain_id'))?->availableRecordTypes($this->getOwnerRecord())->pluck('name', 'value')), + ->options(fn (Get $get) => CloudflareDomain::find($get('domain_id'))?->availableRecordTypes($this->getOwnerRecord())), ]); } } diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index 31a49771..e61dddec 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -156,7 +156,7 @@ public static function form(Schema $schema): Schema ->label(trans_choice('subdomains::strings.domain', 1)) ->disabledOn('edit') ->disabled(fn () => CloudflareDomain::availableDomains($server)->count() <= 1) - ->dehydratedWhenHidden() + ->saved() ->required() ->selectablePlaceholder(false) ->options(CloudflareDomain::availableDomains($server)->mapWithKeys(fn ($domain) => [$domain->id => $domain->nameWithPrefix()])) diff --git a/subdomains/src/Models/CloudflareDomain.php b/subdomains/src/Models/CloudflareDomain.php index d9d9cbbe..edc2fccc 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -111,7 +111,6 @@ public function availableRecordTypes(Server $server): Collection return $types; } - if ($allowed_record_types->contains(RecordType::A) && $allocation && is_ipv4($allocation->ip)) { $types->add(RecordType::A); } From 183e638f3986d89b17872b7ebb0e99c344d44281 Mon Sep 17 00:00:00 2001 From: gavidroselj Date: Tue, 8 Sep 2026 10:18:38 +0000 Subject: [PATCH 07/16] Update RecordType defaults on domain change --- .../Servers/RelationManagers/SubdomainRelationManager.php | 7 +++++-- .../Server/Resources/Subdomains/SubdomainResource.php | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php index 86b14116..303b7576 100644 --- a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php +++ b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php @@ -129,14 +129,17 @@ public function form(Schema $schema): Schema ->default(CloudflareDomain::availableDomains($this->getOwnerRecord())->first()->id) ->preload() ->searchable() - ->afterStateUpdated(fn (Set $set) => $set('record_type', '')) + ->afterStateUpdated(fn (Get $get, Set $set) => $set('record_type', CloudflareDomain::find($get('domain_id'))?->availableRecordTypes($this->getOwnerRecord())->first())) ->live(), Select::make('record_type') ->label(trans('subdomains::strings.record_type')) ->disabledOn('edit') + ->disabled(fn (Get $get) => CloudflareDomain::find($get('domain_id'))?->availableRecordTypes($this->getOwnerRecord())->count() <= 1) + ->saved() ->required() ->selectablePlaceholder(false) - ->options(fn (Get $get) => CloudflareDomain::find($get('domain_id'))?->availableRecordTypes($this->getOwnerRecord())), + ->options(fn (Get $get) => CloudflareDomain::find($get('domain_id'))?->availableRecordTypes($this->getOwnerRecord())->pluck('name', 'value')) + ->default(fn (Get $get) => CloudflareDomain::find($get('domain_id'))?->availableRecordTypes($this->getOwnerRecord())->first()), ]); } } diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index e61dddec..e8fb25b5 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -163,14 +163,17 @@ public static function form(Schema $schema): Schema ->default(CloudflareDomain::availableDomains($server)->first()->id) ->preload() ->searchable() - ->afterStateUpdated(fn (Get $get, Set $set) => $set('record_type', '')) + ->afterStateUpdated(fn (Get $get, Set $set) => $set('record_type', CloudflareDomain::find($get('domain_id'))?->availableRecordTypes($server)->first())) ->live(), Select::make('record_type') ->label(trans('subdomains::strings.record_type')) ->disabledOn('edit') + ->disabled(fn (Get $get) => CloudflareDomain::find($get('domain_id'))?->availableRecordTypes($server)->count() <= 1) + ->saved() ->required() ->selectablePlaceholder(false) - ->options(fn (Get $get) => CloudflareDomain::find($get('domain_id'))?->availableRecordTypes($server)->pluck('name', 'value')), + ->options(fn (Get $get) => CloudflareDomain::find($get('domain_id'))?->availableRecordTypes($server)->pluck('name', 'value')) + ->default(fn (Get $get) => CloudflareDomain::find($get('domain_id'))?->availableRecordTypes($server)->first()), ]); } From 73967951ba8d786f1c787894f0864cbebac6f3ae Mon Sep 17 00:00:00 2001 From: gavidroselj Date: Tue, 8 Sep 2026 10:24:09 +0000 Subject: [PATCH 08/16] Filter domains with no available records --- subdomains/src/Models/CloudflareDomain.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/subdomains/src/Models/CloudflareDomain.php b/subdomains/src/Models/CloudflareDomain.php index edc2fccc..120a37a2 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -135,6 +135,10 @@ public function availableRecordTypes(Server $server): Collection */ public static function availableDomains(Server $server): Collection { - return $server->node->belongsToMany(self::class)->get(); + $viableDomains = $server->node->belongsToMany(self::class)->get(); + + $availableDomains = $viableDomains->filter(fn (self $item) => !$item->availableRecordTypes($server)->isEmpty()); + + return $availableDomains; } } From 6d40627e99236c95e8087005443f29f2481bb5f2 Mon Sep 17 00:00:00 2001 From: gavidroselj Date: Tue, 8 Sep 2026 11:34:43 +0000 Subject: [PATCH 09/16] Add node check to upsert flow --- subdomains/src/Models/Subdomain.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index c934e28a..ce587cb8 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -72,6 +72,11 @@ public function upsertOnCloudflare(): void } $subdomainTarget = $this->server->node->subdomain_target; // @phpstan-ignore property.notFound + $node_id = $this->server->node->id; + + if (!$this->domain->nodes()->where('nodes.id', $node_id)->exists()) { + throw new Exception('Domain ' . $this->domain->nameWithPrefix() . ' is not permitted on node ' . $this->server->node->name); + } if (!$this->domain->allowed_record_types->contains($this->record_type)) { throw new Exception('Record type ' . $this->record_type->value . ' is not permitted on domain ' . $this->domain->nameWithPrefix()); From 9bf4c0f81e225df8ad7fe4a0002062ef7e90bc17 Mon Sep 17 00:00:00 2001 From: gavidroselj Date: Tue, 8 Sep 2026 11:49:41 +0000 Subject: [PATCH 10/16] Update documentation --- subdomains/README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/subdomains/README.md b/subdomains/README.md index 77b4d0ad..1044a336 100644 --- a/subdomains/README.md +++ b/subdomains/README.md @@ -15,11 +15,18 @@ Each domain is composed of a name and an optional prefix. The name must be a val For example: when creating a subdomain `server1` on a domain with name `example.com` and prefix `abc`, the created record will be `server1.abc.example.com`. +#### Domain restrictions + +Domains can be configured to only permit subdomain creation under specific conditions: + +- For each domain you must select which DNS Record types can be created on it +- For each domain you must select the nodes on which it is enabled. Servers on unselected nodes will not have the option to use this domain. + ## Configuration Subdomains support several different DNS Record types. Each type has different requirements before it can be created. -If a DNS Record type is not available, check whether all of it's requirements have been met. +If a DNS Record type is not available, check whether it is enabled on the domain and whether all of it's requirements have been met. ### Server primary allocations From ec7a0ac7feae2cb8c518eb9f6ffac23037decfdf Mon Sep 17 00:00:00 2001 From: gavidroselj Date: Tue, 8 Sep 2026 13:27:36 +0000 Subject: [PATCH 11/16] Rabbit fixes --- .../migrations/007_add_allowed_record_types.php | 2 +- ...8_make_compound_domain_unique_constraint.php | 17 ++++++++++++++++- .../CloudflareDomainResource.php | 7 ++++--- subdomains/src/Models/CloudflareDomain.php | 6 +++--- subdomains/src/Models/Subdomain.php | 6 +++--- 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/subdomains/database/migrations/007_add_allowed_record_types.php b/subdomains/database/migrations/007_add_allowed_record_types.php index a058cf97..dd08ccf4 100644 --- a/subdomains/database/migrations/007_add_allowed_record_types.php +++ b/subdomains/database/migrations/007_add_allowed_record_types.php @@ -9,7 +9,7 @@ public function up(): void { Schema::table('cloudflare_domains', function (Blueprint $table) { - $table->json('allowed_record_types')->after('prefix'); + $table->json('allowed_record_types')->after('prefix')->default('["A","AAAA","CNAME","SRV"]'); }); } diff --git a/subdomains/database/migrations/008_make_compound_domain_unique_constraint.php b/subdomains/database/migrations/008_make_compound_domain_unique_constraint.php index 8d00852e..358fcc2c 100644 --- a/subdomains/database/migrations/008_make_compound_domain_unique_constraint.php +++ b/subdomains/database/migrations/008_make_compound_domain_unique_constraint.php @@ -8,7 +8,11 @@ { public function up(): void { + DB::table('cloudflare_domains')->whereNull('prefix')->update(['prefix' => '']); + Schema::table('cloudflare_domains', function (Blueprint $table) { + $table->string('prefix')->default('')->nullable(false)->change(); + $table->dropUnique(['name']); $table->unique(['name', 'prefix']); }); @@ -16,10 +20,21 @@ public function up(): void public function down(): void { + // Deduplicate domain names + $uniqueDomainIds = DB::table('cloudflare_domains') + ->groupBy('name') + ->select(DB::raw('MIN(id) as id')) + ->pluck('id'); + + DB::table('cloudflare_domains') + ->whereNotIn('id', $uniqueDomainIds) + ->update(['name' => DB::raw("CONCAT(name, '-', id)")]); + Schema::table('cloudflare_domains', function (Blueprint $table) { $table->dropUnique(['name', 'prefix']); + $table->unique('name'); - // Unique on 'name' only not restorable since it's a stricter requirement + $table->string('prefix')->nullable()->change(); }); } }; diff --git a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php index a49879bf..bb4b1e2f 100644 --- a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php +++ b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php @@ -138,14 +138,15 @@ public static function form(Schema $schema): Schema ->required() ->unique(ignoreRecord: true, modifyRuleUsing: fn (Unique $rule, Get $get) => $rule ->where('name', $get('name')) - ->where('prefix', $get('prefix'))) + ->where('prefix', is_null($get('prefix')) ? '' : $get('prefix'))) ->disabledOn('edit'), TextInput::make('prefix') ->label(trans('subdomains::strings.prefix')) ->unique(ignoreRecord: true, modifyRuleUsing: fn (Unique $rule, Get $get) => $rule ->where('name', $get('name')) - ->where('prefix', $get('prefix'))) - ->disabledOn('edit'), + ->where('prefix', is_null($get('prefix')) ? '' : $get('prefix'))) + ->disabledOn('edit') + ->dehydrateStateUsing(fn ($state) => is_null($state) ? '' : $state), Select::make('allowed_record_types') ->label(trans('subdomains::strings.allowed_record_types')) ->options(RecordType::class) diff --git a/subdomains/src/Models/CloudflareDomain.php b/subdomains/src/Models/CloudflareDomain.php index 120a37a2..f07d7ec0 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -16,7 +16,7 @@ /** * @property int $id * @property string $name - * @property ?string $prefix + * @property string $prefix * @property ?string $cloudflare_id * @property Collection|Node[] $nodes * @property Collection $allowed_record_types @@ -62,12 +62,12 @@ public function nodes(): BelongsToMany public function nameWithPrefix(): string { - return is_null($this->prefix) ? $this->name : "$this->prefix.$this->name"; + return $this->prefix == '' ? $this->name : "$this->prefix.$this->name"; } public function prependPrefix(string $subdomain): string { - return is_null($this->prefix) ? $subdomain : "$subdomain.$this->prefix"; + return $this->prefix == '' ? $subdomain : "$subdomain.$this->prefix"; } /** @throws Exception */ diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index ce587cb8..a234ede1 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -102,7 +102,7 @@ public function upsertOnCloudflare(): void $payload = [ 'name' => $searchName, - 'type' => $this->record_type, + 'type' => $this->record_type->value, 'comment' => 'Created by Pelican Subdomains plugin', 'data' => [ 'port' => $this->server->allocation->port, @@ -123,7 +123,7 @@ public function upsertOnCloudflare(): void $payload = [ 'name' => $searchName, - 'type' => $this->record_type, + 'type' => $this->record_type->value, 'comment' => 'Created by Pelican Subdomains plugin', 'content' => $subdomainTarget, 'proxied' => false, @@ -140,7 +140,7 @@ public function upsertOnCloudflare(): void $payload = [ 'name' => $searchName, - 'type' => $this->record_type, + 'type' => $this->record_type->value, 'comment' => 'Created by Pelican Subdomains plugin', 'content' => $this->server->allocation->ip, 'proxied' => false, From 9452960991badcf3be20298c6fd15f9e28f7a040 Mon Sep 17 00:00:00 2001 From: gavidroselj Date: Tue, 8 Sep 2026 13:44:56 +0000 Subject: [PATCH 12/16] More fixes --- .../008_make_compound_domain_unique_constraint.php | 2 +- subdomains/src/Models/CloudflareDomain.php | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/subdomains/database/migrations/008_make_compound_domain_unique_constraint.php b/subdomains/database/migrations/008_make_compound_domain_unique_constraint.php index 358fcc2c..89a58fe0 100644 --- a/subdomains/database/migrations/008_make_compound_domain_unique_constraint.php +++ b/subdomains/database/migrations/008_make_compound_domain_unique_constraint.php @@ -28,7 +28,7 @@ public function down(): void DB::table('cloudflare_domains') ->whereNotIn('id', $uniqueDomainIds) - ->update(['name' => DB::raw("CONCAT(name, '-', id)")]); + ->update(['name' => DB::raw("CONCAT(name, '_', id)")]); Schema::table('cloudflare_domains', function (Blueprint $table) { $table->dropUnique(['name', 'prefix']); diff --git a/subdomains/src/Models/CloudflareDomain.php b/subdomains/src/Models/CloudflareDomain.php index f07d7ec0..63dd1311 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -5,6 +5,7 @@ use App\Models\Node; use App\Models\Server; use Boy132\Subdomains\Enums\RecordType; +use Boy132\Subdomains\Enums\SRVServiceType; use Exception; use Illuminate\Database\Eloquent\Casts\AsEnumCollection; use Illuminate\Database\Eloquent\Model; @@ -101,8 +102,9 @@ public function fetchCloudflareId(): void public function availableRecordTypes(Server $server): Collection { $allocation = $server->allocation; - $subdomain_target = $server->node->subdomain_target; // @phpstan-ignore property.notFound - $allowed_record_types = $this->allowed_record_types; + $subdomainTarget = $server->node->subdomain_target; // @phpstan-ignore property.notFound + $allowedRecordTypes = $this->allowed_record_types; + $srvServiceType = SRVServiceType::fromServer($this->server); $types = new Collection(); @@ -111,19 +113,19 @@ public function availableRecordTypes(Server $server): Collection return $types; } - if ($allowed_record_types->contains(RecordType::A) && $allocation && is_ipv4($allocation->ip)) { + if ($allowedRecordTypes->contains(RecordType::A) && $allocation && is_ipv4($allocation->ip)) { $types->add(RecordType::A); } - if ($allowed_record_types->contains(RecordType::AAAA) && $allocation && is_ipv6($allocation->ip)) { + if ($allowedRecordTypes->contains(RecordType::AAAA) && $allocation && is_ipv6($allocation->ip)) { $types->add(RecordType::AAAA); } - if ($allowed_record_types->contains(RecordType::CNAME) && $subdomain_target) { + if ($allowedRecordTypes->contains(RecordType::CNAME) && $subdomainTarget) { $types->add(RecordType::CNAME); } - if ($allowed_record_types->contains(RecordType::SRV) && $allocation && $subdomain_target) { + if ($allowedRecordTypes->contains(RecordType::SRV) && $allocation && $subdomainTarget && $srvServiceType) { $types->add(RecordType::SRV); } From 71694b391bb2901a8e52a953acdafe40a02393e9 Mon Sep 17 00:00:00 2001 From: gavidroselj Date: Tue, 8 Sep 2026 13:46:21 +0000 Subject: [PATCH 13/16] Typo --- subdomains/src/Models/CloudflareDomain.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subdomains/src/Models/CloudflareDomain.php b/subdomains/src/Models/CloudflareDomain.php index 63dd1311..2d5afe19 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -104,7 +104,7 @@ public function availableRecordTypes(Server $server): Collection $allocation = $server->allocation; $subdomainTarget = $server->node->subdomain_target; // @phpstan-ignore property.notFound $allowedRecordTypes = $this->allowed_record_types; - $srvServiceType = SRVServiceType::fromServer($this->server); + $srvServiceType = SRVServiceType::fromServer($server); $types = new Collection(); From 8de795e6ccc95f0e77f8d810a91882c29a6fa7de Mon Sep 17 00:00:00 2001 From: gavidroselj Date: Fri, 11 Sep 2026 08:00:14 +0000 Subject: [PATCH 14/16] Disable restrictions on empty --- subdomains/README.md | 6 ++++-- .../CloudflareDomainResource.php | 3 +-- subdomains/src/Models/CloudflareDomain.php | 15 ++++++++++----- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/subdomains/README.md b/subdomains/README.md index 1044a336..59450f80 100644 --- a/subdomains/README.md +++ b/subdomains/README.md @@ -19,8 +19,10 @@ For example: when creating a subdomain `server1` on a domain with name `example. Domains can be configured to only permit subdomain creation under specific conditions: -- For each domain you must select which DNS Record types can be created on it -- For each domain you must select the nodes on which it is enabled. Servers on unselected nodes will not have the option to use this domain. +- For each domain you can select which DNS Record types can be created on it +- For each domain you can select the nodes on which it is enabled. Servers on unselected nodes will not have the option to use this domain. + +Leaving these fields empty will keep all record types / nodes enabled. ## Configuration diff --git a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php index bb4b1e2f..6cad8866 100644 --- a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php +++ b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php @@ -150,8 +150,7 @@ public static function form(Schema $schema): Schema Select::make('allowed_record_types') ->label(trans('subdomains::strings.allowed_record_types')) ->options(RecordType::class) - ->multiple() - ->default(RecordType::cases()), + ->multiple(), Select::make('allowed_nodes') ->label(trans('subdomains::strings.allowed_nodes')) ->multiple() diff --git a/subdomains/src/Models/CloudflareDomain.php b/subdomains/src/Models/CloudflareDomain.php index 2d5afe19..0f3e3c08 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -104,6 +104,7 @@ public function availableRecordTypes(Server $server): Collection $allocation = $server->allocation; $subdomainTarget = $server->node->subdomain_target; // @phpstan-ignore property.notFound $allowedRecordTypes = $this->allowed_record_types; + $allowedRecordsFilterDisabled = $allowedRecordTypes->isEmpty(); $srvServiceType = SRVServiceType::fromServer($server); $types = new Collection(); @@ -113,19 +114,19 @@ public function availableRecordTypes(Server $server): Collection return $types; } - if ($allowedRecordTypes->contains(RecordType::A) && $allocation && is_ipv4($allocation->ip)) { + if (($allowedRecordsFilterDisabled || $allowedRecordTypes->contains(RecordType::A)) && $allocation && is_ipv4($allocation->ip)) { $types->add(RecordType::A); } - if ($allowedRecordTypes->contains(RecordType::AAAA) && $allocation && is_ipv6($allocation->ip)) { + if (($allowedRecordsFilterDisabled || $allowedRecordTypes->contains(RecordType::AAAA)) && $allocation && is_ipv6($allocation->ip)) { $types->add(RecordType::AAAA); } - if ($allowedRecordTypes->contains(RecordType::CNAME) && $subdomainTarget) { + if (($allowedRecordsFilterDisabled || $allowedRecordTypes->contains(RecordType::CNAME)) && $subdomainTarget) { $types->add(RecordType::CNAME); } - if ($allowedRecordTypes->contains(RecordType::SRV) && $allocation && $subdomainTarget && $srvServiceType) { + if (($allowedRecordsFilterDisabled || $allowedRecordTypes->contains(RecordType::SRV)) && $allocation && $subdomainTarget && $srvServiceType) { $types->add(RecordType::SRV); } @@ -137,7 +138,11 @@ public function availableRecordTypes(Server $server): Collection */ public static function availableDomains(Server $server): Collection { - $viableDomains = $server->node->belongsToMany(self::class)->get(); + // Fetch all domains with this allowed node, or with no allowed nodes + $viableDomains = CloudflareDomain::query() + ->whereHas('nodes', fn ($query) => $query->whereKey($server->node->id)) + ->orWhereDoesntHave('nodes') + ->get(); $availableDomains = $viableDomains->filter(fn (self $item) => !$item->availableRecordTypes($server)->isEmpty()); From 44f3e564295c5e814d1e3148fcf875d4063a5084 Mon Sep 17 00:00:00 2001 From: gavidroselj Date: Fri, 11 Sep 2026 08:16:03 +0000 Subject: [PATCH 15/16] Fix missing restrictions in cloudflare upsert --- 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 a234ede1..786064e6 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -74,11 +74,11 @@ public function upsertOnCloudflare(): void $subdomainTarget = $this->server->node->subdomain_target; // @phpstan-ignore property.notFound $node_id = $this->server->node->id; - if (!$this->domain->nodes()->where('nodes.id', $node_id)->exists()) { + if (!($this->domain->nodes->isEmpty() || $this->domain->nodes()->where('nodes.id', $node_id)->exists())) { throw new Exception('Domain ' . $this->domain->nameWithPrefix() . ' is not permitted on node ' . $this->server->node->name); } - if (!$this->domain->allowed_record_types->contains($this->record_type)) { + if (!($this->domain->allowed_record_types->isEmpty() || $this->domain->allowed_record_types->contains($this->record_type))) { throw new Exception('Record type ' . $this->record_type->value . ' is not permitted on domain ' . $this->domain->nameWithPrefix()); } From 353247e837b814fbb3eba2e1e77842f68befc6da Mon Sep 17 00:00:00 2001 From: gavidroselj Date: Fri, 11 Sep 2026 08:31:41 +0000 Subject: [PATCH 16/16] Make down migration conflicts less likely --- .../migrations/008_make_compound_domain_unique_constraint.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subdomains/database/migrations/008_make_compound_domain_unique_constraint.php b/subdomains/database/migrations/008_make_compound_domain_unique_constraint.php index 89a58fe0..3a579988 100644 --- a/subdomains/database/migrations/008_make_compound_domain_unique_constraint.php +++ b/subdomains/database/migrations/008_make_compound_domain_unique_constraint.php @@ -28,7 +28,7 @@ public function down(): void DB::table('cloudflare_domains') ->whereNotIn('id', $uniqueDomainIds) - ->update(['name' => DB::raw("CONCAT(name, '_', id)")]); + ->update(['name' => DB::raw("CONCAT(name, '_', prefix, '_', id)")]); Schema::table('cloudflare_domains', function (Blueprint $table) { $table->dropUnique(['name', 'prefix']);