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

@@ -4,6 +4,7 @@ namespace App\Livewire\Project\Application;
use App\Actions\Application\GenerateConfig; use App\Actions\Application\GenerateConfig;
use App\Models\Application; use App\Models\Application;
use App\Support\ValidationPatterns;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
use Spatie\Url\Url; use Spatie\Url\Url;
@@ -52,9 +53,11 @@ class General extends Component
'configurationChanged' => '$refresh', 'configurationChanged' => '$refresh',
]; ];
protected $rules = [ protected function rules(): array
'application.name' => 'required', {
'application.description' => 'nullable', return [
'application.name' => ValidationPatterns::nameRules(),
'application.description' => ValidationPatterns::descriptionRules(),
'application.fqdn' => 'nullable', 'application.fqdn' => 'nullable',
'application.git_repository' => 'required', 'application.git_repository' => 'required',
'application.git_branch' => 'required', 'application.git_branch' => 'required',
@@ -98,6 +101,41 @@ class General extends Component
'application.watch_paths' => 'nullable', 'application.watch_paths' => 'nullable',
'application.redirect' => 'string|required', 'application.redirect' => 'string|required',
]; ];
}
protected function messages(): array
{
return array_merge(
ValidationPatterns::combinedMessages(),
[
'application.name.required' => 'The Name field is required.',
'application.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
'application.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
'application.git_repository.required' => 'The Git Repository field is required.',
'application.git_branch.required' => 'The Git Branch field is required.',
'application.build_pack.required' => 'The Build Pack field is required.',
'application.static_image.required' => 'The Static Image field is required.',
'application.base_directory.required' => 'The Base Directory field is required.',
'application.ports_exposes.required' => 'The Exposed Ports field is required.',
'application.settings.is_static.required' => 'The Static setting is required.',
'application.settings.is_static.boolean' => 'The Static setting must be true or false.',
'application.settings.is_spa.required' => 'The SPA setting is required.',
'application.settings.is_spa.boolean' => 'The SPA setting must be true or false.',
'application.settings.is_build_server_enabled.required' => 'The Build Server setting is required.',
'application.settings.is_build_server_enabled.boolean' => 'The Build Server setting must be true or false.',
'application.settings.is_container_label_escape_enabled.required' => 'The Container Label Escape setting is required.',
'application.settings.is_container_label_escape_enabled.boolean' => 'The Container Label Escape setting must be true or false.',
'application.settings.is_container_label_readonly_enabled.required' => 'The Container Label Readonly setting is required.',
'application.settings.is_container_label_readonly_enabled.boolean' => 'The Container Label Readonly setting must be true or false.',
'application.settings.is_preserve_repository_enabled.required' => 'The Preserve Repository setting is required.',
'application.settings.is_preserve_repository_enabled.boolean' => 'The Preserve Repository setting must be true or false.',
'application.is_http_basic_auth_enabled.required' => 'The HTTP Basic Auth setting is required.',
'application.is_http_basic_auth_enabled.boolean' => 'The HTTP Basic Auth setting must be true or false.',
'application.redirect.required' => 'The Redirect setting is required.',
'application.redirect.string' => 'The Redirect setting must be a string.',
]
);
}
protected $validationAttributes = [ protected $validationAttributes = [
'application.name' => 'name', 'application.name' => 'name',

View File

@@ -6,9 +6,9 @@ use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy; use App\Actions\Database\StopDatabaseProxy;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneClickhouse; use App\Models\StandaloneClickhouse;
use App\Support\ValidationPatterns;
use Exception; use Exception;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
class General extends Component class General extends Component
@@ -17,40 +17,28 @@ class General extends Component
public StandaloneClickhouse $database; public StandaloneClickhouse $database;
#[Validate(['required', 'string'])]
public string $name; public string $name;
#[Validate(['nullable', 'string'])]
public ?string $description = null; public ?string $description = null;
#[Validate(['required', 'string'])]
public string $clickhouseAdminUser; public string $clickhouseAdminUser;
#[Validate(['required', 'string'])]
public string $clickhouseAdminPassword; public string $clickhouseAdminPassword;
#[Validate(['required', 'string'])]
public string $image; public string $image;
#[Validate(['nullable', 'string'])]
public ?string $portsMappings = null; public ?string $portsMappings = null;
#[Validate(['nullable', 'boolean'])]
public ?bool $isPublic = null; public ?bool $isPublic = null;
#[Validate(['nullable', 'integer'])]
public ?int $publicPort = null; public ?int $publicPort = null;
#[Validate(['nullable', 'string'])]
public ?string $customDockerRunOptions = null; public ?string $customDockerRunOptions = null;
#[Validate(['nullable', 'string'])]
public ?string $dbUrl = null; public ?string $dbUrl = null;
#[Validate(['nullable', 'string'])]
public ?string $dbUrlPublic = null; public ?string $dbUrlPublic = null;
#[Validate(['nullable', 'boolean'])]
public bool $isLogDrainEnabled = false; public bool $isLogDrainEnabled = false;
public function getListeners() 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) public function syncData(bool $toModel = false)
{ {
if ($toModel) { if ($toModel) {

View File

@@ -8,10 +8,10 @@ use App\Helpers\SslHelper;
use App\Models\Server; use App\Models\Server;
use App\Models\SslCertificate; use App\Models\SslCertificate;
use App\Models\StandaloneDragonfly; use App\Models\StandaloneDragonfly;
use App\Support\ValidationPatterns;
use Carbon\Carbon; use Carbon\Carbon;
use Exception; use Exception;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
class General extends Component class General extends Component
@@ -20,42 +20,30 @@ class General extends Component
public StandaloneDragonfly $database; public StandaloneDragonfly $database;
#[Validate(['required', 'string'])]
public string $name; public string $name;
#[Validate(['nullable', 'string'])]
public ?string $description = null; public ?string $description = null;
#[Validate(['required', 'string'])]
public string $dragonflyPassword; public string $dragonflyPassword;
#[Validate(['required', 'string'])]
public string $image; public string $image;
#[Validate(['nullable', 'string'])]
public ?string $portsMappings = null; public ?string $portsMappings = null;
#[Validate(['nullable', 'boolean'])]
public ?bool $isPublic = null; public ?bool $isPublic = null;
#[Validate(['nullable', 'integer'])]
public ?int $publicPort = null; public ?int $publicPort = null;
#[Validate(['nullable', 'string'])]
public ?string $customDockerRunOptions = null; public ?string $customDockerRunOptions = null;
#[Validate(['nullable', 'string'])]
public ?string $dbUrl = null; public ?string $dbUrl = null;
#[Validate(['nullable', 'string'])]
public ?string $dbUrlPublic = null; public ?string $dbUrlPublic = null;
#[Validate(['nullable', 'boolean'])]
public bool $isLogDrainEnabled = false; public bool $isLogDrainEnabled = false;
public ?Carbon $certificateValidUntil = null; public ?Carbon $certificateValidUntil = null;
#[Validate(['nullable', 'boolean'])]
public bool $enable_ssl = false; public bool $enable_ssl = false;
public function getListeners() 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) public function syncData(bool $toModel = false)
{ {
if ($toModel) { if ($toModel) {

View File

@@ -8,10 +8,10 @@ use App\Helpers\SslHelper;
use App\Models\Server; use App\Models\Server;
use App\Models\SslCertificate; use App\Models\SslCertificate;
use App\Models\StandaloneKeydb; use App\Models\StandaloneKeydb;
use App\Support\ValidationPatterns;
use Carbon\Carbon; use Carbon\Carbon;
use Exception; use Exception;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
class General extends Component class General extends Component
@@ -20,45 +20,32 @@ class General extends Component
public StandaloneKeydb $database; public StandaloneKeydb $database;
#[Validate(['required', 'string'])]
public string $name; public string $name;
#[Validate(['nullable', 'string'])]
public ?string $description = null; public ?string $description = null;
#[Validate(['nullable', 'string'])]
public ?string $keydbConf = null; public ?string $keydbConf = null;
#[Validate(['required', 'string'])]
public string $keydbPassword; public string $keydbPassword;
#[Validate(['required', 'string'])]
public string $image; public string $image;
#[Validate(['nullable', 'string'])]
public ?string $portsMappings = null; public ?string $portsMappings = null;
#[Validate(['nullable', 'boolean'])]
public ?bool $isPublic = null; public ?bool $isPublic = null;
#[Validate(['nullable', 'integer'])]
public ?int $publicPort = null; public ?int $publicPort = null;
#[Validate(['nullable', 'string'])]
public ?string $customDockerRunOptions = null; public ?string $customDockerRunOptions = null;
#[Validate(['nullable', 'string'])]
public ?string $dbUrl = null; public ?string $dbUrl = null;
#[Validate(['nullable', 'string'])]
public ?string $dbUrlPublic = null; public ?string $dbUrlPublic = null;
#[Validate(['nullable', 'boolean'])]
public bool $isLogDrainEnabled = false; public bool $isLogDrainEnabled = false;
public ?Carbon $certificateValidUntil = null; public ?Carbon $certificateValidUntil = null;
#[Validate(['boolean'])]
public bool $enable_ssl = false; public bool $enable_ssl = false;
public function getListeners() 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) public function syncData(bool $toModel = false)
{ {
if ($toModel) { if ($toModel) {

View File

@@ -8,6 +8,7 @@ use App\Helpers\SslHelper;
use App\Models\Server; use App\Models\Server;
use App\Models\SslCertificate; use App\Models\SslCertificate;
use App\Models\StandaloneMariadb; use App\Models\StandaloneMariadb;
use App\Support\ValidationPatterns;
use Carbon\Carbon; use Carbon\Carbon;
use Exception; use Exception;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@@ -37,9 +38,11 @@ class General extends Component
]; ];
} }
protected $rules = [ protected function rules(): array
'database.name' => 'required', {
'database.description' => 'nullable', return [
'database.name' => ValidationPatterns::nameRules(),
'database.description' => ValidationPatterns::descriptionRules(),
'database.mariadb_root_password' => 'required', 'database.mariadb_root_password' => 'required',
'database.mariadb_user' => 'required', 'database.mariadb_user' => 'required',
'database.mariadb_password' => 'required', 'database.mariadb_password' => 'required',
@@ -53,6 +56,25 @@ class General extends Component
'database.custom_docker_run_options' => 'nullable', 'database.custom_docker_run_options' => 'nullable',
'database.enable_ssl' => 'boolean', '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 = [ protected $validationAttributes = [
'database.name' => 'Name', 'database.name' => 'Name',

View File

@@ -8,6 +8,7 @@ use App\Helpers\SslHelper;
use App\Models\Server; use App\Models\Server;
use App\Models\SslCertificate; use App\Models\SslCertificate;
use App\Models\StandaloneMongodb; use App\Models\StandaloneMongodb;
use App\Support\ValidationPatterns;
use Carbon\Carbon; use Carbon\Carbon;
use Exception; use Exception;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@@ -37,9 +38,11 @@ class General extends Component
]; ];
} }
protected $rules = [ protected function rules(): array
'database.name' => 'required', {
'database.description' => 'nullable', return [
'database.name' => ValidationPatterns::nameRules(),
'database.description' => ValidationPatterns::descriptionRules(),
'database.mongo_conf' => 'nullable', 'database.mongo_conf' => 'nullable',
'database.mongo_initdb_root_username' => 'required', 'database.mongo_initdb_root_username' => 'required',
'database.mongo_initdb_root_password' => 'required', 'database.mongo_initdb_root_password' => 'required',
@@ -53,6 +56,25 @@ class General extends Component
'database.enable_ssl' => 'boolean', 'database.enable_ssl' => 'boolean',
'database.ssl_mode' => 'nullable|string|in:allow,prefer,require,verify-full', '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 = [ protected $validationAttributes = [
'database.name' => 'Name', 'database.name' => 'Name',

View File

@@ -8,6 +8,7 @@ use App\Helpers\SslHelper;
use App\Models\Server; use App\Models\Server;
use App\Models\SslCertificate; use App\Models\SslCertificate;
use App\Models\StandaloneMysql; use App\Models\StandaloneMysql;
use App\Support\ValidationPatterns;
use Carbon\Carbon; use Carbon\Carbon;
use Exception; use Exception;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@@ -37,9 +38,11 @@ class General extends Component
]; ];
} }
protected $rules = [ protected function rules(): array
'database.name' => 'required', {
'database.description' => 'nullable', return [
'database.name' => ValidationPatterns::nameRules(),
'database.description' => ValidationPatterns::descriptionRules(),
'database.mysql_root_password' => 'required', 'database.mysql_root_password' => 'required',
'database.mysql_user' => 'required', 'database.mysql_user' => 'required',
'database.mysql_password' => 'required', 'database.mysql_password' => 'required',
@@ -54,6 +57,26 @@ class General extends Component
'database.enable_ssl' => 'boolean', 'database.enable_ssl' => 'boolean',
'database.ssl_mode' => 'nullable|string|in:PREFERRED,REQUIRED,VERIFY_CA,VERIFY_IDENTITY', '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 = [ protected $validationAttributes = [
'database.name' => 'Name', 'database.name' => 'Name',

View File

@@ -8,6 +8,7 @@ use App\Helpers\SslHelper;
use App\Models\Server; use App\Models\Server;
use App\Models\SslCertificate; use App\Models\SslCertificate;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Support\ValidationPatterns;
use Carbon\Carbon; use Carbon\Carbon;
use Exception; use Exception;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@@ -41,9 +42,11 @@ class General extends Component
]; ];
} }
protected $rules = [ protected function rules(): array
'database.name' => 'required', {
'database.description' => 'nullable', return [
'database.name' => ValidationPatterns::nameRules(),
'database.description' => ValidationPatterns::descriptionRules(),
'database.postgres_user' => 'required', 'database.postgres_user' => 'required',
'database.postgres_password' => 'required', 'database.postgres_password' => 'required',
'database.postgres_db' => 'required', 'database.postgres_db' => 'required',
@@ -60,6 +63,25 @@ class General extends Component
'database.enable_ssl' => 'boolean', 'database.enable_ssl' => 'boolean',
'database.ssl_mode' => 'nullable|string|in:allow,prefer,require,verify-ca,verify-full', '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 = [ protected $validationAttributes = [
'database.name' => 'Name', 'database.name' => 'Name',

View File

@@ -8,6 +8,7 @@ use App\Helpers\SslHelper;
use App\Models\Server; use App\Models\Server;
use App\Models\SslCertificate; use App\Models\SslCertificate;
use App\Models\StandaloneRedis; use App\Models\StandaloneRedis;
use App\Support\ValidationPatterns;
use Carbon\Carbon; use Carbon\Carbon;
use Exception; use Exception;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@@ -42,9 +43,11 @@ class General extends Component
]; ];
} }
protected $rules = [ protected function rules(): array
'database.name' => 'required', {
'database.description' => 'nullable', return [
'database.name' => ValidationPatterns::nameRules(),
'database.description' => ValidationPatterns::descriptionRules(),
'database.redis_conf' => 'nullable', 'database.redis_conf' => 'nullable',
'database.image' => 'required', 'database.image' => 'required',
'database.ports_mappings' => 'nullable', 'database.ports_mappings' => 'nullable',
@@ -56,6 +59,23 @@ class General extends Component
'redis_password' => 'required', 'redis_password' => 'required',
'database.enable_ssl' => 'boolean', '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 = [ protected $validationAttributes = [
'database.name' => 'Name', 'database.name' => 'Name',

View File

@@ -3,6 +3,7 @@
namespace App\Livewire\Project\Service; namespace App\Livewire\Project\Service;
use App\Models\Service; use App\Models\Service;
use App\Support\ValidationPatterns;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
@@ -14,14 +15,39 @@ class StackForm extends Component
protected $listeners = ['saveCompose']; protected $listeners = ['saveCompose'];
public $rules = [ protected function rules(): array
{
$baseRules = [
'service.docker_compose_raw' => 'required', 'service.docker_compose_raw' => 'required',
'service.docker_compose' => 'required', 'service.docker_compose' => 'required',
'service.name' => 'required', 'service.name' => ValidationPatterns::nameRules(),
'service.description' => 'nullable', 'service.description' => ValidationPatterns::descriptionRules(),
'service.connect_to_docker_network' => 'nullable', 'service.connect_to_docker_network' => 'nullable',
]; ];
// Add dynamic field rules
foreach ($this->fields ?? collect() as $key => $field) {
$rules = data_get($field, 'rules', 'nullable');
$baseRules["fields.$key.value"] = $rules;
}
return $baseRules;
}
protected function messages(): array
{
return array_merge(
ValidationPatterns::combinedMessages(),
[
'service.name.required' => 'The Name field is required.',
'service.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
'service.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
'service.docker_compose_raw.required' => 'The Docker Compose Raw field is required.',
'service.docker_compose.required' => 'The Docker Compose field is required.',
]
);
}
public $validationAttributes = []; public $validationAttributes = [];
public function mount() public function mount()
@@ -45,7 +71,6 @@ class StackForm extends Component
'customHelper' => $customHelper, 'customHelper' => $customHelper,
]); ]);
$this->rules["fields.$key.value"] = $rules;
$this->validationAttributes["fields.$key.value"] = $fieldKey; $this->validationAttributes["fields.$key.value"] = $fieldKey;
} }
} }

