diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 1b5cd0d44c..8e31a7051c 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -643,6 +643,7 @@ class DatabasesController extends Controller 'database_backup_retention_amount_s3' => ['type' => 'integer', 'description' => 'Number of backups to retain in S3'], 'database_backup_retention_days_s3' => ['type' => 'integer', 'description' => 'Number of days to retain backups in S3'], 'database_backup_retention_max_storage_s3' => ['type' => 'integer', 'description' => 'Max storage (MB) for S3 backups'], + 'timeout' => ['type' => 'integer', 'description' => 'Backup job timeout in seconds (min: 60, max: 36000)', 'default' => 3600], ], ), ) @@ -679,7 +680,7 @@ class DatabasesController extends Controller )] public function create_backup(Request $request) { - $backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid']; + $backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid', 'timeout']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -706,6 +707,7 @@ class DatabasesController extends Controller 'database_backup_retention_amount_s3' => 'integer|min:0', 'database_backup_retention_days_s3' => 'integer|min:0', 'database_backup_retention_max_storage_s3' => 'integer|min:0', + 'timeout' => 'integer|min:60|max:36000', ]); if ($validator->fails()) { @@ -880,6 +882,7 @@ class DatabasesController extends Controller 'database_backup_retention_amount_s3' => ['type' => 'integer', 'description' => 'Retention amount of the backup in s3'], 'database_backup_retention_days_s3' => ['type' => 'integer', 'description' => 'Retention days of the backup in s3'], 'database_backup_retention_max_storage_s3' => ['type' => 'integer', 'description' => 'Max storage of the backup in S3'], + 'timeout' => ['type' => 'integer', 'description' => 'Backup job timeout in seconds (min: 60, max: 36000)', 'default' => 3600], ], ), ) @@ -909,7 +912,7 @@ class DatabasesController extends Controller )] public function update_backup(Request $request) { - $backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid']; + $backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid', 'timeout']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -927,13 +930,14 @@ class DatabasesController extends Controller 'dump_all' => 'boolean', 's3_storage_uuid' => 'string|exists:s3_storages,uuid|nullable', 'databases_to_backup' => 'string|nullable', - 'frequency' => 'string|in:every_minute,hourly,daily,weekly,monthly,yearly', + 'frequency' => 'string', 'database_backup_retention_amount_locally' => 'integer|min:0', 'database_backup_retention_days_locally' => 'integer|min:0', 'database_backup_retention_max_storage_locally' => 'integer|min:0', 'database_backup_retention_amount_s3' => 'integer|min:0', 'database_backup_retention_days_s3' => 'integer|min:0', 'database_backup_retention_max_storage_s3' => 'integer|min:0', + 'timeout' => 'integer|min:60|max:36000', ]); if ($validator->fails()) { return response()->json([ @@ -960,6 +964,17 @@ class DatabasesController extends Controller $this->authorize('update', $database); + // Validate frequency is a valid cron expression + if ($request->filled('frequency')) { + $isValid = validate_cron_expression($request->frequency); + if (! $isValid) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['frequency' => ['Invalid cron expression or frequency format.']], + ], 422); + } + } + if ($request->boolean('save_s3') && ! $request->filled('s3_storage_uuid')) { return response()->json([ 'message' => 'Validation failed.', diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index 2ef95ce8b6..c13c6665c0 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -598,6 +598,11 @@ class ServersController extends Controller 'is_build_server' => ['type' => 'boolean', 'description' => 'Is build server.'], 'instant_validate' => ['type' => 'boolean', 'description' => 'Instant validate.'], 'proxy_type' => ['type' => 'string', 'enum' => ['traefik', 'caddy', 'none'], 'description' => 'The proxy type.'], + 'concurrent_builds' => ['type' => 'integer', 'description' => 'Number of concurrent builds.'], + 'dynamic_timeout' => ['type' => 'integer', 'description' => 'Deployment timeout in seconds.'], + 'deployment_queue_limit' => ['type' => 'integer', 'description' => 'Maximum number of queued deployments.'], + 'server_disk_usage_notification_threshold' => ['type' => 'integer', 'description' => 'Server disk usage notification threshold (%).'], + 'server_disk_usage_check_frequency' => ['type' => 'string', 'description' => 'Cron expression for disk usage check frequency.'], ], ), ), @@ -634,7 +639,7 @@ class ServersController extends Controller )] public function update_server(Request $request) { - $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate', 'proxy_type']; + $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate', 'proxy_type', 'concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -655,6 +660,11 @@ class ServersController extends Controller 'is_build_server' => 'boolean|nullable', 'instant_validate' => 'boolean|nullable', 'proxy_type' => 'string|nullable', + 'concurrent_builds' => 'integer|min:1', + 'dynamic_timeout' => 'integer|min:1', + 'deployment_queue_limit' => 'integer|min:1', + 'server_disk_usage_notification_threshold' => 'integer|min:1|max:100', + 'server_disk_usage_check_frequency' => 'string', ]); $extraFields = array_diff(array_keys($request->all()), $allowedFields); @@ -691,6 +701,19 @@ class ServersController extends Controller 'is_build_server' => $request->is_build_server, ]); } + + if ($request->has('server_disk_usage_check_frequency') && ! validate_cron_expression($request->server_disk_usage_check_frequency)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['server_disk_usage_check_frequency' => ['Invalid Cron / Human expression for Disk Usage Check Frequency.']], + ], 422); + } + + $advancedSettings = $request->only(['concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency']); + if (! empty($advancedSettings)) { + $server->settings()->update(array_filter($advancedSettings, fn ($value) => ! is_null($value))); + } + if ($request->instant_validate) { ValidateServer::dispatch($server); } diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php index 847f107656..364163ff82 100644 --- a/app/Livewire/Notifications/Email.php +++ b/app/Livewire/Notifications/Email.php @@ -42,7 +42,7 @@ class Email extends Component public ?string $smtpHost = null; #[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])] - public ?int $smtpPort = null; + public ?string $smtpPort = null; #[Validate(['nullable', 'string', 'in:starttls,tls,none'])] public ?string $smtpEncryption = null; @@ -54,7 +54,7 @@ class Email extends Component public ?string $smtpPassword = null; #[Validate(['nullable', 'numeric'])] - public ?int $smtpTimeout = null; + public ?string $smtpTimeout = null; #[Validate(['boolean'])] public bool $resendEnabled = false; diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 6fd063cf3c..25ce82eb0f 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -153,8 +153,8 @@ class General extends Component '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', @@ -212,6 +212,8 @@ class General extends Component '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.', @@ -756,6 +758,12 @@ class General extends Component $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/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index 0fff2bd03f..a18022882c 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -76,7 +76,7 @@ class BackupEdit extends Component public bool $dumpAll = false; #[Validate(['required', 'int', 'min:60', 'max:36000'])] - public int $timeout = 3600; + public int|string $timeout = 3600; public function mount() { diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php index ffce8c9bd2..e06629d103 100644 --- a/app/Livewire/Project/Database/Clickhouse/General.php +++ b/app/Livewire/Project/Database/Clickhouse/General.php @@ -79,7 +79,7 @@ class General extends Component 'clickhouseAdminUser' => 'required|string', 'clickhouseAdminPassword' => 'required|string', 'image' => 'required|string', - 'portsMappings' => 'nullable|string', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer|min:1|max:65535', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -94,6 +94,7 @@ class General extends Component { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'clickhouseAdminUser.required' => 'The Admin User field is required.', 'clickhouseAdminUser.string' => 'The Admin User must be a string.', @@ -209,6 +210,9 @@ class General extends Component 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 2e6c9dca75..591780cfb7 100644 --- a/app/Livewire/Project/Database/Dragonfly/General.php +++ b/app/Livewire/Project/Database/Dragonfly/General.php @@ -90,7 +90,7 @@ class General extends Component 'description' => ValidationPatterns::descriptionRules(), 'dragonflyPassword' => 'required|string', 'image' => 'required|string', - 'portsMappings' => 'nullable|string', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer|min:1|max:65535', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -106,6 +106,7 @@ class General extends Component { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'dragonflyPassword.required' => 'The Dragonfly Password field is required.', 'dragonflyPassword.string' => 'The Dragonfly Password must be a string.', @@ -219,6 +220,9 @@ class General extends Component 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 235e34e208..35799e55f0 100644 --- a/app/Livewire/Project/Database/Keydb/General.php +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -93,7 +93,7 @@ class General extends Component 'keydbConf' => 'nullable|string', 'keydbPassword' => 'required|string', 'image' => 'required|string', - 'portsMappings' => 'nullable|string', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer|min:1|max:65535', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -111,6 +111,7 @@ class General extends Component { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'keydbPassword.required' => 'The KeyDB Password field is required.', 'keydbPassword.string' => 'The KeyDB Password must be a string.', @@ -226,6 +227,9 @@ class General extends Component 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 47e0fd091b..5615765fdc 100644 --- a/app/Livewire/Project/Database/Mariadb/General.php +++ b/app/Livewire/Project/Database/Mariadb/General.php @@ -78,7 +78,7 @@ class General extends Component 'mariadbDatabase' => 'required', 'mariadbConf' => 'nullable', 'image' => 'required', - 'portsMappings' => 'nullable', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer|min:1|max:65535', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -92,6 +92,7 @@ class General extends Component { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'name.required' => 'The Name field is required.', 'mariadbRootPassword.required' => 'The Root Password field is required.', @@ -215,6 +216,9 @@ class General extends Component 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 6a37263715..0bc6d1e2f4 100644 --- a/app/Livewire/Project/Database/Mongodb/General.php +++ b/app/Livewire/Project/Database/Mongodb/General.php @@ -77,7 +77,7 @@ class General extends Component 'mongoInitdbRootPassword' => 'required', 'mongoInitdbDatabase' => 'required', 'image' => 'required', - 'portsMappings' => 'nullable', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer|min:1|max:65535', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -92,6 +92,7 @@ class General extends Component { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'name.required' => 'The Name field is required.', 'mongoInitdbRootUsername.required' => 'The Root Username field is required.', @@ -215,6 +216,9 @@ class General extends Component 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 750be4ce79..df244662e6 100644 --- a/app/Livewire/Project/Database/Mysql/General.php +++ b/app/Livewire/Project/Database/Mysql/General.php @@ -80,7 +80,7 @@ class General extends Component 'mysqlDatabase' => 'required', 'mysqlConf' => 'nullable', 'image' => 'required', - 'portsMappings' => 'nullable', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer|min:1|max:65535', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -95,6 +95,7 @@ class General extends Component { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'name.required' => 'The Name field is required.', 'mysqlRootPassword.required' => 'The Root Password field is required.', @@ -222,6 +223,9 @@ class General extends Component 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 8feb9bd228..f862e0cc60 100644 --- a/app/Livewire/Project/Database/Postgresql/General.php +++ b/app/Livewire/Project/Database/Postgresql/General.php @@ -92,7 +92,7 @@ class General extends Component 'postgresConf' => 'nullable', 'initScripts' => 'nullable', 'image' => 'required', - 'portsMappings' => 'nullable', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer|min:1|max:65535', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -107,6 +107,7 @@ class General extends Component { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'name.required' => 'The Name field is required.', 'postgresUser.required' => 'The Postgres User field is required.', @@ -469,6 +470,9 @@ class General extends Component 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 e131bc5982..2eec14c011 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -73,7 +73,7 @@ class General extends Component 'description' => ValidationPatterns::descriptionRules(), 'redisConf' => 'nullable', 'image' => 'required', - 'portsMappings' => 'nullable', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer|min:1|max:65535', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -89,6 +89,7 @@ class General extends Component { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'name.required' => 'The Name field is required.', 'image.required' => 'The Docker Image field is required.', @@ -203,6 +204,9 @@ class General extends Component 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/Livewire/Project/Shared/ResourceLimits.php b/app/Livewire/Project/Shared/ResourceLimits.php index 0b38402897..8a14dc10c4 100644 --- a/app/Livewire/Project/Shared/ResourceLimits.php +++ b/app/Livewire/Project/Shared/ResourceLimits.php @@ -3,6 +3,7 @@ namespace App\Livewire\Project\Shared; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Illuminate\Validation\ValidationException; use Livewire\Component; class ResourceLimits extends Component @@ -16,24 +17,24 @@ class ResourceLimits extends Component public ?string $limitsCpuset = null; - public ?int $limitsCpuShares = null; + public mixed $limitsCpuShares = null; public string $limitsMemory; public string $limitsMemorySwap; - public int $limitsMemorySwappiness; + public mixed $limitsMemorySwappiness = 0; public string $limitsMemoryReservation; protected $rules = [ - 'limitsMemory' => 'required|string', - 'limitsMemorySwap' => 'required|string', + 'limitsMemory' => ['required', 'string', 'regex:/^(0|\d+[bBkKmMgG])$/'], + 'limitsMemorySwap' => ['required', 'string', 'regex:/^(0|\d+[bBkKmMgG])$/'], 'limitsMemorySwappiness' => 'required|integer|min:0|max:100', - 'limitsMemoryReservation' => 'required|string', - 'limitsCpus' => 'nullable', - 'limitsCpuset' => 'nullable', - 'limitsCpuShares' => 'nullable', + 'limitsMemoryReservation' => ['required', 'string', 'regex:/^(0|\d+[bBkKmMgG])$/'], + 'limitsCpus' => ['nullable', 'regex:/^\d*\.?\d+$/'], + 'limitsCpuset' => ['nullable', 'regex:/^\d+([,-]\d+)*$/'], + 'limitsCpuShares' => 'nullable|integer|min:0', ]; protected $validationAttributes = [ @@ -46,6 +47,19 @@ class ResourceLimits extends Component 'limitsCpuShares' => 'cpu shares', ]; + protected $messages = [ + 'limitsMemory.regex' => 'Maximum Memory Limit must be a number followed by a unit (b, k, m, g). Example: 256m, 1g. Use 0 for unlimited.', + 'limitsMemorySwap.regex' => 'Maximum Swap Limit must be a number followed by a unit (b, k, m, g). Example: 256m, 1g. Use 0 for unlimited.', + 'limitsMemoryReservation.regex' => 'Soft Memory Limit must be a number followed by a unit (b, k, m, g). Example: 256m, 1g. Use 0 for unlimited.', + 'limitsCpus.regex' => 'Number of CPUs must be a number (integer or decimal). Example: 0.5, 2.', + 'limitsCpuset.regex' => 'CPU sets must be a comma-separated list of CPU numbers or ranges. Example: 0-2 or 0,1,3.', + 'limitsMemorySwappiness.integer' => 'Swappiness must be a whole number between 0 and 100.', + 'limitsMemorySwappiness.min' => 'Swappiness must be between 0 and 100.', + 'limitsMemorySwappiness.max' => 'Swappiness must be between 0 and 100.', + 'limitsCpuShares.integer' => 'CPU Weight must be a whole number.', + 'limitsCpuShares.min' => 'CPU Weight must be a positive number.', + ]; + /** * Sync data between component properties and model * @@ -57,10 +71,10 @@ class ResourceLimits extends Component // Sync TO model (before save) $this->resource->limits_cpus = $this->limitsCpus; $this->resource->limits_cpuset = $this->limitsCpuset; - $this->resource->limits_cpu_shares = $this->limitsCpuShares; + $this->resource->limits_cpu_shares = (int) $this->limitsCpuShares; $this->resource->limits_memory = $this->limitsMemory; $this->resource->limits_memory_swap = $this->limitsMemorySwap; - $this->resource->limits_memory_swappiness = $this->limitsMemorySwappiness; + $this->resource->limits_memory_swappiness = (int) $this->limitsMemorySwappiness; $this->resource->limits_memory_reservation = $this->limitsMemoryReservation; } else { // Sync FROM model (on load/refresh) @@ -91,7 +105,7 @@ class ResourceLimits extends Component if (! $this->limitsMemorySwap) { $this->limitsMemorySwap = '0'; } - if (is_null($this->limitsMemorySwappiness)) { + if ($this->limitsMemorySwappiness === '' || is_null($this->limitsMemorySwappiness)) { $this->limitsMemorySwappiness = 60; } if (! $this->limitsMemoryReservation) { @@ -103,7 +117,7 @@ class ResourceLimits extends Component if ($this->limitsCpuset === '') { $this->limitsCpuset = null; } - if (is_null($this->limitsCpuShares)) { + if ($this->limitsCpuShares === '' || is_null($this->limitsCpuShares)) { $this->limitsCpuShares = 1024; } @@ -112,6 +126,12 @@ class ResourceLimits extends Component $this->syncData(true); $this->resource->save(); $this->dispatch('success', 'Resource limits updated.'); + } catch (ValidationException $e) { + foreach ($e->validator->errors()->all() as $message) { + $this->dispatch('error', $message); + } + + return; } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Server/Advanced.php b/app/Livewire/Server/Advanced.php index dba1b49035..b39da5e5aa 100644 --- a/app/Livewire/Server/Advanced.php +++ b/app/Livewire/Server/Advanced.php @@ -15,17 +15,17 @@ class Advanced extends Component #[Validate(['string'])] public string $serverDiskUsageCheckFrequency = '0 23 * * *'; - #[Validate(['integer', 'min:1', 'max:99'])] - public int $serverDiskUsageNotificationThreshold = 50; + #[Validate(['required', 'integer', 'min:1', 'max:99'])] + public int|string $serverDiskUsageNotificationThreshold = 50; - #[Validate(['integer', 'min:1'])] - public int $concurrentBuilds = 1; + #[Validate(['required', 'integer', 'min:1'])] + public int|string $concurrentBuilds = 1; - #[Validate(['integer', 'min:1'])] - public int $dynamicTimeout = 1; + #[Validate(['required', 'integer', 'min:1'])] + public int|string $dynamicTimeout = 1; - #[Validate(['integer', 'min:1'])] - public int $deploymentQueueLimit = 25; + #[Validate(['required', 'integer', 'min:1'])] + public int|string $deploymentQueueLimit = 25; public function mount(string $server_uuid) { diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index d5f30fca05..c2d8205efa 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -6,6 +6,7 @@ use App\Actions\Proxy\GetProxyConfiguration; use App\Actions\Proxy\SaveProxyConfiguration; use App\Enums\ProxyTypes; use App\Models\Server; +use App\Rules\SafeExternalUrl; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; @@ -41,9 +42,13 @@ class Proxy extends Component ]; } - protected $rules = [ - 'generateExactLabels' => 'required|boolean', - ]; + protected function rules() + { + return [ + 'generateExactLabels' => 'required|boolean', + 'redirectUrl' => ['nullable', new SafeExternalUrl], + ]; + } public function mount() { @@ -147,6 +152,7 @@ class Proxy extends Component { try { $this->authorize('update', $this->server); + $this->validate(); SaveProxyConfiguration::run($this->server, $this->proxySettings); $this->server->proxy->redirect_url = $this->redirectUrl; $this->server->save(); diff --git a/app/Livewire/Server/Sentinel.php b/app/Livewire/Server/Sentinel.php index dff379ae19..a4b35891b2 100644 --- a/app/Livewire/Server/Sentinel.php +++ b/app/Livewire/Server/Sentinel.php @@ -25,13 +25,13 @@ class Sentinel extends Component public ?string $sentinelUpdatedAt = null; #[Validate(['required', 'integer', 'min:1'])] - public int $sentinelMetricsRefreshRateSeconds; + public int|string $sentinelMetricsRefreshRateSeconds; #[Validate(['required', 'integer', 'min:1'])] - public int $sentinelMetricsHistoryDays; + public int|string $sentinelMetricsHistoryDays; #[Validate(['required', 'integer', 'min:10'])] - public int $sentinelPushIntervalSeconds; + public int|string $sentinelPushIntervalSeconds; #[Validate(['nullable', 'url'])] public ?string $sentinelCustomUrl = null; diff --git a/app/Livewire/Settings/Advanced.php b/app/Livewire/Settings/Advanced.php index 9e4f94f8a8..d31f688593 100644 --- a/app/Livewire/Settings/Advanced.php +++ b/app/Livewire/Settings/Advanced.php @@ -3,6 +3,7 @@ namespace App\Livewire\Settings; use App\Models\InstanceSettings; +use App\Rules\ValidDnsServers; use App\Rules\ValidIpOrCidr; use Livewire\Attributes\Validate; use Livewire\Component; @@ -20,7 +21,6 @@ class Advanced extends Component #[Validate('boolean')] public bool $is_dns_validation_enabled; - #[Validate('nullable|string')] public ?string $custom_dns_servers = null; #[Validate('boolean')] @@ -43,7 +43,7 @@ class Advanced extends Component 'is_registration_enabled' => 'boolean', 'do_not_track' => 'boolean', 'is_dns_validation_enabled' => 'boolean', - 'custom_dns_servers' => 'nullable|string', + 'custom_dns_servers' => ['nullable', 'string', new ValidDnsServers], 'is_api_enabled' => 'boolean', 'allowed_ips' => ['nullable', 'string', new ValidIpOrCidr], 'is_sponsorship_popup_enabled' => 'boolean', diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php index ca48e9b167..8c0e244001 100644 --- a/app/Livewire/SettingsEmail.php +++ b/app/Livewire/SettingsEmail.php @@ -33,7 +33,7 @@ class SettingsEmail extends Component public ?string $smtpHost = null; #[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])] - public ?int $smtpPort = null; + public ?string $smtpPort = null; #[Validate(['nullable', 'string', 'in:starttls,tls,none'])] public ?string $smtpEncryption = 'starttls'; @@ -45,7 +45,7 @@ class SettingsEmail extends Component public ?string $smtpPassword = null; #[Validate(['nullable', 'numeric'])] - public ?int $smtpTimeout = null; + public ?string $smtpTimeout = null; #[Validate(['boolean'])] public bool $resendEnabled = false; diff --git a/app/Rules/ValidDnsServers.php b/app/Rules/ValidDnsServers.php new file mode 100644 index 0000000000..e3bbd048ff --- /dev/null +++ b/app/Rules/ValidDnsServers.php @@ -0,0 +1,35 @@ + '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. */ diff --git a/resources/views/livewire/notifications/email.blade.php b/resources/views/livewire/notifications/email.blade.php index 5388511378..4107030104 100644 --- a/resources/views/livewire/notifications/email.blade.php +++ b/resources/views/livewire/notifications/email.blade.php @@ -72,7 +72,7 @@
- + @@ -82,7 +82,7 @@
-
diff --git a/resources/views/livewire/project/database/backup-edit.blade.php b/resources/views/livewire/project/database/backup-edit.blade.php index bb5dcfc4d5..d5c25916a3 100644 --- a/resources/views/livewire/project/database/backup-edit.blade.php +++ b/resources/views/livewire/project/database/backup-edit.blade.php @@ -81,10 +81,10 @@ @endif
- + - + helper="The timezone of the server where the backup is scheduled to run (if not set, the instance timezone will be used)" required /> +

Backup Retention Settings

@@ -101,13 +101,13 @@
+ helper="Keeps only the specified number of most recent backups on the server. Set to 0 for unlimited backups." required /> + helper="Automatically removes backups older than the specified number of days. Set to 0 for no time limit." required /> + helper="When total size of all backups in the current backup job exceeds this limit in GB, the oldest backups will be removed. Decimal values are supported (e.g. 0.001 for 1MB). Set to 0 for unlimited storage." required />
@@ -117,13 +117,13 @@
+ helper="Keeps only the specified number of most recent backups on S3 storage. Set to 0 for unlimited backups." required /> + helper="Automatically removes S3 backups older than the specified number of days. Set to 0 for no time limit." required /> + helper="When total size of all backups in the current backup job exceeds this limit in GB, the oldest backups will be removed. Decimal values are supported (e.g. 0.5 for 500MB). Set to 0 for unlimited storage." required />
@endif diff --git a/resources/views/livewire/server/advanced.blade.php b/resources/views/livewire/server/advanced.blade.php index 33086aea1b..f6610c1d59 100644 --- a/resources/views/livewire/server/advanced.blade.php +++ b/resources/views/livewire/server/advanced.blade.php @@ -22,6 +22,7 @@ id="serverDiskUsageCheckFrequency" label="Disk usage check frequency" required helper="Cron expression for disk usage check frequency.
You can use every_minute, hourly, daily, weekly, monthly, yearly.

Default is every night at 11:00 PM." /> @@ -31,12 +32,15 @@

Builds

diff --git a/resources/views/livewire/server/sentinel.blade.php b/resources/views/livewire/server/sentinel.blade.php index 4016a30e47..5ca535cbc1 100644 --- a/resources/views/livewire/server/sentinel.blade.php +++ b/resources/views/livewire/server/sentinel.blade.php @@ -91,13 +91,14 @@
- - -
diff --git a/resources/views/livewire/settings-email.blade.php b/resources/views/livewire/settings-email.blade.php index c58ea189dd..93abd628c3 100644 --- a/resources/views/livewire/settings-email.blade.php +++ b/resources/views/livewire/settings-email.blade.php @@ -53,7 +53,7 @@ - +