diff --git a/README.md b/README.md index 0a3ce0132..dac48d127 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Special thanks to our biggest sponsors! ### Special Sponsors -![image](https://github.com/user-attachments/assets/152bd1e0-e0c1-4d47-8a4f-0eb3700d2e61) +![image](https://github.com/user-attachments/assets/726fb63e-c3b8-4260-b3ac-06780605ec5d) * [CCCareers](https://cccareers.org/) - A career development platform connecting coding bootcamp graduates with job opportunities in the tech industry. * [Hetzner](http://htznr.li/CoolifyXHetzner) - A German web hosting company offering affordable dedicated servers, cloud services, and web hosting solutions. @@ -50,6 +50,7 @@ Special thanks to our biggest sponsors! * [QuantCDN](https://www.quantcdn.io/?ref=coolify.io) - A content delivery network (CDN) optimizing website performance through global content distribution. * [Arcjet](https://arcjet.com/?ref=coolify.io) - A cloud-based platform providing real-time protection against API abuse and bot attacks. * [SupaGuide](https://supa.guide/?ref=coolify.io) - A comprehensive resource hub offering guides and tutorials for web development using Supabase. +* [GoldenVM](https://billing.goldenvm.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes. * [Tigris](https://tigrisdata.com/?ref=coolify.io) - A fully managed serverless object storage service compatible with Amazon S3 API. Offers high performance, scalability, and built-in search capabilities for efficient data management. * [Advin](https://coolify.ad.vin/?ref=coolify.io) - A digital advertising agency specializing in programmatic advertising and data-driven marketing strategies. * [Treive](https://trieve.ai/?ref=coolify.io) - An AI-powered search and discovery platform for enhancing information retrieval in large datasets. @@ -90,7 +91,6 @@ Special thanks to our biggest sponsors! Paweł Pierścionek Michael Mazurczak Formbricks -Adith Suhas StartupFame Jonas Jaeger JP @@ -147,10 +147,10 @@ By subscribing to the cloud version, you get the Coolify server for the same pri # Core Maintainers -| Andras Bacsai | Peak | +| Andras Bacsai | 🏔️ Peak | |------------|------------| -| Andras Bacsai | Peak Labs | -| | | +| Andras Bacsai | peaklabs-dev | +| | | # Repo Activity diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php index 13667e829..42c6e1449 100644 --- a/app/Actions/Database/StartClickhouse.php +++ b/app/Actions/Database/StartClickhouse.php @@ -24,7 +24,7 @@ class StartClickhouse $this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->commands = [ - "echo 'Starting {$database->name}.'", + "echo 'Starting database.'", "mkdir -p $this->configuration_dir", ]; diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php index c72714e1c..ea235be4e 100644 --- a/app/Actions/Database/StartDragonfly.php +++ b/app/Actions/Database/StartDragonfly.php @@ -26,7 +26,7 @@ class StartDragonfly $this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->commands = [ - "echo 'Starting {$database->name}.'", + "echo 'Starting database.'", "mkdir -p $this->configuration_dir", ]; diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php index bd98258ab..010bf5884 100644 --- a/app/Actions/Database/StartKeydb.php +++ b/app/Actions/Database/StartKeydb.php @@ -27,7 +27,7 @@ class StartKeydb $this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->commands = [ - "echo 'Starting {$database->name}.'", + "echo 'Starting database.'", "mkdir -p $this->configuration_dir", ]; diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php index 696dd7ff4..2437a013e 100644 --- a/app/Actions/Database/StartMariadb.php +++ b/app/Actions/Database/StartMariadb.php @@ -24,7 +24,7 @@ class StartMariadb $this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->commands = [ - "echo 'Starting {$database->name}.'", + "echo 'Starting database.'", "mkdir -p $this->configuration_dir", ]; diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php index 26a0f82d0..a33e72c27 100644 --- a/app/Actions/Database/StartMongodb.php +++ b/app/Actions/Database/StartMongodb.php @@ -30,7 +30,7 @@ class StartMongodb } $this->commands = [ - "echo 'Starting {$database->name}.'", + "echo 'Starting database.'", "mkdir -p $this->configuration_dir", ]; diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php index a3694648f..0b19b3f0c 100644 --- a/app/Actions/Database/StartMysql.php +++ b/app/Actions/Database/StartMysql.php @@ -24,7 +24,7 @@ class StartMysql $this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->commands = [ - "echo 'Starting {$database->name}.'", + "echo 'Starting database.'", "mkdir -p $this->configuration_dir", ]; diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php index f5e85087f..7faa232c3 100644 --- a/app/Actions/Database/StartPostgresql.php +++ b/app/Actions/Database/StartPostgresql.php @@ -25,7 +25,7 @@ class StartPostgresql $this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->commands = [ - "echo 'Starting {$database->name}.'", + "echo 'Starting database.'", "mkdir -p $this->configuration_dir", "mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/", ]; diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php index 7a2d2b34d..bacf49f82 100644 --- a/app/Actions/Database/StartRedis.php +++ b/app/Actions/Database/StartRedis.php @@ -25,7 +25,7 @@ class StartRedis $this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->commands = [ - "echo 'Starting {$database->name}.'", + "echo 'Starting database.'", "mkdir -p $this->configuration_dir", ]; diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php index 8aae910a9..706356930 100644 --- a/app/Actions/Docker/GetContainersStatus.php +++ b/app/Actions/Docker/GetContainersStatus.php @@ -179,7 +179,7 @@ class GetContainersStatus })->first(); if (! $foundTcpProxy) { StartDatabaseProxy::run($database); - // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); + // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for database", $this->server)); } } } else { diff --git a/app/Actions/Server/ServerCheck.php b/app/Actions/Server/ServerCheck.php index 5f9a1e357..75b8501f3 100644 --- a/app/Actions/Server/ServerCheck.php +++ b/app/Actions/Server/ServerCheck.php @@ -51,7 +51,6 @@ class ServerCheck $containerReplicates = null; $this->isSentinel = true; - } else { ['containers' => $this->containers, 'containerReplicates' => $containerReplicates] = $this->server->getContainers(); // ServerStorageCheckJob::dispatch($this->server); @@ -148,7 +147,6 @@ class ServerCheck } else { $labels = Arr::undot(data_get($container, 'Config.Labels')); } - } $managed = data_get($labels, 'coolify.managed'); if (! $managed) { @@ -259,7 +257,7 @@ class ServerCheck })->first(); if (! $foundTcpProxy) { StartDatabaseProxy::run($database); - // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); + // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for database", $this->server)); } } } diff --git a/app/Console/Commands/CloudCleanupSubscriptions.php b/app/Console/Commands/CloudCleanupSubscriptions.php index 8bb420ab8..9198b003e 100644 --- a/app/Console/Commands/CloudCleanupSubscriptions.php +++ b/app/Console/Commands/CloudCleanupSubscriptions.php @@ -36,7 +36,7 @@ class CloudCleanupSubscriptions extends Command } // If the team has no subscription id and the invoice is paid, we need to reset the invoice paid status if (! (data_get($team, 'subscription.stripe_subscription_id'))) { - $this->info("Resetting invoice paid status for team {$team->id} {$team->name}"); + $this->info("Resetting invoice paid status for team {$team->id}"); $team->subscription->update([ 'stripe_invoice_paid' => false, @@ -61,9 +61,9 @@ class CloudCleanupSubscriptions extends Command $this->info('Subscription id: '.data_get($team, 'subscription.stripe_subscription_id')); $confirm = $this->confirm('Do you want to cancel the subscription?', true); if (! $confirm) { - $this->info("Skipping team {$team->id} {$team->name}"); + $this->info("Skipping team {$team->id}"); } else { - $this->info("Cancelling subscription for team {$team->id} {$team->name}"); + $this->info("Cancelling subscription for team {$team->id}"); $team->subscription->update([ 'stripe_invoice_paid' => false, 'stripe_trial_already_ended' => false, diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index c69640970..614208c78 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -1591,16 +1591,32 @@ class ApplicationsController extends Controller } $domains = $request->domains; if ($request->has('domains') && $server->isProxyShouldRun()) { - $errors = []; + $uuid = $request->uuid; $fqdn = $request->domains; $fqdn = str($fqdn)->replaceEnd(',', '')->trim(); $fqdn = str($fqdn)->replaceStart(',', '')->trim(); - $application->fqdn = $fqdn; - if (! $application->settings->is_container_label_readonly_enabled) { - $customLabels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); - $application->custom_labels = base64_encode($customLabels); + $errors = []; + $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) { + $domain = trim($domain); + if (filter_var($domain, FILTER_VALIDATE_URL) === false || !preg_match('/^https?:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}/', $domain)) { + $errors[] = 'Invalid domain: '.$domain; + } + return $domain; + }); + if (count($errors) > 0) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'domains' => 'One of the domain is already used.', + ], + ], 422); } - $request->offsetUnset('domains'); } $dockerComposeDomainsJson = collect(); @@ -2811,3 +2827,30 @@ class ApplicationsController extends Controller } } } + + $fqdn = str($fqdn)->replaceStart(',', '')->trim(); + $errors = []; + $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) { + if (filter_var($domain, FILTER_VALIDATE_URL) === false) { + $errors[] = 'Invalid domain: ' . $domain; + } + + return str($domain)->trim()->lower(); + }); + if (count($errors) > 0) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'domains' => 'One of the domain is already used.', + ], + ], 422); + } + } + } +} diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index ce658d2a2..98a076c49 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -211,8 +211,9 @@ class DatabasesController extends Controller 'mongo_conf' => ['type' => 'string', 'description' => 'Mongo conf'], 'mongo_initdb_root_username' => ['type' => 'string', 'description' => 'Mongo initdb root username'], 'mongo_initdb_root_password' => ['type' => 'string', 'description' => 'Mongo initdb root password'], - 'mongo_initdb_init_database' => ['type' => 'string', 'description' => 'Mongo initdb init database'], + 'mongo_initdb_database' => ['type' => 'string', 'description' => 'Mongo initdb init database'], 'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'], + 'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'], 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'], 'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'], 'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'], @@ -241,7 +242,7 @@ class DatabasesController extends Controller )] public function update_by_uuid(Request $request) { - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { return invalidTokenResponse(); @@ -413,12 +414,12 @@ class DatabasesController extends Controller } break; case 'standalone-mongodb': - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database']; $validator = customApiValidator($request->all(), [ 'mongo_conf' => 'string', 'mongo_initdb_root_username' => 'string', 'mongo_initdb_root_password' => 'string', - 'mongo_initdb_init_database' => 'string', + 'mongo_initdb_database' => 'string', ]); if ($request->has('mongo_conf')) { if (! isBase64Encoded($request->mongo_conf)) { @@ -443,9 +444,10 @@ class DatabasesController extends Controller break; case 'standalone-mysql': - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $validator = customApiValidator($request->all(), [ 'mysql_root_password' => 'string', + 'mysql_password' => 'string', 'mysql_user' => 'string', 'mysql_database' => 'string', 'mysql_conf' => 'string', @@ -909,6 +911,7 @@ class DatabasesController extends Controller 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], 'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'], + 'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'], 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'], 'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'], 'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'], @@ -1013,7 +1016,7 @@ class DatabasesController extends Controller public function create_database(Request $request, NewDatabaseTypes $type) { - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -1220,9 +1223,10 @@ class DatabasesController extends Controller return response()->json(serializeApiResponse($payload))->setStatusCode(201); } elseif ($type === NewDatabaseTypes::MYSQL) { - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_user', 'mysql_database', 'mysql_conf']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $validator = customApiValidator($request->all(), [ 'mysql_root_password' => 'string', + 'mysql_password' => 'string', 'mysql_user' => 'string', 'mysql_database' => 'string', 'mysql_conf' => 'string', @@ -1456,12 +1460,12 @@ class DatabasesController extends Controller return response()->json(serializeApiResponse($payload))->setStatusCode(201); } elseif ($type === NewDatabaseTypes::MONGODB) { - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database']; $validator = customApiValidator($request->all(), [ 'mongo_conf' => 'string', 'mongo_initdb_root_username' => 'string', 'mongo_initdb_root_password' => 'string', - 'mongo_initdb_init_database' => 'string', + 'mongo_initdb_database' => 'string', ]); $extraFields = array_diff(array_keys($request->all()), $allowedFields); if ($validator->fails() || ! empty($extraFields)) { @@ -1557,7 +1561,8 @@ class DatabasesController extends Controller ] ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -1632,9 +1637,11 @@ class DatabasesController extends Controller type: 'object', properties: [ 'message' => ['type' => 'string', 'example' => 'Database starting request queued.'], - ]) + ] + ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -1708,9 +1715,11 @@ class DatabasesController extends Controller type: 'object', properties: [ 'message' => ['type' => 'string', 'example' => 'Database stopping request queued.'], - ]) + ] + ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -1784,9 +1793,11 @@ class DatabasesController extends Controller type: 'object', properties: [ 'message' => ['type' => 'string', 'example' => 'Database restaring request queued.'], - ]) + ] + ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index 3683adaa8..ac1d4ded2 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -463,7 +463,7 @@ class Github extends Controller $private_key = data_get($data, 'pem'); $webhook_secret = data_get($data, 'webhook_secret'); $private_key = PrivateKey::create([ - 'name' => $slug, + 'name' => "github-app-{$slug}", 'private_key' => $private_key, 'team_id' => $github_app->team_id, 'is_git_related' => true, diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 6a66bb56d..04e71c4e3 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -140,6 +140,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private ?string $buildTarget = null; + private bool $disableBuildCache = false; + private Collection $saved_outputs; private ?string $full_healthcheck_url = null; @@ -178,7 +180,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->pull_request_id = $this->application_deployment_queue->pull_request_id; $this->commit = $this->application_deployment_queue->commit; $this->rollback = $this->application_deployment_queue->rollback; + $this->disableBuildCache = $this->application->settings->disable_build_cache; $this->force_rebuild = $this->application_deployment_queue->force_rebuild; + if ($this->disableBuildCache) { + $this->force_rebuild = true; + } $this->restart_only = $this->application_deployment_queue->restart_only; $this->restart_only = $this->restart_only && $this->application->build_pack !== 'dockerimage' && $this->application->build_pack !== 'dockerfile'; $this->only_this_server = $this->application_deployment_queue->only_this_server; @@ -1976,6 +1982,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->build_args = $this->build_args->implode(' '); $this->application_deployment_queue->addLogEntry('----------------------------------------'); + if ($this->disableBuildCache) { + $this->application_deployment_queue->addLogEntry('Docker build cache is disabled. It will not be used during the build process.'); + } if ($this->application->build_pack === 'static') { $this->application_deployment_queue->addLogEntry('Static deployment. Copying static assets to the image.'); } else { @@ -2400,7 +2409,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); if (! $this->only_this_server) { $this->deploy_to_additional_destinations(); } - $this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview)); + //$this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview)); } } diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 89674b255..ee702202f 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -306,7 +306,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue if ($this->backup->save_s3) { $this->upload_to_s3(); } - $this->team?->notify(new BackupSuccess($this->backup, $this->database, $database)); + //$this->team?->notify(new BackupSuccess($this->backup, $this->database, $database)); $this->backup_log->update([ 'status' => 'success', 'message' => $this->backup_output, diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index b6c799c4e..eadabba7c 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -172,13 +172,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== public function getProxyType() { - // Set Default Proxy Type $this->selectProxy(ProxyTypes::TRAEFIK->value); - // $proxyTypeSet = $this->createdServer->proxy->type; - // if (!$proxyTypeSet) { - // $this->currentState = 'select-proxy'; - // return; - // } $this->getProjects(); } @@ -189,7 +183,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== return; } - $this->createdPrivateKey = PrivateKey::find($this->selectedExistingPrivateKey); + $this->createdPrivateKey = PrivateKey::where('team_id', currentTeam()->id)->where('id', $this->selectedExistingPrivateKey)->first(); $this->privateKey = $this->createdPrivateKey->private_key; $this->currentState = 'create-server'; } diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php index fcedf1305..ab3768643 100644 --- a/app/Livewire/Notifications/Email.php +++ b/app/Livewire/Notifications/Email.php @@ -37,7 +37,7 @@ class Email extends Component #[Validate(['nullable', 'numeric'])] public ?int $smtpPort = null; - #[Validate(['nullable', 'string'])] + #[Validate(['nullable', 'string', 'in:tls,ssl,none'])] public ?string $smtpEncryption = null; #[Validate(['nullable', 'string'])] @@ -73,8 +73,8 @@ class Email extends Component #[Validate(['nullable', 'string'])] public ?string $resendApiKey = null; - #[Validate(['required', 'email'])] - public string $testEmailAddress = ''; + #[Validate(['nullable', 'email'])] + public ?string $testEmailAddress = null; public function mount() { diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php index 05ac25429..cb63f0e1a 100644 --- a/app/Livewire/Project/Application/Advanced.php +++ b/app/Livewire/Project/Application/Advanced.php @@ -25,6 +25,9 @@ class Advanced extends Component #[Validate(['boolean'])] public bool $isAutoDeployEnabled = true; + #[Validate(['boolean'])] + public bool $disableBuildCache = false; + #[Validate(['boolean'])] public bool $isLogDrainEnabled = false; @@ -95,6 +98,7 @@ class Advanced extends Component $this->application->settings->is_stripprefix_enabled = $this->isStripprefixEnabled; $this->application->settings->is_raw_compose_deployment_enabled = $this->isRawComposeDeploymentEnabled; $this->application->settings->connect_to_docker_network = $this->isConnectToDockerNetworkEnabled; + $this->application->settings->disable_build_cache = $this->disableBuildCache; $this->application->settings->save(); } else { $this->isForceHttpsEnabled = $this->application->isForceHttpsEnabled(); @@ -116,6 +120,7 @@ class Advanced extends Component $this->customInternalName = $this->application->settings->custom_internal_name; $this->isRawComposeDeploymentEnabled = $this->application->settings->is_raw_compose_deployment_enabled; $this->isConnectToDockerNetworkEnabled = $this->application->settings->connect_to_docker_network; + $this->disableBuildCache = $this->application->settings->disable_build_cache; } } diff --git a/app/Livewire/Project/Application/Configuration.php b/app/Livewire/Project/Application/Configuration.php index 5e7f83772..5261a0800 100644 --- a/app/Livewire/Project/Application/Configuration.php +++ b/app/Livewire/Project/Application/Configuration.php @@ -21,19 +21,16 @@ class Configuration extends Component ->select('id', 'uuid', 'team_id') ->where('uuid', request()->route('project_uuid')) ->firstOrFail(); - $environment = $project->environments() ->select('id', 'name', 'project_id') ->where('name', request()->route('environment_name')) ->firstOrFail(); - $application = $environment->applications() ->with(['destination']) ->where('uuid', request()->route('application_uuid')) ->firstOrFail(); $this->application = $application; - if ($application->destination && $application->destination->server) { $mainServer = $application->destination->server; $this->servers = Server::ownedByCurrentTeam() diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index 621ab1bac..d12d8e26a 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -168,18 +168,42 @@ class ExecuteContainerCommand extends Component return; } try { + // Validate container name format + if (! preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/', $this->selected_container)) { + throw new \InvalidArgumentException('Invalid container name format'); + } + + // Verify container exists in our allowed list $container = collect($this->containers)->firstWhere('container.Names', $this->selected_container); if (is_null($container)) { throw new \RuntimeException('Container not found.'); } - $server = data_get($this->container, 'server'); + + // Verify server ownership and status + $server = data_get($container, 'server'); + if (! $server || ! $server instanceof Server) { + throw new \RuntimeException('Invalid server configuration.'); + } if ($server->isForceDisabled()) { throw new \RuntimeException('Server is disabled.'); } + + // Additional ownership verification based on resource type + $resourceServer = match ($this->type) { + 'application' => $this->resource->destination->server, + 'database' => $this->resource->destination->server, + 'service' => $this->resource->server, + default => throw new \RuntimeException('Invalid resource type.') + }; + + if ($server->id !== $resourceServer->id && ! $this->resource->additional_servers->contains('id', $server->id)) { + throw new \RuntimeException('Server ownership verification failed.'); + } + $this->dispatch( 'send-terminal-command', - isset($container), + true, data_get($container, 'container.Names'), data_get($container, 'server.uuid') ); diff --git a/app/Livewire/Project/Shared/Terminal.php b/app/Livewire/Project/Shared/Terminal.php index 5af8f057e..d8f101277 100644 --- a/app/Livewire/Project/Shared/Terminal.php +++ b/app/Livewire/Project/Shared/Terminal.php @@ -29,11 +29,20 @@ class Terminal extends Component $server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail(); if ($isContainer) { + // Validate container identifier format (alphanumeric, dashes, and underscores only) + if (! preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/', $identifier)) { + throw new \InvalidArgumentException('Invalid container identifier format'); + } + + // Verify container exists and belongs to the user's team $status = getContainerStatus($server, $identifier); if ($status !== 'running') { return; } - $command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); + + // Escape the identifier for shell usage + $escapedIdentifier = escapeshellarg($identifier); + $command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$escapedIdentifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); } else { $command = SshMultiplexingHelper::generateSshCommand($server, 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n "$SHELL" ]; then exec $SHELL; else sh; fi'); } diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index a5544489d..ac5211c1b 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -5,7 +5,7 @@ namespace App\Livewire\Server; use App\Actions\Server\StartSentinel; use App\Actions\Server\StopSentinel; use App\Models\Server; -use Livewire\Attributes\Locked; +use Livewire\Attributes\Computed; use Livewire\Attributes\Validate; use Livewire\Component; @@ -79,9 +79,6 @@ class Show extends Component #[Validate(['required'])] public string $serverTimezone; - #[Locked] - public array $timezones; - public function getListeners() { $teamId = auth()->user()->currentTeam()->id; @@ -96,13 +93,21 @@ class Show extends Component { try { $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); - $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray(); $this->syncData(); } catch (\Throwable $e) { return handleError($e, $this); } } + #[Computed] + public function timezones(): array + { + return collect(timezone_identifiers_list()) + ->sort() + ->values() + ->toArray(); + } + public function syncData(bool $toModel = false) { if ($toModel) { diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php index 31dd13c52..c1be35ced 100644 --- a/app/Livewire/Settings/Index.php +++ b/app/Livewire/Settings/Index.php @@ -7,7 +7,7 @@ use App\Models\InstanceSettings; use App\Models\Server; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; -use Livewire\Attributes\Locked; +use Livewire\Attributes\Computed; use Livewire\Attributes\Validate; use Livewire\Component; @@ -17,9 +17,6 @@ class Index extends Component protected Server $server; - #[Locked] - public $timezones; - #[Validate('boolean')] public bool $is_auto_update_enabled; @@ -101,12 +98,20 @@ class Index extends Component $this->is_api_enabled = $this->settings->is_api_enabled; $this->auto_update_frequency = $this->settings->auto_update_frequency; $this->update_check_frequency = $this->settings->update_check_frequency; - $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray(); $this->instance_timezone = $this->settings->instance_timezone; $this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation; } } + #[Computed] + public function timezones(): array + { + return collect(timezone_identifiers_list()) + ->sort() + ->values() + ->toArray(); + } + public function instantSave($isSave = true) { $this->validate(); diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php index 61f720b3a..abf3a12f9 100644 --- a/app/Livewire/SettingsEmail.php +++ b/app/Livewire/SettingsEmail.php @@ -19,7 +19,7 @@ class SettingsEmail extends Component #[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])] public ?int $smtpPort = null; - #[Validate(['nullable', 'string'])] + #[Validate(['nullable', 'string', 'in:tls,ssl,none'])] public ?string $smtpEncryption = null; #[Validate(['nullable', 'string'])] diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index 07cef54f9..467927484 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -4,6 +4,11 @@ namespace App\Livewire\Source\Github; use App\Jobs\GithubAppPermissionJob; use App\Models\GithubApp; +use App\Models\PrivateKey; +use Illuminate\Support\Facades\Http; +use Lcobucci\JWT\Configuration; +use Lcobucci\JWT\Signer\Key\InMemory; +use Lcobucci\JWT\Signer\Rsa\Sha256; use Livewire\Component; class Change extends Component @@ -51,12 +56,20 @@ class Change extends Component 'github_app.administration' => 'nullable|string', ]; + public function boot() + { + if ($this->github_app) { + $this->github_app->makeVisible(['client_secret', 'webhook_secret']); + } + } + public function checkPermissions() { GithubAppPermissionJob::dispatchSync($this->github_app); $this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret'); $this->dispatch('success', 'Github App permissions updated.'); } + // public function check() // { @@ -90,15 +103,16 @@ class Change extends Component // ray($runners_by_repository); // } + public function mount() { try { $github_app_uuid = request()->github_app_uuid; $this->github_app = GithubApp::ownedByCurrentTeam()->whereUuid($github_app_uuid)->firstOrFail(); + $this->github_app->makeVisible(['client_secret', 'webhook_secret']); $this->applications = $this->github_app->applications; $settings = instanceSettings(); - $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); $this->name = str($this->github_app->name)->kebab(); $this->fqdn = $settings->fqdn; @@ -142,6 +156,77 @@ class Change extends Component } } + public function getGithubAppNameUpdatePath() + { + if (str($this->github_app->organization)->isNotEmpty()) { + return "{$this->github_app->html_url}/organizations/{$this->github_app->organization}/settings/apps/{$this->github_app->name}"; + } + + return "{$this->github_app->html_url}/settings/apps/{$this->github_app->name}"; + } + + private function generateGithubJwt($private_key, $app_id): string + { + $configuration = Configuration::forAsymmetricSigner( + new Sha256, + InMemory::plainText($private_key), + InMemory::plainText($private_key) + ); + + $now = time(); + + return $configuration->builder() + ->issuedBy((string) $app_id) + ->permittedFor('https://api.github.com') + ->identifiedBy((string) $now) + ->issuedAt(new \DateTimeImmutable("@{$now}")) + ->expiresAt(new \DateTimeImmutable('@'.($now + 600))) + ->getToken($configuration->signer(), $configuration->signingKey()) + ->toString(); + } + + public function updateGithubAppName() + { + try { + $privateKey = PrivateKey::ownedByCurrentTeam()->find($this->github_app->private_key_id); + + if (! $privateKey) { + $this->dispatch('error', 'No private key found for this GitHub App.'); + + return; + } + + $jwt = $this->generateGithubJwt($privateKey->private_key, $this->github_app->app_id); + + $response = Http::withHeaders([ + 'Accept' => 'application/vnd.github+json', + 'X-GitHub-Api-Version' => '2022-11-28', + 'Authorization' => "Bearer {$jwt}", + ])->get("{$this->github_app->api_url}/app"); + + if ($response->successful()) { + $app_data = $response->json(); + $app_slug = $app_data['slug'] ?? null; + + if ($app_slug) { + $this->github_app->name = $app_slug; + $this->name = str($app_slug)->kebab(); + $privateKey->name = "github-app-{$app_slug}"; + $privateKey->save(); + $this->github_app->save(); + $this->dispatch('success', 'GitHub App name and SSH key name synchronized successfully.'); + } else { + $this->dispatch('info', 'Could not find App Name (slug) in GitHub response.'); + } + } else { + $error_message = $response->json()['message'] ?? 'Unknown error'; + $this->dispatch('error', "Failed to fetch GitHub App information: {$error_message}"); + } + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function submit() { try { diff --git a/app/Models/Application.php b/app/Models/Application.php index c284528f1..a68c1d54a 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -4,6 +4,7 @@ namespace App\Models; use App\Enums\ApplicationDeploymentStatus; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Process\InvokedProcess; @@ -104,7 +105,7 @@ use Visus\Cuid2\Cuid2; class Application extends BaseModel { - use SoftDeletes; + use HasFactory, SoftDeletes; private static $parserVersion = '4'; diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php index 17201ea6e..79801987b 100644 --- a/app/Models/BaseModel.php +++ b/app/Models/BaseModel.php @@ -2,6 +2,7 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Visus\Cuid2\Cuid2; @@ -18,4 +19,18 @@ abstract class BaseModel extends Model } }); } + + public function name(): Attribute + { + return new Attribute( + get: fn () => sanitize_string($this->getRawOriginal('name')), + ); + } + + public function image(): Attribute + { + return new Attribute( + get: fn () => sanitize_string($this->getRawOriginal('image')), + ); + } } diff --git a/app/Models/Server.php b/app/Models/Server.php index 27c2b9b99..77673b959 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -11,6 +11,7 @@ use App\Notifications\Server\Reachable; use App\Notifications\Server\Unreachable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; @@ -48,7 +49,7 @@ use Symfony\Component\Yaml\Yaml; class Server extends BaseModel { - use SchemalessAttributesTrait, SoftDeletes; + use HasFactory, SchemalessAttributesTrait, SoftDeletes; public static $batch_counter = 0; @@ -610,7 +611,8 @@ $schema://$host { } $memory = json_decode($memory, true); $parsedCollection = collect($memory)->map(function ($metric) { - return [(int) $metric['time'], (float) $metric['usedPercent']]; + $usedPercent = $metric['usedPercent'] ?? 0.0; + return [(int) $metric['time'], (float) $usedPercent]; }); return $parsedCollection->toArray(); @@ -1039,7 +1041,7 @@ $schema://$host { $this->unreachable_notification_sent = false; $this->save(); $this->refresh(); - $this->team->notify(new Reachable($this)); + // $this->team->notify(new Reachable($this)); } public function sendUnreachableNotification() @@ -1047,7 +1049,7 @@ $schema://$host { $this->unreachable_notification_sent = true; $this->save(); $this->refresh(); - $this->team->notify(new Unreachable($this)); + // $this->team->notify(new Unreachable($this)); } public function validateConnection(bool $justCheckingNewKey = false) diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index af9af978d..5394f6106 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -66,11 +66,12 @@ class EmailChannel 'transport' => 'smtp', 'host' => data_get($notifiable, 'smtp_host'), 'port' => data_get($notifiable, 'smtp_port'), - 'encryption' => data_get($notifiable, 'smtp_encryption'), + 'encryption' => data_get($notifiable, 'smtp_encryption') === 'none' ? null : data_get($notifiable, 'smtp_encryption'), 'username' => data_get($notifiable, 'smtp_username'), 'password' => data_get($notifiable, 'smtp_password'), 'timeout' => data_get($notifiable, 'smtp_timeout'), 'local_domain' => null, + 'auto_tls' => data_get($notifiable, 'smtp_encryption') === 'none' ? '0' : '', ]); } } diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php index 303fcab8e..b568e090c 100644 --- a/bootstrap/helpers/constants.php +++ b/bootstrap/helpers/constants.php @@ -46,7 +46,7 @@ const SPECIFIC_SERVICES = [ // Based on /etc/os-release const SUPPORTED_OS = [ - 'ubuntu debian raspbian', + 'ubuntu debian raspbian pop', 'centos fedora rhel ol rocky amzn almalinux', 'sles opensuse-leap opensuse-tumbleweed', 'arch', diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 8dd01a162..eda2133a7 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -288,9 +288,9 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, $host_without_www = str($host)->replace('www.', ''); $schema = $url->getScheme(); $port = $url->getPort(); - $handle = "handle_path"; - if ( ! $is_stripprefix_enabled){ - $handle = "handle"; + $handle = 'handle_path'; + if (! $is_stripprefix_enabled) { + $handle = 'handle'; } if (is_null($port) && ! is_null($onlyPort)) { $port = $onlyPort; @@ -302,7 +302,6 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, $labels->push("caddy_{$loop}.header=-Server"); $labels->push("caddy_{$loop}.try_files={path} /index.html /index.php"); - if ($port) { $labels->push("caddy_{$loop}.{$handle}.{$loop}_reverse_proxy={{upstreams $port}}"); } else { diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index f09255ea0..40c5acb21 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -91,8 +91,31 @@ function metrics_dir(): string return base_configuration_dir() . '/metrics'; } +function sanitize_string(?string $input = null): ?string +{ + if (is_null($input)) { + return null; + } + // Remove any HTML/PHP tags + $sanitized = strip_tags($input); + + // Convert special characters to HTML entities + $sanitized = htmlspecialchars($sanitized, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + + // Remove any control characters + $sanitized = preg_replace('/[\x00-\x1F\x7F]/u', '', $sanitized); + + // Trim whitespace + $sanitized = trim($sanitized); + + return $sanitized; +} + function generate_readme_file(string $name, string $updated_at): string { + $name = sanitize_string($name); + $updated_at = sanitize_string($updated_at); + return "Resource name: $name\nLatest Deployment Date: $updated_at"; } diff --git a/config/constants.php b/config/constants.php index c947635be..f7d5a7831 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.374', + 'version' => '4.0.0-beta.376', 'self_hosted' => env('SELF_HOSTED', true), 'autoupdate' => env('AUTOUPDATE'), 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), diff --git a/config/database.php b/config/database.php index f48a68082..6f4acbfd2 100644 --- a/config/database.php +++ b/config/database.php @@ -49,6 +49,22 @@ return [ 'search_path' => 'public', 'sslmode' => 'prefer', ], + + 'testing' => [ + 'driver' => 'pgsql', + 'url' => env('DATABASE_TEST_URL'), + 'host' => env('DB_TEST_HOST', 'postgres'), + 'port' => env('DB_TEST_PORT', '5432'), + 'database' => env('DB_TEST_DATABASE', 'coolify_test'), + 'username' => env('DB_TEST_USERNAME', 'coolify'), + 'password' => env('DB_TEST_PASSWORD', 'password'), + 'charset' => 'utf8', + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => 'prefer', + ], + ], /* diff --git a/database/factories/ApplicationFactory.php b/database/factories/ApplicationFactory.php new file mode 100644 index 000000000..ded507c56 --- /dev/null +++ b/database/factories/ApplicationFactory.php @@ -0,0 +1,22 @@ + fake()->unique()->name(), + 'destination_id' => 1, + 'git_repository' => fake()->url(), + 'git_branch' => fake()->word(), + 'build_pack' => 'nixpacks', + 'ports_exposes' => '3000', + 'environment_id' => 1, + 'destination_id' => 1, + ]; + } +} diff --git a/database/factories/ServerFactory.php b/database/factories/ServerFactory.php new file mode 100644 index 000000000..29546bf56 --- /dev/null +++ b/database/factories/ServerFactory.php @@ -0,0 +1,17 @@ + fake()->unique()->name(), + 'ip' => fake()->unique()->ipv4(), + 'private_key_id' => 1, + ]; + } +} diff --git a/database/migrations/2024_12_05_091823_add_disable_build_cache_advanced_option.php b/database/migrations/2024_12_05_091823_add_disable_build_cache_advanced_option.php new file mode 100644 index 000000000..751342302 --- /dev/null +++ b/database/migrations/2024_12_05_091823_add_disable_build_cache_advanced_option.php @@ -0,0 +1,22 @@ +boolean('disable_build_cache')->default(false); + }); + } + + public function down(): void + { + Schema::table('application_settings', function (Blueprint $table) { + $table->dropColumn('disable_build_cache'); + }); + } +}; diff --git a/database/seeders/GithubAppSeeder.php b/database/seeders/GithubAppSeeder.php index 2ece7a05b..3cfb82e64 100644 --- a/database/seeders/GithubAppSeeder.php +++ b/database/seeders/GithubAppSeeder.php @@ -23,6 +23,7 @@ class GithubAppSeeder extends Seeder GithubApp::create([ 'name' => 'coolify-laravel-development-public', 'uuid' => '69420', + 'organization' => 'coollabsio', 'api_url' => 'https://api.github.com', 'html_url' => 'https://github.com', 'is_public' => false, diff --git a/openapi.json b/openapi.json index 2ec218438..5d35331ec 100644 --- a/openapi.json +++ b/openapi.json @@ -3011,7 +3011,7 @@ "type": "string", "description": "Mongo initdb root password" }, - "mongo_initdb_init_database": { + "mongo_initdb_database": { "type": "string", "description": "Mongo initdb init database" }, @@ -3019,6 +3019,10 @@ "type": "string", "description": "MySQL root password" }, + "mysql_password": { + "type": "string", + "description": "MySQL password" + }, "mysql_user": { "type": "string", "description": "MySQL user" @@ -3842,6 +3846,10 @@ "type": "string", "description": "MySQL root password" }, + "mysql_password": { + "type": "string", + "description": "MySQL password" + }, "mysql_user": { "type": "string", "description": "MySQL user" diff --git a/openapi.yaml b/openapi.yaml index 2a22c730c..20bf34873 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2089,12 +2089,15 @@ paths: mongo_initdb_root_password: type: string description: 'Mongo initdb root password' - mongo_initdb_init_database: + mongo_initdb_database: type: string description: 'Mongo initdb init database' mysql_root_password: type: string description: 'MySQL root password' + mysql_password: + type: string + description: 'MySQL password' mysql_user: type: string description: 'MySQL user' @@ -2684,6 +2687,9 @@ paths: mysql_root_password: type: string description: 'MySQL root password' + mysql_password: + type: string + description: 'MySQL password' mysql_user: type: string description: 'MySQL user' diff --git a/phpunit.xml b/phpunit.xml index 45cb69439..f1c2be92d 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -13,8 +13,8 @@ - - + + diff --git a/public/svgs/overseerr.svg b/public/svgs/overseerr.svg new file mode 100644 index 000000000..8116787c2 --- /dev/null +++ b/public/svgs/overseerr.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/svgs/plex.svg b/public/svgs/plex.svg new file mode 100644 index 000000000..872b135cf --- /dev/null +++ b/public/svgs/plex.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/prowlarr.svg b/public/svgs/prowlarr.svg new file mode 100644 index 000000000..a1a5a8ce3 --- /dev/null +++ b/public/svgs/prowlarr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/pterodactyl.png b/public/svgs/pterodactyl.png new file mode 100644 index 000000000..a5addb87c Binary files /dev/null and b/public/svgs/pterodactyl.png differ diff --git a/public/svgs/radarr.svg b/public/svgs/radarr.svg new file mode 100644 index 000000000..93a4c9232 --- /dev/null +++ b/public/svgs/radarr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/sonarr.svg b/public/svgs/sonarr.svg new file mode 100644 index 000000000..91c04e289 --- /dev/null +++ b/public/svgs/sonarr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/css/app.css b/resources/css/app.css index 1195a3058..32d476c1a 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -6,7 +6,7 @@ html, body { - @apply h-full bg-neutral-50 text-neutral-800 dark:bg-base dark:text-neutral-400; + @apply h-full bg-neutral-50 text-neutral-800 dark:bg-base dark:text-neutral-400 w-full; } body { @@ -322,4 +322,4 @@ section { .dz-button { @apply w-full p-4 py-10 my-4 font-bold bg-white border dark:border-coolgray-400 dark:text-white dark:bg-transparent hover:dark:bg-coolgray-400; -} +} \ No newline at end of file diff --git a/resources/views/components/modal-input.blade.php b/resources/views/components/modal-input.blade.php index c090037d5..acd3b8bdf 100644 --- a/resources/views/components/modal-input.blade.php +++ b/resources/views/components/modal-input.blade.php @@ -7,6 +7,7 @@ 'action' => 'delete', 'content' => null, 'closeOutside' => true, + 'minWidth' => '36rem', ])
@@ -40,7 +41,7 @@ x-transition:leave="ease-in duration-100" x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100" x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95" - class="relative w-full py-6 border rounded drop-shadow min-w-full lg:min-w-[36rem] max-w-fit bg-white border-neutral-200 dark:bg-base px-6 dark:border-coolgray-300"> + class="relative w-full py-6 border rounded drop-shadow min-w-full lg:min-w-[{{ $minWidth }}] max-w-fit bg-white border-neutral-200 dark:bg-base px-6 dark:border-coolgray-300">

