diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index 6c3b2da00..2c2195ea3 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,10 +488,12 @@ 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', + ], [ + ...ValidationPatterns::serverUsernameMessages(), ]); $extraFields = array_diff(array_keys($request->all()), $allowedFields); @@ -666,7 +669,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', @@ -676,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 33c75bf70..2d0ae939d 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; @@ -212,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); @@ -274,12 +292,7 @@ class Index extends Component public function saveServer() { - $this->validate([ - 'remoteServerName' => 'required|string', - 'remoteServerHost' => 'required|string', - 'remoteServerPort' => 'required|integer', - 'remoteServerUser' => 'required|string', - ]); + $this->validate(); $this->privateKey = formatPrivateKey($this->privateKey); $foundServer = Server::whereIp($this->remoteServerHost)->first(); @@ -465,10 +478,10 @@ class Index extends Component public function saveAndValidateServer() { - $this->validate([ - 'remoteServerPort' => 'required|integer|min:1|max:65535', - 'remoteServerUser' => 'required|string', - ]); + $this->validate(array_intersect_key($this->rules(), array_flip([ + 'remoteServerPort', + 'remoteServerUser', + ]))); $this->createdServer->update([ 'port' => $this->remoteServerPort, diff --git a/app/Livewire/Server/New/ByIp.php b/app/Livewire/Server/New/ByIp.php index 51c6a06ee..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 d7339dcdb..4a6e2335e 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 74e8ba5b0..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 7e3974dd7..bdb8654b9 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). * @@ -283,6 +294,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/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/resources/views/components/modal-input.blade.php b/resources/views/components/modal-input.blade.php index 2860d25e6..dc1191b44 100644 --- a/resources/views/components/modal-input.blade.php +++ b/resources/views/components/modal-input.blade.php @@ -36,32 +36,34 @@