diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 5c186af70..3e78c1732 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -153,8 +153,8 @@ protected function rules(): array 'staticImage' => 'required', 'baseDirectory' => array_merge(['required'], array_slice(ValidationPatterns::directoryPathRules(), 1)), 'publishDirectory' => ValidationPatterns::directoryPathRules(), - 'portsExposes' => 'required', - 'portsMappings' => 'nullable', + 'portsExposes' => ['required', 'string', 'regex:/^(\d+)(,\d+)*$/'], + 'portsMappings' => ValidationPatterns::portMappingRules(), 'customNetworkAliases' => 'nullable', 'dockerfile' => 'nullable', 'dockerRegistryImageName' => 'nullable', @@ -209,6 +209,8 @@ protected function messages(): array 'staticImage.required' => 'The Static Image field is required.', 'baseDirectory.required' => 'The Base Directory field is required.', 'portsExposes.required' => 'The Exposed Ports field is required.', + 'portsExposes.regex' => 'Ports exposes must be a comma-separated list of port numbers (e.g. 3000,3001).', + ...ValidationPatterns::portMappingMessages(), 'isStatic.required' => 'The Static setting is required.', 'isStatic.boolean' => 'The Static setting must be true or false.', 'isSpa.required' => 'The SPA setting is required.', @@ -752,6 +754,12 @@ public function submit($showToaster = true) $this->authorize('update', $this->application); $this->resetErrorBag(); + + $this->portsExposes = str($this->portsExposes)->replace(' ', '')->trim()->toString(); + if ($this->portsMappings) { + $this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString(); + } + $this->validate(); $oldPortsExposes = $this->application->ports_exposes; diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php index 9de75c1c5..0913ca797 100644 --- a/app/Livewire/Project/Database/Clickhouse/General.php +++ b/app/Livewire/Project/Database/Clickhouse/General.php @@ -79,7 +79,7 @@ protected function rules(): array 'clickhouseAdminUser' => 'required|string', 'clickhouseAdminPassword' => 'required|string', 'image' => 'required|string', - 'portsMappings' => 'nullable|string', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -94,6 +94,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'clickhouseAdminUser.required' => 'The Admin User field is required.', 'clickhouseAdminUser.string' => 'The Admin User must be a string.', @@ -207,6 +208,9 @@ public function submit() try { $this->authorize('update', $this->database); + if ($this->portsMappings) { + $this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString(); + } if (str($this->publicPort)->isEmpty()) { $this->publicPort = null; } diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php index d35e57a9d..23503bd98 100644 --- a/app/Livewire/Project/Database/Dragonfly/General.php +++ b/app/Livewire/Project/Database/Dragonfly/General.php @@ -90,7 +90,7 @@ protected function rules(): array 'description' => ValidationPatterns::descriptionRules(), 'dragonflyPassword' => 'required|string', 'image' => 'required|string', - 'portsMappings' => 'nullable|string', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -106,6 +106,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'dragonflyPassword.required' => 'The Dragonfly Password field is required.', 'dragonflyPassword.string' => 'The Dragonfly Password must be a string.', @@ -217,6 +218,9 @@ public function submit() try { $this->authorize('update', $this->database); + if ($this->portsMappings) { + $this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString(); + } if (str($this->publicPort)->isEmpty()) { $this->publicPort = null; } diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php index adb4ccb5f..ff9dc19ad 100644 --- a/app/Livewire/Project/Database/Keydb/General.php +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -93,7 +93,7 @@ protected function rules(): array 'keydbConf' => 'nullable|string', 'keydbPassword' => 'required|string', 'image' => 'required|string', - 'portsMappings' => 'nullable|string', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -111,6 +111,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'keydbPassword.required' => 'The KeyDB Password field is required.', 'keydbPassword.string' => 'The KeyDB Password must be a string.', @@ -224,6 +225,9 @@ public function submit() try { $this->authorize('manageEnvironment', $this->database); + if ($this->portsMappings) { + $this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString(); + } if (str($this->publicPort)->isEmpty()) { $this->publicPort = null; } diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php index 14240c82d..a9ac47b97 100644 --- a/app/Livewire/Project/Database/Mariadb/General.php +++ b/app/Livewire/Project/Database/Mariadb/General.php @@ -78,7 +78,7 @@ protected function rules(): array 'mariadbDatabase' => 'required', 'mariadbConf' => 'nullable', 'image' => 'required', - 'portsMappings' => 'nullable', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -92,6 +92,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'name.required' => 'The Name field is required.', 'mariadbRootPassword.required' => 'The Root Password field is required.', @@ -213,6 +214,9 @@ public function submit() try { $this->authorize('update', $this->database); + if ($this->portsMappings) { + $this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString(); + } if (str($this->publicPort)->isEmpty()) { $this->publicPort = null; } diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php index 11419ec71..2b6538edf 100644 --- a/app/Livewire/Project/Database/Mongodb/General.php +++ b/app/Livewire/Project/Database/Mongodb/General.php @@ -77,7 +77,7 @@ protected function rules(): array 'mongoInitdbRootPassword' => 'required', 'mongoInitdbDatabase' => 'required', 'image' => 'required', - 'portsMappings' => 'nullable', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -92,6 +92,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'name.required' => 'The Name field is required.', 'mongoInitdbRootUsername.required' => 'The Root Username field is required.', @@ -213,6 +214,9 @@ public function submit() try { $this->authorize('update', $this->database); + if ($this->portsMappings) { + $this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString(); + } if (str($this->publicPort)->isEmpty()) { $this->publicPort = null; } diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php index 4f0f5eb19..f3c554522 100644 --- a/app/Livewire/Project/Database/Mysql/General.php +++ b/app/Livewire/Project/Database/Mysql/General.php @@ -80,7 +80,7 @@ protected function rules(): array 'mysqlDatabase' => 'required', 'mysqlConf' => 'nullable', 'image' => 'required', - 'portsMappings' => 'nullable', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -95,6 +95,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'name.required' => 'The Name field is required.', 'mysqlRootPassword.required' => 'The Root Password field is required.', @@ -220,6 +221,9 @@ public function submit() try { $this->authorize('update', $this->database); + if ($this->portsMappings) { + $this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString(); + } if (str($this->publicPort)->isEmpty()) { $this->publicPort = null; } diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php index 4e044672b..7a4ff057e 100644 --- a/app/Livewire/Project/Database/Postgresql/General.php +++ b/app/Livewire/Project/Database/Postgresql/General.php @@ -92,7 +92,7 @@ protected function rules(): array 'postgresConf' => 'nullable', 'initScripts' => 'nullable', 'image' => 'required', - 'portsMappings' => 'nullable', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -107,6 +107,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'name.required' => 'The Name field is required.', 'postgresUser.required' => 'The Postgres User field is required.', @@ -456,6 +457,9 @@ public function submit() try { $this->authorize('update', $this->database); + if ($this->portsMappings) { + $this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString(); + } if (str($this->publicPort)->isEmpty()) { $this->publicPort = null; } diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php index ebe2f3ba0..a1a22ab8b 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -73,7 +73,7 @@ protected function rules(): array 'description' => ValidationPatterns::descriptionRules(), 'redisConf' => 'nullable', 'image' => 'required', - 'portsMappings' => 'nullable', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -89,6 +89,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'name.required' => 'The Name field is required.', 'image.required' => 'The Docker Image field is required.', @@ -201,6 +202,9 @@ public function submit() try { $this->authorize('manageEnvironment', $this->database); + if ($this->portsMappings) { + $this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString(); + } $this->syncData(true); if (version_compare($this->redisVersion, '6.0', '>=')) { diff --git a/app/Support/ValidationPatterns.php b/app/Support/ValidationPatterns.php index 7084b4cc2..5d53076ea 100644 --- a/app/Support/ValidationPatterns.php +++ b/app/Support/ValidationPatterns.php @@ -194,6 +194,12 @@ public static function volumeNameMessages(string $field = 'name'): array ]; } + /** + * Pattern for port mappings (e.g. 3000:3000, 8080:80, 8000-8010:8000-8010) + * Each entry requires host:container format, where each side can be a number or a range (number-number) + */ + public const PORT_MAPPINGS_PATTERN = '/^(\d+(-\d+)?:\d+(-\d+)?)(,\d+(-\d+)?:\d+(-\d+)?)*$/'; + /** * Get validation rules for container name fields */ @@ -202,6 +208,24 @@ public static function containerNameRules(int $maxLength = 255): array return ['string', 'max:'.$maxLength, 'regex:'.self::CONTAINER_NAME_PATTERN]; } + /** + * Get validation rules for port mapping fields + */ + public static function portMappingRules(): array + { + return ['nullable', 'string', 'regex:'.self::PORT_MAPPINGS_PATTERN]; + } + + /** + * Get validation messages for port mapping fields + */ + public static function portMappingMessages(string $field = 'portsMappings'): array + { + return [ + "{$field}.regex" => 'Port mappings must be a comma-separated list of port pairs or ranges (e.g. 3000:3000,8080:80,8000-8010:8000-8010).', + ]; + } + /** * Check if a string is a valid Docker container name. */