Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
e603353
add srv_target column to cloudflare_domains migration
HarlequinSin Dec 28, 2025
5948e9c
add SRV record and target strings
HarlequinSin Dec 28, 2025
cc76657
add srv_target field to CloudflareDomain model and resource
HarlequinSin Dec 28, 2025
e17c617
add srv_record toggle to SubdomainRelationManager and SubdomainResource
HarlequinSin Dec 28, 2025
2e72466
Adding logic for srv records
HarlequinSin Dec 28, 2025
f20e58e
add error messages for missing SRV target and enhance tooltip logic i…
HarlequinSin Dec 28, 2025
58de843
add zone request status messages and improve logging for missing SRV …
HarlequinSin Dec 28, 2025
2ff7a99
add CloudflareService class for managing DNS records with upsert and …
HarlequinSin Dec 28, 2025
1572187
cleanup
HarlequinSin Dec 28, 2025
eb1a0eb
Remade script to utilize upsert with better error handling
HarlequinSin Dec 28, 2025
6951bc7
Update to CloudflareDomain model to fetch Cloudflare Zone ID with not…
HarlequinSin Dec 28, 2025
f18101a
Changed upsert to require a recordId to be known (prevents changing r…
HarlequinSin Dec 28, 2025
58f73e1
Cleanup
HarlequinSin Dec 28, 2025
89f45b8
Fixing notifications
HarlequinSin Dec 28, 2025
d369cc3
- Fixed issue with create record as SRV
HarlequinSin Dec 28, 2025
7d1b33a
Remove unused Hidden component
HarlequinSin Dec 28, 2025
910b812
- Added default for subdomain selection
HarlequinSin Dec 28, 2025
b42370d
- Fixed erroneous success logic
HarlequinSin Dec 28, 2025
ea4e602
Removed unused classes
HarlequinSin Dec 28, 2025
66d4384
lang support
HarlequinSin Dec 28, 2025
f7b98d3
corrected notification log
HarlequinSin Dec 28, 2025
9804937
Fixed http success logic
HarlequinSin Dec 28, 2025
d004dda
fixed style issues and syntax error
HarlequinSin Dec 28, 2025
a4d9827
fixing blank_line_before_statement
HarlequinSin Dec 28, 2025
c4654fd
append version to 1.1.0
HarlequinSin Dec 28, 2025
348099d
Add notification for missing SRV target in Cloudflare integration
HarlequinSin Dec 29, 2025
d64612d
minor wording updates
HarlequinSin Dec 29, 2025
5bfd8df
Dynamically disable SRV record toggle if domain is missing srv_target
HarlequinSin Dec 29, 2025
e781f6a
lang cleanup, removed redundent strings
HarlequinSin Dec 29, 2025
8a7e932
Make SRV record toggle reactive
HarlequinSin Dec 29, 2025
23c4e39
Fix capitalization in Cloudflare notification titles
HarlequinSin Dec 29, 2025
f31a101
clean up
HarlequinSin Dec 29, 2025
6b31686
migration to move srv_target from cloudflare domains to nodes
HarlequinSin Jan 1, 2026
d2c0e18
Changing srv_target from domains to node
HarlequinSin Jan 1, 2026
c476a60
lang updates for new notifications
HarlequinSin Jan 1, 2026
5125263
Changing SRV record service from hard-coded for minecraft to based on…
HarlequinSin Jan 1, 2026
de62b89
cleaning up record_type/srv_record code
HarlequinSin Jan 1, 2026
8b7df3e
Fixed undefined class
HarlequinSin Jan 1, 2026
194243c
cx and pint formatting
HarlequinSin Jan 1, 2026
079563f
pint style fixes
HarlequinSin Jan 1, 2026
12db61c
moving to different branch
HarlequinSin Jan 1, 2026
f2ba928
shortened/simplified all npe checks
HarlequinSin Jan 1, 2026
ccaa94e
more simplified npe checks
HarlequinSin Jan 1, 2026
f4757fd
npe and pint style issues
HarlequinSin Jan 1, 2026
060dd6e
Added field updated check for srv_target
HarlequinSin Jan 1, 2026
fa0e3a6
creating set srv_target button
HarlequinSin Jan 2, 2026
565adf6
changing subdomain model logic to:
HarlequinSin Jan 2, 2026
912984c
fixing notification issues
HarlequinSin Jan 2, 2026
98b5f64
Merge branch 'main' into main
HarlequinSin Jan 2, 2026
c771faa
moved to button
HarlequinSin Jan 2, 2026
b51f784
Merge branch 'main' of https://github.com/HarlequinSin/pelican-plugins
HarlequinSin Jan 2, 2026
8b6c3b0
unused imports
HarlequinSin Jan 2, 2026
2b30342
phpstan issues
HarlequinSin Jan 2, 2026
109a2c6
fixing phpstan issues
HarlequinSin Jan 3, 2026
f16ed67
pint/phpstan issues
HarlequinSin Jan 3, 2026
0d8b080
fixing more pint/phpstan issues
HarlequinSin Jan 4, 2026
5e4f739
phpstan issues
HarlequinSin Jan 4, 2026
c54cbeb
phpstan issues, again
HarlequinSin Jan 4, 2026
0fe6e8f
change domains to require cloudflare_id to be set
HarlequinSin Jan 4, 2026
03a351d
- Added check for existing record before updating/deleting
HarlequinSin Jan 4, 2026
5c2b23f
php stan fixes
HarlequinSin Jan 4, 2026
dc91cca
Updated subdomains information
HarlequinSin Jan 4, 2026
3b2dd88
fixed invalid column issue
HarlequinSin Jan 4, 2026
c09e050
Remove null cloudflare_id entries from subdomains table
HarlequinSin Jan 4, 2026
660b0b6
fixing various null issues
HarlequinSin Jan 4, 2026
52b1589
phpstan issues
HarlequinSin Jan 4, 2026
75c17da
more npes
HarlequinSin Jan 4, 2026
0173530
more null checks
HarlequinSin Jan 4, 2026
a746d35
phpstan ignore errors
HarlequinSin Jan 4, 2026
53e6dab
phpstan ignores
HarlequinSin Jan 4, 2026
87d4de8
more phpstan ignores...
HarlequinSin Jan 4, 2026
732e925
npe fixes
HarlequinSin Jan 4, 2026
a288d46
phpstan ignores... again
HarlequinSin Jan 4, 2026
54cc9ee
improve server relation handling during creation
HarlequinSin Jan 4, 2026
0c75e5a
no_unused_imports
HarlequinSin Jan 4, 2026
a82334a
simplify server relation handling during creation
HarlequinSin Jan 4, 2026
de10ede
nullcheck for domainName & zoneId
HarlequinSin Jan 4, 2026
0ba3dc6
phpstan ignores
HarlequinSin Jan 4, 2026
113db00
Change requests for:
HarlequinSin Jan 4, 2026
465e8b0
pint and missing use reference
HarlequinSin Jan 4, 2026
543393f
Added some additional egg tag types for SRV records
HarlequinSin Jan 5, 2026
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
43 changes: 42 additions & 1 deletion subdomains/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,47 @@ Allows users to create and manage custom subdomains for their game servers using
## Features

- Create custom subdomains for game servers
- Cloudflare DNS integration for automatic record management
- Cloudflare DNS integration for automatic record management (A/AAAA and SRV)
- Admin management of Cloudflare domains
- Per-server subdomain limits

### Supported games & required tags

| Game | Required Egg tag |
|------|------------------|
| Factorio | `factorio` |
| Minecraft | `minecraft` |
| Mumble | `mumble` |
| Rust | `rust` |
| SCP: Secret Laboratory | `scpsl` |
| TeamSpeak 3 | `teamspeak` |

### SRV requirements

SRV records require a few things to be configured before they can be created:

1. **Node SRV target** — the node must have an `SRV Target` defined (this points SRV records to the correct host).
2. **Server allocation port** — the server must have an allocation with a port (SRV records include the port number).
3. **Egg tag** — the server's Egg must include a supported tag (e.g., `minecraft`) so the plugin knows the service and protocol to use (for example `_minecraft._tcp`).

### Troubleshooting

- **Cloudflare zone fetch failed / missing zone ID:** ensure the API token has access to the zone, or paste the Zone ID manually in **Admin > Domains**.
- **Missing IP for A/AAAA:** ensure the server has an allocation with a valid IP address.
- **Missing SRV port:** ensure the server allocation includes a port.
- **Missing SRV target:** configure the node's SRV target.
- **Unsupported SRV service:** add the appropriate tag to the Egg (e.g., `minecraft`).

### Examples

#### A/AAAA Record

1. Admin > Domains > Create > `example.com`
2. Server > Subdomains > Create > Name: `play`, Select: `A` > Save

#### SRV Record

1. Admin > Domains > Create > `example.com`
2. Node > Set SRV Target > `play.example.com` - This is the public hostname/ip that SRV records will point to.
3. Ensure the server egg has the been tagged correctly. Refer to the Supported games & required tags table above.
4. Server > Subdomains > Create > Name: `play`, Select: `SRV` > Save
45 changes: 45 additions & 0 deletions subdomains/database/migrations/004_add_srv_target_to_nodes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

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
{
public function up(): void
{
// Can safely delete any Cloudflare domains without IDs as they're useless anyway
DB::table('cloudflare_domains')->whereNull('cloudflare_id')->delete();
DB::table('subdomains')->whereNull('cloudflare_id')->delete();

Schema::table('cloudflare_domains', function (Blueprint $table) {
$table->dropColumn('srv_target');
$table->string('cloudflare_id')->nullable(false)->change();
});

Schema::table('subdomains', function (Blueprint $table) {
$table->string('cloudflare_id')->nullable(false)->change();
});

Schema::table('nodes', function (Blueprint $table) {
$table->string('srv_target')->nullable()->after('fqdn');
});
}

public function down(): void
{
Schema::table('cloudflare_domains', function (Blueprint $table) {
$table->string('srv_target')->nullable()->after('cloudflare_id');
$table->string('cloudflare_id')->nullable()->change();
});

Schema::table('subdomains', function (Blueprint $table) {
$table->string('cloudflare_id')->nullable()->change();
});

Schema::table('nodes', function (Blueprint $table) {
$table->dropColumn('srv_target');
});
}
};
56 changes: 54 additions & 2 deletions subdomains/lang/en/strings.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,69 @@
return [
'no_domains' => 'No Domains',
'domain' => 'Domain|Domains',

'no_subdomains' => 'No Subdomains',
'subdomain' => 'Subdomain|Subdomains',
'name' => 'Name',

'limit' => 'Limit',
'change_limit' => 'Change Limit',
'limit_changed' => 'Limit changed',
'limit_reached' => 'Subdomain Limit Reached',

'create_subdomain' => 'Create Subdomain',
'subdomain_change_limit' => 'Change Subdomain Limit',
'subdomain_limit' => 'Subdomain Limit',

'name' => 'Name',
'record_type' => 'Record Type',
'srv_record' => 'SRV Record',
'srv_record_help' => 'Enable this option to create a SRV record instead of an A or AAAA record.',

'set_srv_target' => 'Set SRV Target',
'srv_target' => 'SRV Target',
'srv_target_help' => 'The hostname that SRV records point to (for example: play.example.com).',
'srv_target_missing' => 'SRV record is missing from node configuration.',
'srv_target_confirmation' => 'Changing the SRV target will require all existing subdomains using SRV records to be updated to reflect the new target.',

'api_token' => 'Cloudflare API Token',
'api_token_help' => 'The token needs to have read permissions for Zone.Zone and write for Zone.Dns. For better security you can also set the "Zone Resources" to exclude certain domains and add the panel ip to the "Client IP Adress Filtering".',

'notifications' => [
'srv_target_updated_title' => 'SRV target changed successfully.',
'srv_target_updated' => 'Existing subdomains using SRV records will need to be updated to use the new target.',

'cloudflare_missing_zone_title' => 'Cloudflare: Missing Zone ID',
'cloudflare_missing_zone' => 'Cloudflare zone ID is not configured for :domain. Cannot save DNS record for :subdomain.',

'cloudflare_missing_srv_port_title' => 'Cloudflare: Missing SRV Port',
'cloudflare_missing_srv_port' => 'SRV port is missing for :server.',

'cloudflare_missing_srv_target_title' => 'Cloudflare: Missing SRV Target',
'cloudflare_missing_srv_target' => 'SRV target is missing from :node.',

'cloudflare_record_created_title' => 'Cloudflare: Record Created',
'cloudflare_record_created' => 'Successfully created :subdomain record',

'cloudflare_record_updated_title' => 'Cloudflare: Record Updated',
'cloudflare_record_updated' => 'Successfully updated :subdomain record to :record_type',

'cloudflare_record_deleted_title' => 'Cloudflare: Record Deleted',
'cloudflare_record_deleted' => 'Successfully deleted Cloudflare record for :subdomain.',

'cloudflare_delete_failed_title' => 'Cloudflare: Delete Failed',
'cloudflare_delete_failed' => 'Failed to delete Cloudflare record for :subdomain.',

'cloudflare_missing_ip_title' => 'Cloudflare: Missing IP',
'cloudflare_missing_ip' => 'Server allocation IP is missing or invalid for :subdomain. Cannot save A/AAAA record.',

'cloudflare_upsert_failed_title' => 'Cloudflare: Save Failed',
'cloudflare_upsert_failed' => 'Failed to save record for :subdomain. See logs for details. Errors: :errors',

'cloudflare_zone_fetch_failed' => 'Failed to fetch Cloudflare Zone ID for domain: :domain',
'cloudflare_domain_saved' => 'Successfully saved domain: :domain',

'cloudflare_invalid_service_record_type_title' => 'Cloudflare: Unable to determine service type',
'cloudflare_invalid_service_record_type' => 'Unable to determine service type for SRV record of :subdomain. Please check egg is correctly tagged',
],

'settings_saved' => 'Settings saved',
];
55 changes: 55 additions & 0 deletions subdomains/src/Enums/ServiceRecordType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace Boy132\Subdomains\Enums;

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

enum ServiceRecordType: string implements HasLabel
{
// Service record types
case factorio = '_factorio._udp';
case minecraft = '_minecraft._tcp';
case mumble = '_mumble._tcp';
case rust = '_rust._udp';
case scpsl = '_scpsl._udp';
case teamspeak = '_ts3._udp';

public function getLabel(): string
{
return str($this->name)->title();
}

public static function fromServer(Server $server): ?self
{
$tags = $server->egg->tags ?? [];

return self::fromTags($tags);
}

/** @param string[] $tags */
public static function fromTags(array $tags): ?self
{
foreach (self::cases() as $type) {
if (in_array($type->name, $tags)) {
return $type;
}
}

return null;
}

public function service(): string
{
$parts = explode('.', $this->value);

return $parts[0];
}

public function protocol(): string
{
$parts = explode('.', $this->value);

return $parts[1];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ protected function getHeaderActions(): array
{
return [
CreateAction::make()
->createAnother(false),
->createAnother(false)
->successNotification(null),
];
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace Boy132\Subdomains\Filament\Admin\Resources\Users\RelationManagers;
namespace Boy132\Subdomains\Filament\Admin\Resources\Servers\RelationManagers;

use App\Models\Server;
use Boy132\Subdomains\Models\CloudflareDomain;
Expand All @@ -9,9 +9,9 @@
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Notifications\Notification;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Schema;
Expand Down Expand Up @@ -83,8 +83,10 @@ public function form(Schema $schema): Schema
->relationship('domain', 'name')
->preload()
->searchable(),
Hidden::make('record_type')
->default(fn () => is_ipv6($this->getOwnerRecord()->allocation->ip) ? 'AAAA' : 'A'),
Toggle::make('srv_record')
->label(trans('subdomains::strings.srv_record'))
->helperText(trans('subdomains::strings.srv_record_help'))
->default(false),
]);
}
}
46 changes: 46 additions & 0 deletions subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace Boy132\Subdomains\Filament\Components\Actions;

