diff --git a/subdomains/database/migrations/004_add_srv_target_to_cloudflare_domains.php b/subdomains/database/migrations/004_add_srv_target_to_cloudflare_domains.php new file mode 100644 index 0000000..3cbe2a1 --- /dev/null +++ b/subdomains/database/migrations/004_add_srv_target_to_cloudflare_domains.php @@ -0,0 +1,22 @@ +string('srv_target')->nullable()->after('cloudflare_id'); + }); + } + + public function down(): void + { + Schema::table('cloudflare_domains', function (Blueprint $table) { + $table->dropColumn('srv_target'); + }); + } +}; diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index e1cd0ea..3b8f387 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -14,6 +14,47 @@ 'name' => 'Name', + 'srv_record' => 'SRV Record', + 'srv_record_help' => 'Enable this option to create a SRV record instead of an A or AAAA record.', + + 'srv_target' => 'SRV Target', + 'srv_target_help' => 'The hostname that SRV records point to (for example: play.example.com).', + + 'errors' => [ + 'srv_target_missing' => 'Cannot enable SRV record because the selected domain does not have an SRV target set.', + ], + 'api_token' => 'Cloudflare API Token', 'api_token_help' => 'The token needs to have read permissions for Zone.Zone and write for Zone.Dns. For better security you can also set the "Zone Resources" to exclude certain domains and add the panel ip to the "Client IP Adress Filtering".', + + 'notifications' => [ + 'cloudflare_missing_zone_title' => 'Cloudflare: Missing Zone ID', + 'cloudflare_missing_zone' => 'Cloudflare zone ID is not configured for :domain. Cannot save DNS record for :subdomain.', + + 'cloudflare_missing_srv_port_title' => 'Cloudflare: Missing SRV Port', + 'cloudflare_missing_srv_port' => 'SRV port is missing for :subdomain. Cannot save SRV record.', + + 'cloudflare_missing_srv_target_title' => 'Cloudflare: Missing SRV Target', + 'cloudflare_missing_srv_target' => 'SRV target is missing for :subdomain. Cannot save SRV record.', + + 'cloudflare_record_updated_title' => 'Cloudflare: Record Updated', + 'cloudflare_record_updated' => 'Successfully updated :subdomain record to :record_type', + + 'cloudflare_missing_ip_title' => 'Cloudflare: Missing IP', + 'cloudflare_missing_ip' => 'Server allocation IP is missing or invalid for :subdomain. Cannot save A/AAAA record.', + + 'cloudflare_upsert_failed_title' => 'Cloudflare: Save Failed', + 'cloudflare_upsert_failed' => 'Failed to save record for :subdomain. See logs for details. Errors: :errors', + + 'cloudflare_delete_success_title' => 'Cloudflare: Record Deleted', + 'cloudflare_delete_success' => 'Successfully deleted Cloudflare record for :subdomain.', + + 'cloudflare_delete_failed_title' => 'Cloudflare: Delete Failed', + 'cloudflare_delete_failed' => 'Failed to delete Cloudflare record for :subdomain. See logs for details. Errors: :errors', + + 'cloudflare_zone_fetch_failed' => 'Failed to fetch Cloudflare Zone ID for domain: :domain', + 'cloudflare_domain_saved' => 'Successfully saved domain: :domain', + + 'settings_saved' => 'Settings saved', + ], ]; diff --git a/subdomains/plugin.json b/subdomains/plugin.json index 7ff7cbc..23e3ee6 100644 --- a/subdomains/plugin.json +++ b/subdomains/plugin.json @@ -2,7 +2,7 @@ "id": "subdomains", "name": "Subdomains", "author": "Boy132", - "version": "1.0.0", + "version": "1.1.0", "description": "Allows users to create subdomains for their servers", "category": "plugin", "url": "https://github.com/pelican-dev/plugins/tree/main/subdomains", diff --git a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php index 571166f..7108768 100644 --- a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php +++ b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php @@ -53,6 +53,8 @@ public static function table(Table $table): Table ->columns([ TextColumn::make('name') ->label(trans('subdomains::strings.name')), + TextColumn::make('srv_target') + ->label(trans('subdomains::strings.srv_target')), TextColumn::make('subdomains_count') ->label(trans_choice('subdomains::strings.subdomain', 2)) ->counts('subdomains'), @@ -77,6 +79,11 @@ public static function form(Schema $schema): Schema ->label(trans('subdomains::strings.name')) ->required() ->unique(), + TextInput::make('srv_target') + ->label(trans('subdomains::strings.srv_target')) + ->helperText(trans('subdomains::strings.srv_target_help')) + ->placeholder('play.example.com') + ->rules(['nullable', 'string', 'regex:/^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(?createAnother(false), + ->createAnother(false) + ->successNotification(null), ]; } } diff --git a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php index 45de854..ab5c570 100644 --- a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php +++ b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php @@ -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; @@ -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), ]); } } diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index ec372b3..1d9fe3b 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -12,13 +12,14 @@ 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; use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Columns\ToggleColumn; use Filament\Tables\Table; class SubdomainResource extends Resource @@ -78,10 +79,16 @@ public static function table(Table $table): Table TextColumn::make('label') ->label(trans('subdomains::strings.name')) ->state(fn (Subdomain $subdomain) => $subdomain->getLabel()), + ToggleColumn::make('srv_record') + ->label(trans('subdomains::strings.srv_record')) + ->tooltip(fn (Subdomain $record) => $record->domain && $record->domain->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.srv_target_missing')) + ->disabled(fn (Subdomain $record) => !$record->domain || empty($record->domain->srv_target)), ]) ->recordActions([ - EditAction::make(), - DeleteAction::make(), + EditAction::make() + ->successNotification(null), + DeleteAction::make() + ->successNotification(null), ]) ->toolbarActions([ CreateAction::make() @@ -92,6 +99,7 @@ public static function table(Table $table): Table ->createAnother(false) ->hiddenLabel() ->iconButton() + ->successNotification(null) ->iconSize(IconSize::ExtraLarge), ]); } @@ -110,14 +118,14 @@ public static function form(Schema $schema): Schema ->required() ->relationship('domain', 'name') ->preload() + ->default(fn () => CloudflareDomain::first()?->id ?? null) ->searchable(), - Hidden::make('record_type') - ->default(function () { - /** @var Server $server */ - $server = Filament::getTenant(); - - return is_ipv6($server->allocation->ip) ? 'AAAA' : 'A'; - }), + Toggle::make('srv_record') + ->label(trans('subdomains::strings.srv_record')) + ->helperText(trans('subdomains::strings.srv_record_help')) + ->reactive() + ->disabled(fn (callable $get) => (empty($get('domain_id')) || empty(CloudflareDomain::find($get('domain_id'))?->srv_target))) + ->default(false), ]); } diff --git a/subdomains/src/Models/CloudflareDomain.php b/subdomains/src/Models/CloudflareDomain.php index fa4ef19..1dbd87c 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -2,20 +2,23 @@ namespace Boy132\Subdomains\Models; +use Boy132\Subdomains\Services\CloudflareService; +use Filament\Notifications\Notification; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Support\Facades\Http; /** * @property int $id * @property string $name * @property ?string $cloudflare_id + * @property ?string $srv_target */ class CloudflareDomain extends Model { protected $fillable = [ 'name', 'cloudflare_id', + 'srv_target', ]; protected static function boot(): void @@ -23,7 +26,24 @@ protected static function boot(): void parent::boot(); static::created(function (self $model) { - $model->fetchCloudflareId(); + $service = new CloudflareService(); + + $zoneId = $service->getZoneId($model->name); + if (!$zoneId) { + Notification::make() + ->title(trans('subdomains::strings.notifications.cloudflare_zone_fetch_failed', ['domain' => $model->name])) + ->danger() + ->send(); + } else { + Notification::make() + ->title(trans('subdomains::strings.notifications.cloudflare_domain_saved', ['domain' => $model->name])) + ->success() + ->send(); + + $model->update([ + 'cloudflare_id' => $zoneId, + ]); + } }); } @@ -31,21 +51,4 @@ public function subdomains(): HasMany { return $this->hasMany(Subdomain::class, 'domain_id'); } - - public function fetchCloudflareId(): void - { - $response = Http::cloudflare()->get('zones', [ - 'name' => $this->name, - ])->json(); - - if ($response['success']) { - $zones = $response['result']; - - if (count($zones) > 0) { - $this->update([ - 'cloudflare_id' => $zones[0]['id'], - ]); - } - } - } } diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 14cd323..7288429 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -3,11 +3,13 @@ namespace Boy132\Subdomains\Models; use App\Models\Server; +use Boy132\Subdomains\Services\CloudflareService; +use Filament\Notifications\Notification; use Filament\Support\Contracts\HasLabel; use Illuminate\Contracts\Support\Htmlable; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; -use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Log; /** * @property int $id @@ -27,18 +29,26 @@ class Subdomain extends Model implements HasLabel 'cloudflare_id', 'domain_id', 'server_id', + 'srv_record', + ]; + + protected $casts = [ + 'srv_record' => 'boolean', ]; protected static function boot(): void { parent::boot(); - static::created(function (self $model) { - $model->createOnCloudflare(); + static::saving(function (self $model) { + if (array_key_exists('srv_record', $model->attributes)) { + $model->setRecordType($model->attributes['srv_record']); + unset($model->attributes['srv_record']); + } }); - static::updated(function (self $model) { - $model->updateOnCloudflare(); + static::saved(function (self $model) { + $model->upsertOnCloudflare(); }); static::deleted(function (self $model) { @@ -61,54 +71,155 @@ public function getLabel(): string|Htmlable|null return $this->name . '.' . $this->domain->name; } - protected function createOnCloudflare(): void + public function getSrvRecordAttribute(): bool { - if (!$this->server->allocation || $this->server->allocation->ip === '0.0.0.0' || $this->server->allocation->ip === '::') { - return; - } + return $this->record_type === 'SRV'; + } - if (!$this->cloudflare_id) { - $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", [ - 'name' => $this->name, - 'ttl' => 120, - 'type' => $this->record_type, - 'comment' => 'Created by Pelican Subdomains plugin', - 'content' => $this->server->allocation->ip, - 'proxied' => false, - ])->json(); - - if ($response['success']) { - $dnsRecord = $response['result']; - - $this->updateQuietly([ - 'cloudflare_id' => $dnsRecord['id'], - ]); + public function setRecordType($value): void + { + if ($value) { + $this->attributes['record_type'] = 'SRV'; + } else { + if ($this->server && $this->server->allocation && is_ipv6($this->server->allocation->ip)) { + $this->attributes['record_type'] = 'AAAA'; + } else { + $this->attributes['record_type'] = 'A'; } } } - protected function updateOnCloudflare(): void + protected function upsertOnCloudflare(): void { - if (!$this->server->allocation || $this->server->allocation->ip === '0.0.0.0' || $this->server->allocation->ip === '::') { + $service = app(CloudflareService::class); + + $zoneId = $this->domain->cloudflare_id; + if (empty($zoneId)) { + Log::warning('Cloudflare zone id missing for domain', ['domain_id' => $this->domain_id]); + + Notification::make() + ->danger() + ->title(trans('subdomains::strings.notifications.cloudflare_missing_zone_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_zone', ['domain' => $this->domain->name ?? 'unknown', 'subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) + ->send(); + return; } - if ($this->cloudflare_id) { - Http::cloudflare()->patch("zones/{$this->domain->cloudflare_id}/dns_records/{$this->cloudflare_id}", [ - 'name' => $this->name, - 'ttl' => 120, - 'type' => $this->record_type, - 'comment' => 'Created by Pelican Subdomains plugin', - 'content' => $this->server->allocation->ip, - 'proxied' => false, - ]); + // SRV: target comes from domain, port from server allocation + if ($this->record_type === 'SRV') { + $port = $this->server && $this->server->allocation ? ($this->server->allocation->port ?? null) : null; + + if (empty($port)) { + Log::warning('Server missing allocation with port', $this->toArray()); + + Notification::make() + ->danger() + ->title(trans('subdomains::strings.notifications.cloudflare_missing_srv_port_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_port', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) + ->send(); + + return; + } + + if (empty($this->domain->srv_target)) { + Log::warning('Domain missing SRV target for SRV record', ['domain_id' => $this->domain_id]); + + Notification::make() + ->danger() + ->title(trans('subdomains::strings.notifications.cloudflare_missing_srv_target_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_target', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) + ->send(); + + return; + } + + $result = $service->upsertDnsRecord($zoneId, $this->name, 'SRV', $this->domain->srv_target, $this->cloudflare_id, $port); + + if ($result['success'] && !empty($result['id'])) { + if ($this->cloudflare_id !== $result['id']) { + $this->updateQuietly(['cloudflare_id' => $result['id']]); + } + + Notification::make() + ->success() + ->title(trans('subdomains::strings.notifications.cloudflare_record_updated_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_record_updated', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'record_type' => $this->record_type])) + ->send(); + } else { + Log::error('Failed to upsert SRV record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); + + Notification::make() + ->danger() + ->title(trans('subdomains::strings.notifications.cloudflare_upsert_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) + ->send(); + } + + return; + } + + // A/AAAA + if (!$this->server || !$this->server->allocation || $this->server->allocation->ip === '0.0.0.0' || $this->server->allocation->ip === '::') { + Log::warning('Server allocation missing or invalid IP', ['server_id' => $this->server_id]); + + Notification::make() + ->danger() + ->title(trans('subdomains::strings.notifications.cloudflare_missing_ip_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_ip', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) + ->send(); + + return; + } + + $result = $service->upsertDnsRecord($zoneId, $this->name, $this->record_type, $this->server->allocation->ip, $this->cloudflare_id, null); + + if ($result['success'] && !empty($result['id'])) { + if ($this->cloudflare_id !== $result['id']) { + $this->updateQuietly(['cloudflare_id' => $result['id']]); + } + + Notification::make() + ->success() + ->title(trans('subdomains::strings.notifications.cloudflare_record_updated_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_record_updated', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'record_type' => $this->record_type])) + ->send(); + } else { + Log::error('Failed to upsert record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); + + $domainName = $this->domain->name ?? 'unknown'; + $sub = sprintf('%s.%s', $this->name, $domainName); + + Notification::make() + ->danger() + ->title(trans('subdomains::strings.notifications.cloudflare_upsert_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $sub, 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) + ->send(); } } protected function deleteOnCloudflare(): void { - if ($this->cloudflare_id) { - Http::cloudflare()->delete("zones/{$this->domain->cloudflare_id}/dns_records/{$this->cloudflare_id}"); + if ($this->cloudflare_id && $this->domain && $this->domain->cloudflare_id) { + $service = app(CloudflareService::class); + + $result = $service->deleteDnsRecord($this->domain->cloudflare_id, $this->cloudflare_id); + + if (!empty($result['success'])) { + Notification::make() + ->success() + ->title(trans('subdomains::strings.notifications.cloudflare_delete_success_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_delete_success', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) + ->send(); + } else { + Notification::make() + ->danger() + ->title(trans('subdomains::strings.notifications.cloudflare_delete_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_delete_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) + ->send(); + } + + return; } } } diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php new file mode 100644 index 0000000..b0525e5 --- /dev/null +++ b/subdomains/src/Services/CloudflareService.php @@ -0,0 +1,162 @@ + $domainName]); + + return null; + } + + try { + $response = Http::cloudflare()->get('zones', [ + 'name' => $domainName, + ]); + } catch (\Throwable $e) { + Log::error('Cloudflare getZoneId request failed: ' . $e->getMessage(), ['domain' => $domainName]); + + return null; + } + + $status = $response->status(); + $body = $response->json() ?? []; + + if ($response->successful() && !empty($body['result']) && count($body['result']) > 0) { + return $body['result'][0]['id'] ?? null; + } + + if (!empty($body['errors'])) { + Log::warning('Cloudflare getZoneId returned errors', ['domain' => $domainName, 'status' => $status, 'errors' => $body['errors']]); + } + + return null; + } + + public function upsertDnsRecord(string $zoneId, string $name, string $recordType, string $target, ?string $recordId = null, ?int $port = null): array + { + if (empty($zoneId) || empty($name) || empty($recordType)) { + Log::error('Cloudflare upsertDnsRecord missing required parameters', ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); + + return ['success' => false, 'id' => null, 'errors' => ['missing_parameters' => true], 'status' => 0, 'body' => null]; + } + + // Hardcoded/derived defaults + $priority = 0; + $weight = 0; + $ttl = 1; + $comment = 'Created by Pelican Subdomains plugin'; + $proxied = false; + + // Build payload based on type + if ($recordType === 'SRV') { + if (empty($port) || empty($target)) { + Log::error('Cloudflare upsert missing SRV target or port', ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); + + return ['success' => false, 'id' => null, 'errors' => ['missing_srv_target_or_port' => true], 'status' => 0, 'body' => null]; + } + + $payload = [ + 'name' => sprintf('_minecraft._tcp.%s', $name), + 'ttl' => $ttl, + 'type' => 'SRV', + 'comment' => $comment, + 'content' => sprintf('%d %d %d %s', $priority, $weight, $port, $target), + 'proxied' => $proxied, + 'data' => [ + 'priority' => $priority, + 'weight' => $weight, + 'port' => (int) $port, + 'target' => $target, + ], + ]; + } else { + $payload = [ + 'name' => $name, + 'ttl' => $ttl, + 'type' => $recordType, + 'comment' => $comment, + 'content' => $target, + 'proxied' => $proxied, + ]; + } + + try { + if ($recordId) { + $response = Http::cloudflare()->put("zones/{$zoneId}/dns_records/{$recordId}", $payload); + $parsed = $this->parseCloudflareHttpResponse($response); + + if ($parsed['success']) { + return $parsed; + } + + Log::error('Cloudflare update failed', ['zone' => $zoneId, 'recordId' => $recordId, 'response' => $parsed]); + + return $parsed; + } + + $response = Http::cloudflare()->post("zones/{$zoneId}/dns_records", $payload); + $parsed = $this->parseCloudflareHttpResponse($response); + + if ($parsed['success'] && !empty($parsed['id'])) { + return $parsed; + } + + Log::error('Cloudflare create failed', ['zone' => $zoneId, 'payload' => $payload, 'response' => $parsed]); + + return $parsed; + } catch (\Throwable $e) { + Log::error('Cloudflare upsert exception: ' . $e->getMessage(), ['zone' => $zoneId, 'payload' => $payload]); + + return ['success' => false, 'id' => null, 'errors' => ['exception' => $e->getMessage()], 'status' => 0, 'body' => null]; + } + } + + protected function parseCloudflareHttpResponse(Response $response): array + { + $status = $response->status(); + $body = $response->json() ?? []; + + $success = $response->successful() && ($body['success'] === true || count($body['result']) > 0); + + return [ + 'success' => $success, + 'id' => $body['result']['id'] ?? null, + 'errors' => $body['errors'] ?? [], + 'status' => $status, + 'body' => $body, + ]; + } + + public function deleteDnsRecord(string $zoneId, string $recordId): array + { + if (empty($zoneId) || empty($recordId)) { + return ['success' => false, 'errors' => ['missing_parameters' => true], 'status' => 0, 'body' => null]; + } + + try { + $response = Http::cloudflare()->delete("zones/{$zoneId}/dns_records/{$recordId}"); + + $parsed = $this->parseCloudflareHttpResponse($response); + + if ($parsed['success']) { + return $parsed; + } + + Log::error('Cloudflare delete failed', ['zone' => $zoneId, 'id' => $recordId, 'response' => $parsed]); + + return $parsed; + } catch (\Throwable $e) { + Log::error('Cloudflare delete exception: ' . $e->getMessage(), ['zone' => $zoneId, 'id' => $recordId]); + + return ['success' => false, 'errors' => ['exception' => $e->getMessage()], 'status' => 0, 'body' => null]; + } + } +} diff --git a/subdomains/src/SubdomainsPlugin.php b/subdomains/src/SubdomainsPlugin.php index 17e7164..655a465 100644 --- a/subdomains/src/SubdomainsPlugin.php +++ b/subdomains/src/SubdomainsPlugin.php @@ -47,7 +47,7 @@ public function saveSettings(array $data): void ]); Notification::make() - ->title('Settings saved') + ->title(trans('subdomains::strings.notifications.settings_saved')) ->success() ->send(); }