Compare commits

...

2 Commits

Author SHA1 Message Date
Boy132
2bbfb0eef9 Fix "undefined key" error for TagsFilter (#2312) 2026-05-04 10:48:34 +02:00
Boy132
2a64ea8536 Cleanup node fqdn/ip validation (#2307) 2026-05-04 09:01:03 +02:00
4 changed files with 154 additions and 157 deletions

View File

@@ -64,9 +64,8 @@ class CreateNode extends CreateRecord
->icon(TablerIcon::Server) ->icon(TablerIcon::Server)
->columnSpanFull() ->columnSpanFull()
->columns([ ->columns([
'default' => 2, 'default' => 1,
'sm' => 3, 'md' => 2,
'md' => 3,
'lg' => 4, 'lg' => 4,
]) ])
->schema([ ->schema([
@@ -76,81 +75,83 @@ class CreateNode extends CreateRecord
->autofocus() ->autofocus()
->live(debounce: 1500) ->live(debounce: 1500)
->rules(Node::getRulesForField('fqdn')) ->rules(Node::getRulesForField('fqdn'))
->prohibited(fn ($state) => is_ip($state) && request()->isSecure())
->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain')) ->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain'))
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com') ->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
->helperText(function ($state) { ->helperText(fn () => request()->isSecure() ? trans('admin/node.fqdn_ssl') : null)
->validationMessages([
'prohibited' => trans('admin/node.dns_error'),
])
->prohibited(function ($state, Get $get) {
if (!$state) {
return true;
}
if (is_ip($state)) {
return false;
}
$ip = $get('ip');
return !is_ip($ip);
})
->hintColor(function ($state, Get $get) {
if (!$state) {
return null;
}
if (is_ip($state)) { if (is_ip($state)) {
if (request()->isSecure()) { if (request()->isSecure()) {
return trans('admin/node.fqdn_help'); return 'warning';
} }
} else {
$ip = $get('ip');
return ''; return is_ip($ip) ? 'success' : 'danger';
} }
return trans('admin/node.error'); return null;
}) })
->hintColor('danger') ->hint(function ($state, Get $get) {
->hint(function ($state) { if (!$state) {
if (is_ip($state) && request()->isSecure()) { return null;
return trans('admin/node.ssl_ip');
} }
return ''; if (is_ip($state)) {
if (request()->isSecure()) {
return trans('admin/node.ssl_ip');
}
} else {
$ip = $get('ip');
return is_ip($ip) ? trans('admin/node.valid') . ': ' . $ip : trans('admin/node.invalid');
}
return null;
}) })
->afterStateUpdated(function (Set $set, ?string $state) { ->afterStateUpdated(function (Set $set, ?string $state) {
$set('dns', null);
$set('ip', null); $set('ip', null);
if (!$state) {
return;
}
[$subdomain] = str($state)->explode('.', 2); [$subdomain] = str($state)->explode('.', 2);
if (!is_numeric($subdomain)) { if (!is_numeric($subdomain)) {
$set('name', $subdomain); $set('name', $subdomain);
} }
if (!$state || is_ip($state)) { if (!is_ip($state)) {
$set('dns', null); $ip = get_ip_from_hostname($state);
if (is_ip($ip)) {
return; $set('ip', $ip);
} } else {
$set('ip', null);
$ip = get_ip_from_hostname($state); }
if ($ip) {
$set('dns', true);
$set('ip', $ip);
} else {
$set('dns', false);
} }
}) })
->maxLength(255), ->maxLength(255),
Hidden::make('ip')
TextInput::make('ip') ->saved(false),
->disabled()
->hidden(),
ToggleButtons::make('dns')
->label(trans('admin/node.dns'))
->helperText(trans('admin/node.dns_help'))
->disabled()
->inline()
->default(null)
->hint(fn (Get $get) => $get('ip'))
->hintColor('success')
->options([
true => trans('admin/node.valid'),
false => trans('admin/node.invalid'),
])
->colors([
true => 'success',
false => 'danger',
])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 1,
]),
TextInput::make('daemon_connect') TextInput::make('daemon_connect')
->columnSpan(1) ->columnSpan(1)
->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port')) ->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port'))
@@ -160,7 +161,16 @@ class CreateNode extends CreateRecord
->default(8080) ->default(8080)
->required() ->required()
->integer(), ->integer(),
TextInput::make('daemon_listen')
->columnSpan(1)
->label(trans('admin/node.listen_port'))
->helperText(trans('admin/node.listen_port_help'))
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer()
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
TextInput::make('name') TextInput::make('name')
->label(trans('admin/node.display_name')) ->label(trans('admin/node.display_name'))
->columnSpan([ ->columnSpan([
@@ -171,27 +181,16 @@ class CreateNode extends CreateRecord
]) ])
->required() ->required()
->maxLength(100), ->maxLength(100),
Hidden::make('scheme')
->default(fn () => request()->isSecure() ? 'https' : 'http'),
Hidden::make('behind_proxy')
->default(false),
ToggleButtons::make('connection') ToggleButtons::make('connection')
->label(trans('admin/node.ssl')) ->label(trans('admin/node.ssl'))
->columnSpan(1) ->columnSpan(2)
->inline() ->inline()
->helperText(function (Get $get) { ->helperText(function () {
if (request()->isSecure()) { if (request()->isSecure()) {
return new HtmlString(trans('admin/node.panel_on_ssl')); return trans('admin/node.panel_on_ssl');
} }
if (is_ip($get('fqdn'))) { return null;
return trans('admin/node.ssl_help');
}
return '';
}) })
->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure()) ->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
->options([ ->options([
@@ -219,17 +218,10 @@ class CreateNode extends CreateRecord
$set('daemon_connect', $state === 'https_proxy' ? 443 : 8080); $set('daemon_connect', $state === 'https_proxy' ? 443 : 8080);
$set('daemon_listen', 8080); $set('daemon_listen', 8080);
}), }),
Hidden::make('scheme')
TextInput::make('daemon_listen') ->default(fn () => request()->isSecure() ? 'https' : 'http'),
->columnSpan(1) Hidden::make('behind_proxy')
->label(trans('admin/node.listen_port')) ->default(false),
->helperText(trans('admin/node.listen_port_help'))
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer()
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
]), ]),
Step::make('advanced') Step::make('advanced')
->label(trans('admin/node.tabs.advanced_settings')) ->label(trans('admin/node.tabs.advanced_settings'))