use App\Models\Node;
use Filament\Actions\Action;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;

class SetSrvTargetAction extends Action
{
public static function getDefaultName(): ?string
{
return 'set_srv_target';
}

protected function setUp(): void
{
parent::setUp();

$this->label(fn () => trans('subdomains::strings.set_srv_target'));

$this->icon('tabler-world-www');

$this->schema(function (Node $node) {
return [
TextInput::make('srv_target')
->label(fn () => trans('subdomains::strings.srv_target'))
->default(fn () => $node->srv_target) // @phpstan-ignore property.notFound
->placeholder('play.example.com OR IPv4/IPv6 address')
->helperText(trans('subdomains::strings.srv_target_confirmation')),
];
});

$this->action(function (Node $node, array $data) {
// ForceFill so we don't need to overwrite on Node::$fillable
$node->forceFill(['srv_target' => $data['srv_target']])->save();

Notification::make()
->title(trans('subdomains::strings.notifications.srv_target_updated_title'))
->body(trans('subdomains::strings.notifications.srv_target_updated'))
->warning()
->send();
})->requiresConfirmation()->modalIconColor('danger');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Facades\Filament;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Enums\IconSize;
Expand Down Expand Up @@ -78,10 +78,17 @@ public static function table(Table $table): Table
TextColumn::make('label')
->label(trans('subdomains::strings.name'))
->state(fn (Subdomain $subdomain) => $subdomain->getLabel()),
TextColumn::make('record_type')
->label(trans('subdomains::strings.record_type'))
->icon(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server->node->srv_target ? 'tabler-alert-triangle' : null) // @phpstan-ignore property.notFound
->color(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server->node->srv_target ? 'danger' : null) // @phpstan-ignore property.notFound
->tooltip(fn (Subdomain $subdomain) => $subdomain->srv_record && !$subdomain->server->node->srv_target ? trans('subdomains::strings.srv_target_missing') : null), // @phpstan-ignore property.notFound
])
->recordActions([
EditAction::make(),
DeleteAction::make(),
EditAction::make()
->successNotification(null),
DeleteAction::make()
->successNotification(null),
])
->toolbarActions([
CreateAction::make()
Expand All @@ -92,6 +99,7 @@ public static function table(Table $table): Table
->createAnother(false)
->hiddenLabel()
->iconButton()
->successNotification(null)
->iconSize(IconSize::ExtraLarge),
]);
}
Expand All @@ -103,21 +111,21 @@ public static function form(Schema $schema): Schema
TextInput::make('name')
->label(trans('subdomains::strings.name'))
->required()
->suffix(fn (callable $get) => CloudflareDomain::find($get('domain_id'))?->name)
->unique(),
Select::make('domain_id')
->label(trans_choice('subdomains::strings.domain', 1))
->disabledOn('edit')
->required()
->relationship('domain', 'name')
->preload()
->default(fn () => CloudflareDomain::first()?->id)
->searchable(),
Hidden::make('record_type')
->default(function () {
/** @var Server $server */
$server = Filament::getTenant();

return is_ipv6($server->allocation->ip) ? 'AAAA' : 'A';
}),
Toggle::make('srv_record')
->label(trans('subdomains::strings.srv_record'))
->helperText(fn () => Filament::getTenant()->node->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.srv_target_missing')) // @phpstan-ignore property.notFound
->reactive()
->disabled(fn () => !Filament::getTenant()->node->srv_target), // @phpstan-ignore property.notFound
]);
}

Expand Down
Loading