View File

@@ -3,6 +3,7 @@
namespace App\Livewire\Security\PrivateKey; namespace App\Livewire\Security\PrivateKey;
use App\Models\PrivateKey; use App\Models\PrivateKey;
use App\Support\ValidationPatterns;
use Livewire\Component; use Livewire\Component;
class Create extends Component class Create extends Component
@@ -17,10 +18,25 @@ class Create extends Component
public ?string $publicKey = null; public ?string $publicKey = null;
protected $rules = [ protected function rules(): array
'name' => 'required|string', {
return [
'name' => ValidationPatterns::nameRules(),
'description' => ValidationPatterns::descriptionRules(),
'value' => 'required|string', 'value' => 'required|string',
]; ];
}
protected function messages(): array
{
return array_merge(
ValidationPatterns::combinedMessages(),
[
'value.required' => 'The Private Key field is required.',
'value.string' => 'The Private Key must be a valid string.',
]
);
}
public function generateNewRSAKey() public function generateNewRSAKey()
{ {

View File

@@ -3,6 +3,7 @@
namespace App\Livewire\Security\PrivateKey; namespace App\Livewire\Security\PrivateKey;
use App\Models\PrivateKey; use App\Models\PrivateKey;
use App\Support\ValidationPatterns;
use Livewire\Component; use Livewire\Component;
class Show extends Component class Show extends Component
@@ -11,12 +12,29 @@ class Show extends Component
public $public_key = 'Loading...'; public $public_key = 'Loading...';
protected $rules = [ protected function rules(): array
'private_key.name' => 'required|string', {
'private_key.description' => 'nullable|string', return [
'private_key.name' => ValidationPatterns::nameRules(),
'private_key.description' => ValidationPatterns::descriptionRules(),
'private_key.private_key' => 'required|string', 'private_key.private_key' => 'required|string',
'private_key.is_git_related' => 'nullable|boolean', 'private_key.is_git_related' => 'nullable|boolean',
]; ];
}
protected function messages(): array
{
return array_merge(
ValidationPatterns::combinedMessages(),
[
'private_key.name.required' => 'The Name field is required.',
'private_key.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
'private_key.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
'private_key.private_key.required' => 'The Private Key field is required.',
'private_key.private_key.string' => 'The Private Key must be a valid string.',
]
);
}
protected $validationAttributes = [ protected $validationAttributes = [
'private_key.name' => 'name', 'private_key.name' => 'name',

View File

@@ -5,9 +5,9 @@ namespace App\Livewire\Server\New;
use App\Enums\ProxyTypes; use App\Enums\ProxyTypes;
use App\Models\Server; use App\Models\Server;
use App\Models\Team; use App\Models\Team;
use App\Support\ValidationPatterns;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Livewire\Attributes\Locked; use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
class ByIp extends Component class ByIp extends Component
@@ -18,43 +18,30 @@ class ByIp extends Component
#[Locked] #[Locked]
public $limit_reached; public $limit_reached;
#[Validate('nullable|integer', as: 'Private Key')]
public ?int $private_key_id = null; public ?int $private_key_id = null;
#[Validate('nullable|string', as: 'Private Key Name')]
public $new_private_key_name; public $new_private_key_name;
#[Validate('nullable|string', as: 'Private Key Description')]
public $new_private_key_description; public $new_private_key_description;
#[Validate('nullable|string', as: 'Private Key Value')]
public $new_private_key_value; public $new_private_key_value;
#[Validate('required|string', as: 'Name')]
public string $name; public string $name;
#[Validate('nullable|string', as: 'Description')]
public ?string $description = null; public ?string $description = null;
#[Validate('required|string', as: 'IP Address/Domain')]
public string $ip; public string $ip;
#[Validate('required|string', as: 'User')]
public string $user = 'root'; public string $user = 'root';
#[Validate('required|integer|between:1,65535', as: 'Port')]
public int $port = 22; public int $port = 22;
#[Validate('required|boolean', as: 'Swarm Manager')]
public bool $is_swarm_manager = false; public bool $is_swarm_manager = false;
#[Validate('required|boolean', as: 'Swarm Worker')]
public bool $is_swarm_worker = false; public bool $is_swarm_worker = false;
#[Validate('nullable|integer', as: 'Swarm Cluster')]
public $selected_swarm_cluster = null; public $selected_swarm_cluster = null;
#[Validate('required|boolean', as: 'Build Server')]
public bool $is_build_server = false; public bool $is_build_server = false;
#[Locked] #[Locked]
@@ -70,6 +57,50 @@ class ByIp extends Component
} }
} }
protected function rules(): array
{
return [
'private_key_id' => 'nullable|integer',
'new_private_key_name' => 'nullable|string',
'new_private_key_description' => 'nullable|string',
'new_private_key_value' => 'nullable|string',
'name' => ValidationPatterns::nameRules(),
'description' => ValidationPatterns::descriptionRules(),
'ip' => 'required|string',
'user' => 'required|string',
'port' => 'required|integer|between:1,65535',
'is_swarm_manager' => 'required|boolean',
'is_swarm_worker' => 'required|boolean',
'selected_swarm_cluster' => 'nullable|integer',
'is_build_server' => 'required|boolean',
];
}
protected function messages(): array
{
return array_merge(ValidationPatterns::combinedMessages(), [
'private_key_id.integer' => 'The Private Key field must be an integer.',
'private_key_id.nullable' => 'The Private Key field is optional.',
'new_private_key_name.string' => 'The Private Key Name must be a string.',
'new_private_key_description.string' => 'The Private Key Description must be a string.',
'new_private_key_value.string' => 'The Private Key Value must be a string.',
'ip.required' => 'The IP Address/Domain is required.',
'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.',
'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.',
'is_swarm_manager.required' => 'The Swarm Manager field is required.',
'is_swarm_manager.boolean' => 'The Swarm Manager field must be true or false.',
'is_swarm_worker.required' => 'The Swarm Worker field is required.',
'is_swarm_worker.boolean' => 'The Swarm Worker field must be true or false.',
'selected_swarm_cluster.integer' => 'The Swarm Cluster field must be an integer.',
'is_build_server.required' => 'The Build Server field is required.',
'is_build_server.boolean' => 'The Build Server field must be true or false.',
]);
}
public function setPrivateKey(string $private_key_id) public function setPrivateKey(string $private_key_id)
{ {
$this->private_key_id = $private_key_id; $this->private_key_id = $private_key_id;

View File

@@ -6,82 +6,60 @@ use App\Actions\Server\StartSentinel;
use App\Actions\Server\StopSentinel; use App\Actions\Server\StopSentinel;
use App\Events\ServerReachabilityChanged; use App\Events\ServerReachabilityChanged;
use App\Models\Server; use App\Models\Server;
use App\Support\ValidationPatterns;
use Livewire\Attributes\Computed; use Livewire\Attributes\Computed;
use Livewire\Attributes\Locked; use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
class Show extends Component class Show extends Component
{ {
public Server $server; public Server $server;
#[Validate(['required'])]
public string $name; public string $name;
#[Validate(['nullable'])]
public ?string $description = null; public ?string $description = null;
#[Validate(['required'])]
public string $ip; public string $ip;
#[Validate(['required'])]
public string $user; public string $user;
#[Validate(['required'])]
public string $port; public string $port;
#[Validate(['nullable'])]
public ?string $validationLogs = null; public ?string $validationLogs = null;
#[Validate(['nullable', 'url'])]
public ?string $wildcardDomain = null; public ?string $wildcardDomain = null;
#[Validate(['required'])]
public bool $isReachable; public bool $isReachable;
#[Validate(['required'])]
public bool $isUsable; public bool $isUsable;
#[Validate(['required'])]
public bool $isSwarmManager; public bool $isSwarmManager;
#[Validate(['required'])]
public bool $isSwarmWorker; public bool $isSwarmWorker;
#[Validate(['required'])]
public bool $isBuildServer; public bool $isBuildServer;
#[Locked] #[Locked]
public bool $isBuildServerLocked = false; public bool $isBuildServerLocked = false;
#[Validate(['required'])]
public bool $isMetricsEnabled; public bool $isMetricsEnabled;
#[Validate(['required'])]
public string $sentinelToken; public string $sentinelToken;
#[Validate(['nullable'])]
public ?string $sentinelUpdatedAt = null; public ?string $sentinelUpdatedAt = null;
#[Validate(['required', 'integer', 'min:1'])]
public int $sentinelMetricsRefreshRateSeconds; public int $sentinelMetricsRefreshRateSeconds;
#[Validate(['required', 'integer', 'min:1'])]
public int $sentinelMetricsHistoryDays; public int $sentinelMetricsHistoryDays;
#[Validate(['required', 'integer', 'min:10'])]
public int $sentinelPushIntervalSeconds; public int $sentinelPushIntervalSeconds;
#[Validate(['nullable', 'url'])]
public ?string $sentinelCustomUrl = null; public ?string $sentinelCustomUrl = null;
#[Validate(['required'])]
public bool $isSentinelEnabled; public bool $isSentinelEnabled;
#[Validate(['required'])]
public bool $isSentinelDebugEnabled; public bool $isSentinelDebugEnabled;
#[Validate(['required'])]
public string $serverTimezone; public string $serverTimezone;
public function getListeners() public function getListeners()
@@ -91,6 +69,59 @@ class Show extends Component
]; ];
} }
protected function rules(): array
{
return [
'name' => ValidationPatterns::nameRules(),
'description' => ValidationPatterns::descriptionRules(),
'ip' => 'required',
'user' => 'required',
'port' => 'required',
'validationLogs' => 'nullable',
'wildcardDomain' => 'nullable|url',
'isReachable' => 'required',
'isUsable' => 'required',
'isSwarmManager' => 'required',
'isSwarmWorker' => 'required',
'isBuildServer' => 'required',
'isMetricsEnabled' => 'required',
'sentinelToken' => 'required',
'sentinelUpdatedAt' => 'nullable',
'sentinelMetricsRefreshRateSeconds' => 'required|integer|min:1',
'sentinelMetricsHistoryDays' => 'required|integer|min:1',
'sentinelPushIntervalSeconds' => 'required|integer|min:10',
'sentinelCustomUrl' => 'nullable|url',
'isSentinelEnabled' => 'required',
'isSentinelDebugEnabled' => 'required',
'serverTimezone' => 'required',
];
}
protected function messages(): array
{
return array_merge(
ValidationPatterns::combinedMessages(),
[
'ip.required' => 'The IP Address field is required.',
'user.required' => 'The User field is required.',
'port.required' => 'The Port field is required.',
'wildcardDomain.url' => 'The Wildcard Domain must be a valid URL.',
'sentinelToken.required' => 'The Sentinel Token field is required.',
'sentinelMetricsRefreshRateSeconds.required' => 'The Metrics Refresh Rate field is required.',
'sentinelMetricsRefreshRateSeconds.integer' => 'The Metrics Refresh Rate must be an integer.',
'sentinelMetricsRefreshRateSeconds.min' => 'The Metrics Refresh Rate must be at least 1 second.',
'sentinelMetricsHistoryDays.required' => 'The Metrics History Days field is required.',
'sentinelMetricsHistoryDays.integer' => 'The Metrics History Days must be an integer.',
'sentinelMetricsHistoryDays.min' => 'The Metrics History Days must be at least 1 day.',
'sentinelPushIntervalSeconds.required' => 'The Push Interval field is required.',
'sentinelPushIntervalSeconds.integer' => 'The Push Interval must be an integer.',
'sentinelPushIntervalSeconds.min' => 'The Push Interval must be at least 10 seconds.',
'sentinelCustomUrl.url' => 'The Custom Sentinel URL must be a valid URL.',
'serverTimezone.required' => 'The Server Timezone field is required.',
]
);
}
public function mount(string $server_uuid) public function mount(string $server_uuid)
{ {
try { try {

View File

@@ -3,6 +3,7 @@
namespace App\Livewire\Storage; namespace App\Livewire\Storage;
use App\Models\S3Storage; use App\Models\S3Storage;
use App\Support\ValidationPatterns;
use Illuminate\Support\Uri; use Illuminate\Support\Uri;
use Livewire\Component; use Livewire\Component;
@@ -24,15 +25,38 @@ class Create extends Component
public S3Storage $storage; public S3Storage $storage;
protected $rules = [ protected function rules(): array
'name' => 'required|min:3|max:255', {
'description' => 'nullable|min:3|max:255', return [
'name' => ValidationPatterns::nameRules(),
'description' => ValidationPatterns::descriptionRules(),
'region' => 'required|max:255', 'region' => 'required|max:255',
'key' => 'required|max:255', 'key' => 'required|max:255',
'secret' => 'required|max:255', 'secret' => 'required|max:255',
'bucket' => 'required|max:255', 'bucket' => 'required|max:255',
'endpoint' => 'required|url|max:255', 'endpoint' => 'required|url|max:255',
]; ];
}
protected function messages(): array
{
return array_merge(
ValidationPatterns::combinedMessages(),
[
'region.required' => 'The Region field is required.',
'region.max' => 'The Region may not be greater than 255 characters.',
'key.required' => 'The Access Key field is required.',
'key.max' => 'The Access Key may not be greater than 255 characters.',
'secret.required' => 'The Secret Key field is required.',
'secret.max' => 'The Secret Key may not be greater than 255 characters.',
'bucket.required' => 'The Bucket field is required.',
'bucket.max' => 'The Bucket may not be greater than 255 characters.',
'endpoint.required' => 'The Endpoint field is required.',
'endpoint.url' => 'The Endpoint must be a valid URL.',
'endpoint.max' => 'The Endpoint may not be greater than 255 characters.',
]
);
}
protected $validationAttributes = [ protected $validationAttributes = [
'name' => 'Name', 'name' => 'Name',

View File

@@ -3,22 +3,48 @@
namespace App\Livewire\Storage; namespace App\Livewire\Storage;
use App\Models\S3Storage; use App\Models\S3Storage;
use App\Support\ValidationPatterns;
use Livewire\Component; use Livewire\Component;
class Form extends Component class Form extends Component
{ {
public S3Storage $storage; public S3Storage $storage;
protected $rules = [ protected function rules(): array
{
return [
'storage.is_usable' => 'nullable|boolean', 'storage.is_usable' => 'nullable|boolean',
'storage.name' => 'nullable|min:3|max:255', 'storage.name' => ValidationPatterns::nameRules(required: false),
'storage.description' => 'nullable|min:3|max:255', 'storage.description' => ValidationPatterns::descriptionRules(),
'storage.region' => 'required|max:255', 'storage.region' => 'required|max:255',
'storage.key' => 'required|max:255', 'storage.key' => 'required|max:255',
'storage.secret' => 'required|max:255', 'storage.secret' => 'required|max:255',
'storage.bucket' => 'required|max:255', 'storage.bucket' => 'required|max:255',
'storage.endpoint' => 'required|url|max:255', 'storage.endpoint' => 'required|url|max:255',
]; ];
}
protected function messages(): array
{
return array_merge(
ValidationPatterns::combinedMessages(),
[
'storage.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
'storage.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
'storage.region.required' => 'The Region field is required.',
'storage.region.max' => 'The Region may not be greater than 255 characters.',
'storage.key.required' => 'The Access Key field is required.',
'storage.key.max' => 'The Access Key may not be greater than 255 characters.',
'storage.secret.required' => 'The Secret Key field is required.',
'storage.secret.max' => 'The Secret Key may not be greater than 255 characters.',
'storage.bucket.required' => 'The Bucket field is required.',
'storage.bucket.max' => 'The Bucket may not be greater than 255 characters.',
'storage.endpoint.required' => 'The Endpoint field is required.',
'storage.endpoint.url' => 'The Endpoint must be a valid URL.',
'storage.endpoint.max' => 'The Endpoint may not be greater than 255 characters.',
]
);
}
protected $validationAttributes = [ protected $validationAttributes = [
'storage.is_usable' => 'Is Usable', 'storage.is_usable' => 'Is Usable',

View File

@@ -3,17 +3,28 @@
namespace App\Livewire\Team; namespace App\Livewire\Team;
use App\Models\Team; use App\Models\Team;
use Livewire\Attributes\Validate; use App\Support\ValidationPatterns;
use Livewire\Component; use Livewire\Component;
class Create extends Component class Create extends Component
{ {
#[Validate(['required', 'min:3', 'max:255'])]
public string $name = ''; public string $name = '';
#[Validate(['nullable', 'min:3', 'max:255'])]
public ?string $description = null; public ?string $description = null;
protected function rules(): array
{
return [
'name' => ValidationPatterns::nameRules(),
'description' => ValidationPatterns::descriptionRules(),
];
}
protected function messages(): array
{
return ValidationPatterns::combinedMessages();
}
public function submit() public function submit()
{ {
try { try {

View File

@@ -4,6 +4,7 @@ namespace App\Livewire\Team;
use App\Models\Team; use App\Models\Team;
use App\Models\TeamInvitation; use App\Models\TeamInvitation;
use App\Support\ValidationPatterns;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Livewire\Component; use Livewire\Component;
@@ -14,10 +15,25 @@ class Index extends Component
public Team $team; public Team $team;
protected $rules = [ protected function rules(): array
'team.name' => 'required|min:3|max:255', {
'team.description' => 'nullable|min:3|max:255', return [
'team.name' => ValidationPatterns::nameRules(),
'team.description' => ValidationPatterns::descriptionRules(),
]; ];
}
protected function messages(): array
{
return array_merge(
ValidationPatterns::combinedMessages(),
[
'team.name.required' => 'The Name field is required.',
'team.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
'team.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
]
);
}
protected $validationAttributes = [ protected $validationAttributes = [
'team.name' => 'name', 'team.name' => 'name',

View File

@@ -1,5 +1,5 @@
<div> <div>
<div class="pb-0 subtitle"> <div class="pb-2 subtitle">
<div>Private Keys are used to connect to your servers without passwords.</div> <div>Private Keys are used to connect to your servers without passwords.</div>
<div class="font-bold">You should not use passphrase protected keys.</div> <div class="font-bold">You should not use passphrase protected keys.</div>
</div> </div>