refactor(validation): implement centralized validation patterns across components

- Introduced `ValidationPatterns` class to standardize validation rules and messages for various fields across multiple components.
- Updated components including `General`, `StackForm`, `Create`, and `Show` to utilize the new validation patterns, ensuring consistent validation logic.
- Enhanced error messages for required fields and added regex validation for names and descriptions to improve user feedback.
- Adjusted styling in the `create.blade.php` view for better visual hierarchy.
This commit is contained in:
Andras Bacsai
2025-08-19 14:15:31 +02:00
parent eaee87d008
commit 5c4a265542
19 changed files with 659 additions and 250 deletions

View File

@@ -6,9 +6,9 @@ use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\Server;
use App\Models\StandaloneClickhouse;
use App\Support\ValidationPatterns;
use Exception;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Validate;
use Livewire\Component;
class General extends Component
@@ -17,40 +17,28 @@ class General extends Component
public StandaloneClickhouse $database;
#[Validate(['required', 'string'])]
public string $name;
#[Validate(['nullable', 'string'])]
public ?string $description = null;
#[Validate(['required', 'string'])]
public string $clickhouseAdminUser;
#[Validate(['required', 'string'])]
public string $clickhouseAdminPassword;
#[Validate(['required', 'string'])]
public string $image;
#[Validate(['nullable', 'string'])]
public ?string $portsMappings = null;
#[Validate(['nullable', 'boolean'])]
public ?bool $isPublic = null;
#[Validate(['nullable', 'integer'])]
public ?int $publicPort = null;
#[Validate(['nullable', 'string'])]
public ?string $customDockerRunOptions = null;
#[Validate(['nullable', 'string'])]
public ?string $dbUrl = null;
#[Validate(['nullable', 'string'])]
public ?string $dbUrlPublic = null;
#[Validate(['nullable', 'boolean'])]
public bool $isLogDrainEnabled = false;
public function getListeners()
@@ -72,6 +60,40 @@ class General extends Component
}
}
protected function rules(): array
{
return [
'name' => ValidationPatterns::nameRules(),
'description' => ValidationPatterns::descriptionRules(),
'clickhouseAdminUser' => 'required|string',
'clickhouseAdminPassword' => 'required|string',
'image' => 'required|string',
'portsMappings' => 'nullable|string',
'isPublic' => 'nullable|boolean',
'publicPort' => 'nullable|integer',
'customDockerRunOptions' => 'nullable|string',
'dbUrl' => 'nullable|string',
'dbUrlPublic' => 'nullable|string',
'isLogDrainEnabled' => 'nullable|boolean',
];
}
protected function messages(): array
{
return array_merge(
ValidationPatterns::combinedMessages(),
[
'clickhouseAdminUser.required' => 'The Admin User field is required.',
'clickhouseAdminUser.string' => 'The Admin User must be a string.',
'clickhouseAdminPassword.required' => 'The Admin Password field is required.',
'clickhouseAdminPassword.string' => 'The Admin Password must be a string.',
'image.required' => 'The Docker Image field is required.',
'image.string' => 'The Docker Image must be a string.',
'publicPort.integer' => 'The Public Port must be an integer.',
]
);
}
public function syncData(bool $toModel = false)
{
if ($toModel) {

View File

@@ -8,10 +8,10 @@ use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneDragonfly;
use App\Support\ValidationPatterns;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Validate;
use Livewire\Component;
class General extends Component
@@ -20,42 +20,30 @@ class General extends Component
public StandaloneDragonfly $database;
#[Validate(['required', 'string'])]
public string $name;
#[Validate(['nullable', 'string'])]
public ?string $description = null;
#[Validate(['required', 'string'])]
public string $dragonflyPassword;
#[Validate(['required', 'string'])]
public string $image;
#[Validate(['nullable', 'string'])]
public ?string $portsMappings = null;
#[Validate(['nullable', 'boolean'])]
public ?bool $isPublic = null;
#[Validate(['nullable', 'integer'])]
public ?int $publicPort = null;
#[Validate(['nullable', 'string'])]
public ?string $customDockerRunOptions = null;
#[Validate(['nullable', 'string'])]
public ?string $dbUrl = null;
#[Validate(['nullable', 'string'])]
public ?string $dbUrlPublic = null;
#[Validate(['nullable', 'boolean'])]
public bool $isLogDrainEnabled = false;
public ?Carbon $certificateValidUntil = null;
#[Validate(['nullable', 'boolean'])]
public bool $enable_ssl = false;
public function getListeners()
@@ -85,6 +73,38 @@ class General extends Component
}
}
protected function rules(): array
{
return [
'name' => ValidationPatterns::nameRules(),
'description' => ValidationPatterns::descriptionRules(),
'dragonflyPassword' => 'required|string',
'image' => 'required|string',
'portsMappings' => 'nullable|string',
'isPublic' => 'nullable|boolean',
'publicPort' => 'nullable|integer',
'customDockerRunOptions' => 'nullable|string',
'dbUrl' => 'nullable|string',
'dbUrlPublic' => 'nullable|string',
'isLogDrainEnabled' => 'nullable|boolean',
'enable_ssl' => 'nullable|boolean',
];
}
protected function messages(): array
{
return array_merge(
ValidationPatterns::combinedMessages(),
[
'dragonflyPassword.required' => 'The Dragonfly Password field is required.',
'dragonflyPassword.string' => 'The Dragonfly Password must be a string.',
'image.required' => 'The Docker Image field is required.',
'image.string' => 'The Docker Image must be a string.',
'publicPort.integer' => 'The Public Port must be an integer.',
]
);
}
public function syncData(bool $toModel = false)
{
if ($toModel) {

View File

@@ -8,10 +8,10 @@ use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneKeydb;
use App\Support\ValidationPatterns;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Validate;
use Livewire\Component;
class General extends Component
@@ -20,45 +20,32 @@ class General extends Component
public StandaloneKeydb $database;
#[Validate(['required', 'string'])]
public string $name;
#[Validate(['nullable', 'string'])]
public ?string $description = null;
#[Validate(['nullable', 'string'])]
public ?string $keydbConf = null;
#[Validate(['required', 'string'])]
public string $keydbPassword;
#[Validate(['required', 'string'])]
public string $image;
#[Validate(['nullable', 'string'])]
public ?string $portsMappings = null;
#[Validate(['nullable', 'boolean'])]
public ?bool $isPublic = null;
#[Validate(['nullable', 'integer'])]
public ?int $publicPort = null;
#[Validate(['nullable', 'string'])]
public ?string $customDockerRunOptions = null;
#[Validate(['nullable', 'string'])]
public ?string $dbUrl = null;
#[Validate(['nullable', 'string'])]
public ?string $dbUrlPublic = null;
#[Validate(['nullable', 'boolean'])]
public bool $isLogDrainEnabled = false;
public ?Carbon $certificateValidUntil = null;
#[Validate(['boolean'])]
public bool $enable_ssl = false;
public function getListeners()
@@ -89,6 +76,41 @@ class General extends Component
}
}
protected function rules(): array
{
$baseRules = [
'name' => ValidationPatterns::nameRules(),
'description' => ValidationPatterns::descriptionRules(),
'keydbConf' => 'nullable|string',
'keydbPassword' => 'required|string',
'image' => 'required|string',
'portsMappings' => 'nullable|string',
'isPublic' => 'nullable|boolean',
'publicPort' => 'nullable|integer',
'customDockerRunOptions' => 'nullable|string',
'dbUrl' => 'nullable|string',
'dbUrlPublic' => 'nullable|string',
'isLogDrainEnabled' => 'nullable|boolean',
'enable_ssl' => 'boolean',
];
return $baseRules;
}
protected function messages(): array
{
return array_merge(
ValidationPatterns::combinedMessages(),
[
'keydbPassword.required' => 'The KeyDB Password field is required.',
'keydbPassword.string' => 'The KeyDB Password must be a string.',
'image.required' => 'The Docker Image field is required.',
'image.string' => 'The Docker Image must be a string.',
'publicPort.integer' => 'The Public Port must be an integer.',
]
);
}
public function syncData(bool $toModel = false)
{
if ($toModel) {

View File

@@ -8,6 +8,7 @@ use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneMariadb;
use App\Support\ValidationPatterns;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
@@ -37,22 +38,43 @@ class General extends Component
];
}
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
'database.mariadb_root_password' => 'required',
'database.mariadb_user' => 'required',
'database.mariadb_password' => 'required',
'database.mariadb_database' => 'required',
'database.mariadb_conf' => 'nullable',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
'database.enable_ssl' => 'boolean',
];
protected function rules(): array
{
return [
'database.name' => ValidationPatterns::nameRules(),
'database.description' => ValidationPatterns::descriptionRules(),
'database.mariadb_root_password' => 'required',
'database.mariadb_user' => 'required',
'database.mariadb_password' => 'required',
'database.mariadb_database' => 'required',
'database.mariadb_conf' => 'nullable',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
'database.enable_ssl' => 'boolean',
];
}
protected function messages(): array
{
return array_merge(
ValidationPatterns::combinedMessages(),
[
'database.name.required' => 'The Name field is required.',
'database.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
'database.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
'database.mariadb_root_password.required' => 'The Root Password field is required.',
'database.mariadb_user.required' => 'The MariaDB User field is required.',
'database.mariadb_password.required' => 'The MariaDB Password field is required.',
'database.mariadb_database.required' => 'The MariaDB Database field is required.',
'database.image.required' => 'The Docker Image field is required.',
'database.public_port.integer' => 'The Public Port must be an integer.',
]
);
}
protected $validationAttributes = [
'database.name' => 'Name',

View File

@@ -8,6 +8,7 @@ use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneMongodb;
use App\Support\ValidationPatterns;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
@@ -37,22 +38,43 @@ class General extends Component
];
}
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
'database.mongo_conf' => 'nullable',
'database.mongo_initdb_root_username' => 'required',
'database.mongo_initdb_root_password' => 'required',
'database.mongo_initdb_database' => 'required',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
'database.enable_ssl' => 'boolean',
'database.ssl_mode' => 'nullable|string|in:allow,prefer,require,verify-full',
];
protected function rules(): array
{
return [
'database.name' => ValidationPatterns::nameRules(),
'database.description' => ValidationPatterns::descriptionRules(),
'database.mongo_conf' => 'nullable',
'database.mongo_initdb_root_username' => 'required',
'database.mongo_initdb_root_password' => 'required',
'database.mongo_initdb_database' => 'required',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
'database.enable_ssl' => 'boolean',
'database.ssl_mode' => 'nullable|string|in:allow,prefer,require,verify-full',
];
}
protected function messages(): array
{
return array_merge(
ValidationPatterns::combinedMessages(),
[
'database.name.required' => 'The Name field is required.',
'database.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
'database.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
'database.mongo_initdb_root_username.required' => 'The Root Username field is required.',
'database.mongo_initdb_root_password.required' => 'The Root Password field is required.',
'database.mongo_initdb_database.required' => 'The MongoDB Database field is required.',
'database.image.required' => 'The Docker Image field is required.',
'database.public_port.integer' => 'The Public Port must be an integer.',
'database.ssl_mode.in' => 'The SSL Mode must be one of: allow, prefer, require, verify-full.',
]
);
}
protected $validationAttributes = [
'database.name' => 'Name',

View File

@@ -8,6 +8,7 @@ use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneMysql;
use App\Support\ValidationPatterns;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
@@ -37,23 +38,45 @@ class General extends Component
];
}
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
'database.mysql_root_password' => 'required',
'database.mysql_user' => 'required',
'database.mysql_password' => 'required',
'database.mysql_database' => 'required',
'database.mysql_conf' => 'nullable',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
'database.enable_ssl' => 'boolean',
'database.ssl_mode' => 'nullable|string|in:PREFERRED,REQUIRED,VERIFY_CA,VERIFY_IDENTITY',
];
protected function rules(): array
{
return [
'database.name' => ValidationPatterns::nameRules(),
'database.description' => ValidationPatterns::descriptionRules(),
'database.mysql_root_password' => 'required',
'database.mysql_user' => 'required',
'database.mysql_password' => 'required',
'database.mysql_database' => 'required',
'database.mysql_conf' => 'nullable',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
'database.enable_ssl' => 'boolean',
'database.ssl_mode' => 'nullable|string|in:PREFERRED,REQUIRED,VERIFY_CA,VERIFY_IDENTITY',
];
}
protected function messages(): array
{
return array_merge(
ValidationPatterns::combinedMessages(),
[
'database.name.required' => 'The Name field is required.',
'database.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
'database.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
'database.mysql_root_password.required' => 'The Root Password field is required.',
'database.mysql_user.required' => 'The MySQL User field is required.',
'database.mysql_password.required' => 'The MySQL Password field is required.',
'database.mysql_database.required' => 'The MySQL Database field is required.',
'database.image.required' => 'The Docker Image field is required.',
'database.public_port.integer' => 'The Public Port must be an integer.',
'database.ssl_mode.in' => 'The SSL Mode must be one of: PREFERRED, REQUIRED, VERIFY_CA, VERIFY_IDENTITY.',
]
);
}
protected $validationAttributes = [
'database.name' => 'Name',

View File

@@ -8,6 +8,7 @@ use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandalonePostgresql;
use App\Support\ValidationPatterns;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
@@ -41,25 +42,46 @@ class General extends Component
];
}
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
'database.postgres_user' => 'required',
'database.postgres_password' => 'required',
'database.postgres_db' => 'required',
'database.postgres_initdb_args' => 'nullable',
'database.postgres_host_auth_method' => 'nullable',
'database.postgres_conf' => 'nullable',
'database.init_scripts' => 'nullable',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
'database.enable_ssl' => 'boolean',
'database.ssl_mode' => 'nullable|string|in:allow,prefer,require,verify-ca,verify-full',
];
protected function rules(): array
{
return [
'database.name' => ValidationPatterns::nameRules(),
'database.description' => ValidationPatterns::descriptionRules(),
'database.postgres_user' => 'required',
'database.postgres_password' => 'required',
'database.postgres_db' => 'required',
'database.postgres_initdb_args' => 'nullable',
'database.postgres_host_auth_method' => 'nullable',
'database.postgres_conf' => 'nullable',
'database.init_scripts' => 'nullable',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
'database.enable_ssl' => 'boolean',
'database.ssl_mode' => 'nullable|string|in:allow,prefer,require,verify-ca,verify-full',
];
}
protected function messages(): array
{
return array_merge(
ValidationPatterns::combinedMessages(),
[
'database.name.required' => 'The Name field is required.',
'database.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
'database.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
'database.postgres_user.required' => 'The Postgres User field is required.',
'database.postgres_password.required' => 'The Postgres Password field is required.',
'database.postgres_db.required' => 'The Postgres Database field is required.',
'database.image.required' => 'The Docker Image field is required.',
'database.public_port.integer' => 'The Public Port must be an integer.',
'database.ssl_mode.in' => 'The SSL Mode must be one of: allow, prefer, require, verify-ca, verify-full.',
]
);
}
protected $validationAttributes = [
'database.name' => 'Name',

View File

@@ -8,6 +8,7 @@ use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneRedis;
use App\Support\ValidationPatterns;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
@@ -42,20 +43,39 @@ class General extends Component
];
}
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
'database.redis_conf' => 'nullable',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
'redis_username' => 'required',
'redis_password' => 'required',
'database.enable_ssl' => 'boolean',
];
protected function rules(): array
{
return [
'database.name' => ValidationPatterns::nameRules(),
'database.description' => ValidationPatterns::descriptionRules(),
'database.redis_conf' => 'nullable',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
'redis_username' => 'required',
'redis_password' => 'required',
'database.enable_ssl' => 'boolean',
];
}
protected function messages(): array
{
return array_merge(
ValidationPatterns::combinedMessages(),
[
'database.name.required' => 'The Name field is required.',
'database.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
'database.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
'database.image.required' => 'The Docker Image field is required.',
'database.public_port.integer' => 'The Public Port must be an integer.',
'redis_username.required' => 'The Redis Username field is required.',
'redis_password.required' => 'The Redis Password field is required.',
]
);
}
protected $validationAttributes = [
'database.name' => 'Name',