From 712a058872a68c22a96d85226d0abf09e55da5d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Devrim=20Tun=C3=A7er?= Date: Thu, 12 Mar 2026 23:20:00 +0300 Subject: [PATCH 01/11] fix(logs): handle missing clipboard API in non-HTTPS contexts navigator.clipboard is undefined in insecure contexts (HTTP), causing a silent TypeError when clicking the copy logs button. Added a secure context check and proper Promise chaining so failures surface as user- visible error messages instead of silent crashes. Affects deployment log view and shared get-logs component. --- .../project/application/deployment/show.blade.php | 11 +++++++++-- .../views/livewire/project/shared/get-logs.blade.php | 11 +++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/resources/views/livewire/project/application/deployment/show.blade.php b/resources/views/livewire/project/application/deployment/show.blade.php index 28872f4bc..5c149f526 100644 --- a/resources/views/livewire/project/application/deployment/show.blade.php +++ b/resources/views/livewire/project/application/deployment/show.blade.php @@ -212,8 +212,15 @@ - -
- {{ $slot }} +
+
+
+

{{ $title }}

+ +
+
+ {{ $slot }} +
From 0ca6ebdfad119de756788de1a0bfa9ba22685523 Mon Sep 17 00:00:00 2001 From: Jan Thiel Date: Sun, 19 Apr 2026 07:35:21 +0200 Subject: [PATCH 05/11] fix(modal): add some padding to the top of the modal content to prevent cuttoffs of the content area and restore close-outside click behaviour --- resources/views/components/modal-input.blade.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/resources/views/components/modal-input.blade.php b/resources/views/components/modal-input.blade.php index 59fd19a63..344290f72 100644 --- a/resources/views/components/modal-input.blade.php +++ b/resources/views/components/modal-input.blade.php @@ -40,9 +40,8 @@
-
+
-
+
{{ $slot }}
From ff4794ffec24f142654fe5b5213057feea3576a8 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Wed, 6 May 2026 21:21:37 +0530 Subject: [PATCH 06/11] fix(server): allow dots in ssh username --- app/Http/Controllers/Api/ServersController.php | 4 ++-- app/Livewire/Server/New/ByIp.php | 2 +- app/Livewire/Server/Show.php | 2 +- app/Models/Server.php | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index 6c3b2da00..8f5a39ed0 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -487,7 +487,7 @@ class ServersController extends Controller 'ip' => ['string', 'required', new ValidServerIp], 'port' => 'integer|nullable|between:1,65535', 'private_key_uuid' => 'string|required', - 'user' => ['string', 'nullable', 'regex:/^[a-zA-Z0-9_-]+$/'], + 'user' => ['string', 'nullable', 'regex:/^[a-zA-Z0-9._-]+$/'], 'is_build_server' => 'boolean|nullable', 'instant_validate' => 'boolean|nullable', 'proxy_type' => 'string|nullable', @@ -666,7 +666,7 @@ class ServersController extends Controller 'ip' => ['string', 'nullable', new ValidServerIp], 'port' => 'integer|nullable|between:1,65535', 'private_key_uuid' => 'string|nullable', - 'user' => ['string', 'nullable', 'regex:/^[a-zA-Z0-9_-]+$/'], + 'user' => ['string', 'nullable', 'regex:/^[a-zA-Z0-9._-]+$/'], 'is_build_server' => 'boolean|nullable', 'instant_validate' => 'boolean|nullable', 'proxy_type' => 'string|nullable', diff --git a/app/Livewire/Server/New/ByIp.php b/app/Livewire/Server/New/ByIp.php index 51c6a06ee..13888d992 100644 --- a/app/Livewire/Server/New/ByIp.php +++ b/app/Livewire/Server/New/ByIp.php @@ -57,7 +57,7 @@ class ByIp extends Component 'name' => ValidationPatterns::nameRules(), 'description' => ValidationPatterns::descriptionRules(), 'ip' => ['required', 'string', new ValidServerIp], - 'user' => ['required', 'string', 'regex:/^[a-zA-Z0-9_-]+$/'], + 'user' => ['required', 'string', 'regex:/^[a-zA-Z0-9._-]+$/'], 'port' => 'required|integer|between:1,65535', 'is_build_server' => 'required|boolean', ]; diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index 3e05d9306..acdfb98fd 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -110,7 +110,7 @@ class Show extends Component 'name' => ValidationPatterns::nameRules(), 'description' => ValidationPatterns::descriptionRules(), 'ip' => ['required', new ValidServerIp], - 'user' => ['required', 'regex:/^[a-zA-Z0-9_-]+$/'], + 'user' => ['required', 'regex:/^[a-zA-Z0-9._-]+$/'], 'port' => 'required|integer|between:1,65535', 'connectionTimeout' => 'required|integer|min:1|max:300', 'validationLogs' => 'nullable', diff --git a/app/Models/Server.php b/app/Models/Server.php index 74e8ba5b0..4f2340e72 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -945,10 +945,10 @@ $schema://$host { { return Attribute::make( get: function ($value) { - return preg_replace('/[^A-Za-z0-9\-_]/', '', $value); + return preg_replace('/[^A-Za-z0-9.\-_]/', '', $value); }, set: function ($value) { - return preg_replace('/[^A-Za-z0-9\-_]/', '', $value); + return preg_replace('/[^A-Za-z0-9.\-_]/', '', $value); } ); } From 7c97b8bfb354d27358b47f324379cce85dfd080e Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Wed, 6 May 2026 21:21:57 +0530 Subject: [PATCH 07/11] fix(onboarding): validate ssh username --- app/Livewire/Boarding/Index.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index 33c75bf70..74bfc9192 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -278,7 +278,7 @@ class Index extends Component 'remoteServerName' => 'required|string', 'remoteServerHost' => 'required|string', 'remoteServerPort' => 'required|integer', - 'remoteServerUser' => 'required|string', + 'remoteServerUser' => 'required|string|regex:/^[a-zA-Z0-9._-]+$/', ]); $this->privateKey = formatPrivateKey($this->privateKey); @@ -467,7 +467,7 @@ class Index extends Component { $this->validate([ 'remoteServerPort' => 'required|integer|min:1|max:65535', - 'remoteServerUser' => 'required|string', + 'remoteServerUser' => 'required|string|regex:/^[a-zA-Z0-9._-]+$/', ]); $this->createdServer->update([ From bc2afdf02ea498d1e2cfbc96b1f01868e544c674 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:38:48 +0200 Subject: [PATCH 08/11] fix(server): share SSH username validation Centralize SSH username rules and sanitization so dotted usernames are accepted consistently across API, onboarding, and Livewire server forms. --- .../Controllers/Api/ServersController.php | 5 +- app/Livewire/Boarding/Index.php | 5 +- app/Livewire/Server/New/ByIp.php | 3 +- app/Livewire/Server/Show.php | 3 +- app/Models/Server.php | 5 +- app/Support/ValidationPatterns.php | 33 ++++ .../Feature/ServerUsernameValidationTest.php | 156 ++++++++++++++++++ tests/Unit/ServerUsernamePatternTest.php | 19 +++ 8 files changed, 221 insertions(+), 8 deletions(-) create mode 100644 tests/Feature/ServerUsernameValidationTest.php create mode 100644 tests/Unit/ServerUsernamePatternTest.php diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index 8f5a39ed0..0322b2240 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -13,6 +13,7 @@ use App\Models\PrivateKey; use App\Models\Project; use App\Models\Server as ModelsServer; use App\Rules\ValidServerIp; +use App\Support\ValidationPatterns; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use OpenApi\Attributes as OA; @@ -487,7 +488,7 @@ class ServersController extends Controller 'ip' => ['string', 'required', new ValidServerIp], 'port' => 'integer|nullable|between:1,65535', 'private_key_uuid' => 'string|required', - 'user' => ['string', 'nullable', 'regex:/^[a-zA-Z0-9._-]+$/'], + 'user' => ValidationPatterns::serverUsernameRules(required: false), 'is_build_server' => 'boolean|nullable', 'instant_validate' => 'boolean|nullable', 'proxy_type' => 'string|nullable', @@ -666,7 +667,7 @@ class ServersController extends Controller 'ip' => ['string', 'nullable', new ValidServerIp], 'port' => 'integer|nullable|between:1,65535', 'private_key_uuid' => 'string|nullable', - 'user' => ['string', 'nullable', 'regex:/^[a-zA-Z0-9._-]+$/'], + 'user' => ValidationPatterns::serverUsernameRules(required: false), 'is_build_server' => 'boolean|nullable', 'instant_validate' => 'boolean|nullable', 'proxy_type' => 'string|nullable', diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index 74bfc9192..e72a2507c 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -8,6 +8,7 @@ use App\Models\Project; use App\Models\Server; use App\Models\Team; use App\Services\ConfigurationRepository; +use App\Support\ValidationPatterns; use Illuminate\Support\Collection; use Livewire\Attributes\Url; use Livewire\Component; @@ -278,7 +279,7 @@ class Index extends Component 'remoteServerName' => 'required|string', 'remoteServerHost' => 'required|string', 'remoteServerPort' => 'required|integer', - 'remoteServerUser' => 'required|string|regex:/^[a-zA-Z0-9._-]+$/', + 'remoteServerUser' => ValidationPatterns::serverUsernameRules(), ]); $this->privateKey = formatPrivateKey($this->privateKey); @@ -467,7 +468,7 @@ class Index extends Component { $this->validate([ 'remoteServerPort' => 'required|integer|min:1|max:65535', - 'remoteServerUser' => 'required|string|regex:/^[a-zA-Z0-9._-]+$/', + 'remoteServerUser' => ValidationPatterns::serverUsernameRules(), ]); $this->createdServer->update([ diff --git a/app/Livewire/Server/New/ByIp.php b/app/Livewire/Server/New/ByIp.php index 13888d992..f5ea2ae80 100644 --- a/app/Livewire/Server/New/ByIp.php +++ b/app/Livewire/Server/New/ByIp.php @@ -57,7 +57,7 @@ class ByIp extends Component 'name' => ValidationPatterns::nameRules(), 'description' => ValidationPatterns::descriptionRules(), 'ip' => ['required', 'string', new ValidServerIp], - 'user' => ['required', 'string', 'regex:/^[a-zA-Z0-9._-]+$/'], + 'user' => ValidationPatterns::serverUsernameRules(), 'port' => 'required|integer|between:1,65535', 'is_build_server' => 'required|boolean', ]; @@ -75,6 +75,7 @@ class ByIp extends Component 'ip.string' => 'The IP Address/Domain must be a string.', 'user.required' => 'The User field is required.', 'user.string' => 'The User field must be a string.', + ...ValidationPatterns::serverUsernameMessages(), 'port.required' => 'The Port field is required.', 'port.integer' => 'The Port field must be an integer.', 'port.between' => 'The Port field must be between 1 and 65535.', diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index acdfb98fd..680089256 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -110,7 +110,7 @@ class Show extends Component 'name' => ValidationPatterns::nameRules(), 'description' => ValidationPatterns::descriptionRules(), 'ip' => ['required', new ValidServerIp], - 'user' => ['required', 'regex:/^[a-zA-Z0-9._-]+$/'], + 'user' => ValidationPatterns::serverUsernameRules(), 'port' => 'required|integer|between:1,65535', 'connectionTimeout' => 'required|integer|min:1|max:300', 'validationLogs' => 'nullable', @@ -140,6 +140,7 @@ class Show extends Component [ 'ip.required' => 'The IP Address field is required.', 'user.required' => 'The User field is required.', + ...ValidationPatterns::serverUsernameMessages(), 'port.required' => 'The Port field is required.', 'connectionTimeout.required' => 'The SSH Connection Timeout field is required.', 'connectionTimeout.integer' => 'The SSH Connection Timeout must be an integer.', diff --git a/app/Models/Server.php b/app/Models/Server.php index 4f2340e72..2b7bbac55 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -17,6 +17,7 @@ use App\Livewire\Server\Proxy; use App\Notifications\Server\Reachable; use App\Notifications\Server\Unreachable; use App\Services\ConfigurationRepository; +use App\Support\ValidationPatterns; use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasMetrics; use App\Traits\HasSafeStringAttribute; @@ -945,10 +946,10 @@ $schema://$host { { return Attribute::make( get: function ($value) { - return preg_replace('/[^A-Za-z0-9.\-_]/', '', $value); + return preg_replace(ValidationPatterns::INVALID_SERVER_USERNAME_CHARACTERS_PATTERN, '', $value); }, set: function ($value) { - return preg_replace('/[^A-Za-z0-9.\-_]/', '', $value); + return preg_replace(ValidationPatterns::INVALID_SERVER_USERNAME_CHARACTERS_PATTERN, '', $value); } ); } diff --git a/app/Support/ValidationPatterns.php b/app/Support/ValidationPatterns.php index 58dbbe1ac..2ee61ba22 100644 --- a/app/Support/ValidationPatterns.php +++ b/app/Support/ValidationPatterns.php @@ -35,6 +35,17 @@ class ValidationPatterns */ public const DOCKER_TARGET_PATTERN = '/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/'; + /** + * Pattern for SSH usernames. + * Allows alphanumeric characters, dots, hyphens, and underscores. + */ + public const SERVER_USERNAME_PATTERN = '/^[a-zA-Z0-9._-]+$/'; + + /** + * Pattern for removing characters not allowed in SSH usernames. + */ + public const INVALID_SERVER_USERNAME_CHARACTERS_PATTERN = '/[^A-Za-z0-9.\-_]/'; + /** * Token-aware pattern for shell-safe command strings (docker compose commands, docker run options). * @@ -124,6 +135,28 @@ class ValidationPatterns return $rules; } + /** + * Get validation rules for SSH username fields. + */ + public static function serverUsernameRules(bool $required = true): array + { + return [ + $required ? 'required' : 'nullable', + 'string', + 'regex:'.self::SERVER_USERNAME_PATTERN, + ]; + } + + /** + * Get validation messages for SSH username fields. + */ + public static function serverUsernameMessages(string $field = 'user', string $label = 'User'): array + { + return [ + "{$field}.regex" => "The {$label} may only contain letters, numbers, dots, hyphens, and underscores.", + ]; + } + /** * Get validation messages for database identifier fields. */ diff --git a/tests/Feature/ServerUsernameValidationTest.php b/tests/Feature/ServerUsernameValidationTest.php new file mode 100644 index 000000000..96236935a --- /dev/null +++ b/tests/Feature/ServerUsernameValidationTest.php @@ -0,0 +1,156 @@ + 'file']); + + InstanceSettings::forceCreate(['id' => 0, 'is_api_enabled' => true]); + + $this->team = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + + session(['currentTeam' => $this->team]); + + $this->privateKey = PrivateKey::withoutEvents(fn () => PrivateKey::forceCreate([ + 'uuid' => (string) new Cuid2, + 'name' => 'Test SSH Key', + 'description' => 'Test SSH Key', + 'private_key' => 'test-private-key', + 'team_id' => $this->team->id, + ])); + + $token = $this->user->createToken('write-token', ['write']); + $token->accessToken->forceFill(['team_id' => $this->team->id])->save(); + $this->token = $token->plainTextToken; +}); + +it('creates a server through the API with a dotted SSH username', function () { + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->token, + 'Content-Type' => 'application/json', + ])->postJson('/api/v1/servers', [ + 'name' => 'Dotted User Server', + 'ip' => '192.0.2.10', + 'private_key_uuid' => $this->privateKey->uuid, + 'user' => 'deploy.user', + ]); + + $response->assertCreated(); + $this->assertDatabaseHas('servers', [ + 'ip' => '192.0.2.10', + 'user' => 'deploy.user', + ]); +}); + +it('updates a server through the API with a dotted SSH username', function () { + $server = Server::factory()->create([ + 'team_id' => $this->team->id, + 'private_key_id' => $this->privateKey->id, + 'user' => 'deploy', + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->token, + 'Content-Type' => 'application/json', + ])->patchJson('/api/v1/servers/'.$server->uuid, [ + 'user' => 'deploy.user', + ]); + + $response->assertStatus(201); + expect($server->fresh()->user)->toBe('deploy.user'); +}); + +it('rejects unsafe SSH usernames through the API', function () { + $server = Server::factory()->create([ + 'team_id' => $this->team->id, + 'private_key_id' => $this->privateKey->id, + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->token, + 'Content-Type' => 'application/json', + ])->patchJson('/api/v1/servers/'.$server->uuid, [ + 'user' => 'deploy$user', + ]); + + $response->assertStatus(422); + $response->assertJsonStructure(['errors' => ['user']]); +}); + +it('allows dotted SSH usernames in the server creation form', function () { + $this->actingAs($this->user); + + Livewire::test(ByIp::class, [ + 'private_keys' => collect([$this->privateKey]), + 'limit_reached' => false, + ]) + ->set('name', 'Dotted User Server') + ->set('ip', '192.0.2.20') + ->set('user', 'deploy.user') + ->set('private_key_id', $this->privateKey->id) + ->call('submit') + ->assertHasNoErrors(['user']); + + $this->assertDatabaseHas('servers', [ + 'ip' => '192.0.2.20', + 'user' => 'deploy.user', + ]); +}); + +it('rejects unsafe SSH usernames in the server creation form', function () { + $this->actingAs($this->user); + + Livewire::test(ByIp::class, [ + 'private_keys' => collect([$this->privateKey]), + 'limit_reached' => false, + ]) + ->set('name', 'Unsafe User Server') + ->set('ip', '192.0.2.21') + ->set('user', 'deploy$user') + ->set('private_key_id', $this->privateKey->id) + ->call('submit') + ->assertHasErrors(['user' => ['regex']]); +}); + +it('rejects unsafe SSH usernames during onboarding server creation', function () { + $this->actingAs($this->user); + + Livewire::test(BoardingIndex::class) + ->set('createdPrivateKey', $this->privateKey) + ->set('remoteServerName', 'Unsafe User Server') + ->set('remoteServerHost', '192.0.2.30') + ->set('remoteServerPort', 22) + ->set('remoteServerUser', 'deploy$user') + ->call('saveServer') + ->assertHasErrors(['remoteServerUser' => ['regex']]); +}); + +it('rejects unsafe SSH usernames during onboarding server validation', function () { + $this->actingAs($this->user); + + $server = Server::factory()->create([ + 'team_id' => $this->team->id, + 'private_key_id' => $this->privateKey->id, + 'user' => 'deploy', + ]); + + Livewire::test(BoardingIndex::class) + ->set('createdServer', $server) + ->set('remoteServerPort', 22) + ->set('remoteServerUser', 'deploy$user') + ->call('saveAndValidateServer') + ->assertHasErrors(['remoteServerUser' => ['regex']]); +}); diff --git a/tests/Unit/ServerUsernamePatternTest.php b/tests/Unit/ServerUsernamePatternTest.php new file mode 100644 index 000000000..dab3f05a8 --- /dev/null +++ b/tests/Unit/ServerUsernamePatternTest.php @@ -0,0 +1,19 @@ +toBe('/^[a-zA-Z0-9._-]+$/'); + expect(ValidationPatterns::serverUsernameRules())->toContain('regex:'.ValidationPatterns::SERVER_USERNAME_PATTERN); + + expect(preg_match(ValidationPatterns::SERVER_USERNAME_PATTERN, 'deploy.user'))->toBe(1); + expect(preg_match(ValidationPatterns::SERVER_USERNAME_PATTERN, 'deploy$user'))->toBe(0); +}); + +it('preserves dots when sanitizing server SSH usernames', function () { + $server = new Server; + $server->user = 'deploy.user'; + + expect($server->user)->toBe('deploy.user'); +}); From 7405bd7088d722b85867cefa4f60cdca7ec231e5 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:39:43 +0200 Subject: [PATCH 09/11] chore: inspect staged modal changes --- resources/views/components/modal-input.blade.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/views/components/modal-input.blade.php b/resources/views/components/modal-input.blade.php index 344290f72..1c750ea3a 100644 --- a/resources/views/components/modal-input.blade.php +++ b/resources/views/components/modal-input.blade.php @@ -60,7 +60,8 @@
-
+
{{ $slot }}
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 10/11] 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.', + ], + ]); }); From 787de4059e38d09029d57a574415b763b6776483 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:04:24 +0200 Subject: [PATCH 11/11] fix(ui): improve slide-over close focus styles --- resources/views/components/slide-over.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/components/slide-over.blade.php b/resources/views/components/slide-over.blade.php index 5f89b5041..114135463 100644 --- a/resources/views/components/slide-over.blade.php +++ b/resources/views/components/slide-over.blade.php @@ -36,7 +36,7 @@ x-init="$watch('slideOverOpen', value => {