From 419a551d766f33c503164e981e7d41bddf91fce4 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:57:46 +0200 Subject: [PATCH] fix(server): return SSH username validation messages --- .../Controllers/Api/ServersController.php | 4 +++ app/Livewire/Boarding/Index.php | 32 +++++++++++++------ bootstrap/helpers/shared.php | 6 ++-- .../Feature/ServerUsernameValidationTest.php | 30 +++++++++++++++-- 4 files changed, 57 insertions(+), 15 deletions(-) diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index 0322b2240..2c2195ea3 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -492,6 +492,8 @@ class ServersController extends Controller 'is_build_server' => 'boolean|nullable', 'instant_validate' => 'boolean|nullable', 'proxy_type' => 'string|nullable', + ], [ + ...ValidationPatterns::serverUsernameMessages(), ]); $extraFields = array_diff(array_keys($request->all()), $allowedFields); @@ -677,6 +679,8 @@ class ServersController extends Controller 'server_disk_usage_notification_threshold' => 'integer|min:1|max:100', 'server_disk_usage_check_frequency' => 'string', 'connection_timeout' => 'integer|min:1|max:300', + ], [ + ...ValidationPatterns::serverUsernameMessages(), ]); $extraFields = array_diff(array_keys($request->all()), $allowedFields); diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index e72a2507c..2d0ae939d 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -213,6 +213,23 @@ class Index extends Component } } + protected function rules(): array + { + return [ + 'remoteServerName' => 'required|string', + 'remoteServerHost' => 'required|string', + 'remoteServerPort' => 'required|integer|min:1|max:65535', + 'remoteServerUser' => ValidationPatterns::serverUsernameRules(), + ]; + } + + protected function messages(): array + { + return [ + ...ValidationPatterns::serverUsernameMessages('remoteServerUser', 'SSH User'), + ]; + } + public function getProxyType() { $this->selectProxy(ProxyTypes::TRAEFIK->value); @@ -275,12 +292,7 @@ class Index extends Component public function saveServer() { - $this->validate([ - 'remoteServerName' => 'required|string', - 'remoteServerHost' => 'required|string', - 'remoteServerPort' => 'required|integer', - 'remoteServerUser' => ValidationPatterns::serverUsernameRules(), - ]); + $this->validate(); $this->privateKey = formatPrivateKey($this->privateKey); $foundServer = Server::whereIp($this->remoteServerHost)->first(); @@ -466,10 +478,10 @@ class Index extends Component public function saveAndValidateServer() { - $this->validate([ - 'remoteServerPort' => 'required|integer|min:1|max:65535', - 'remoteServerUser' => ValidationPatterns::serverUsernameRules(), - ]); + $this->validate(array_intersect_key($this->rules(), array_flip([ + 'remoteServerPort', + 'remoteServerUser', + ]))); $this->createdServer->update([ 'port' => $this->remoteServerPort, diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 08af8ee42..f2b672fef 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1865,15 +1865,15 @@ function isBase64Encoded($strValue) { return base64_encode(base64_decode($strValue, true)) === $strValue; } -function customApiValidator(Collection|array $item, array $rules) +function customApiValidator(Collection|array $item, array $rules, array $messages = []) { if (is_array($item)) { $item = collect($item); } - return Validator::make($item->toArray(), $rules, [ + return Validator::make($item->toArray(), $rules, array_merge([ 'required' => 'This field is required.', - ]); + ], $messages)); } function parseDockerComposeFile(Service|Application $resource, bool $isNew = false, int $pull_request_id = 0, ?int $preview_id = null) { diff --git a/tests/Feature/ServerUsernameValidationTest.php b/tests/Feature/ServerUsernameValidationTest.php index 96236935a..4213ee21d 100644 --- a/tests/Feature/ServerUsernameValidationTest.php +++ b/tests/Feature/ServerUsernameValidationTest.php @@ -73,6 +73,21 @@ it('updates a server through the API with a dotted SSH username', function () { expect($server->fresh()->user)->toBe('deploy.user'); }); +it('rejects unsafe SSH usernames when creating a server through the API', function () { + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->token, + 'Content-Type' => 'application/json', + ])->postJson('/api/v1/servers', [ + 'name' => 'Unsafe User Server', + 'ip' => '192.0.2.11', + 'private_key_uuid' => $this->privateKey->uuid, + 'user' => 'deploy$user', + ]); + + $response->assertStatus(422); + $response->assertJsonPath('errors.user.0', 'The User may only contain letters, numbers, dots, hyphens, and underscores.'); +}); + it('rejects unsafe SSH usernames through the API', function () { $server = Server::factory()->create([ 'team_id' => $this->team->id, @@ -88,6 +103,7 @@ it('rejects unsafe SSH usernames through the API', function () { $response->assertStatus(422); $response->assertJsonStructure(['errors' => ['user']]); + $response->assertJsonPath('errors.user.0', 'The User may only contain letters, numbers, dots, hyphens, and underscores.'); }); it('allows dotted SSH usernames in the server creation form', function () { @@ -135,7 +151,12 @@ it('rejects unsafe SSH usernames during onboarding server creation', function () ->set('remoteServerPort', 22) ->set('remoteServerUser', 'deploy$user') ->call('saveServer') - ->assertHasErrors(['remoteServerUser' => ['regex']]); + ->assertHasErrors([ + 'remoteServerUser' => [ + 'regex', + 'The SSH User may only contain letters, numbers, dots, hyphens, and underscores.', + ], + ]); }); it('rejects unsafe SSH usernames during onboarding server validation', function () { @@ -152,5 +173,10 @@ it('rejects unsafe SSH usernames during onboarding server validation', function ->set('remoteServerPort', 22) ->set('remoteServerUser', 'deploy$user') ->call('saveAndValidateServer') - ->assertHasErrors(['remoteServerUser' => ['regex']]); + ->assertHasErrors([ + 'remoteServerUser' => [ + 'regex', + 'The SSH User may only contain letters, numbers, dots, hyphens, and underscores.', + ], + ]); });