View File

@@ -137,74 +137,83 @@ class EditNode extends EditRecord
->autofocus() ->autofocus()
->live(debounce: 1500) ->live(debounce: 1500)
->rules(Node::getRulesForField('fqdn')) ->rules(Node::getRulesForField('fqdn'))
->prohibited(fn ($state) => is_ip($state) && request()->isSecure())
->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain')) ->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain'))
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com') ->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
->helperText(function ($state) { ->helperText(fn () => request()->isSecure() ? trans('admin/node.fqdn_ssl') : null)
->validationMessages([
'prohibited' => trans('admin/node.dns_error'),
])
->prohibited(function ($state, Get $get) {
if (!$state) {
return true;
}
if (is_ip($state)) {
return false;
}
$ip = $get('ip');
return !is_ip($ip);
})
->hintColor(function ($state, Get $get) {
if (!$state) {
return null;
}
if (is_ip($state)) { if (is_ip($state)) {
if (request()->isSecure()) { if (request()->isSecure()) {
return trans('admin/node.fqdn_help'); return 'warning';
} }
} else {
$ip = $get('ip');
return ''; return is_ip($ip) ? 'success' : 'danger';
} }
return trans('admin/node.error'); return null;
}) })
->hintColor('danger') ->hint(function ($state, Get $get) {
->hint(function ($state) { if (!$state) {
if (is_ip($state) && request()->isSecure()) { return null;
return trans('admin/node.ssl_ip');
} }
return ''; if (is_ip($state)) {
if (request()->isSecure()) {
return trans('admin/node.ssl_ip');
}
} else {
$ip = $get('ip');
return is_ip($ip) ? trans('admin/node.valid') . ': ' . $ip : trans('admin/node.invalid');
}
return null;
}) })
->afterStateUpdated(function (Set $set, ?string $state) { ->afterStateUpdated(function (Set $set, ?string $state) {
$set('dns', null);
$set('ip', null); $set('ip', null);
if (!$state) {
return;
}
[$subdomain] = str($state)->explode('.', 2); [$subdomain] = str($state)->explode('.', 2);
if (!is_numeric($subdomain)) { if (!is_numeric($subdomain)) {
$set('name', $subdomain); $set('name', $subdomain);
} }
if (!$state || is_ip($state)) { if (!is_ip($state)) {
$set('dns', null); $ip = get_ip_from_hostname($state);
if (is_ip($ip)) {
return; $set('ip', $ip);
} } else {
$set('ip', null);
$ip = get_ip_from_hostname($state); }
if ($ip) {
$set('dns', true);
$set('ip', $ip);
} else {
$set('dns', false);
} }
}) })
->maxLength(255), ->maxLength(255),
TextInput::make('ip') Hidden::make('ip')
->disabled() ->saved(false),
->hidden(),
ToggleButtons::make('dns')
->label(trans('admin/node.dns'))
->helperText(trans('admin/node.dns_help'))
->disabled()
->inline()
->default(null)
->hint(fn (Get $get) => $get('ip'))
->hintColor('success')
->stateCast(new BooleanStateCast(false, true))
->options([
1 => trans('admin/node.valid'),
0 => trans('admin/node.invalid'),
])
->colors([
1 => 'success',
0 => 'danger',
])
->columnSpan(1),
TextInput::make('daemon_connect') TextInput::make('daemon_connect')
->columnSpan(1) ->columnSpan(1)
->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port')) ->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port'))
@@ -214,6 +223,16 @@ class EditNode extends EditRecord
->default(8080) ->default(8080)
->required() ->required()
->integer(), ->integer(),
TextInput::make('daemon_listen')
->columnSpan(1)
->label(trans('admin/node.listen_port'))
->helperText(trans('admin/node.listen_port_help'))
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer()
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
TextInput::make('name') TextInput::make('name')
->label(trans('admin/node.display_name')) ->label(trans('admin/node.display_name'))
->columnSpan([ ->columnSpan([
@@ -224,22 +243,16 @@ class EditNode extends EditRecord
]) ])
->required() ->required()
->maxLength(100), ->maxLength(100),
Hidden::make('scheme'),
Hidden::make('behind_proxy'),
ToggleButtons::make('connection') ToggleButtons::make('connection')
->label(trans('admin/node.ssl')) ->label(trans('admin/node.ssl'))
->columnSpan(1) ->columnSpan(2)
->inline() ->inline()
->helperText(function (Get $get) { ->helperText(function () {
if (request()->isSecure()) { if (request()->isSecure()) {
return new HtmlString(trans('admin/node.panel_on_ssl')); return trans('admin/node.panel_on_ssl');
} }
if (is_ip($get('fqdn'))) { return null;
return trans('admin/node.ssl_help');
}
return '';
}) })
->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure()) ->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
->options([ ->options([
@@ -267,16 +280,10 @@ class EditNode extends EditRecord
$set('daemon_connect', $state === 'https_proxy' ? 443 : 8080); $set('daemon_connect', $state === 'https_proxy' ? 443 : 8080);
$set('daemon_listen', 8080); $set('daemon_listen', 8080);
}), }),
TextInput::make('daemon_listen') Hidden::make('scheme')
->columnSpan(1) ->default(fn () => request()->isSecure() ? 'https' : 'http'),
->label(trans('admin/node.listen_port')) Hidden::make('behind_proxy')
->helperText(trans('admin/node.listen_port_help')) ->default(false),
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer()
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
]), ]),
Tab::make('advanced_settings') Tab::make('advanced_settings')
->label(trans('admin/node.tabs.advanced_settings')) ->label(trans('admin/node.tabs.advanced_settings'))

