Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion subdomains/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,20 @@ 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 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

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

Expand Down
22 changes: 22 additions & 0 deletions subdomains/database/migrations/007_add_allowed_record_types.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
Schema::table('cloudflare_domains', function (Blueprint $table) {
$table->json('allowed_record_types')->after('prefix')->default('["A","AAAA","CNAME","SRV"]');
});
}

public function down(): void
{
Schema::table('cloudflare_domains', function (Blueprint $table) {
$table->dropColumn('allowed_record_types');
});
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
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']);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
}

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, '_', prefix, '_', id)")]);

Schema::table('cloudflare_domains', function (Blueprint $table) {
$table->dropUnique(['name', 'prefix']);
$table->unique('name');

$table->string('prefix')->nullable()->change();
});
Comment thread
gavidroselj marked this conversation as resolved.
}
};
28 changes: 28 additions & 0 deletions subdomains/database/migrations/009_add_domain_nodes_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
Schema::create('cloudflare_domain_node', function (Blueprint $table) {
$table->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');
}
};
2 changes: 2 additions & 0 deletions subdomains/lang/de/strings.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
'name' => 'Name',
'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',
Expand Down
2 changes: 2 additions & 0 deletions subdomains/lang/en/strings.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
'name' => 'Name',
'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',
Expand Down
33 changes: 0 additions & 33 deletions subdomains/src/Enums/RecordType.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace Boy132\Subdomains\Enums;

use App\Models\Server;
use Filament\Support\Contracts\HasLabel;

enum RecordType: string implements HasLabel
Expand All @@ -16,36 +15,4 @@ public function getLabel(): string
{
return $this->name;
}

/**
* @return array<string>
*/
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,26 @@

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;
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\Database\Eloquent\Builder;
use Illuminate\Validation\Rules\Unique;

class CloudflareDomainResource extends Resource
{
Expand Down Expand Up @@ -61,6 +67,12 @@ 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(),
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))
Expand All @@ -70,6 +82,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')
Expand Down Expand Up @@ -123,9 +136,27 @@ 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', is_null($get('prefix')) ? '' : $get('prefix')))
->disabledOn('edit'),
TextInput::make('prefix')
->label(trans('subdomains::strings.prefix')),
->label(trans('subdomains::strings.prefix'))
->unique(ignoreRecord: true, modifyRuleUsing: fn (Unique $rule, Get $get) => $rule
->where('name', $get('name'))
->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)
->multiple(),
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'))),
]);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -121,23 +120,26 @@ public function form(Schema $schema): Schema
Select::make('domain_id')
->label(trans_choice('subdomains::strings.domain', 1))
->disabledOn('edit')
->hidden(fn () => CloudflareDomain::count() <= 1)
->dehydratedWhenHidden()
->disabled(fn () => CloudflareDomain::availableDomains($this->getOwnerRecord())->count() <= 1)
->saved()
->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 (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 () => count(RecordType::availableRecordTypes($this->getOwnerRecord())) <= 1)
->disabled(fn (Get $get) => CloudflareDomain::find($get('domain_id'))?->availableRecordTypes($this->getOwnerRecord())->count() <= 1)
->saved()
->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())->pluck('name', 'value'))
->default(fn (Get $get) => CloudflareDomain::find($get('domain_id'))?->availableRecordTypes($this->getOwnerRecord())->first()),
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -155,23 +155,25 @@ 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)
->dehydratedWhenHidden()
->disabled(fn () => CloudflareDomain::availableDomains($server)->count() <= 1)
->saved()
->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', CloudflareDomain::find($get('domain_id'))?->availableRecordTypes($server)->first()))
->live(),
Select::make('record_type')
->label(trans('subdomains::strings.record_type'))
->disabledOn('edit')
->disabled(fn () => count(RecordType::availableRecordTypes($server)) <= 1)
->disabled(fn (Get $get) => CloudflareDomain::find($get('domain_id'))?->availableRecordTypes($server)->count() <= 1)
->saved()
->required()
->selectablePlaceholder(false)
->options(RecordType::availableRecordTypes($server))
->default(array_first(RecordType::availableRecordTypes($server))),
->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()),
]);
}

Expand Down
Loading