{{ $title }}

@@ -78,6 +108,9 @@ x-show="full === 'full'">Center +
Zoom
+ +
@@ -163,8 +196,8 @@ class="{{ request()->is('storages*') ? 'menu-item-active menu-item' : 'menu-item' }}" href="{{ route('storage.index') }}"> - + diff --git a/resources/views/livewire/notifications/email.blade.php b/resources/views/livewire/notifications/email.blade.php index 182c73d6a..fba21d0b2 100644 --- a/resources/views/livewire/notifications/email.blade.php +++ b/resources/views/livewire/notifications/email.blade.php @@ -17,7 +17,8 @@ @if (isEmailEnabled($team) && auth()->user()->isAdminFromSession() && isTestEmailEnabled($team))
- + Send Email @@ -62,8 +63,11 @@
- + + + + +
diff --git a/resources/views/livewire/project/application/advanced.blade.php b/resources/views/livewire/project/application/advanced.blade.php index 6658c0ed2..f3fb0485f 100644 --- a/resources/views/livewire/project/application/advanced.blade.php +++ b/resources/views/livewire/project/application/advanced.blade.php @@ -13,6 +13,8 @@ helper="Allow to automatically deploy Preview Deployments for all opened PR's.

Closing a PR will delete Preview Deployments." instantSave id="isPreviewDeploymentsEnabled" label="Preview Deployments" /> @endif + diff --git a/resources/views/livewire/project/application/deployment/index.blade.php b/resources/views/livewire/project/application/deployment/index.blade.php index f6fdb64ab..3fa52b7f3 100644 --- a/resources/views/livewire/project/application/deployment/index.blade.php +++ b/resources/views/livewire/project/application/deployment/index.blade.php @@ -32,7 +32,7 @@ @forelse ($deployments as $deployment)
+ 'border-white border-dashed ' => data_get($deployment, 'status') === 'in_progress' || data_get($deployment, 'status') === 'cancelled-by-user', 'border-error border-dashed ' => diff --git a/resources/views/livewire/project/application/deployment/show.blade.php b/resources/views/livewire/project/application/deployment/show.blade.php index 9d9301d5c..92ed72981 100644 --- a/resources/views/livewire/project/application/deployment/show.blade.php +++ b/resources/views/livewire/project/application/deployment/show.blade.php @@ -19,7 +19,7 @@ }, toggleScroll() { this.alwaysScroll = !this.alwaysScroll; - + if (this.alwaysScroll) { this.intervalId = setInterval(() => { const screen = document.getElementById('screen'); @@ -58,30 +58,34 @@
-
+