diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php
index 9f97dd0d4..7ea6a871e 100644
--- a/app/Actions/Fortify/CreateNewUser.php
+++ b/app/Actions/Fortify/CreateNewUser.php
@@ -37,12 +37,13 @@ public function create(array $input): User
if (User::count() == 0) {
// If this is the first user, make them the root user
// Team is already created in the database/seeders/ProductionSeeder.php
- $user = User::create([
+ $user = (new User)->forceFill([
'id' => 0,
'name' => $input['name'],
'email' => $input['email'],
'password' => Hash::make($input['password']),
]);
+ $user->save();
$team = $user->teams()->first();
// Disable registration after first user is created
diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php
index 6b5e1d4ac..17948d93b 100644
--- a/app/Actions/Service/StartService.php
+++ b/app/Actions/Service/StartService.php
@@ -40,10 +40,10 @@ public function handle(Service $service, bool $pullLatestImages = false, bool $s
$commands[] = "docker network connect $service->uuid coolify-proxy >/dev/null 2>&1 || true";
if (data_get($service, 'connect_to_docker_network')) {
$compose = data_get($service, 'docker_compose', []);
- $network = $service->destination->network;
+ $safeNetwork = escapeshellarg($service->destination->network);
$serviceNames = data_get(Yaml::parse($compose), 'services', []);
foreach ($serviceNames as $serviceName => $serviceConfig) {
- $commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} >/dev/null 2>&1 || true";
+ $commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} {$safeNetwork} {$serviceName}-{$service->uuid} >/dev/null 2>&1 || true";
}
}
diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php
index 66cb77838..e95c29f72 100644
--- a/app/Console/Commands/Init.php
+++ b/app/Console/Commands/Init.php
@@ -212,18 +212,19 @@ private function cleanupUnusedNetworkFromCoolifyProxy()
$removeNetworks = $allNetworks->diff($networks);
$commands = collect();
foreach ($removeNetworks as $network) {
- $out = instant_remote_process(["docker network inspect -f json $network | jq '.[].Containers | if . == {} then null else . end'"], $server, false);
+ $safe = escapeshellarg($network);
+ $out = instant_remote_process(["docker network inspect -f json {$safe} | jq '.[].Containers | if . == {} then null else . end'"], $server, false);
if (empty($out)) {
- $commands->push("docker network disconnect $network coolify-proxy >/dev/null 2>&1 || true");
- $commands->push("docker network rm $network >/dev/null 2>&1 || true");
+ $commands->push("docker network disconnect {$safe} coolify-proxy >/dev/null 2>&1 || true");
+ $commands->push("docker network rm {$safe} >/dev/null 2>&1 || true");
} else {
$data = collect(json_decode($out, true));
if ($data->count() === 1) {
// If only coolify-proxy itself is connected to that network (it should not be possible, but who knows)
$isCoolifyProxyItself = data_get($data->first(), 'Name') === 'coolify-proxy';
if ($isCoolifyProxyItself) {
- $commands->push("docker network disconnect $network coolify-proxy >/dev/null 2>&1 || true");
- $commands->push("docker network rm $network >/dev/null 2>&1 || true");
+ $commands->push("docker network disconnect {$safe} coolify-proxy >/dev/null 2>&1 || true");
+ $commands->push("docker network rm {$safe} >/dev/null 2>&1 || true");
}
}
}
diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php
index d6e0de340..daa7d17e4 100644
--- a/app/Http/Controllers/Api/ApplicationsController.php
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -243,6 +243,7 @@ public function applications(Request $request)
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the application.'],
+ 'is_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'],
],
)
),
@@ -409,6 +410,7 @@ public function create_public_application(Request $request)
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the application.'],
+ 'is_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'],
],
)
),
@@ -575,6 +577,7 @@ public function create_private_gh_app_application(Request $request)
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the application.'],
+ 'is_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'],
],
)
),
@@ -1023,7 +1026,7 @@ private function create_application(Request $request, $type)
if ($return instanceof JsonResponse) {
return $return;
}
- $allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled', 'tags'];
+ $allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled', 'tags', 'is_preserve_repository_enabled'];
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255',
@@ -1074,6 +1077,7 @@ private function create_application(Request $request, $type)
$connectToDockerNetwork = $request->connect_to_docker_network;
$customNginxConfiguration = $request->custom_nginx_configuration;
$isContainerLabelEscapeEnabled = $request->boolean('is_container_label_escape_enabled', true);
+ $isPreserveRepositoryEnabled = $request->boolean('is_preserve_repository_enabled',false);
if (! is_null($customNginxConfiguration)) {
if (! isBase64Encoded($customNginxConfiguration)) {
@@ -1177,7 +1181,7 @@ private function create_application(Request $request, $type)
$application = new Application;
removeUnnecessaryFieldsFromRequest($request);
- $application->fill($request->all());
+ $application->fill($request->only($allowedFields));
$dockerComposeDomainsJson = collect();
if ($request->has('docker_compose_domains')) {
$dockerComposeDomains = collect($request->docker_compose_domains);
@@ -1286,6 +1290,10 @@ private function create_application(Request $request, $type)
$application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled;
$application->settings->save();
}
+ if (isset($isPreserveRepositoryEnabled)) {
+ $application->settings->is_preserve_repository_enabled = $isPreserveRepositoryEnabled;
+ $application->settings->save();
+ }
$application->refresh();
// Auto-generate domain if requested and no custom domain provided
if ($autogenerateDomain && blank($fqdn)) {
@@ -1407,7 +1415,7 @@ private function create_application(Request $request, $type)
$application = new Application;
removeUnnecessaryFieldsFromRequest($request);
- $application->fill($request->all());
+ $application->fill($request->only($allowedFields));
$dockerComposeDomainsJson = collect();
if ($request->has('docker_compose_domains')) {
@@ -1521,6 +1529,10 @@ private function create_application(Request $request, $type)
$application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled;
$application->settings->save();
}
+ if (isset($isPreserveRepositoryEnabled)) {
+ $application->settings->is_preserve_repository_enabled = $isPreserveRepositoryEnabled;
+ $application->settings->save();
+ }
if ($application->settings->is_container_label_readonly_enabled) {
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
$application->save();
@@ -1610,7 +1622,7 @@ private function create_application(Request $request, $type)
$application = new Application;
removeUnnecessaryFieldsFromRequest($request);
- $application->fill($request->all());
+ $application->fill($request->only($allowedFields));
$dockerComposeDomainsJson = collect();
if ($request->has('docker_compose_domains')) {
@@ -1720,6 +1732,10 @@ private function create_application(Request $request, $type)
$application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled;
$application->settings->save();
}
+ if (isset($isPreserveRepositoryEnabled)) {
+ $application->settings->is_preserve_repository_enabled = $isPreserveRepositoryEnabled;
+ $application->settings->save();
+ }
if ($application->settings->is_container_label_readonly_enabled) {
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
$application->save();
@@ -1800,7 +1816,7 @@ private function create_application(Request $request, $type)
}
$application = new Application;
- $application->fill($request->all());
+ $application->fill($request->only($allowedFields));
$application->fqdn = $fqdn;
$application->ports_exposes = $port;
$application->build_pack = 'dockerfile';
@@ -1915,7 +1931,7 @@ private function create_application(Request $request, $type)
$application = new Application;
removeUnnecessaryFieldsFromRequest($request);
- $application->fill($request->all());
+ $application->fill($request->only($allowedFields));
$application->fqdn = $fqdn;
$application->build_pack = 'dockerimage';
$application->destination_id = $destination->id;
@@ -2034,7 +2050,7 @@ private function create_application(Request $request, $type)
$service = new Service;
removeUnnecessaryFieldsFromRequest($request);
- $service->fill($request->all());
+ $service->fill($request->only($allowedFields));
$service->docker_compose_raw = $dockerComposeRaw;
$service->environment_id = $environment->id;
@@ -2428,6 +2444,7 @@ public function delete_by_uuid(Request $request)
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
+ 'is_preserve_repository_enabled' => ['type' => 'boolean', 'description' => 'Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false.'],
],
)
),
@@ -2513,7 +2530,7 @@ public function update_by_uuid(Request $request)
$this->authorize('update', $application);
$server = $application->destination->server;
- $allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'dockerfile_target_build', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled'];
+ $allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'dockerfile_target_build', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled', 'is_preserve_repository_enabled'];
$validationRules = [
'name' => 'string|max:255',
@@ -2760,7 +2777,7 @@ public function update_by_uuid(Request $request)
$connectToDockerNetwork = $request->connect_to_docker_network;
$useBuildServer = $request->use_build_server;
$isContainerLabelEscapeEnabled = $request->boolean('is_container_label_escape_enabled');
-
+ $isPreserveRepositoryEnabled = $request->boolean('is_preserve_repository_enabled');
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
@@ -2795,10 +2812,13 @@ public function update_by_uuid(Request $request)
$application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled;
$application->settings->save();
}
-
+ if ($request->has('is_preserve_repository_enabled')) {
+ $application->settings->is_preserve_repository_enabled = $isPreserveRepositoryEnabled;
+ $application->settings->save();
+ }
removeUnnecessaryFieldsFromRequest($request);
- $data = $request->all();
+ $data = $request->only($allowedFields);
if ($requestHasDomains && $server->isProxyShouldRun()) {
data_set($data, 'fqdn', $domains);
}
diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php
index c96bffa9b..aa135749a 100644
--- a/app/Http/Controllers/Api/DatabasesController.php
+++ b/app/Http/Controllers/Api/DatabasesController.php
@@ -1773,13 +1773,13 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('postgres_conf', $postgresConf);
}
- $database = create_standalone_postgresql($environment->id, $destination->uuid, $request->all());
- if ($request->has('tags')) {
- $this->attachTagsToResource($database, $request->tags, $teamId);
- }
+ $database = create_standalone_postgresql($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
+ if ($request->has('tags')) {
+ $this->attachTagsToResource($database, $request->tags, $teamId);
+ }
$database->refresh();
$payload = [
'uuid' => $database->uuid,
@@ -1831,13 +1831,13 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('mariadb_conf', $mariadbConf);
}
- $database = create_standalone_mariadb($environment->id, $destination->uuid, $request->all());
- if ($request->has('tags')) {
- $this->attachTagsToResource($database, $request->tags, $teamId);
- }
+ $database = create_standalone_mariadb($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
+ if ($request->has('tags')) {
+ $this->attachTagsToResource($database, $request->tags, $teamId);
+ }
$database->refresh();
$payload = [
@@ -1893,13 +1893,13 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('mysql_conf', $mysqlConf);
}
- $database = create_standalone_mysql($environment->id, $destination->uuid, $request->all());
- if ($request->has('tags')) {
- $this->attachTagsToResource($database, $request->tags, $teamId);
- }
+ $database = create_standalone_mysql($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
+ if ($request->has('tags')) {
+ $this->attachTagsToResource($database, $request->tags, $teamId);
+ }
$database->refresh();
$payload = [
@@ -1952,13 +1952,13 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('redis_conf', $redisConf);
}
- $database = create_standalone_redis($environment->id, $destination->uuid, $request->all());
- if ($request->has('tags')) {
- $this->attachTagsToResource($database, $request->tags, $teamId);
- }
+ $database = create_standalone_redis($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
+ if ($request->has('tags')) {
+ $this->attachTagsToResource($database, $request->tags, $teamId);
+ }
$database->refresh();
$payload = [
@@ -1992,13 +1992,13 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
removeUnnecessaryFieldsFromRequest($request);
- $database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->all());
- if ($request->has('tags')) {
- $this->attachTagsToResource($database, $request->tags, $teamId);
- }
+ $database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
+ if ($request->has('tags')) {
+ $this->attachTagsToResource($database, $request->tags, $teamId);
+ }
return response()->json(serializeApiResponse([
'uuid' => $database->uuid,
@@ -2044,13 +2044,13 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('keydb_conf', $keydbConf);
}
- $database = create_standalone_keydb($environment->id, $destination->uuid, $request->all());
- if ($request->has('tags')) {
- $this->attachTagsToResource($database, $request->tags, $teamId);
- }
+ $database = create_standalone_keydb($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
+ if ($request->has('tags')) {
+ $this->attachTagsToResource($database, $request->tags, $teamId);
+ }
$database->refresh();
$payload = [
@@ -2083,13 +2083,13 @@ public function create_database(Request $request, NewDatabaseTypes $type)
], 422);
}
removeUnnecessaryFieldsFromRequest($request);
- $database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->all());
- if ($request->has('tags')) {
- $this->attachTagsToResource($database, $request->tags, $teamId);
- }
+ $database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
+ if ($request->has('tags')) {
+ $this->attachTagsToResource($database, $request->tags, $teamId);
+ }
$database->refresh();
$payload = [
@@ -2144,13 +2144,13 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('mongo_conf', $mongoConf);
}
- $database = create_standalone_mongodb($environment->id, $destination->uuid, $request->all());
- if ($request->has('tags')) {
- $this->attachTagsToResource($database, $request->tags, $teamId);
- }
+ $database = create_standalone_mongodb($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
+ if ($request->has('tags')) {
+ $this->attachTagsToResource($database, $request->tags, $teamId);
+ }
$database->refresh();
$payload = [
diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php
index 85d532f62..e490f3b0c 100644
--- a/app/Http/Controllers/Api/DeployController.php
+++ b/app/Http/Controllers/Api/DeployController.php
@@ -250,7 +250,7 @@ public function cancel_deployment(Request $request)
]);
// Get the server
- $server = Server::find($build_server_id);
+ $server = Server::whereTeamId($teamId)->find($build_server_id);
if ($server) {
// Add cancellation log entry
diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php
index e7b36cb9a..2c62928c2 100644
--- a/app/Http/Controllers/Api/SecurityController.php
+++ b/app/Http/Controllers/Api/SecurityController.php
@@ -4,6 +4,7 @@
use App\Http\Controllers\Controller;
use App\Models\PrivateKey;
+use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
@@ -176,7 +177,7 @@ public function create_key(Request $request)
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
- if ($return instanceof \Illuminate\Http\JsonResponse) {
+ if ($return instanceof JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
@@ -300,7 +301,7 @@ public function update_key(Request $request)
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
- if ($return instanceof \Illuminate\Http\JsonResponse) {
+ if ($return instanceof JsonResponse) {
return $return;
}
@@ -330,7 +331,7 @@ public function update_key(Request $request)
'message' => 'Private Key not found.',
], 404);
}
- $foundKey->update($request->all());
+ $foundKey->update($request->only($allowedFields));
return response()->json(serializeApiResponse([
'uuid' => $foundKey->uuid,
diff --git a/app/Http/Controllers/Api/TeamController.php b/app/Http/Controllers/Api/TeamController.php
index fd0282d96..03b36e4e0 100644
--- a/app/Http/Controllers/Api/TeamController.php
+++ b/app/Http/Controllers/Api/TeamController.php
@@ -14,14 +14,6 @@ private function removeSensitiveData($team)
'custom_server_limit',
'pivot',
]);
- if (request()->attributes->get('can_read_sensitive', false) === false) {
- $team->makeHidden([
- 'smtp_username',
- 'smtp_password',
- 'resend_api_key',
- 'telegram_token',
- ]);
- }
return serializeApiResponse($team);
}
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 785e8c8e3..dc8bc4374 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -288,7 +288,8 @@ public function handle(): void
// Make sure the private key is stored in the filesystem
$this->server->privateKey->storeInFileSystem();
// Generate custom host<->ip mapping
- $allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
+ $safeNetwork = escapeshellarg($this->destination->network);
+ $allContainers = instant_remote_process(["docker network inspect {$safeNetwork} -f '{{json .Containers}}' "], $this->server);
if (! is_null($allContainers)) {
$allContainers = format_docker_command_output_to_json($allContainers);
@@ -2015,9 +2016,11 @@ private function prepare_builder_image(bool $firstTry = true)
$runCommand = "docker run -d --name {$this->deployment_uuid} {$env_flags} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} else {
if ($this->dockerConfigFileExists === 'OK') {
- $runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} {$env_flags} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
+ $safeNetwork = escapeshellarg($this->destination->network);
+ $runCommand = "docker run -d --network {$safeNetwork} --name {$this->deployment_uuid} {$env_flags} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} else {
- $runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} {$env_flags} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
+ $safeNetwork = escapeshellarg($this->destination->network);
+ $runCommand = "docker run -d --network {$safeNetwork} --name {$this->deployment_uuid} {$env_flags} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
}
}
if ($firstTry) {
@@ -3046,28 +3049,29 @@ private function build_image()
$this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm '.self::NIXPACKS_PLAN_PATH), 'hidden' => true]);
} else {
// Dockerfile buildpack
+ $safeNetwork = escapeshellarg($this->destination->network);
if ($this->dockerSecretsSupported) {
// Modify the Dockerfile to use build secrets
$this->modify_dockerfile_for_secrets("{$this->workdir}{$this->dockerfile_location}");
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
if ($this->force_rebuild) {
- $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->workdir}");
+ $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->workdir}");
} else {
- $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->workdir}");
+ $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->workdir}");
}
} elseif ($this->dockerBuildkitSupported) {
// BuildKit without secrets
if ($this->force_rebuild) {
- $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}");
+ $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}");
} else {
- $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}");
+ $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}");
}
} else {
// Traditional build with args
if ($this->force_rebuild) {
- $build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t $this->build_image_name {$this->workdir}");
+ $build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t $this->build_image_name {$this->workdir}");
} else {
- $build_command = $this->wrap_build_command_with_env_export("docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t $this->build_image_name {$this->workdir}");
+ $build_command = $this->wrap_build_command_with_env_export("docker build {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t $this->build_image_name {$this->workdir}");
}
}
$base64_build_command = base64_encode($build_command);
diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php
index 7f1feaa21..a2d08e1e8 100644
--- a/app/Jobs/DatabaseBackupJob.php
+++ b/app/Jobs/DatabaseBackupJob.php
@@ -678,6 +678,7 @@ private function upload_to_s3(): void
} else {
$network = $this->database->destination->network;
}
+ $safeNetwork = escapeshellarg($network);
$fullImageName = $this->getFullImageName();
@@ -689,13 +690,13 @@ private function upload_to_s3(): void
if (isDev()) {
if ($this->database->name === 'coolify-db') {
$backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/coolify/coolify-db-'.$this->server->ip.$this->backup_file;
- $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup_log_uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
+ $commands[] = "docker run -d --network {$safeNetwork} --name backup-of-{$this->backup_log_uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
} else {
$backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name.$this->backup_file;
- $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup_log_uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
+ $commands[] = "docker run -d --network {$safeNetwork} --name backup-of-{$this->backup_log_uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
}
} else {
- $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup_log_uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
+ $commands[] = "docker run -d --network {$safeNetwork} --name backup-of-{$this->backup_log_uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
}
// Escape S3 credentials to prevent command injection
diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php
index 0f6f45d83..7e1121860 100644
--- a/app/Livewire/Boarding/Index.php
+++ b/app/Livewire/Boarding/Index.php
@@ -121,7 +121,7 @@ public function mount()
}
if ($this->selectedExistingServer) {
- $this->createdServer = Server::find($this->selectedExistingServer);
+ $this->createdServer = Server::ownedByCurrentTeam()->find($this->selectedExistingServer);
if ($this->createdServer) {
$this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
$this->updateServerDetails();
@@ -145,7 +145,7 @@ public function mount()
}
if ($this->selectedProject) {
- $this->createdProject = Project::find($this->selectedProject);
+ $this->createdProject = Project::ownedByCurrentTeam()->find($this->selectedProject);
if (! $this->createdProject) {
$this->projects = Project::ownedByCurrentTeam(['name'])->get();
}
@@ -431,7 +431,10 @@ public function getProjects()
public function selectExistingProject()
{
- $this->createdProject = Project::find($this->selectedProject);
+ $this->createdProject = Project::ownedByCurrentTeam()->find($this->selectedProject);
+ if (! $this->createdProject) {
+ return $this->dispatch('error', 'Project not found.');
+ }
$this->currentState = 'create-resource';
}
diff --git a/app/Livewire/Destination/New/Docker.php b/app/Livewire/Destination/New/Docker.php
index 70751fa03..5c1b178d7 100644
--- a/app/Livewire/Destination/New/Docker.php
+++ b/app/Livewire/Destination/New/Docker.php
@@ -5,6 +5,7 @@
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
+use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;
@@ -24,7 +25,7 @@ class Docker extends Component
#[Validate(['required', 'string'])]
public string $name;
- #[Validate(['required', 'string'])]
+ #[Validate(['required', 'string', 'max:255', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/'])]
public string $network;
#[Validate(['required', 'string'])]
diff --git a/app/Livewire/Destination/Show.php b/app/Livewire/Destination/Show.php
index 98cf72376..f2cdad074 100644
--- a/app/Livewire/Destination/Show.php
+++ b/app/Livewire/Destination/Show.php
@@ -20,7 +20,7 @@ class Show extends Component
#[Validate(['string', 'required'])]
public string $name;
- #[Validate(['string', 'required'])]
+ #[Validate(['string', 'required', 'max:255', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/'])]
public string $network;
#[Validate(['string', 'required'])]
@@ -84,8 +84,9 @@ public function delete()
if ($this->destination->attachedTo()) {
return $this->dispatch('error', 'You must delete all resources before deleting this destination.');
}
- instant_remote_process(["docker network disconnect {$this->destination->network} coolify-proxy"], $this->destination->server, throwError: false);
- instant_remote_process(['docker network rm -f '.$this->destination->network], $this->destination->server);
+ $safeNetwork = escapeshellarg($this->destination->network);
+ instant_remote_process(["docker network disconnect {$safeNetwork} coolify-proxy"], $this->destination->server, throwError: false);
+ instant_remote_process(["docker network rm -f {$safeNetwork}"], $this->destination->server);
}
$this->destination->delete();
diff --git a/app/Livewire/GlobalSearch.php b/app/Livewire/GlobalSearch.php
index f910110dc..154748b47 100644
--- a/app/Livewire/GlobalSearch.php
+++ b/app/Livewire/GlobalSearch.php
@@ -1203,7 +1203,7 @@ public function selectServer($serverId, $shouldProgress = true)
public function loadDestinations()
{
$this->loadingDestinations = true;
- $server = Server::find($this->selectedServerId);
+ $server = Server::ownedByCurrentTeam()->find($this->selectedServerId);
if (! $server) {
$this->loadingDestinations = false;
@@ -1280,7 +1280,7 @@ public function selectProject($projectUuid, $shouldProgress = true)
public function loadEnvironments()
{
$this->loadingEnvironments = true;
- $project = Project::where('uuid', $this->selectedProjectUuid)->first();
+ $project = Project::ownedByCurrentTeam()->where('uuid', $this->selectedProjectUuid)->first();
if (! $project) {
$this->loadingEnvironments = false;
diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index c12fec76a..6fd063cf3 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -735,6 +735,7 @@ public function setRedirect()
$this->authorize('update', $this->application);
try {
+ $this->application->redirect = $this->redirect;
$has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count();
if ($has_www === 0 && $this->application->redirect === 'www') {
$this->dispatch('error', 'You want to redirect to www, but you do not have a www domain set.
Please add www to your domain list and as an A DNS record (if applicable).');
diff --git a/app/Livewire/Project/CloneMe.php b/app/Livewire/Project/CloneMe.php
index 3b3e42619..e236124e9 100644
--- a/app/Livewire/Project/CloneMe.php
+++ b/app/Livewire/Project/CloneMe.php
@@ -54,7 +54,7 @@ protected function messages(): array
public function mount($project_uuid)
{
$this->project_uuid = $project_uuid;
- $this->project = Project::where('uuid', $project_uuid)->firstOrFail();
+ $this->project = Project::ownedByCurrentTeam()->where('uuid', $project_uuid)->firstOrFail();
$this->environment = $this->project->environments->where('uuid', $this->environment_uuid)->first();
$this->project_id = $this->project->id;
$this->servers = currentTeam()
@@ -139,7 +139,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'uuid' => $uuid,
'status' => 'exited',
'started_at' => null,
@@ -187,7 +187,8 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ 'uuid',
+ ])->forceFill([
'name' => $newName,
'resource_id' => $newDatabase->id,
]);
@@ -216,7 +217,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'resource_id' => $newDatabase->id,
]);
$newStorage->save();
@@ -229,7 +230,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'uuid' => $uuid,
'database_id' => $newDatabase->id,
'database_type' => $newDatabase->getMorphClass(),
@@ -247,7 +248,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill($payload);
+ ])->forceFill($payload);
$newEnvironmentVariable->save();
}
}
@@ -258,7 +259,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'uuid' => $uuid,
'environment_id' => $environment->id,
'destination_id' => $this->selectedDestination,
@@ -276,7 +277,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'uuid' => (string) new Cuid2,
'service_id' => $newService->id,
'team_id' => currentTeam()->id,
@@ -290,7 +291,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'resourceable_id' => $newService->id,
'resourceable_type' => $newService->getMorphClass(),
]);
@@ -298,9 +299,9 @@ public function clone(string $type)
}
foreach ($newService->applications() as $application) {
- $application->update([
+ $application->forceFill([
'status' => 'exited',
- ]);
+ ])->save();
$persistentVolumes = $application->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
@@ -315,7 +316,8 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ 'uuid',
+ ])->forceFill([
'name' => $newName,
'resource_id' => $application->id,
]);
@@ -344,7 +346,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'resource_id' => $application->id,
]);
$newStorage->save();
@@ -352,9 +354,9 @@ public function clone(string $type)
}
foreach ($newService->databases() as $database) {
- $database->update([
+ $database->forceFill([
'status' => 'exited',
- ]);
+ ])->save();
$persistentVolumes = $database->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
@@ -369,7 +371,8 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ 'uuid',
+ ])->forceFill([
'name' => $newName,
'resource_id' => $database->id,
]);
@@ -398,7 +401,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'resource_id' => $database->id,
]);
$newStorage->save();
@@ -411,7 +414,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'uuid' => $uuid,
'database_id' => $database->id,
'database_type' => $database->getMorphClass(),
diff --git a/app/Livewire/Project/DeleteProject.php b/app/Livewire/Project/DeleteProject.php
index a018046fd..d95041c2d 100644
--- a/app/Livewire/Project/DeleteProject.php
+++ b/app/Livewire/Project/DeleteProject.php
@@ -21,7 +21,7 @@ class DeleteProject extends Component
public function mount()
{
$this->parameters = get_route_parameters();
- $this->projectName = Project::findOrFail($this->project_id)->name;
+ $this->projectName = Project::ownedByCurrentTeam()->findOrFail($this->project_id)->name;
}
public function delete()
@@ -29,7 +29,7 @@ public function delete()
$this->validate([
'project_id' => 'required|int',
]);
- $project = Project::findOrFail($this->project_id);
+ $project = Project::ownedByCurrentTeam()->findOrFail($this->project_id);
$this->authorize('delete', $project);
if ($project->isEmpty()) {
diff --git a/app/Livewire/Project/New/DockerCompose.php b/app/Livewire/Project/New/DockerCompose.php
index 634a012c0..2b92902c6 100644
--- a/app/Livewire/Project/New/DockerCompose.php
+++ b/app/Livewire/Project/New/DockerCompose.php
@@ -41,8 +41,8 @@ public function submit()
// Validate for command injection BEFORE saving to database
validateDockerComposeForInjection($this->dockerComposeRaw);
- $project = Project::where('uuid', $this->parameters['project_uuid'])->first();
- $environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
+ $project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
+ $environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
$destination_uuid = $this->query['destination'];
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php
index 8aff83153..268333d07 100644
--- a/app/Livewire/Project/New/DockerImage.php
+++ b/app/Livewire/Project/New/DockerImage.php
@@ -121,8 +121,8 @@ public function submit()
}
$destination_class = $destination->getMorphClass();
- $project = Project::where('uuid', $this->parameters['project_uuid'])->first();
- $environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
+ $project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
+ $environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
// Append @sha256 to image name if using digest and not already present
$imageName = $parser->getFullImageNameWithoutTag();
diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php
index 61ae0e151..86424642d 100644
--- a/app/Livewire/Project/New/GithubPrivateRepository.php
+++ b/app/Livewire/Project/New/GithubPrivateRepository.php
@@ -185,8 +185,8 @@ public function submit()
}
$destination_class = $destination->getMorphClass();
- $project = Project::where('uuid', $this->parameters['project_uuid'])->first();
- $environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
+ $project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
+ $environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
$application = Application::create([
'name' => generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name),
diff --git a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php
index e46ad7d78..94ef23cc9 100644
--- a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php
+++ b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php
@@ -144,8 +144,8 @@ public function submit()
// Note: git_repository has already been validated and transformed in get_git_source()
// It may now be in SSH format (git@host:repo.git) which is valid for deploy keys
- $project = Project::where('uuid', $this->parameters['project_uuid'])->first();
- $environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
+ $project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
+ $environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
if ($this->git_source === 'other') {
$application_init = [
'name' => generate_random_name(),
diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php
index 3df31a6a3..9c9ddb8ce 100644
--- a/app/Livewire/Project/New/PublicGitRepository.php
+++ b/app/Livewire/Project/New/PublicGitRepository.php
@@ -278,8 +278,8 @@ public function submit()
}
$destination_class = $destination->getMorphClass();
- $project = Project::where('uuid', $project_uuid)->first();
- $environment = $project->load(['environments'])->environments->where('uuid', $environment_uuid)->first();
+ $project = Project::ownedByCurrentTeam()->where('uuid', $project_uuid)->firstOrFail();
+ $environment = $project->environments()->where('uuid', $environment_uuid)->firstOrFail();
if ($this->build_pack === 'dockercompose' && isDev() && $this->new_compose_services) {
$server = $destination->server;
diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php
index c5dc13987..165e4b59e 100644
--- a/app/Livewire/Project/New/Select.php
+++ b/app/Livewire/Project/New/Select.php
@@ -65,7 +65,7 @@ public function mount()
$this->existingPostgresqlUrl = 'postgres://coolify:password@coolify-db:5432';
}
$projectUuid = data_get($this->parameters, 'project_uuid');
- $project = Project::whereUuid($projectUuid)->firstOrFail();
+ $project = Project::ownedByCurrentTeam()->whereUuid($projectUuid)->firstOrFail();
$this->environments = $project->environments;
$this->selectedEnvironment = $this->environments->where('uuid', data_get($this->parameters, 'environment_uuid'))->firstOrFail()->name;
@@ -79,7 +79,7 @@ public function mount()
$this->type = $queryType;
$this->server_id = $queryServerId;
$this->destination_uuid = $queryDestination;
- $this->server = Server::find($queryServerId);
+ $this->server = Server::ownedByCurrentTeam()->find($queryServerId);
$this->current_step = 'select-postgresql-type';
}
} catch (\Exception $e) {
diff --git a/app/Livewire/Project/New/SimpleDockerfile.php b/app/Livewire/Project/New/SimpleDockerfile.php
index 9cc4fbbe2..1073157e6 100644
--- a/app/Livewire/Project/New/SimpleDockerfile.php
+++ b/app/Livewire/Project/New/SimpleDockerfile.php
@@ -45,8 +45,8 @@ public function submit()
}
$destination_class = $destination->getMorphClass();
- $project = Project::where('uuid', $this->parameters['project_uuid'])->first();
- $environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
+ $project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
+ $environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
$port = get_port_from_dockerfile($this->dockerfile);
if (! $port) {
diff --git a/app/Livewire/Project/Shared/GetLogs.php b/app/Livewire/Project/Shared/GetLogs.php
index 22605e1bb..d0121bdc5 100644
--- a/app/Livewire/Project/Shared/GetLogs.php
+++ b/app/Livewire/Project/Shared/GetLogs.php
@@ -16,7 +16,9 @@
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
+use App\Support\ValidationPatterns;
use Illuminate\Support\Facades\Process;
+use Livewire\Attributes\Locked;
use Livewire\Component;
class GetLogs extends Component
@@ -29,12 +31,16 @@ class GetLogs extends Component
public string $errors = '';
+ #[Locked]
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|null $resource = null;
+ #[Locked]
public ServiceApplication|ServiceDatabase|null $servicesubtype = null;
+ #[Locked]
public Server $server;
+ #[Locked]
public ?string $container = null;
public ?string $displayName = null;
@@ -54,7 +60,7 @@ class GetLogs extends Component
public function mount()
{
if (! is_null($this->resource)) {
- if ($this->resource->getMorphClass() === \App\Models\Application::class) {
+ if ($this->resource->getMorphClass() === Application::class) {
$this->showTimeStamps = $this->resource->settings->is_include_timestamps;
} else {
if ($this->servicesubtype) {
@@ -63,7 +69,7 @@ public function mount()
$this->showTimeStamps = $this->resource->is_include_timestamps;
}
}
- if ($this->resource?->getMorphClass() === \App\Models\Application::class) {
+ if ($this->resource?->getMorphClass() === Application::class) {
if (str($this->container)->contains('-pr-')) {
$this->pull_request = 'Pull Request: '.str($this->container)->afterLast('-pr-')->beforeLast('_')->value();
}
@@ -74,11 +80,11 @@ public function mount()
public function instantSave()
{
if (! is_null($this->resource)) {
- if ($this->resource->getMorphClass() === \App\Models\Application::class) {
+ if ($this->resource->getMorphClass() === Application::class) {
$this->resource->settings->is_include_timestamps = $this->showTimeStamps;
$this->resource->settings->save();
}
- if ($this->resource->getMorphClass() === \App\Models\Service::class) {
+ if ($this->resource->getMorphClass() === Service::class) {
$serviceName = str($this->container)->beforeLast('-')->value();
$subType = $this->resource->applications()->where('name', $serviceName)->first();
if ($subType) {
@@ -118,10 +124,20 @@ public function toggleStreamLogs()
public function getLogs($refresh = false)
{
+ if (! Server::ownedByCurrentTeam()->where('id', $this->server->id)->exists()) {
+ $this->outputs = 'Unauthorized.';
+
+ return;
+ }
if (! $this->server->isFunctional()) {
return;
}
- if (! $refresh && ! $this->expandByDefault && ($this->resource?->getMorphClass() === \App\Models\Service::class || str($this->container)->contains('-pr-'))) {
+ if ($this->container && ! ValidationPatterns::isValidContainerName($this->container)) {
+ $this->outputs = 'Invalid container name.';
+
+ return;
+ }
+ if (! $refresh && ! $this->expandByDefault && ($this->resource?->getMorphClass() === Service::class || str($this->container)->contains('-pr-'))) {
return;
}
if ($this->numberOfLines <= 0 || is_null($this->numberOfLines)) {
@@ -194,9 +210,15 @@ public function copyLogs(): string
public function downloadAllLogs(): string
{
+ if (! Server::ownedByCurrentTeam()->where('id', $this->server->id)->exists()) {
+ return '';
+ }
if (! $this->server->isFunctional() || ! $this->container) {
return '';
}
+ if (! ValidationPatterns::isValidContainerName($this->container)) {
+ return '';
+ }
if ($this->showTimeStamps) {
if ($this->server->isSwarm()) {
diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php
index e769e4bcb..301c51be9 100644
--- a/app/Livewire/Project/Shared/ResourceOperations.php
+++ b/app/Livewire/Project/Shared/ResourceOperations.php
@@ -7,9 +7,18 @@
use App\Actions\Service\StartService;
use App\Actions\Service\StopService;
use App\Jobs\VolumeCloneJob;
+use App\Models\Application;
use App\Models\Environment;
use App\Models\Project;
+use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDocker;
+use App\Models\StandaloneDragonfly;
+use App\Models\StandaloneKeydb;
+use App\Models\StandaloneMariadb;
+use App\Models\StandaloneMongodb;
+use App\Models\StandaloneMysql;
+use App\Models\StandalonePostgresql;
+use App\Models\StandaloneRedis;
use App\Models\SwarmDocker;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
@@ -60,7 +69,7 @@ public function cloneTo($destination_id)
$uuid = (string) new Cuid2;
$server = $new_destination->server;
- if ($this->resource->getMorphClass() === \App\Models\Application::class) {
+ if ($this->resource->getMorphClass() === Application::class) {
$new_resource = clone_application($this->resource, $new_destination, ['uuid' => $uuid], $this->cloneVolumeData);
$route = route('project.application.configuration', [
@@ -71,21 +80,21 @@ public function cloneTo($destination_id)
return redirect()->to($route);
} elseif (
- $this->resource->getMorphClass() === \App\Models\StandalonePostgresql::class ||
- $this->resource->getMorphClass() === \App\Models\StandaloneMongodb::class ||
- $this->resource->getMorphClass() === \App\Models\StandaloneMysql::class ||
- $this->resource->getMorphClass() === \App\Models\StandaloneMariadb::class ||
- $this->resource->getMorphClass() === \App\Models\StandaloneRedis::class ||
- $this->resource->getMorphClass() === \App\Models\StandaloneKeydb::class ||
- $this->resource->getMorphClass() === \App\Models\StandaloneDragonfly::class ||
- $this->resource->getMorphClass() === \App\Models\StandaloneClickhouse::class
+ $this->resource->getMorphClass() === StandalonePostgresql::class ||
+ $this->resource->getMorphClass() === StandaloneMongodb::class ||
+ $this->resource->getMorphClass() === StandaloneMysql::class ||
+ $this->resource->getMorphClass() === StandaloneMariadb::class ||
+ $this->resource->getMorphClass() === StandaloneRedis::class ||
+ $this->resource->getMorphClass() === StandaloneKeydb::class ||
+ $this->resource->getMorphClass() === StandaloneDragonfly::class ||
+ $this->resource->getMorphClass() === StandaloneClickhouse::class
) {
$uuid = (string) new Cuid2;
$new_resource = $this->resource->replicate([
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'uuid' => $uuid,
'name' => $this->resource->name.'-clone-'.$uuid,
'status' => 'exited',
@@ -133,7 +142,8 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
- ])->fill([
+ 'uuid',
+ ])->forceFill([
'name' => $newName,
'resource_id' => $new_resource->id,
]);
@@ -162,7 +172,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'resource_id' => $new_resource->id,
]);
$newStorage->save();
@@ -175,7 +185,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'uuid' => $uuid,
'database_id' => $new_resource->id,
'database_type' => $new_resource->getMorphClass(),
@@ -194,7 +204,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
- ])->fill($payload);
+ ])->forceFill($payload);
$newEnvironmentVariable->save();
}
@@ -211,7 +221,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'uuid' => $uuid,
'name' => $this->resource->name.'-clone-'.$uuid,
'destination_id' => $new_destination->id,
@@ -232,7 +242,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'uuid' => (string) new Cuid2,
'service_id' => $new_resource->id,
'team_id' => currentTeam()->id,
@@ -246,7 +256,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'resourceable_id' => $new_resource->id,
'resourceable_type' => $new_resource->getMorphClass(),
]);
@@ -254,9 +264,9 @@ public function cloneTo($destination_id)
}
foreach ($new_resource->applications() as $application) {
- $application->update([
+ $application->forceFill([
'status' => 'exited',
- ]);
+ ])->save();
$persistentVolumes = $application->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
@@ -271,7 +281,8 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
- ])->fill([
+ 'uuid',
+ ])->forceFill([
'name' => $newName,
'resource_id' => $application->id,
]);
@@ -296,9 +307,9 @@ public function cloneTo($destination_id)
}
foreach ($new_resource->databases() as $database) {
- $database->update([
+ $database->forceFill([
'status' => 'exited',
- ]);
+ ])->save();
$persistentVolumes = $database->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
@@ -313,7 +324,8 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
- ])->fill([
+ 'uuid',
+ ])->forceFill([
'name' => $newName,
'resource_id' => $database->id,
]);
@@ -354,9 +366,9 @@ public function moveTo($environment_id)
try {
$this->authorize('update', $this->resource);
$new_environment = Environment::ownedByCurrentTeam()->findOrFail($environment_id);
- $this->resource->update([
+ $this->resource->forceFill([
'environment_id' => $environment_id,
- ]);
+ ])->save();
if ($this->resource->type() === 'application') {
$route = route('project.application.configuration', [
'project_uuid' => $new_environment->project->uuid,
diff --git a/app/Models/Application.php b/app/Models/Application.php
index c446052b3..3312f4c76 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -118,7 +118,92 @@ class Application extends BaseModel
private static $parserVersion = '5';
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ 'fqdn',
+ 'git_repository',
+ 'git_branch',
+ 'git_commit_sha',
+ 'git_full_url',
+ 'docker_registry_image_name',
+ 'docker_registry_image_tag',
+ 'build_pack',
+ 'static_image',
+ 'install_command',
+ 'build_command',
+ 'start_command',
+ 'ports_exposes',
+ 'ports_mappings',
+ 'base_directory',
+ 'publish_directory',
+ 'health_check_enabled',
+ 'health_check_path',
+ 'health_check_port',
+ 'health_check_host',
+ 'health_check_method',
+ 'health_check_return_code',
+ 'health_check_scheme',
+ 'health_check_response_text',
+ 'health_check_interval',
+ 'health_check_timeout',
+ 'health_check_retries',
+ 'health_check_start_period',
+ 'health_check_type',
+ 'health_check_command',
+ 'limits_memory',
+ 'limits_memory_swap',
+ 'limits_memory_swappiness',
+ 'limits_memory_reservation',
+ 'limits_cpus',
+ 'limits_cpuset',
+ 'limits_cpu_shares',
+ 'status',
+ 'preview_url_template',
+ 'dockerfile',
+ 'dockerfile_location',
+ 'dockerfile_target_build',
+ 'custom_labels',
+ 'custom_docker_run_options',
+ 'post_deployment_command',
+ 'post_deployment_command_container',
+ 'pre_deployment_command',
+ 'pre_deployment_command_container',
+ 'manual_webhook_secret_github',
+ 'manual_webhook_secret_gitlab',
+ 'manual_webhook_secret_bitbucket',
+ 'manual_webhook_secret_gitea',
+ 'docker_compose_location',
+ 'docker_compose_pr_location',
+ 'docker_compose',
+ 'docker_compose_pr',
+ 'docker_compose_raw',
+ 'docker_compose_pr_raw',
+ 'docker_compose_domains',
+ 'docker_compose_custom_start_command',
+ 'docker_compose_custom_build_command',
+ 'swarm_replicas',
+ 'swarm_placement_constraints',
+ 'watch_paths',
+ 'redirect',
+ 'compose_parsing_version',
+ 'custom_nginx_configuration',
+ 'custom_network_aliases',
+ 'custom_healthcheck_found',
+ 'nixpkgsarchive',
+ 'is_http_basic_auth_enabled',
+ 'http_basic_auth_username',
+ 'http_basic_auth_password',
+ 'connect_to_docker_network',
+ 'force_domain_override',
+ 'is_container_label_escape_enabled',
+ 'use_build_server',
+ 'config_hash',
+ 'last_online_at',
+ 'restart_count',
+ 'last_restart_at',
+ 'last_restart_type',
+ ];
protected $appends = ['server_status'];
@@ -1145,7 +1230,7 @@ public function getGitRemoteStatus(string $deployment_uuid)
'is_accessible' => true,
'error' => null,
];
- } catch (\RuntimeException $ex) {
+ } catch (RuntimeException $ex) {
return [
'is_accessible' => false,
'error' => $ex->getMessage(),
@@ -1202,7 +1287,7 @@ public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_
];
}
- if ($this->source->getMorphClass() === \App\Models\GitlabApp::class) {
+ if ($this->source->getMorphClass() === GitlabApp::class) {
$gitlabSource = $this->source;
$private_key = data_get($gitlabSource, 'privateKey.private_key');
@@ -1354,7 +1439,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
$source_html_url_host = $url['host'];
$source_html_url_scheme = $url['scheme'];
- if ($this->source->getMorphClass() === \App\Models\GithubApp::class) {
+ if ($this->source->getMorphClass() === GithubApp::class) {
if ($this->source->is_public) {
$fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
$escapedRepoUrl = escapeshellarg("{$this->source->html_url}/{$customRepository}");
@@ -1409,7 +1494,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
];
}
- if ($this->source->getMorphClass() === \App\Models\GitlabApp::class) {
+ if ($this->source->getMorphClass() === GitlabApp::class) {
$gitlabSource = $this->source;
$private_key = data_get($gitlabSource, 'privateKey.private_key');
@@ -1600,7 +1685,7 @@ public function oldRawParser()
try {
$yaml = Yaml::parse($this->docker_compose_raw);
} catch (\Exception $e) {
- throw new \RuntimeException($e->getMessage());
+ throw new RuntimeException($e->getMessage());
}
$services = data_get($yaml, 'services');
@@ -1682,7 +1767,7 @@ public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory =
$fileList = collect([".$workdir$composeFile"]);
$gitRemoteStatus = $this->getGitRemoteStatus(deployment_uuid: $uuid);
if (! $gitRemoteStatus['is_accessible']) {
- throw new \RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}");
+ throw new RuntimeException('Failed to read Git source. Please verify repository access and try again.');
}
$getGitVersion = instant_remote_process(['git --version'], $this->destination->server, false);
$gitVersion = str($getGitVersion)->explode(' ')->last();
@@ -1732,15 +1817,15 @@ public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory =
$this->save();
if (str($e->getMessage())->contains('No such file')) {
- throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile (branch: {$this->git_branch})
Check if you used the right extension (.yaml or .yml) in the compose file name.");
+ throw new RuntimeException("Docker Compose file not found at: $workdir$composeFile (branch: {$this->git_branch})
Check if you used the right extension (.yaml or .yml) in the compose file name.");
}
if (str($e->getMessage())->contains('fatal: repository') && str($e->getMessage())->contains('does not exist')) {
if ($this->deploymentType() === 'deploy_key') {
- throw new \RuntimeException('Your deploy key does not have access to the repository. Please check your deploy key and try again.');
+ throw new RuntimeException('Your deploy key does not have access to the repository. Please check your deploy key and try again.');
}
- throw new \RuntimeException('Repository does not exist. Please check your repository URL and try again.');
+ throw new RuntimeException('Repository does not exist. Please check your repository URL and try again.');
}
- throw new \RuntimeException($e->getMessage());
+ throw new RuntimeException('Failed to read the Docker Compose file from the repository.');
} finally {
// Cleanup only - restoration happens in catch block
$commands = collect([
@@ -1793,7 +1878,7 @@ public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory =
$this->base_directory = $initialBaseDirectory;
$this->save();
- throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile (branch: {$this->git_branch})
Check if you used the right extension (.yaml or .yml) in the compose file name.");
+ throw new RuntimeException("Docker Compose file not found at: $workdir$composeFile (branch: {$this->git_branch})
Check if you used the right extension (.yaml or .yml) in the compose file name.");
}
}
diff --git a/app/Models/Server.php b/app/Models/Server.php
index 00843b3da..0ae524f49 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -265,8 +265,6 @@ public static function flushIdentityMap(): void
'server_metadata',
];
- protected $guarded = [];
-
use HasSafeStringAttribute;
public function setValidationLogsAttribute($value): void
diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php
index 504cfa60a..efc7bc8de 100644
--- a/app/Models/ServerSetting.php
+++ b/app/Models/ServerSetting.php
@@ -56,6 +56,7 @@ class ServerSetting extends Model
protected $guarded = [];
protected $casts = [
+ 'force_disabled' => 'boolean',
'force_docker_cleanup' => 'boolean',
'docker_cleanup_threshold' => 'integer',
'sentinel_token' => 'encrypted',
diff --git a/app/Models/Service.php b/app/Models/Service.php
index 84c047bb7..b3ff85e53 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -15,6 +15,7 @@
use OpenApi\Attributes as OA;
use Spatie\Activitylog\Models\Activity;
use Spatie\Url\Url;
+use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
#[OA\Schema(
@@ -47,7 +48,16 @@ class Service extends BaseModel
private static $parserVersion = '5';
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ 'docker_compose_raw',
+ 'docker_compose',
+ 'connect_to_docker_network',
+ 'service_type',
+ 'config_hash',
+ 'compose_parsing_version',
+ ];
protected $appends = ['server_status', 'status'];
@@ -1552,7 +1562,7 @@ public function saveComposeConfigs()
// Generate SERVICE_NAME_* environment variables from docker-compose services
if ($this->docker_compose) {
try {
- $dockerCompose = \Symfony\Component\Yaml\Yaml::parse($this->docker_compose);
+ $dockerCompose = Yaml::parse($this->docker_compose);
$services = data_get($dockerCompose, 'services', []);
foreach ($services as $serviceName => $_) {
$envs->push('SERVICE_NAME_'.str($serviceName)->replace('-', '_')->replace('.', '_')->upper().'='.$serviceName);
diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php
index 143aadb6a..c192e5360 100644
--- a/app/Models/StandaloneClickhouse.php
+++ b/app/Models/StandaloneClickhouse.php
@@ -13,12 +13,36 @@ class StandaloneClickhouse extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ 'clickhouse_admin_user',
+ 'clickhouse_admin_password',
+ 'is_log_drain_enabled',
+ 'is_include_timestamps',
+ 'status',
+ 'image',
+ 'is_public',
+ 'public_port',
+ 'ports_mappings',
+ 'limits_memory',
+ 'limits_memory_swap',
+ 'limits_memory_swappiness',
+ 'limits_memory_reservation',
+ 'limits_cpus',
+ 'limits_cpuset',
+ 'limits_cpu_shares',
+ 'started_at',
+ 'restart_count',
+ 'last_restart_at',
+ 'last_restart_type',
+ 'last_online_at',
+ ];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
protected $casts = [
- 'clickhouse_password' => 'encrypted',
+ 'clickhouse_admin_password' => 'encrypted',
'public_port_timeout' => 'integer',
'restart_count' => 'integer',
'last_restart_at' => 'datetime',
diff --git a/app/Models/StandaloneDocker.php b/app/Models/StandaloneDocker.php
index 0407c2255..abd6e168f 100644
--- a/app/Models/StandaloneDocker.php
+++ b/app/Models/StandaloneDocker.php
@@ -3,6 +3,7 @@
namespace App\Models;
use App\Jobs\ConnectProxyToNetworksJob;
+use App\Support\ValidationPatterns;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -18,13 +19,23 @@ protected static function boot()
parent::boot();
static::created(function ($newStandaloneDocker) {
$server = $newStandaloneDocker->server;
+ $safeNetwork = escapeshellarg($newStandaloneDocker->network);
instant_remote_process([
- "docker network inspect $newStandaloneDocker->network >/dev/null 2>&1 || docker network create --driver overlay --attachable $newStandaloneDocker->network >/dev/null",
+ "docker network inspect {$safeNetwork} >/dev/null 2>&1 || docker network create --driver overlay --attachable {$safeNetwork} >/dev/null",
], $server, false);
ConnectProxyToNetworksJob::dispatchSync($server);
});
}
+ public function setNetworkAttribute(string $value): void
+ {
+ if (! ValidationPatterns::isValidDockerNetwork($value)) {
+ throw new \InvalidArgumentException('Invalid Docker network name. Must start with alphanumeric and contain only alphanumeric characters, dots, hyphens, and underscores.');
+ }
+
+ $this->attributes['network'] = $value;
+ }
+
public function applications()
{
return $this->morphMany(Application::class, 'destination');
diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php
index c823c305b..7cc74f0ce 100644
--- a/app/Models/StandaloneDragonfly.php
+++ b/app/Models/StandaloneDragonfly.php
@@ -13,7 +13,30 @@ class StandaloneDragonfly extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ 'dragonfly_password',
+ 'is_log_drain_enabled',
+ 'is_include_timestamps',
+ 'status',
+ 'image',
+ 'is_public',
+ 'public_port',
+ 'ports_mappings',
+ 'limits_memory',
+ 'limits_memory_swap',
+ 'limits_memory_swappiness',
+ 'limits_memory_reservation',
+ 'limits_cpus',
+ 'limits_cpuset',
+ 'limits_cpu_shares',
+ 'started_at',
+ 'restart_count',
+ 'last_restart_at',
+ 'last_restart_type',
+ 'last_online_at',
+ ];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php
index f286e8538..7a0d7f03d 100644
--- a/app/Models/StandaloneKeydb.php
+++ b/app/Models/StandaloneKeydb.php
@@ -13,7 +13,31 @@ class StandaloneKeydb extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ 'keydb_password',
+ 'keydb_conf',
+ 'is_log_drain_enabled',
+ 'is_include_timestamps',
+ 'status',
+ 'image',
+ 'is_public',
+ 'public_port',
+ 'ports_mappings',
+ 'limits_memory',
+ 'limits_memory_swap',
+ 'limits_memory_swappiness',
+ 'limits_memory_reservation',
+ 'limits_cpus',
+ 'limits_cpuset',
+ 'limits_cpu_shares',
+ 'started_at',
+ 'restart_count',
+ 'last_restart_at',
+ 'last_restart_type',
+ 'last_online_at',
+ ];
protected $appends = ['internal_db_url', 'external_db_url', 'server_status'];
diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php
index efa62353c..6cac9e5f4 100644
--- a/app/Models/StandaloneMariadb.php
+++ b/app/Models/StandaloneMariadb.php
@@ -14,7 +14,32 @@ class StandaloneMariadb extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ 'mariadb_root_password',
+ 'mariadb_user',
+ 'mariadb_password',
+ 'mariadb_database',
+ 'mariadb_conf',
+ 'status',
+ 'image',
+ 'is_public',
+ 'public_port',
+ 'ports_mappings',
+ 'limits_memory',
+ 'limits_memory_swap',
+ 'limits_memory_swappiness',
+ 'limits_memory_reservation',
+ 'limits_cpus',
+ 'limits_cpuset',
+ 'limits_cpu_shares',
+ 'started_at',
+ 'restart_count',
+ 'last_restart_at',
+ 'last_restart_type',
+ 'last_online_at',
+ ];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php
index 9418ebc21..5ca4ef5d3 100644
--- a/app/Models/StandaloneMongodb.php
+++ b/app/Models/StandaloneMongodb.php
@@ -13,7 +13,31 @@ class StandaloneMongodb extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ 'mongo_conf',
+ 'mongo_initdb_root_username',
+ 'mongo_initdb_root_password',
+ 'mongo_initdb_database',
+ 'status',
+ 'image',
+ 'is_public',
+ 'public_port',
+ 'ports_mappings',
+ 'limits_memory',
+ 'limits_memory_swap',
+ 'limits_memory_swappiness',
+ 'limits_memory_reservation',
+ 'limits_cpus',
+ 'limits_cpuset',
+ 'limits_cpu_shares',
+ 'started_at',
+ 'restart_count',
+ 'last_restart_at',
+ 'last_restart_type',
+ 'last_online_at',
+ ];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php
index 2b7e9f2b6..cf8d78a9c 100644
--- a/app/Models/StandaloneMysql.php
+++ b/app/Models/StandaloneMysql.php
@@ -13,7 +13,32 @@ class StandaloneMysql extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ 'mysql_root_password',
+ 'mysql_user',
+ 'mysql_password',
+ 'mysql_database',
+ 'mysql_conf',
+ 'status',
+ 'image',
+ 'is_public',
+ 'public_port',
+ 'ports_mappings',
+ 'limits_memory',
+ 'limits_memory_swap',
+ 'limits_memory_swappiness',
+ 'limits_memory_reservation',
+ 'limits_cpus',
+ 'limits_cpuset',
+ 'limits_cpu_shares',
+ 'started_at',
+ 'restart_count',
+ 'last_restart_at',
+ 'last_restart_type',
+ 'last_online_at',
+ ];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php
index cea600236..7db334c5d 100644
--- a/app/Models/StandalonePostgresql.php
+++ b/app/Models/StandalonePostgresql.php
@@ -13,7 +13,34 @@ class StandalonePostgresql extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ 'postgres_user',
+ 'postgres_password',
+ 'postgres_db',
+ 'postgres_initdb_args',
+ 'postgres_host_auth_method',
+ 'postgres_conf',
+ 'init_scripts',
+ 'status',
+ 'image',
+ 'is_public',
+ 'public_port',
+ 'ports_mappings',
+ 'limits_memory',
+ 'limits_memory_swap',
+ 'limits_memory_swappiness',
+ 'limits_memory_reservation',
+ 'limits_cpus',
+ 'limits_cpuset',
+ 'limits_cpu_shares',
+ 'started_at',
+ 'restart_count',
+ 'last_restart_at',
+ 'last_restart_type',
+ 'last_online_at',
+ ];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php
index 0e904ab31..2320619cf 100644
--- a/app/Models/StandaloneRedis.php
+++ b/app/Models/StandaloneRedis.php
@@ -13,7 +13,28 @@ class StandaloneRedis extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ 'redis_conf',
+ 'status',
+ 'image',
+ 'is_public',
+ 'public_port',
+ 'ports_mappings',
+ 'limits_memory',
+ 'limits_memory_swap',
+ 'limits_memory_swappiness',
+ 'limits_memory_reservation',
+ 'limits_cpus',
+ 'limits_cpuset',
+ 'limits_cpu_shares',
+ 'started_at',
+ 'restart_count',
+ 'last_restart_at',
+ 'last_restart_type',
+ 'last_online_at',
+ ];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
diff --git a/app/Models/SwarmDocker.php b/app/Models/SwarmDocker.php
index 08be81970..3144432c5 100644
--- a/app/Models/SwarmDocker.php
+++ b/app/Models/SwarmDocker.php
@@ -2,10 +2,21 @@
namespace App\Models;
+use App\Support\ValidationPatterns;
+
class SwarmDocker extends BaseModel
{
protected $guarded = [];
+ public function setNetworkAttribute(string $value): void
+ {
+ if (! ValidationPatterns::isValidDockerNetwork($value)) {
+ throw new \InvalidArgumentException('Invalid Docker network name. Must start with alphanumeric and contain only alphanumeric characters, dots, hyphens, and underscores.');
+ }
+
+ $this->attributes['network'] = $value;
+ }
+
public function applications()
{
return $this->morphMany(Application::class, 'destination');
diff --git a/app/Models/Team.php b/app/Models/Team.php
index 5a7b377b6..8eb8fa050 100644
--- a/app/Models/Team.php
+++ b/app/Models/Team.php
@@ -40,7 +40,13 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
{
use HasFactory, HasNotificationSettings, HasSafeStringAttribute, Notifiable;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ 'personal_team',
+ 'show_boarding',
+ 'custom_server_limit',
+ ];
protected $casts = [
'personal_team' => 'boolean',
diff --git a/app/Models/User.php b/app/Models/User.php
index 7c68657e7..ad9a7af31 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -4,7 +4,9 @@
use App\Jobs\UpdateStripeCustomerEmailJob;
use App\Notifications\Channels\SendsEmail;
+use App\Notifications\TransactionalEmails\EmailChangeVerification;
use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword;
+use App\Services\ChangelogService;
use App\Traits\DeletesUserSessions;
use DateTimeInterface;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -41,7 +43,13 @@ class User extends Authenticatable implements SendsEmail
{
use DeletesUserSessions, HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'email',
+ 'password',
+ 'force_password_reset',
+ 'marketing_emails',
+ ];
protected $hidden = [
'password',
@@ -87,7 +95,7 @@ protected static function boot()
$team['id'] = 0;
$team['name'] = 'Root Team';
}
- $new_team = Team::create($team);
+ $new_team = Team::forceCreate($team);
$user->teams()->attach($new_team, ['role' => 'owner']);
});
@@ -190,7 +198,7 @@ public function recreate_personal_team()
$team['id'] = 0;
$team['name'] = 'Root Team';
}
- $new_team = Team::create($team);
+ $new_team = Team::forceCreate($team);
$this->teams()->attach($new_team, ['role' => 'owner']);
return $new_team;
@@ -228,7 +236,7 @@ public function changelogReads()
public function getUnreadChangelogCount(): int
{
- return app(\App\Services\ChangelogService::class)->getUnreadCountForUser($this);
+ return app(ChangelogService::class)->getUnreadCountForUser($this);
}
public function getRecipients(): array
@@ -239,7 +247,7 @@ public function getRecipients(): array
public function sendVerificationEmail()
{
$mail = new MailMessage;
- $url = Url::temporarySignedRoute(
+ $url = URL::temporarySignedRoute(
'verify.verify',
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
[
@@ -401,14 +409,14 @@ public function requestEmailChange(string $newEmail): void
$expiryMinutes = config('constants.email_change.verification_code_expiry_minutes', 10);
$expiresAt = Carbon::now()->addMinutes($expiryMinutes);
- $this->update([
+ $this->forceFill([
'pending_email' => $newEmail,
'email_change_code' => $code,
'email_change_code_expires_at' => $expiresAt,
- ]);
+ ])->save();
// Send verification email to new address
- $this->notify(new \App\Notifications\TransactionalEmails\EmailChangeVerification($this, $code, $newEmail, $expiresAt));
+ $this->notify(new EmailChangeVerification($this, $code, $newEmail, $expiresAt));
}
public function isEmailChangeCodeValid(string $code): bool
diff --git a/app/Support/ValidationPatterns.php b/app/Support/ValidationPatterns.php
index 7084b4cc2..cec607f4e 100644
--- a/app/Support/ValidationPatterns.php
+++ b/app/Support/ValidationPatterns.php
@@ -58,6 +58,13 @@ class ValidationPatterns
*/
public const CONTAINER_NAME_PATTERN = '/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/';
+ /**
+ * Pattern for Docker network names
+ * Must start with alphanumeric, followed by alphanumeric, dots, hyphens, or underscores
+ * Matches Docker's network naming rules and prevents shell injection
+ */
+ public const DOCKER_NETWORK_PATTERN = '/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/';
+
/**
* Get validation rules for name fields
*/
@@ -210,6 +217,44 @@ public static function isValidContainerName(string $name): bool
return preg_match(self::CONTAINER_NAME_PATTERN, $name) === 1;
}
+ /**
+ * Get validation rules for Docker network name fields
+ */
+ public static function dockerNetworkRules(bool $required = true, int $maxLength = 255): array
+ {
+ $rules = [];
+
+ if ($required) {
+ $rules[] = 'required';
+ } else {
+ $rules[] = 'nullable';
+ }
+
+ $rules[] = 'string';
+ $rules[] = "max:$maxLength";
+ $rules[] = 'regex:'.self::DOCKER_NETWORK_PATTERN;
+
+ return $rules;
+ }
+
+ /**
+ * Get validation messages for Docker network name fields
+ */
+ public static function dockerNetworkMessages(string $field = 'network'): array
+ {
+ return [
+ "{$field}.regex" => 'The network name must start with an alphanumeric character and contain only alphanumeric characters, dots, hyphens, and underscores.',
+ ];
+ }
+
+ /**
+ * Check if a string is a valid Docker network name.
+ */
+ public static function isValidDockerNetwork(string $name): bool
+ {
+ return preg_match(self::DOCKER_NETWORK_PATTERN, $name) === 1;
+ }
+
/**
* Get combined validation messages for both name and description fields
*/
diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php
index e3a611ceb..961148a89 100644
--- a/bootstrap/helpers/api.php
+++ b/bootstrap/helpers/api.php
@@ -144,6 +144,7 @@ function sharedDataApplications()
'docker_compose_custom_start_command' => \App\Support\ValidationPatterns::shellSafeCommandRules(),
'docker_compose_custom_build_command' => \App\Support\ValidationPatterns::shellSafeCommandRules(),
'is_container_label_escape_enabled' => 'boolean',
+ 'is_preserve_repository_enabled' => 'boolean'
];
}
@@ -193,6 +194,7 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
$request->offsetUnset('force_domain_override');
$request->offsetUnset('autogenerate_domain');
$request->offsetUnset('is_container_label_escape_enabled');
+ $request->offsetUnset('is_preserve_repository_enabled');
$request->offsetUnset('docker_compose_raw');
$request->offsetUnset('tags');
}
diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php
index c522cd0ca..4af6ac90a 100644
--- a/bootstrap/helpers/applications.php
+++ b/bootstrap/helpers/applications.php
@@ -6,6 +6,7 @@
use App\Jobs\VolumeCloneJob;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
+use App\Models\EnvironmentVariable;
use App\Models\Server;
use App\Models\StandaloneDocker;
use Spatie\Url\Url;
@@ -192,7 +193,7 @@ function clone_application(Application $source, $destination, array $overrides =
$server = $destination->server;
if ($server->team_id !== currentTeam()->id) {
- throw new \RuntimeException('Destination does not belong to the current team.');
+ throw new RuntimeException('Destination does not belong to the current team.');
}
// Prepare name and URL
@@ -211,7 +212,7 @@ function clone_application(Application $source, $destination, array $overrides =
'updated_at',
'additional_servers_count',
'additional_networks_count',
- ])->fill(array_merge([
+ ])->forceFill(array_merge([
'uuid' => $uuid,
'name' => $name,
'fqdn' => $url,
@@ -299,6 +300,7 @@ function clone_application(Application $source, $destination, array $overrides =
'id',
'created_at',
'updated_at',
+ 'uuid',
])->fill([
'name' => $newName,
'resource_id' => $newApplication->id,
@@ -322,8 +324,8 @@ function clone_application(Application $source, $destination, array $overrides =
destination: $source->destination,
no_questions_asked: true
);
- } catch (\Exception $e) {
- \Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
+ } catch (Exception $e) {
+ Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
}
}
}
@@ -344,7 +346,7 @@ function clone_application(Application $source, $destination, array $overrides =
// Clone production environment variables without triggering the created hook
$environmentVariables = $source->environment_variables()->get();
foreach ($environmentVariables as $environmentVariable) {
- \App\Models\EnvironmentVariable::withoutEvents(function () use ($environmentVariable, $newApplication) {
+ EnvironmentVariable::withoutEvents(function () use ($environmentVariable, $newApplication) {
$newEnvironmentVariable = $environmentVariable->replicate([
'id',
'created_at',
@@ -361,7 +363,7 @@ function clone_application(Application $source, $destination, array $overrides =
// Clone preview environment variables
$previewEnvironmentVariables = $source->environment_variables_preview()->get();
foreach ($previewEnvironmentVariables as $previewEnvironmentVariable) {
- \App\Models\EnvironmentVariable::withoutEvents(function () use ($previewEnvironmentVariable, $newApplication) {
+ EnvironmentVariable::withoutEvents(function () use ($previewEnvironmentVariable, $newApplication) {
$newPreviewEnvironmentVariable = $previewEnvironmentVariable->replicate([
'id',
'created_at',
diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php
index cf9f648bb..ed18dfe76 100644
--- a/bootstrap/helpers/proxy.php
+++ b/bootstrap/helpers/proxy.php
@@ -109,18 +109,20 @@ function connectProxyToNetworks(Server $server)
['networks' => $networks] = collectDockerNetworksByServer($server);
if ($server->isSwarm()) {
$commands = $networks->map(function ($network) {
+ $safe = escapeshellarg($network);
return [
- "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --driver overlay --attachable $network >/dev/null",
- "docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
- "echo 'Successfully connected coolify-proxy to $network network.'",
+ "docker network ls --format '{{.Name}}' | grep '^{$network}$' >/dev/null || docker network create --driver overlay --attachable {$safe} >/dev/null",
+ "docker network connect {$safe} coolify-proxy >/dev/null 2>&1 || true",
+ "echo 'Successfully connected coolify-proxy to {$safe} network.'",
];
});
} else {
$commands = $networks->map(function ($network) {
+ $safe = escapeshellarg($network);
return [
- "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --attachable $network >/dev/null",
- "docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
- "echo 'Successfully connected coolify-proxy to $network network.'",
+ "docker network ls --format '{{.Name}}' | grep '^{$network}$' >/dev/null || docker network create --attachable {$safe} >/dev/null",
+ "docker network connect {$safe} coolify-proxy >/dev/null 2>&1 || true",
+ "echo 'Successfully connected coolify-proxy to {$safe} network.'",
];
});
}
@@ -141,16 +143,18 @@ function ensureProxyNetworksExist(Server $server)
if ($server->isSwarm()) {
$commands = $networks->map(function ($network) {
+ $safe = escapeshellarg($network);
return [
- "echo 'Ensuring network $network exists...'",
- "docker network ls --format '{{.Name}}' | grep -q '^{$network}$' || docker network create --driver overlay --attachable $network",
+ "echo 'Ensuring network {$safe} exists...'",
+ "docker network ls --format '{{.Name}}' | grep -q '^{$network}$' || docker network create --driver overlay --attachable {$safe}",
];
});
} else {
$commands = $networks->map(function ($network) {
+ $safe = escapeshellarg($network);
return [
- "echo 'Ensuring network $network exists...'",
- "docker network ls --format '{{.Name}}' | grep -q '^{$network}$' || docker network create --attachable $network",
+ "echo 'Ensuring network {$safe} exists...'",
+ "docker network ls --format '{{.Name}}' | grep -q '^{$network}$' || docker network create --attachable {$safe}",
];
});
}
diff --git a/database/migrations/2026_03_29_000000_encrypt_existing_clickhouse_admin_passwords.php b/database/migrations/2026_03_29_000000_encrypt_existing_clickhouse_admin_passwords.php
new file mode 100644
index 000000000..a4a6988f2
--- /dev/null
+++ b/database/migrations/2026_03_29_000000_encrypt_existing_clickhouse_admin_passwords.php
@@ -0,0 +1,39 @@
+chunkById(100, function ($clickhouses) {
+ foreach ($clickhouses as $clickhouse) {
+ $password = $clickhouse->clickhouse_admin_password;
+
+ if (empty($password)) {
+ continue;
+ }
+
+ // Skip if already encrypted (idempotent)
+ try {
+ Crypt::decryptString($password);
+
+ continue;
+ } catch (Exception) {
+ // Not encrypted yet — encrypt it
+ }
+
+ DB::table('standalone_clickhouses')
+ ->where('id', $clickhouse->id)
+ ->update(['clickhouse_admin_password' => Crypt::encryptString($password)]);
+ }
+ });
+ } catch (Exception $e) {
+ echo 'Encrypting ClickHouse admin passwords failed.';
+ echo $e->getMessage();
+ }
+ }
+}
diff --git a/database/seeders/RootUserSeeder.php b/database/seeders/RootUserSeeder.php
index e3968a1c9..c4e93af63 100644
--- a/database/seeders/RootUserSeeder.php
+++ b/database/seeders/RootUserSeeder.php
@@ -45,12 +45,13 @@ public function run(): void
}
try {
- User::create([
+ $user = (new User)->forceFill([
'id' => 0,
'name' => env('ROOT_USERNAME', 'Root User'),
'email' => env('ROOT_USER_EMAIL'),
'password' => Hash::make(env('ROOT_USER_PASSWORD')),
]);
+ $user->save();
echo "\n SUCCESS Root user created successfully.\n\n";
} catch (\Exception $e) {
echo "\n ERROR Failed to create root user: {$e->getMessage()}\n\n";
diff --git a/openapi.json b/openapi.json
index ee970c5c3..277f485f9 100644
--- a/openapi.json
+++ b/openapi.json
@@ -407,6 +407,11 @@
"type": "boolean",
"default": true,
"description": "Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off."
+ },
+ "is_preserve_repository_enabled": {
+ "type": "boolean",
+ "default": false,
+ "description": "Preserve repository during deployment."
}
},
"type": "object"
@@ -852,6 +857,11 @@
"type": "boolean",
"default": true,
"description": "Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off."
+ },
+ "is_preserve_repository_enabled": {
+ "type": "boolean",
+ "default": false,
+ "description": "Preserve repository during deployment."
}
},
"type": "object"
@@ -1297,6 +1307,11 @@
"type": "boolean",
"default": true,
"description": "Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off."
+ },
+ "is_preserve_repository_enabled": {
+ "type": "boolean",
+ "default": false,
+ "description": "Preserve repository during deployment."
}
},
"type": "object"
@@ -2704,6 +2719,11 @@
"type": "boolean",
"default": true,
"description": "Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off."
+ },
+ "is_preserve_repository_enabled": {
+ "type": "boolean",
+ "default": false,
+ "description": "Preserve repository during deployment."
}
},
"type": "object"
diff --git a/openapi.yaml b/openapi.yaml
index 80744d3d4..9475364b6 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -291,6 +291,10 @@ paths:
type: boolean
default: true
description: 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'
+ is_preserve_repository_enabled:
+ type: boolean
+ default: false
+ description: 'Preserve repository during deployment.'
type: object
responses:
'201':
@@ -575,6 +579,10 @@ paths:
type: boolean
default: true
description: 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'
+ is_preserve_repository_enabled:
+ type: boolean
+ default: false
+ description: 'Preserve repository during deployment.'
type: object
responses:
'201':
@@ -859,6 +867,10 @@ paths:
type: boolean
default: true
description: 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'
+ is_preserve_repository_enabled:
+ type: boolean
+ default: false
+ description: 'Preserve repository during deployment.'
type: object
responses:
'201':
@@ -1741,6 +1753,10 @@ paths:
type: boolean
default: true
description: 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'
+ is_preserve_repository_enabled:
+ type: boolean
+ default: false
+ description: 'Preserve repository during deployment.'
type: object
responses:
'200':
diff --git a/resources/views/components/forms/env-var-input.blade.php b/resources/views/components/forms/env-var-input.blade.php
index 2466a57f9..d26e248c1 100644
--- a/resources/views/components/forms/env-var-input.blade.php
+++ b/resources/views/components/forms/env-var-input.blade.php
@@ -10,7 +10,7 @@
@endif
-