View File

@@ -21,9 +21,9 @@ class TagsFilter extends BaseFilter
{ {
parent::setUp(); parent::setUp();
$this->query(fn (Builder $query, array $data) => $query->when($data['tag'], fn (Builder $query, $tag) => $query->whereJsonContains('tags', $tag))); $this->query(fn (Builder $query, array $data) => $query->when($data['tag'] ?? null, fn (Builder $query, $tag) => $query->whereJsonContains('tags', $tag)));
$this->indicateUsing(fn (array $data) => $data['tag'] ? 'Tag: ' . $data['tag'] : null); $this->indicateUsing(fn (array $data) => ($data['tag'] ?? null) ? 'Tag: ' . $data['tag'] : null);
$this->resetState(['tag' => null]); $this->resetState(['tag' => null]);

View File

@@ -42,13 +42,11 @@ return [
'refresh' => 'Refresh', 'refresh' => 'Refresh',
'custom_ip' => 'Enter Custom IP', 'custom_ip' => 'Enter Custom IP',
'domain' => 'Domain Name', 'domain' => 'Domain Name',
'ssl_ip' => 'You cannot connect to an IP Address over SSL', 'ssl_ip' => 'Consider using a domain name instead of an ip address',
'error' => 'This is the domain name that points to your node\'s IP Address. If you\'ve already set up this, you can verify it by checking the next field!', 'fqdn_ssl' => 'Your panel is currently secured via an SSL certificate and that means your nodes require one too.',
'fqdn_help' => 'Your panel is currently secured via an SSL certificate and that means your nodes require one too. You must use a domain name, because you cannot get SSL certificates for IP Addresses.', 'dns_error' => 'No valid DNS records were found for the provided domain name.',
'dns' => 'DNS Record Check', 'valid' => 'Valid DNS',
'dns_help' => 'This lets you know if your DNS record is pointing to the correct IP address.', 'invalid' => 'Invalid DNS',
'valid' => 'Valid',
'invalid' => 'Invalid',
'port' => 'Port', 'port' => 'Port',
'ports' => 'Ports', 'ports' => 'Ports',
'port_help' => 'If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.', 'port_help' => 'If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.',
@@ -58,7 +56,7 @@ return [
'listen_port_help' => 'Wings will listen on this port.', 'listen_port_help' => 'Wings will listen on this port.',
'display_name' => 'Display Name', 'display_name' => 'Display Name',
'ssl' => 'Communicate over SSL', 'ssl' => 'Communicate over SSL',
'panel_on_ssl' => 'Your Panel is using a secure SSL connection,<br>so your Daemon must too.', 'panel_on_ssl' => 'Your Panel is using a secure SSL connection, so your Daemon must too.',
'ssl_help' => 'An IP address cannot use SSL.', 'ssl_help' => 'An IP address cannot use SSL.',
'tags' => 'Tags', 'tags' => 'Tags',