diff --git a/README.md b/README.md index 8868bcea6..dac48d127 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ # About the Project -Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc. +Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc. It helps you manage your servers, applications, and databases on your own hardware; you only need an SSH connection. You can manage VPS, Bare Metal, Raspberry PIs, and anything else. @@ -40,21 +40,21 @@ Special thanks to our biggest sponsors! ### Special Sponsors -![image](https://github.com/user-attachments/assets/c95a07df-7c5a-4e77-a35a-81f25fcbece1) +![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. * [Logto](https://logto.io/?ref=coolify) - An open-source authentication and authorization solution for building secure login systems and managing user identities. +* [Tolgee](https://tolgee.io/?ref=coolify) - Developer & translator friendly web-based localization platform. * [BC Direct](https://bc.direct/?ref=coolify.io) - A digital marketing agency specializing in e-commerce solutions and online business growth strategies. * [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. -* [Fractal Networks](https://fractalnetworks.co/?ref=coolify.io) - A decentralized network infrastructure company focusing on secure and private communication solutions. * [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. * [Blacksmith](https://blacksmith.sh/?ref=coolify.io) - A cloud-native platform for automating infrastructure provisioning and management across multiple cloud providers. -* [Latitude](https://latitude.sh/?ref=coolify.io) - A cloud computing platform offering bare metal servers and cloud instances for developers and businesses. * [Brand Dev](https://brand.dev/?ref=coolify.io) - A web development agency specializing in creating custom digital experiences and brand identities. * [Jobscollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - A job search platform connecting professionals with remote work opportunities across various industries. * [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - A web hosting provider offering affordable hosting solutions, domain registration, and website building tools. @@ -63,6 +63,7 @@ Special thanks to our biggest sponsors! * [Juxtdigital](https://juxtdigital.dev/?ref=coolify.io) - A digital agency offering web development, design, and digital marketing services for businesses. * [Saasykit](https://saasykit.com/?ref=coolify.io) - A Laravel-based boilerplate providing essential components and features for building SaaS applications quickly. * [Massivegrid](https://massivegrid.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes. +* [LiquidWeb](https://liquidweb.com/?utm_source=coolify.io) - Fast web hosting provider. ## Github Sponsors ($40+) @@ -90,7 +91,11 @@ Special thanks to our biggest sponsors! Paweł Pierścionek Michael Mazurczak Formbricks -Adith Suhas +StartupFame +Jonas Jaeger +JP +Evercam +Web3 Career ## Organizations @@ -142,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/Application/StopApplication.php b/app/Actions/Application/StopApplication.php index cab7e45f0..642b4ba45 100644 --- a/app/Actions/Application/StopApplication.php +++ b/app/Actions/Application/StopApplication.php @@ -10,6 +10,8 @@ class StopApplication { use AsAction; + public string $jobQueue = 'high'; + public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true) { try { diff --git a/app/Actions/CoolifyTask/PrepareCoolifyTask.php b/app/Actions/CoolifyTask/PrepareCoolifyTask.php index 6676b7937..3f76a2e3c 100644 --- a/app/Actions/CoolifyTask/PrepareCoolifyTask.php +++ b/app/Actions/CoolifyTask/PrepareCoolifyTask.php @@ -3,7 +3,6 @@ namespace App\Actions\CoolifyTask; use App\Data\CoolifyTaskArgs; -use App\Enums\ActivityTypes; use App\Jobs\CoolifyTask; use Spatie\Activitylog\Models\Activity; @@ -47,11 +46,7 @@ class PrepareCoolifyTask call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish, call_event_data: $this->remoteProcessArgs->call_event_data, ); - if ($this->remoteProcessArgs->type === ActivityTypes::COMMAND->value) { - dispatch($job)->onQueue('high'); - } else { - dispatch($job); - } + dispatch($job); $this->activity->refresh(); return $this->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/StartDatabase.php b/app/Actions/Database/StartDatabase.php index 869a88521..e2fa6fc87 100644 --- a/app/Actions/Database/StartDatabase.php +++ b/app/Actions/Database/StartDatabase.php @@ -16,6 +16,8 @@ class StartDatabase { use AsAction; + public string $jobQueue = 'high'; + public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database) { $server = $database->destination->server; @@ -49,7 +51,7 @@ class StartDatabase break; } if ($database->is_public && $database->public_port) { - StartDatabaseProxy::dispatch($database)->onQueue('high'); + StartDatabaseProxy::dispatch($database); } return $activity; diff --git a/app/Actions/Database/StartDatabaseProxy.php b/app/Actions/Database/StartDatabaseProxy.php index d7a3bc697..3ddf6c036 100644 --- a/app/Actions/Database/StartDatabaseProxy.php +++ b/app/Actions/Database/StartDatabaseProxy.php @@ -18,6 +18,8 @@ class StartDatabaseProxy { use AsAction; + public string $jobQueue = 'high'; + public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|ServiceDatabase $database) { $internalPort = null; 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/Database/StopDatabaseProxy.php b/app/Actions/Database/StopDatabaseProxy.php index 0a166d24a..9ee794351 100644 --- a/app/Actions/Database/StopDatabaseProxy.php +++ b/app/Actions/Database/StopDatabaseProxy.php @@ -18,6 +18,8 @@ class StopDatabaseProxy { use AsAction; + public string $jobQueue = 'high'; + public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|ServiceDatabase|StandaloneDragonfly|StandaloneClickhouse $database) { $server = data_get($database, 'destination.server'); diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php index a08056837..706356930 100644 --- a/app/Actions/Docker/GetContainersStatus.php +++ b/app/Actions/Docker/GetContainersStatus.php @@ -7,7 +7,6 @@ use App\Actions\Shared\ComplexStatusCheck; use App\Models\ApplicationPreview; use App\Models\Server; use App\Models\ServiceDatabase; -use App\Notifications\Container\ContainerRestarted; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Lorisleiva\Actions\Concerns\AsAction; @@ -16,6 +15,8 @@ class GetContainersStatus { use AsAction; + public string $jobQueue = 'high'; + public $applications; public ?Collection $containers; @@ -178,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/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php index 51303d87a..6c8dd5234 100644 --- a/app/Actions/Proxy/CheckProxy.php +++ b/app/Actions/Proxy/CheckProxy.php @@ -30,7 +30,7 @@ class CheckProxy if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) { return false; } - ['uptime' => $uptime, 'error' => $error] = $server->validateConnection(false); + ['uptime' => $uptime, 'error' => $error] = $server->validateConnection(); if (! $uptime) { throw new \Exception($error); } diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php index 1b8e6c9a6..0349ead89 100644 --- a/app/Actions/Server/CleanupDocker.php +++ b/app/Actions/Server/CleanupDocker.php @@ -9,6 +9,8 @@ class CleanupDocker { use AsAction; + public string $jobQueue = 'high'; + public function handle(Server $server) { $settings = instanceSettings(); diff --git a/app/Actions/Server/ServerCheck.php b/app/Actions/Server/ServerCheck.php index 1dae03fd9..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); @@ -130,10 +129,10 @@ class ServerCheck if ($foundLogDrainContainer) { $status = data_get($foundLogDrainContainer, 'State.Status'); if ($status !== 'running') { - StartLogDrain::dispatch($this->server)->onQueue('high'); + StartLogDrain::dispatch($this->server); } } else { - StartLogDrain::dispatch($this->server)->onQueue('high'); + StartLogDrain::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/Actions/Server/StartLogDrain.php b/app/Actions/Server/StartLogDrain.php index 1997b58d6..0d28a0099 100644 --- a/app/Actions/Server/StartLogDrain.php +++ b/app/Actions/Server/StartLogDrain.php @@ -9,6 +9,8 @@ class StartLogDrain { use AsAction; + public string $jobQueue = 'high'; + public function handle(Server $server) { if ($server->settings->is_logdrain_newrelic_enabled) { diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php index d57a4fe46..be9b4062c 100644 --- a/app/Actions/Server/UpdateCoolify.php +++ b/app/Actions/Server/UpdateCoolify.php @@ -29,9 +29,9 @@ class UpdateCoolify if (! $this->server) { return; } - CleanupDocker::dispatch($this->server)->onQueue('high'); + CleanupDocker::dispatch($this->server); $this->latestVersion = get_latest_version_of_coolify(); - $this->currentVersion = config('version'); + $this->currentVersion = config('constants.coolify.version'); if (! $manual_update) { if (! $settings->is_auto_update_enabled) { return; diff --git a/app/Actions/Server/ValidateServer.php b/app/Actions/Server/ValidateServer.php index d0a4cd6be..55b37a77c 100644 --- a/app/Actions/Server/ValidateServer.php +++ b/app/Actions/Server/ValidateServer.php @@ -9,6 +9,8 @@ class ValidateServer { use AsAction; + public string $jobQueue = 'high'; + public ?string $uptime = null; public ?string $error = null; diff --git a/app/Actions/Service/RestartService.php b/app/Actions/Service/RestartService.php index 1b6a5c32c..4151ea947 100644 --- a/app/Actions/Service/RestartService.php +++ b/app/Actions/Service/RestartService.php @@ -9,6 +9,8 @@ class RestartService { use AsAction; + public string $jobQueue = 'high'; + public function handle(Service $service) { StopService::run($service); diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php index 82de066d7..1dfaf6c49 100644 --- a/app/Actions/Service/StartService.php +++ b/app/Actions/Service/StartService.php @@ -10,6 +10,8 @@ class StartService { use AsAction; + public string $jobQueue = 'high'; + public function handle(Service $service) { $service->saveComposeConfigs(); diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php index 046d94ced..95b08b437 100644 --- a/app/Actions/Service/StopService.php +++ b/app/Actions/Service/StopService.php @@ -10,6 +10,8 @@ class StopService { use AsAction; + public string $jobQueue = 'high'; + public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true) { try { 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/Console/Commands/Emails.php b/app/Console/Commands/Emails.php index cda4ca84f..f0e0e7fa0 100644 --- a/app/Console/Commands/Emails.php +++ b/app/Console/Commands/Emails.php @@ -187,7 +187,7 @@ class Emails extends Command 'team_id' => 0, ]); } - $this->mail = (new BackupSuccess($backup, $db))->toMail(); + // $this->mail = (new BackupSuccess($backup->frequency, $db->name))->toMail(); $this->sendEmail(); break; // case 'invitation-link': diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index 57bbe896b..216262819 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -200,7 +200,7 @@ class Init extends Command private function restore_coolify_db_backup() { - if (version_compare('4.0.0-beta.179', config('version'), '<=')) { + if (version_compare('4.0.0-beta.179', config('constants.coolify.version'), '<=')) { try { $database = StandalonePostgresql::withTrashed()->find(0); if ($database && $database->trashed()) { @@ -228,7 +228,7 @@ class Init extends Command private function send_alive_signal() { $id = config('app.id'); - $version = config('version'); + $version = config('constants.coolify.version'); $settings = instanceSettings(); $do_not_track = data_get($settings, 'do_not_track'); if ($do_not_track == true) { @@ -264,7 +264,7 @@ class Init extends Command private function replace_slash_in_environment_name() { - if (version_compare('4.0.0-beta.298', config('version'), '<=')) { + if (version_compare('4.0.0-beta.298', config('constants.coolify.version'), '<=')) { $environments = Environment::all(); foreach ($environments as $environment) { if (str_contains($environment->name, '/')) { diff --git a/app/Console/Commands/ServicesDelete.php b/app/Console/Commands/ServicesDelete.php index 1e5d5808c..b5a74166a 100644 --- a/app/Console/Commands/ServicesDelete.php +++ b/app/Console/Commands/ServicesDelete.php @@ -96,7 +96,7 @@ class ServicesDelete extends Command if (! $confirmed) { break; } - DeleteResourceJob::dispatch($toDelete)->onQueue('high'); + DeleteResourceJob::dispatch($toDelete); } } } @@ -122,7 +122,7 @@ class ServicesDelete extends Command if (! $confirmed) { return; } - DeleteResourceJob::dispatch($toDelete)->onQueue('high'); + DeleteResourceJob::dispatch($toDelete); } } } @@ -148,7 +148,7 @@ class ServicesDelete extends Command if (! $confirmed) { return; } - DeleteResourceJob::dispatch($toDelete)->onQueue('high'); + DeleteResourceJob::dispatch($toDelete); } } } diff --git a/app/Console/Commands/ServicesGenerate.php b/app/Console/Commands/ServicesGenerate.php index 1559e5f6d..b45707c5c 100644 --- a/app/Console/Commands/ServicesGenerate.php +++ b/app/Console/Commands/ServicesGenerate.php @@ -20,7 +20,10 @@ class ServicesGenerate extends Command public function handle(): int { - $serviceTemplatesJson = collect(glob(base_path('templates/compose/*.yaml'))) + $serviceTemplatesJson = collect(array_merge( + glob(base_path('templates/compose/*.yaml')), + glob(base_path('templates/compose/*.yml')) + )) ->mapWithKeys(function ($file): array { $file = basename($file); $parsed = $this->processFile($file); @@ -68,7 +71,7 @@ class ServicesGenerate extends Command 'slogan' => $data->get('slogan', str($file)->headline()), 'compose' => $compose, 'tags' => $tags, - 'logo' => $data->get('logo', 'svgs/coolify.png'), + 'logo' => $data->get('logo', 'svgs/default.webp'), 'minversion' => $data->get('minversion', '0.0.0'), ]; diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index e113dbe9a..19d22ae21 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -50,7 +50,7 @@ class Kernel extends ConsoleKernel $this->instanceTimezone = config('app.timezone'); } - $this->scheduleInstance->job(new CleanupStaleMultiplexedConnections)->hourly(); + // $this->scheduleInstance->job(new CleanupStaleMultiplexedConnections)->hourly(); if (isDev()) { // Instance Jobs @@ -132,7 +132,7 @@ class Kernel extends ConsoleKernel } foreach ($servers as $server) { - $serverTimezone = $server->settings->server_timezone; + $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone); // Sentinel check $lastSentinelUpdate = $server->sentinel_updated_at; @@ -141,8 +141,12 @@ class Kernel extends ConsoleKernel if (validate_timezone($serverTimezone) === false) { $serverTimezone = config('app.timezone'); } - $this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyMinute()->onOneServer(); - // $this->scheduleInstance->job(new \App\Jobs\ServerCheckNewJob($server))->everyMinute()->onOneServer(); + if (isCloud()) { + $this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyFiveMinutes()->onOneServer(); + } else { + $this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyMinute()->onOneServer(); + } + // $this->scheduleInstance->job(new \App\Jobs\ServerCheckNewJob($server))->everyFiveMinutes()->onOneServer(); // Check storage usage every 10 minutes if Sentinel does not activated $this->scheduleInstance->job(new ServerStorageCheckJob($server))->everyTenMinutes()->onOneServer(); @@ -154,7 +158,7 @@ class Kernel extends ConsoleKernel } // Cleanup multiplexed connections every hour - $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer(); + // $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer(); // Temporary solution until we have better memory management for Sentinel if ($server->isSentinelEnabled()) { diff --git a/app/Events/DatabaseProxyStopped.php b/app/Events/DatabaseProxyStopped.php index b457dc6a0..96b35a5ca 100644 --- a/app/Events/DatabaseProxyStopped.php +++ b/app/Events/DatabaseProxyStopped.php @@ -18,7 +18,7 @@ class DatabaseProxyStopped implements ShouldBroadcast public function __construct($teamId = null) { if (is_null($teamId)) { - $teamId = Auth::user()->currentTeam()->id ?? null; + $teamId = Auth::user()?->currentTeam()?->id ?? null; } if (is_null($teamId)) { throw new \Exception('Team id is null'); diff --git a/app/Helpers/SshMultiplexingHelper.php b/app/Helpers/SshMultiplexingHelper.php index 0e840c3ce..8da476b9e 100644 --- a/app/Helpers/SshMultiplexingHelper.php +++ b/app/Helpers/SshMultiplexingHelper.php @@ -21,17 +21,14 @@ class SshMultiplexingHelper ]; } - public static function ensureMultiplexedConnection(Server $server) + public static function ensureMultiplexedConnection(Server $server): bool { if (! self::isMultiplexingEnabled()) { - return; + return false; } $sshConfig = self::serverSshConfiguration($server); $muxSocket = $sshConfig['muxFilename']; - $sshKeyLocation = $sshConfig['sshKeyLocation']; - - self::validateSshKey($sshKeyLocation); $checkCommand = "ssh -O check -o ControlPath=$muxSocket "; if (data_get($server, 'settings.is_cloudflare_tunnel')) { @@ -41,16 +38,17 @@ class SshMultiplexingHelper $process = Process::run($checkCommand); if ($process->exitCode() !== 0) { - self::establishNewMultiplexedConnection($server); + return self::establishNewMultiplexedConnection($server); } + + return true; } - public static function establishNewMultiplexedConnection(Server $server) + public static function establishNewMultiplexedConnection(Server $server): bool { $sshConfig = self::serverSshConfiguration($server); $sshKeyLocation = $sshConfig['sshKeyLocation']; $muxSocket = $sshConfig['muxFilename']; - $connectionTimeout = config('constants.ssh.connection_timeout'); $serverInterval = config('constants.ssh.server_interval'); $muxPersistTime = config('constants.ssh.mux_persist_time'); @@ -60,15 +58,14 @@ class SshMultiplexingHelper if (data_get($server, 'settings.is_cloudflare_tunnel')) { $establishCommand .= ' -o ProxyCommand="cloudflared access ssh --hostname %h" '; } - $establishCommand .= self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval); $establishCommand .= "{$server->user}@{$server->ip}"; - $establishProcess = Process::run($establishCommand); - if ($establishProcess->exitCode() !== 0) { - throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput()); + return false; } + + return true; } public static function removeMuxFile(Server $server) @@ -97,9 +94,8 @@ class SshMultiplexingHelper if ($server->isIpv6()) { $scp_command .= '-6 '; } - if (self::isMultiplexingEnabled()) { + if (self::isMultiplexingEnabled() && self::ensureMultiplexedConnection($server)) { $scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; - self::ensureMultiplexedConnection($server); } if (data_get($server, 'settings.is_cloudflare_tunnel')) { @@ -120,6 +116,9 @@ class SshMultiplexingHelper $sshConfig = self::serverSshConfiguration($server); $sshKeyLocation = $sshConfig['sshKeyLocation']; + + self::validateSshKey($server->privateKey); + $muxSocket = $sshConfig['muxFilename']; $timeout = config('constants.ssh.command_timeout'); @@ -127,9 +126,8 @@ class SshMultiplexingHelper $ssh_command = "timeout $timeout ssh "; - if (self::isMultiplexingEnabled()) { + if (self::isMultiplexingEnabled() && self::ensureMultiplexedConnection($server)) { $ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; - self::ensureMultiplexedConnection($server); } if (data_get($server, 'settings.is_cloudflare_tunnel')) { @@ -154,13 +152,14 @@ class SshMultiplexingHelper return config('constants.ssh.mux_enabled') && ! config('constants.coolify.is_windows_docker_desktop'); } - private static function validateSshKey(string $sshKeyLocation): void + private static function validateSshKey(PrivateKey $privateKey): void { - $checkKeyCommand = "ls $sshKeyLocation 2>/dev/null"; + $keyLocation = $privateKey->getKeyLocation(); + $checkKeyCommand = "ls $keyLocation 2>/dev/null"; $keyCheckProcess = Process::run($checkKeyCommand); if ($keyCheckProcess->exitCode() !== 0) { - throw new \RuntimeException("SSH key file not accessible: $sshKeyLocation"); + $privateKey->storeInFileSystem(); } } diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 500db3922..614208c78 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -1224,7 +1224,7 @@ class ApplicationsController extends Controller $service->name = "service-$service->uuid"; $service->parse(isNew: true); if ($instantDeploy) { - StartService::dispatch($service)->onQueue('high'); + StartService::dispatch($service); } return response()->json(serializeApiResponse([ @@ -1379,7 +1379,7 @@ class ApplicationsController extends Controller deleteVolumes: $request->query->get('delete_volumes', true), dockerCleanup: $request->query->get('docker_cleanup', true), deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) - )->onQueue('high'); + ); return response()->json([ 'message' => 'Application deletion request queued.', @@ -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(); @@ -2523,7 +2539,7 @@ class ApplicationsController extends Controller if (! $application) { return response()->json(['message' => 'Application not found.'], 404); } - StopApplication::dispatch($application)->onQueue('high'); + StopApplication::dispatch($application); return response()->json( [ @@ -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 eaa542a83..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', @@ -497,9 +499,9 @@ class DatabasesController extends Controller $database->update($request->all()); if ($whatToDoWithDatabaseProxy === 'start') { - StartDatabaseProxy::dispatch($database)->onQueue('high'); + StartDatabaseProxy::dispatch($database); } elseif ($whatToDoWithDatabaseProxy === 'stop') { - StopDatabaseProxy::dispatch($database)->onQueue('high'); + StopDatabaseProxy::dispatch($database); } return response()->json([ @@ -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)) { @@ -1151,7 +1154,7 @@ class DatabasesController extends Controller } $database = create_standalone_postgresql($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database)->onQueue('high'); + StartDatabase::dispatch($database); } $database->refresh(); $payload = [ @@ -1206,7 +1209,7 @@ class DatabasesController extends Controller } $database = create_standalone_mariadb($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database)->onQueue('high'); + StartDatabase::dispatch($database); } $database->refresh(); @@ -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', @@ -1264,7 +1268,7 @@ class DatabasesController extends Controller } $database = create_standalone_mysql($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database)->onQueue('high'); + StartDatabase::dispatch($database); } $database->refresh(); @@ -1320,7 +1324,7 @@ class DatabasesController extends Controller } $database = create_standalone_redis($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database)->onQueue('high'); + StartDatabase::dispatch($database); } $database->refresh(); @@ -1357,7 +1361,7 @@ class DatabasesController extends Controller removeUnnecessaryFieldsFromRequest($request); $database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database)->onQueue('high'); + StartDatabase::dispatch($database); } return response()->json(serializeApiResponse([ @@ -1406,7 +1410,7 @@ class DatabasesController extends Controller } $database = create_standalone_keydb($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database)->onQueue('high'); + StartDatabase::dispatch($database); } $database->refresh(); @@ -1442,7 +1446,7 @@ class DatabasesController extends Controller removeUnnecessaryFieldsFromRequest($request); $database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database)->onQueue('high'); + StartDatabase::dispatch($database); } $database->refresh(); @@ -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)) { @@ -1500,7 +1504,7 @@ class DatabasesController extends Controller } $database = create_standalone_mongodb($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database)->onQueue('high'); + StartDatabase::dispatch($database); } $database->refresh(); @@ -1557,7 +1561,8 @@ class DatabasesController extends Controller ] ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -1593,7 +1598,7 @@ class DatabasesController extends Controller deleteVolumes: $request->query->get('delete_volumes', true), dockerCleanup: $request->query->get('docker_cleanup', true), deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) - )->onQueue('high'); + ); return response()->json([ 'message' => 'Database deletion request queued.', @@ -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', @@ -1666,7 +1673,7 @@ class DatabasesController extends Controller if (str($database->status)->contains('running')) { return response()->json(['message' => 'Database is already running.'], 400); } - StartDatabase::dispatch($database)->onQueue('high'); + StartDatabase::dispatch($database); return response()->json( [ @@ -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', @@ -1742,7 +1751,7 @@ class DatabasesController extends Controller if (str($database->status)->contains('stopped') || str($database->status)->contains('exited')) { return response()->json(['message' => 'Database is already stopped.'], 400); } - StopDatabase::dispatch($database)->onQueue('high'); + StopDatabase::dispatch($database); return response()->json( [ @@ -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', @@ -1815,7 +1826,7 @@ class DatabasesController extends Controller if (! $database) { return response()->json(['message' => 'Database not found.'], 404); } - RestartDatabase::dispatch($database)->onQueue('high'); + RestartDatabase::dispatch($database); return response()->json( [ diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php index 59b199d87..666dc55a5 100644 --- a/app/Http/Controllers/Api/DeployController.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -307,7 +307,7 @@ class DeployController extends Controller break; default: // Database resource - StartDatabase::dispatch($resource)->onQueue('high'); + StartDatabase::dispatch($resource); $resource->update([ 'started_at' => now(), ]); diff --git a/app/Http/Controllers/Api/OtherController.php b/app/Http/Controllers/Api/OtherController.php index b35c72116..303e6535d 100644 --- a/app/Http/Controllers/Api/OtherController.php +++ b/app/Http/Controllers/Api/OtherController.php @@ -37,7 +37,7 @@ class OtherController extends Controller )] public function version(Request $request) { - return response(config('version')); + return response(config('constants.coolify.version')); } #[OA\Get( diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index cbee00642..8c13b1a01 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -550,7 +550,7 @@ class ServersController extends Controller 'is_build_server' => $request->is_build_server, ]); if ($request->instant_validate) { - ValidateServer::dispatch($server)->onQueue('high'); + ValidateServer::dispatch($server); } return response()->json([ @@ -567,6 +567,9 @@ class ServersController extends Controller ['bearerAuth' => []], ], tags: ['Servers'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server UUID', schema: new OA\Schema(type: 'string')), + ], requestBody: new OA\RequestBody( required: true, description: 'Server updated.', @@ -596,8 +599,7 @@ class ServersController extends Controller new OA\MediaType( mediaType: 'application/json', schema: new OA\Schema( - type: 'array', - items: new OA\Items(ref: '#/components/schemas/Server') + ref: '#/components/schemas/Server' ) ), ]), @@ -675,11 +677,11 @@ class ServersController extends Controller ]); } if ($request->instant_validate) { - ValidateServer::dispatch($server)->onQueue('high'); + ValidateServer::dispatch($server); } return response()->json([ - + 'uuid' => $server->uuid, ])->setStatusCode(201); } @@ -813,7 +815,7 @@ class ServersController extends Controller if (! $server) { return response()->json(['message' => 'Server not found.'], 404); } - ValidateServer::dispatch($server)->onQueue('high'); + ValidateServer::dispatch($server); return response()->json(['message' => 'Validation started.']); } diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index bdb5612ad..bf90322e2 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -342,7 +342,7 @@ class ServicesController extends Controller } $service->parse(isNew: true); if ($instantDeploy) { - StartService::dispatch($service)->onQueue('high'); + StartService::dispatch($service); } $domains = $service->applications()->get()->pluck('fqdn')->sort(); $domains = $domains->map(function ($domain) { @@ -487,7 +487,7 @@ class ServicesController extends Controller deleteVolumes: $request->query->get('delete_volumes', true), dockerCleanup: $request->query->get('docker_cleanup', true), deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) - )->onQueue('high'); + ); return response()->json([ 'message' => 'Service deletion request queued.', @@ -1076,7 +1076,7 @@ class ServicesController extends Controller if (str($service->status())->contains('running')) { return response()->json(['message' => 'Service is already running.'], 400); } - StartService::dispatch($service)->onQueue('high'); + StartService::dispatch($service); return response()->json( [ @@ -1154,7 +1154,7 @@ class ServicesController extends Controller if (str($service->status())->contains('stopped') || str($service->status())->contains('exited')) { return response()->json(['message' => 'Service is already stopped.'], 400); } - StopService::dispatch($service)->onQueue('high'); + StopService::dispatch($service); return response()->json( [ @@ -1229,7 +1229,7 @@ class ServicesController extends Controller if (! $service) { return response()->json(['message' => 'Service not found.'], 404); } - RestartService::dispatch($service)->onQueue('high'); + RestartService::dispatch($service); return response()->json( [ 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/Http/Controllers/Webhook/Stripe.php b/app/Http/Controllers/Webhook/Stripe.php index e94209b23..83ba16699 100644 --- a/app/Http/Controllers/Webhook/Stripe.php +++ b/app/Http/Controllers/Webhook/Stripe.php @@ -3,21 +3,26 @@ namespace App\Http\Controllers\Webhook; use App\Http\Controllers\Controller; -use App\Jobs\ServerLimitCheckJob; -use App\Jobs\SubscriptionInvoiceFailedJob; -use App\Models\Subscription; -use App\Models\Team; +use App\Jobs\StripeProcessJob; use App\Models\Webhook; use Exception; use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; -use Illuminate\Support\Str; class Stripe extends Controller { + protected $webhook; + public function events(Request $request) { try { + $webhookSecret = config('subscription.stripe_webhook_secret'); + $signature = $request->header('Stripe-Signature'); + $event = \Stripe\Webhook::constructEvent( + $request->getContent(), + $signature, + $webhookSecret + ); if (app()->isDownForMaintenance()) { $epoch = now()->valueOf(); $data = [ @@ -33,241 +38,17 @@ class Stripe extends Controller $json = json_encode($data); Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Stripe::events_stripe", $json); - return; + return response('Webhook received. Cool cool cool cool cool.', 200); } - $webhookSecret = config('subscription.stripe_webhook_secret'); - $signature = $request->header('Stripe-Signature'); - $excludedPlans = config('subscription.stripe_excluded_plans'); - $event = \Stripe\Webhook::constructEvent( - $request->getContent(), - $signature, - $webhookSecret - ); - $webhook = Webhook::create([ + $this->webhook = Webhook::create([ 'type' => 'stripe', 'payload' => $request->getContent(), ]); - $type = data_get($event, 'type'); - $data = data_get($event, 'data.object'); - switch ($type) { - case 'radar.early_fraud_warning.created': - $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); - $id = data_get($data, 'id'); - $charge = data_get($data, 'charge'); - if ($charge) { - $stripe->refunds->create(['charge' => $charge]); - } - $pi = data_get($data, 'payment_intent'); - $piData = $stripe->paymentIntents->retrieve($pi, []); - $customerId = data_get($piData, 'customer'); - $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); - if ($subscription) { - $subscriptionId = data_get($subscription, 'stripe_subscription_id'); - $stripe->subscriptions->cancel($subscriptionId, []); - $subscription->update([ - 'stripe_invoice_paid' => false, - ]); - send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}"); - } else { - send_internal_notification("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}"); + StripeProcessJob::dispatch($event); - return response("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}", 400); - } - break; - case 'checkout.session.completed': - $clientReferenceId = data_get($data, 'client_reference_id'); - if (is_null($clientReferenceId)) { - send_internal_notification('Checkout session completed without client reference id.'); - break; - } - $userId = Str::before($clientReferenceId, ':'); - $teamId = Str::after($clientReferenceId, ':'); - $subscriptionId = data_get($data, 'subscription'); - $customerId = data_get($data, 'customer'); - $team = Team::find($teamId); - $found = $team->members->where('id', $userId)->first(); - if (! $found->isAdmin()) { - send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}."); - - return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.", 400); - } - $subscription = Subscription::where('team_id', $teamId)->first(); - if ($subscription) { - // send_internal_notification('Old subscription activated for team: '.$teamId); - $subscription->update([ - 'stripe_subscription_id' => $subscriptionId, - 'stripe_customer_id' => $customerId, - 'stripe_invoice_paid' => true, - ]); - } else { - // send_internal_notification('New subscription for team: '.$teamId); - Subscription::create([ - 'team_id' => $teamId, - 'stripe_subscription_id' => $subscriptionId, - 'stripe_customer_id' => $customerId, - 'stripe_invoice_paid' => true, - ]); - } - break; - case 'invoice.paid': - $customerId = data_get($data, 'customer'); - $planId = data_get($data, 'lines.data.0.plan.id'); - if (Str::contains($excludedPlans, $planId)) { - // send_internal_notification('Subscription excluded.'); - break; - } - $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); - if ($subscription) { - $subscription->update([ - 'stripe_invoice_paid' => true, - ]); - } else { - return response("No subscription found for customer: {$customerId}", 400); - } - break; - case 'invoice.payment_failed': - $customerId = data_get($data, 'customer'); - $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); - if (! $subscription) { - // send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId); - - return response('No subscription found in Coolify.'); - } - $team = data_get($subscription, 'team'); - if (! $team) { - // send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId); - - return response('No team found in Coolify.'); - } - if (! $subscription->stripe_invoice_paid) { - SubscriptionInvoiceFailedJob::dispatch($team); - // send_internal_notification('Invoice payment failed: '.$customerId); - } else { - // send_internal_notification('Invoice payment failed but already paid: '.$customerId); - } - break; - case 'payment_intent.payment_failed': - $customerId = data_get($data, 'customer'); - $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); - if (! $subscription) { - // send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId); - - return response('No subscription found in Coolify.'); - } - if ($subscription->stripe_invoice_paid) { - // send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId); - - return; - } - send_internal_notification('Subscription payment failed for customer: '.$customerId); - break; - case 'customer.subscription.created': - $customerId = data_get($data, 'customer'); - $subscriptionId = data_get($data, 'id'); - $teamId = data_get($data, 'metadata.team_id'); - $userId = data_get($data, 'metadata.user_id'); - if (! $teamId || ! $userId) { - $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); - if ($subscription) { - return response("Subscription already exists for customer: {$customerId}", 200); - } - - return response('No team id or user id found', 400); - } - $team = Team::find($teamId); - $found = $team->members->where('id', $userId)->first(); - if (! $found->isAdmin()) { - send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}."); - - return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.", 400); - } - $subscription = Subscription::where('team_id', $teamId)->first(); - if ($subscription) { - return response("Subscription already exists for team: {$teamId}", 200); - } else { - Subscription::create([ - 'team_id' => $teamId, - 'stripe_subscription_id' => $subscriptionId, - 'stripe_customer_id' => $customerId, - 'stripe_invoice_paid' => false, - ]); - - return response('Subscription created'); - } - case 'customer.subscription.updated': - $teamId = data_get($data, 'metadata.team_id'); - $userId = data_get($data, 'metadata.user_id'); - $customerId = data_get($data, 'customer'); - $status = data_get($data, 'status'); - $subscriptionId = data_get($data, 'items.data.0.subscription'); - $planId = data_get($data, 'items.data.0.plan.id'); - if (Str::contains($excludedPlans, $planId)) { - // send_internal_notification('Subscription excluded.'); - break; - } - $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); - if (! $subscription) { - if ($status === 'incomplete_expired') { - return response('Subscription incomplete expired', 200); - } - if ($teamId) { - $subscription = Subscription::create([ - 'team_id' => $teamId, - 'stripe_subscription_id' => $subscriptionId, - 'stripe_customer_id' => $customerId, - 'stripe_invoice_paid' => false, - ]); - } else { - return response('No subscription and team id found', 400); - } - } - $cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end'); - $feedback = data_get($data, 'cancellation_details.feedback'); - $comment = data_get($data, 'cancellation_details.comment'); - $lookup_key = data_get($data, 'items.data.0.price.lookup_key'); - if (str($lookup_key)->contains('dynamic')) { - $quantity = data_get($data, 'items.data.0.quantity', 2); - $team = data_get($subscription, 'team'); - if ($team) { - $team->update([ - 'custom_server_limit' => $quantity, - ]); - } - ServerLimitCheckJob::dispatch($team); - } - $subscription->update([ - 'stripe_feedback' => $feedback, - 'stripe_comment' => $comment, - 'stripe_plan_id' => $planId, - 'stripe_cancel_at_period_end' => $cancelAtPeriodEnd, - ]); - if ($status === 'paused' || $status === 'incomplete_expired') { - $subscription->update([ - 'stripe_invoice_paid' => false, - ]); - } - if ($feedback) { - $reason = "Cancellation feedback for {$customerId}: '".$feedback."'"; - if ($comment) { - $reason .= ' with comment: \''.$comment."'"; - } - } - break; - case 'customer.subscription.deleted': - // End subscription - $customerId = data_get($data, 'customer'); - $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); - $team = data_get($subscription, 'team'); - $team?->subscriptionEnded(); - break; - default: - // Unhandled event type - } + return response('Webhook received. Cool cool cool cool cool.', 200); } catch (Exception $e) { - if ($type !== 'payment_intent.payment_failed') { - send_internal_notification("Subscription webhook ($type) failed: ".$e->getMessage()); - } - $webhook->update([ + $this->webhook->update([ 'status' => 'failed', 'failure_reason' => $e->getMessage(), ]); diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index cd89f55f3..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; @@ -166,6 +168,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue public function __construct(int $application_deployment_queue_id) { + $this->onQueue('high'); + $this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id); $this->application = Application::find($this->application_deployment_queue->application_id); $this->build_pack = data_get($this->application, 'build_pack'); @@ -176,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; @@ -349,8 +357,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private function post_deployment() { if ($this->server->isProxyShouldRun()) { - GetContainersStatus::dispatch($this->server)->onQueue('high'); - // dispatch(new ContainerStatusJob($this->server)); + GetContainersStatus::dispatch($this->server); } $this->next(ApplicationDeploymentStatus::FINISHED->value); if ($this->pull_request_id !== 0) { @@ -462,7 +469,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id')); $this->save_environment_variables(); if (! is_null($this->env_filename)) { - $services = collect($composeFile['services']); + $services = collect(data_get($composeFile, 'services', [])); $services = $services->map(function ($service, $name) { $service['env_file'] = [$this->env_filename]; @@ -1975,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 { @@ -2399,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/ApplicationPullRequestUpdateJob.php b/app/Jobs/ApplicationPullRequestUpdateJob.php index 2eefc4dd2..ef8e6efb6 100755 --- a/app/Jobs/ApplicationPullRequestUpdateJob.php +++ b/app/Jobs/ApplicationPullRequestUpdateJob.php @@ -25,7 +25,9 @@ class ApplicationPullRequestUpdateJob implements ShouldBeEncrypted, ShouldQueue public ApplicationPreview $preview, public ProcessStatus $status, public ?string $deployment_uuid = null - ) {} + ) { + $this->onQueue('high'); + } public function handle() { diff --git a/app/Jobs/CheckForUpdatesJob.php b/app/Jobs/CheckForUpdatesJob.php index f2348118a..1d3a345e1 100644 --- a/app/Jobs/CheckForUpdatesJob.php +++ b/app/Jobs/CheckForUpdatesJob.php @@ -27,7 +27,7 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue $versions = $response->json(); $latest_version = data_get($versions, 'coolify.v4.version'); - $current_version = config('version'); + $current_version = config('constants.coolify.version'); if (version_compare($latest_version, $current_version, '>')) { // New version available diff --git a/app/Jobs/CoolifyTask.php b/app/Jobs/CoolifyTask.php index c3692c30b..49a5ba8dd 100755 --- a/app/Jobs/CoolifyTask.php +++ b/app/Jobs/CoolifyTask.php @@ -23,7 +23,10 @@ class CoolifyTask implements ShouldBeEncrypted, ShouldQueue public bool $ignore_errors, public $call_event_on_finish, public $call_event_data, - ) {} + ) { + + $this->onQueue('high'); + } /** * Execute the job. diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 94f185882..ee702202f 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -60,12 +60,15 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue public function __construct($backup) { + $this->onQueue('high'); $this->backup = $backup; } public function handle(): void { try { + $databasesToBackup = null; + $this->team = Team::find($this->backup->team_id); if (! $this->team) { $this->backup->delete(); @@ -197,8 +200,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue $databaseType = $this->database->type(); $databasesToBackup = data_get($this->backup, 'databases_to_backup'); } - - if (is_null($databasesToBackup)) { + if (blank($databasesToBackup)) { if (str($databaseType)->contains('postgres')) { $databasesToBackup = [$this->database->postgres_db]; } elseif (str($databaseType)->contains('mongodb')) { @@ -304,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, @@ -319,12 +321,10 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue 'filename' => null, ]); } - send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage()); $this->team?->notify(new BackupFailed($this->backup, $this->database, $this->backup_output, $database)); } } } catch (\Throwable $e) { - send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage()); throw $e; } finally { if ($this->team) { diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index 2442d5b06..8b9228e5f 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -35,7 +35,9 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue public bool $deleteVolumes = true, public bool $dockerCleanup = true, public bool $deleteConnectedNetworks = true - ) {} + ) { + $this->onQueue('high'); + } public function handle() { @@ -87,7 +89,6 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue $this->resource?->delete_connected_networks($this->resource->uuid); } } catch (\Throwable $e) { - send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage()); throw $e; } finally { $this->resource->forceDelete(); diff --git a/app/Jobs/PullHelperImageJob.php b/app/Jobs/PullHelperImageJob.php index cfc0c5a94..b92886d38 100644 --- a/app/Jobs/PullHelperImageJob.php +++ b/app/Jobs/PullHelperImageJob.php @@ -16,7 +16,10 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue public $timeout = 1000; - public function __construct(public Server $server) {} + public function __construct(public Server $server) + { + $this->onQueue('high'); + } public function handle(): void { diff --git a/app/Jobs/PullTemplatesFromCDN.php b/app/Jobs/PullTemplatesFromCDN.php index bde5e6c7a..45c536e06 100644 --- a/app/Jobs/PullTemplatesFromCDN.php +++ b/app/Jobs/PullTemplatesFromCDN.php @@ -17,7 +17,10 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue public $timeout = 10; - public function __construct() {} + public function __construct() + { + $this->onQueue('high'); + } public function handle(): void { diff --git a/app/Jobs/PushServerUpdateJob.php b/app/Jobs/PushServerUpdateJob.php index 9822ca071..24f8d1e6b 100644 --- a/app/Jobs/PushServerUpdateJob.php +++ b/app/Jobs/PushServerUpdateJob.php @@ -360,7 +360,7 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue private function checkLogDrainContainer() { if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) { - StartLogDrain::dispatch($this->server)->onQueue('high'); + StartLogDrain::dispatch($this->server); } } } diff --git a/app/Jobs/ScheduledTaskJob.php b/app/Jobs/ScheduledTaskJob.php index 7bfc29af3..00575e187 100644 --- a/app/Jobs/ScheduledTaskJob.php +++ b/app/Jobs/ScheduledTaskJob.php @@ -40,6 +40,8 @@ class ScheduledTaskJob implements ShouldQueue public function __construct($task) { + $this->onQueue('high'); + $this->task = $task; if ($service = $task->service()->first()) { $this->resource = $service; diff --git a/app/Jobs/SendMessageToDiscordJob.php b/app/Jobs/SendMessageToDiscordJob.php index 5b406f50f..99aeaeea2 100644 --- a/app/Jobs/SendMessageToDiscordJob.php +++ b/app/Jobs/SendMessageToDiscordJob.php @@ -32,7 +32,9 @@ class SendMessageToDiscordJob implements ShouldBeEncrypted, ShouldQueue public function __construct( public DiscordMessage $message, public string $webhookUrl - ) {} + ) { + $this->onQueue('high'); + } /** * Execute the job. diff --git a/app/Jobs/SendMessageToTelegramJob.php b/app/Jobs/SendMessageToTelegramJob.php index bf52b782f..85f4fc934 100644 --- a/app/Jobs/SendMessageToTelegramJob.php +++ b/app/Jobs/SendMessageToTelegramJob.php @@ -33,7 +33,9 @@ class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue public string $token, public string $chatId, public ?string $topicId = null, - ) {} + ) { + $this->onQueue('high'); + } /** * Execute the job. @@ -70,7 +72,7 @@ class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue } $response = Http::post($url, $payload); if ($response->failed()) { - throw new \Exception('Telegram notification failed with '.$response->status().' status code.'.$response->body()); + throw new \RuntimeException('Telegram notification failed with '.$response->status().' status code.'.$response->body()); } } } diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php index 0d5e2fd36..49d8dfe08 100644 --- a/app/Jobs/ServerCheckJob.php +++ b/app/Jobs/ServerCheckJob.php @@ -31,7 +31,12 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue return [(new WithoutOverlapping($this->server->uuid))->dontRelease()]; } - public function __construct(public Server $server) {} + public function __construct(public Server $server) + { + if (isDev()) { + $this->handle(); + } + } public function handle() { @@ -94,10 +99,10 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue if ($foundLogDrainContainer) { $status = data_get($foundLogDrainContainer, 'State.Status'); if ($status !== 'running') { - StartLogDrain::dispatch($this->server)->onQueue('high'); + StartLogDrain::dispatch($this->server); } } else { - StartLogDrain::dispatch($this->server)->onQueue('high'); + StartLogDrain::dispatch($this->server); } } } diff --git a/app/Jobs/ServerFilesFromServerJob.php b/app/Jobs/ServerFilesFromServerJob.php index 769dfc004..58455df2f 100644 --- a/app/Jobs/ServerFilesFromServerJob.php +++ b/app/Jobs/ServerFilesFromServerJob.php @@ -16,7 +16,10 @@ class ServerFilesFromServerJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(public ServiceApplication|ServiceDatabase|Application $resource) {} + public function __construct(public ServiceApplication|ServiceDatabase|Application $resource) + { + $this->onQueue('high'); + } public function handle() { diff --git a/app/Jobs/ServerStorageSaveJob.php b/app/Jobs/ServerStorageSaveJob.php index 526cd5375..17a293f94 100644 --- a/app/Jobs/ServerStorageSaveJob.php +++ b/app/Jobs/ServerStorageSaveJob.php @@ -14,7 +14,10 @@ class ServerStorageSaveJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(public LocalFileVolume $localFileVolume) {} + public function __construct(public LocalFileVolume $localFileVolume) + { + $this->onQueue('high'); + } public function handle() { diff --git a/app/Jobs/StripeProcessJob.php b/app/Jobs/StripeProcessJob.php new file mode 100644 index 000000000..00c9b6d18 --- /dev/null +++ b/app/Jobs/StripeProcessJob.php @@ -0,0 +1,246 @@ +onQueue('high'); + } + + public function handle(): void + { + try { + $excludedPlans = config('subscription.stripe_excluded_plans'); + + $type = data_get($this->event, 'type'); + $this->type = $type; + $data = data_get($this->event, 'data.object'); + switch ($type) { + case 'radar.early_fraud_warning.created': + $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); + $id = data_get($data, 'id'); + $charge = data_get($data, 'charge'); + if ($charge) { + $stripe->refunds->create(['charge' => $charge]); + } + $pi = data_get($data, 'payment_intent'); + $piData = $stripe->paymentIntents->retrieve($pi, []); + $customerId = data_get($piData, 'customer'); + $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); + if ($subscription) { + $subscriptionId = data_get($subscription, 'stripe_subscription_id'); + $stripe->subscriptions->cancel($subscriptionId, []); + $subscription->update([ + 'stripe_invoice_paid' => false, + ]); + send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}"); + } else { + send_internal_notification("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}"); + throw new \RuntimeException("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}"); + } + break; + case 'checkout.session.completed': + $clientReferenceId = data_get($data, 'client_reference_id'); + if (is_null($clientReferenceId)) { + send_internal_notification('Checkout session completed without client reference id.'); + break; + } + $userId = Str::before($clientReferenceId, ':'); + $teamId = Str::after($clientReferenceId, ':'); + $subscriptionId = data_get($data, 'subscription'); + $customerId = data_get($data, 'customer'); + $team = Team::find($teamId); + $found = $team->members->where('id', $userId)->first(); + if (! $found->isAdmin()) { + send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}."); + throw new \RuntimeException("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}."); + } + $subscription = Subscription::where('team_id', $teamId)->first(); + if ($subscription) { + send_internal_notification('Old subscription activated for team: '.$teamId); + $subscription->update([ + 'stripe_subscription_id' => $subscriptionId, + 'stripe_customer_id' => $customerId, + 'stripe_invoice_paid' => true, + ]); + } else { + send_internal_notification('New subscription for team: '.$teamId); + Subscription::create([ + 'team_id' => $teamId, + 'stripe_subscription_id' => $subscriptionId, + 'stripe_customer_id' => $customerId, + 'stripe_invoice_paid' => true, + ]); + } + break; + case 'invoice.paid': + $customerId = data_get($data, 'customer'); + $planId = data_get($data, 'lines.data.0.plan.id'); + if (Str::contains($excludedPlans, $planId)) { + send_internal_notification('Subscription excluded.'); + break; + } + $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); + if ($subscription) { + $subscription->update([ + 'stripe_invoice_paid' => true, + ]); + } else { + throw new \RuntimeException("No subscription found for customer: {$customerId}"); + } + break; + case 'invoice.payment_failed': + $customerId = data_get($data, 'customer'); + $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); + if (! $subscription) { + send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId); + throw new \RuntimeException("No subscription found for customer: {$customerId}"); + } + $team = data_get($subscription, 'team'); + if (! $team) { + send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId); + throw new \RuntimeException("No team found in Coolify for customer: {$customerId}"); + } + if (! $subscription->stripe_invoice_paid) { + SubscriptionInvoiceFailedJob::dispatch($team); + send_internal_notification('Invoice payment failed: '.$customerId); + } else { + send_internal_notification('Invoice payment failed but already paid: '.$customerId); + } + break; + case 'payment_intent.payment_failed': + $customerId = data_get($data, 'customer'); + $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); + if (! $subscription) { + send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId); + throw new \RuntimeException("No subscription found in Coolify for customer: {$customerId}"); + } + if ($subscription->stripe_invoice_paid) { + send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId); + + return; + } + send_internal_notification('Subscription payment failed for customer: '.$customerId); + break; + case 'customer.subscription.created': + $customerId = data_get($data, 'customer'); + $subscriptionId = data_get($data, 'id'); + $teamId = data_get($data, 'metadata.team_id'); + $userId = data_get($data, 'metadata.user_id'); + if (! $teamId || ! $userId) { + $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); + if ($subscription) { + throw new \RuntimeException("Subscription already exists for customer: {$customerId}"); + } + throw new \RuntimeException('No team id or user id found'); + } + $team = Team::find($teamId); + $found = $team->members->where('id', $userId)->first(); + if (! $found->isAdmin()) { + send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}."); + throw new \RuntimeException("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}."); + } + $subscription = Subscription::where('team_id', $teamId)->first(); + if ($subscription) { + send_internal_notification("Subscription already exists for team: {$teamId}"); + throw new \RuntimeException("Subscription already exists for team: {$teamId}"); + } else { + Subscription::create([ + 'team_id' => $teamId, + 'stripe_subscription_id' => $subscriptionId, + 'stripe_customer_id' => $customerId, + 'stripe_invoice_paid' => false, + ]); + } + case 'customer.subscription.updated': + $teamId = data_get($data, 'metadata.team_id'); + $userId = data_get($data, 'metadata.user_id'); + $customerId = data_get($data, 'customer'); + $status = data_get($data, 'status'); + $subscriptionId = data_get($data, 'items.data.0.subscription'); + $planId = data_get($data, 'items.data.0.plan.id'); + if (Str::contains($excludedPlans, $planId)) { + send_internal_notification('Subscription excluded.'); + break; + } + $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); + if (! $subscription) { + if ($status === 'incomplete_expired') { + send_internal_notification('Subscription incomplete expired'); + throw new \RuntimeException('Subscription incomplete expired'); + } + if ($teamId) { + $subscription = Subscription::create([ + 'team_id' => $teamId, + 'stripe_subscription_id' => $subscriptionId, + 'stripe_customer_id' => $customerId, + 'stripe_invoice_paid' => false, + ]); + } else { + send_internal_notification('No subscription and team id found'); + throw new \RuntimeException('No subscription and team id found'); + } + } + $cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end'); + $feedback = data_get($data, 'cancellation_details.feedback'); + $comment = data_get($data, 'cancellation_details.comment'); + $lookup_key = data_get($data, 'items.data.0.price.lookup_key'); + if (str($lookup_key)->contains('dynamic')) { + $quantity = data_get($data, 'items.data.0.quantity', 2); + $team = data_get($subscription, 'team'); + if ($team) { + $team->update([ + 'custom_server_limit' => $quantity, + ]); + } + ServerLimitCheckJob::dispatch($team); + } + $subscription->update([ + 'stripe_feedback' => $feedback, + 'stripe_comment' => $comment, + 'stripe_plan_id' => $planId, + 'stripe_cancel_at_period_end' => $cancelAtPeriodEnd, + ]); + if ($status === 'paused' || $status === 'incomplete_expired') { + $subscription->update([ + 'stripe_invoice_paid' => false, + ]); + } + if ($feedback) { + $reason = "Cancellation feedback for {$customerId}: '".$feedback."'"; + if ($comment) { + $reason .= ' with comment: \''.$comment."'"; + } + } + break; + case 'customer.subscription.deleted': + // End subscription + $customerId = data_get($data, 'customer'); + $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); + $team = data_get($subscription, 'team'); + $team?->subscriptionEnded(); + break; + default: + throw new \RuntimeException("Unhandled event type: {$type}"); + } + } catch (\Exception $e) { + send_internal_notification('StripeProcessJob error: '.$e->getMessage()); + } + } +} diff --git a/app/Jobs/SubscriptionInvoiceFailedJob.php b/app/Jobs/SubscriptionInvoiceFailedJob.php index aabeecef5..dc511f445 100755 --- a/app/Jobs/SubscriptionInvoiceFailedJob.php +++ b/app/Jobs/SubscriptionInvoiceFailedJob.php @@ -15,7 +15,10 @@ class SubscriptionInvoiceFailedJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(protected Team $team) {} + public function __construct(protected Team $team) + { + $this->onQueue('high'); + } public function handle() { diff --git a/app/Jobs/UpdateCoolifyJob.php b/app/Jobs/UpdateCoolifyJob.php index 1e5197b6f..f0e43cbc0 100644 --- a/app/Jobs/UpdateCoolifyJob.php +++ b/app/Jobs/UpdateCoolifyJob.php @@ -18,6 +18,11 @@ class UpdateCoolifyJob implements ShouldBeEncrypted, ShouldQueue public $timeout = 600; + public function __construct() + { + $this->onQueue('high'); + } + public function handle(): void { try { 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/Destination/New/Docker.php b/app/Livewire/Destination/New/Docker.php index f86f42e34..337f1d067 100644 --- a/app/Livewire/Destination/New/Docker.php +++ b/app/Livewire/Destination/New/Docker.php @@ -35,7 +35,7 @@ class Docker extends Component $this->network = new Cuid2; $this->servers = Server::isUsable()->get(); if ($server_id) { - $this->selectedServer = $this->servers->find($server_id); + $this->selectedServer = $this->servers->find($server_id) ?: $this->servers->first(); $this->serverId = $this->selectedServer->id; } else { $this->selectedServer = $this->servers->first(); diff --git a/app/Livewire/NavbarDeleteTeam.php b/app/Livewire/NavbarDeleteTeam.php index cc5d78f60..e97cceb0d 100644 --- a/app/Livewire/NavbarDeleteTeam.php +++ b/app/Livewire/NavbarDeleteTeam.php @@ -3,7 +3,6 @@ namespace App\Livewire; use App\Models\InstanceSettings; -use Illuminate\Container\Attributes\Auth as AttributesAuth; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; @@ -32,7 +31,7 @@ class NavbarDeleteTeam extends Component $currentTeam->delete(); $currentTeam->members->each(function ($user) use ($currentTeam) { - if ($user->id === AttributesAuth::id()) { + if ($user->id === Auth::id()) { return; } $user->teams()->detach($currentTeam); diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php index 56f07f3a9..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,6 +73,9 @@ class Email extends Component #[Validate(['nullable', 'string'])] public ?string $resendApiKey = null; + #[Validate(['nullable', 'email'])] + public ?string $testEmailAddress = null; + public function mount() { try { @@ -132,14 +135,21 @@ class Email extends Component } } - public function sendTestNotification() + public function sendTestEmail() { try { + $this->validate([ + 'testEmailAddress' => 'required|email', + ], [ + 'testEmailAddress.required' => 'Test email address is required.', + 'testEmailAddress.email' => 'Please enter a valid email address.', + ]); + $executed = RateLimiter::attempt( 'test-email:'.$this->team->id, $perMinute = 0, function () { - $this->team?->notify(new Test($this->emails)); + $this->team?->notify(new Test($this->testEmailAddress)); $this->dispatch('success', 'Test Email sent.'); }, $decaySeconds = 10, 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 d4ec8f581..5261a0800 100644 --- a/app/Livewire/Project/Application/Configuration.php +++ b/app/Livewire/Project/Application/Configuration.php @@ -16,24 +16,30 @@ class Configuration extends Component public function mount() { - $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); - if (! $project) { - return redirect()->route('dashboard'); - } - $environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']); - if (! $environment) { - return redirect()->route('dashboard'); - } - $application = $environment->applications->where('uuid', request()->route('application_uuid'))->first(); - if (! $application) { - return redirect()->route('dashboard'); - } + $project = currentTeam() + ->projects() + ->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; - $mainServer = $this->application->destination->server; - $servers = Server::ownedByCurrentTeam()->get(); - $this->servers = $servers->filter(function ($server) use ($mainServer) { - return $server->id != $mainServer->id; - }); + if ($application->destination && $application->destination->server) { + $mainServer = $application->destination->server; + $this->servers = Server::ownedByCurrentTeam() + ->select('id', 'name') + ->where('id', '!=', $mainServer->id) + ->get(); + } else { + $this->servers = collect(); + } } public function render() diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php index 1082b48cd..19a6145b7 100644 --- a/app/Livewire/Project/Application/Heading.php +++ b/app/Livewire/Project/Application/Heading.php @@ -36,7 +36,11 @@ class Heading extends Component public function mount() { - $this->parameters = get_route_parameters(); + $this->parameters = [ + 'project_uuid' => $this->application->project()->uuid, + 'environment_name' => $this->application->environment->name, + 'application_uuid' => $this->application->uuid, + ]; $lastDeployment = $this->application->get_last_successful_deployment(); $this->lastDeploymentInfo = data_get_str($lastDeployment, 'commit')->limit(7).' '.data_get($lastDeployment, 'commit_message'); $this->lastDeploymentLink = $this->application->gitCommitLink(data_get($lastDeployment, 'commit')); @@ -45,13 +49,11 @@ class Heading extends Component public function check_status($showNotification = false) { if ($this->application->destination->server->isFunctional()) { - GetContainersStatus::dispatch($this->application->destination->server)->onQueue('high'); + GetContainersStatus::dispatch($this->application->destination->server); } if ($showNotification) { $this->dispatch('success', 'Success', 'Application status updated.'); } - // Removed because it caused flickering - // $this->dispatch('configurationChanged'); } public function force_deploy_without_cache() diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php index 3dedc11af..0b6d075a4 100644 --- a/app/Livewire/Project/New/Select.php +++ b/app/Livewire/Project/New/Select.php @@ -91,12 +91,16 @@ class Select extends Component { $services = get_service_templates(true); $services = collect($services)->map(function ($service, $key) { - $logo = data_get($service, 'logo', 'svgs/coolify.png'); + $default_logo = 'images/default.webp'; + $logo = data_get($service, 'logo', $default_logo); + $local_logo_path = public_path($logo); return [ 'name' => str($key)->headline(), 'logo' => asset($logo), - 'logo_github_url' => 'https://raw.githubusercontent.com/coollabsio/coolify/refs/heads/main/public/a'.$logo, + 'logo_github_url' => file_exists($local_logo_path) + ? 'https://raw.githubusercontent.com/coollabsio/coolify/refs/heads/main/public/'.$logo + : asset($default_logo), ] + (array) $service; })->all(); $gitBasedApplications = [ @@ -144,14 +148,14 @@ class Select extends Component 'id' => 'postgresql', 'name' => 'PostgreSQL', 'description' => 'PostgreSQL is an object-relational database known for its robustness, advanced features, and strong standards compliance.', - 'logo' => ' + 'logo' => ' ', ], [ 'id' => 'mysql', 'name' => 'MySQL', 'description' => 'MySQL is an open-source relational database management system. ', - 'logo' => ' + 'logo' => ' @@ -162,37 +166,37 @@ class Select extends Component 'id' => 'mariadb', 'name' => 'MariaDB', 'description' => 'MariaDB is a community-developed, commercially supported fork of the MySQL relational database management system, intended to remain free and open-source.', - 'logo' => '', + 'logo' => '', ], [ 'id' => 'redis', 'name' => 'Redis', 'description' => 'Redis is a source-available, in-memory storage, used as a distributed, in-memory key–value database, cache and message broker, with optional durability.', - 'logo' => '', + 'logo' => '', ], [ 'id' => 'keydb', 'name' => 'KeyDB', 'description' => 'KeyDB is a database that offers high performance, low latency, and scalability for various data structures and workloads.', - 'logo' => '
', + 'logo' => '
', ], [ 'id' => 'dragonfly', 'name' => 'Dragonfly', 'description' => 'Dragonfly DB is a drop-in Redis replacement that delivers 25x more throughput and 12x faster snapshotting than Redis.', - 'logo' => '
', + 'logo' => '
', ], [ 'id' => 'mongodb', 'name' => 'MongoDB', 'description' => 'MongoDB is a source-available, cross-platform, document-oriented database program.', - 'logo' => '', + 'logo' => '', ], [ 'id' => 'clickhouse', 'name' => 'ClickHouse', 'description' => 'ClickHouse is a column-oriented database that supports real-time analytics, business intelligence, observability, ML and GenAI, and more.', - 'logo' => '
', + 'logo' => '
', ], ]; 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/Advanced.php b/app/Livewire/Server/Advanced.php index 0852abebf..0650de9a0 100644 --- a/app/Livewire/Server/Advanced.php +++ b/app/Livewire/Server/Advanced.php @@ -22,7 +22,7 @@ class Advanced extends Component #[Validate('boolean')] public bool $forceDockerCleanup = false; - #[Validate('string')] + #[Validate(['string', 'required'])] public string $dockerCleanupFrequency = '*/10 * * * *'; #[Validate(['integer', 'min:1', 'max:99'])] @@ -78,7 +78,6 @@ class Advanced extends Component try { $this->syncData(true); $this->dispatch('success', 'Server updated.'); - // $this->dispatch('refreshServerShow'); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Server/LogDrains.php b/app/Livewire/Server/LogDrains.php index 6599149c4..edddfc755 100644 --- a/app/Livewire/Server/LogDrains.php +++ b/app/Livewire/Server/LogDrains.php @@ -49,33 +49,73 @@ class LogDrains extends Component } } - public function syncData(bool $toModel = false) + public function syncDataNewRelic(bool $toModel = false) + { + if ($toModel) { + $this->server->settings->is_logdrain_newrelic_enabled = $this->isLogDrainNewRelicEnabled; + $this->server->settings->logdrain_newrelic_license_key = $this->logDrainNewRelicLicenseKey; + $this->server->settings->logdrain_newrelic_base_uri = $this->logDrainNewRelicBaseUri; + } else { + $this->isLogDrainNewRelicEnabled = $this->server->settings->is_logdrain_newrelic_enabled; + $this->logDrainNewRelicLicenseKey = $this->server->settings->logdrain_newrelic_license_key; + $this->logDrainNewRelicBaseUri = $this->server->settings->logdrain_newrelic_base_uri; + } + } + + public function syncDataAxiom(bool $toModel = false) + { + if ($toModel) { + $this->server->settings->is_logdrain_axiom_enabled = $this->isLogDrainAxiomEnabled; + $this->server->settings->logdrain_axiom_dataset_name = $this->logDrainAxiomDatasetName; + $this->server->settings->logdrain_axiom_api_key = $this->logDrainAxiomApiKey; + } else { + $this->isLogDrainAxiomEnabled = $this->server->settings->is_logdrain_axiom_enabled; + $this->logDrainAxiomDatasetName = $this->server->settings->logdrain_axiom_dataset_name; + $this->logDrainAxiomApiKey = $this->server->settings->logdrain_axiom_api_key; + } + } + + public function syncDataCustom(bool $toModel = false) + { + if ($toModel) { + $this->server->settings->is_logdrain_custom_enabled = $this->isLogDrainCustomEnabled; + $this->server->settings->logdrain_custom_config = $this->logDrainCustomConfig; + $this->server->settings->logdrain_custom_config_parser = $this->logDrainCustomConfigParser; + } else { + $this->isLogDrainCustomEnabled = $this->server->settings->is_logdrain_custom_enabled; + $this->logDrainCustomConfig = $this->server->settings->logdrain_custom_config; + $this->logDrainCustomConfigParser = $this->server->settings->logdrain_custom_config_parser; + } + } + + public function syncData(bool $toModel = false, ?string $type = null) { if ($toModel) { $this->customValidation(); - $this->server->settings->is_logdrain_newrelic_enabled = $this->isLogDrainNewRelicEnabled; - $this->server->settings->is_logdrain_axiom_enabled = $this->isLogDrainAxiomEnabled; - $this->server->settings->is_logdrain_custom_enabled = $this->isLogDrainCustomEnabled; - - $this->server->settings->logdrain_newrelic_license_key = $this->logDrainNewRelicLicenseKey; - $this->server->settings->logdrain_newrelic_base_uri = $this->logDrainNewRelicBaseUri; - $this->server->settings->logdrain_axiom_dataset_name = $this->logDrainAxiomDatasetName; - $this->server->settings->logdrain_axiom_api_key = $this->logDrainAxiomApiKey; - $this->server->settings->logdrain_custom_config = $this->logDrainCustomConfig; - $this->server->settings->logdrain_custom_config_parser = $this->logDrainCustomConfigParser; - + if ($type === 'newrelic') { + $this->syncDataNewRelic($toModel); + } elseif ($type === 'axiom') { + $this->syncDataAxiom($toModel); + } elseif ($type === 'custom') { + $this->syncDataCustom($toModel); + } else { + $this->syncDataNewRelic($toModel); + $this->syncDataAxiom($toModel); + $this->syncDataCustom($toModel); + } $this->server->settings->save(); } else { - $this->isLogDrainNewRelicEnabled = $this->server->settings->is_logdrain_newrelic_enabled; - $this->isLogDrainAxiomEnabled = $this->server->settings->is_logdrain_axiom_enabled; - $this->isLogDrainCustomEnabled = $this->server->settings->is_logdrain_custom_enabled; - - $this->logDrainNewRelicLicenseKey = $this->server->settings->logdrain_newrelic_license_key; - $this->logDrainNewRelicBaseUri = $this->server->settings->logdrain_newrelic_base_uri; - $this->logDrainAxiomDatasetName = $this->server->settings->logdrain_axiom_dataset_name; - $this->logDrainAxiomApiKey = $this->server->settings->logdrain_axiom_api_key; - $this->logDrainCustomConfig = $this->server->settings->logdrain_custom_config; - $this->logDrainCustomConfigParser = $this->server->settings->logdrain_custom_config_parser; + if ($type === 'newrelic') { + $this->syncDataNewRelic($toModel); + } elseif ($type === 'axiom') { + $this->syncDataAxiom($toModel); + } elseif ($type === 'custom') { + $this->syncDataCustom($toModel); + } else { + $this->syncDataNewRelic($toModel); + $this->syncDataAxiom($toModel); + $this->syncDataCustom($toModel); + } } } @@ -136,7 +176,7 @@ class LogDrains extends Component public function submit(string $type) { try { - $this->syncData(true); + $this->syncData(true, $type); $this->dispatch('success', 'Settings saved.'); } catch (\Throwable $e) { return handleError($e, $this); 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 55ba49867..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; @@ -53,7 +50,7 @@ class Index extends Component #[Validate('string')] public string $auto_update_frequency; - #[Validate('string')] + #[Validate('string|required')] public string $update_check_frequency; #[Validate('required|string|timezone')] @@ -101,14 +98,29 @@ 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(); + if ($this->settings->is_auto_update_enabled === true) { + $this->validate([ + 'auto_update_frequency' => ['required', 'string'], + ]); + } + $this->settings->fqdn = $this->fqdn; $this->settings->resale_license = $this->resale_license; $this->settings->public_port_min = $this->public_port_min; 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/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index 08f23d7ab..96c57e63e 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -71,7 +71,7 @@ class EnvironmentVariable extends Model } } $environment_variable->update([ - 'version' => config('version'), + 'version' => config('constants.coolify.version'), ]); }); static::saving(function (EnvironmentVariable $environmentVariable) { diff --git a/app/Models/Server.php b/app/Models/Server.php index 64d7b88d1..3ddea4f32 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; @@ -42,14 +43,13 @@ use Symfony\Component\Yaml\Yaml; 'validation_logs' => ['type' => 'string', 'description' => 'The validation logs.'], 'log_drain_notification_sent' => ['type' => 'boolean', 'description' => 'The flag to indicate if the log drain notification has been sent.'], 'swarm_cluster' => ['type' => 'string', 'description' => 'The swarm cluster configuration.'], - 'delete_unused_volumes' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused volumes should be deleted.'], - 'delete_unused_networks' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused networks should be deleted.'], + 'settings' => ['$ref' => '#/components/schemas/ServerSetting'], ] )] class Server extends BaseModel { - use SchemalessAttributesTrait, SoftDeletes; + use HasFactory, SchemalessAttributesTrait, SoftDeletes; public static $batch_counter = 0; @@ -606,7 +606,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(); @@ -809,7 +810,7 @@ $schema://$host { { return Attribute::make( get: function ($value) { - return preg_replace('/[^0-9]/', '', $value); + return (int) preg_replace('/[^0-9]/', '', $value); } ); } @@ -983,7 +984,7 @@ $schema://$host { public function status(): bool { - ['uptime' => $uptime] = $this->validateConnection(false); + ['uptime' => $uptime] = $this->validateConnection(); if ($uptime === false) { foreach ($this->applications() as $application) { $application->status = 'exited'; @@ -1035,7 +1036,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() @@ -1043,21 +1044,17 @@ $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 $isManualCheck = true, bool $justCheckingNewKey = false) + public function validateConnection(bool $justCheckingNewKey = false) { - config()->set('constants.ssh.mux_enabled', ! $isManualCheck); + config()->set('constants.ssh.mux_enabled', false); if ($this->skipServer()) { return ['uptime' => false, 'error' => 'Server skipped.']; } try { - // Make sure the private key is stored - if ($this->privateKey) { - $this->privateKey->storeInFileSystem(); - } instant_remote_process(['ls /'], $this); if ($this->settings->is_reachable === false) { $this->settings->is_reachable = true; diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php index fc2c5a0f4..e078372e2 100644 --- a/app/Models/ServerSetting.php +++ b/app/Models/ServerSetting.php @@ -45,6 +45,8 @@ use OpenApi\Attributes as OA; 'wildcard_domain' => ['type' => 'string'], 'created_at' => ['type' => 'string'], 'updated_at' => ['type' => 'string'], + 'delete_unused_volumes' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused volumes should be deleted.'], + 'delete_unused_networks' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused networks should be deleted.'], ] )] class ServerSetting extends Model diff --git a/app/Notifications/Application/DeploymentFailed.php b/app/Notifications/Application/DeploymentFailed.php index 242980e00..ce1f99d77 100644 --- a/app/Notifications/Application/DeploymentFailed.php +++ b/app/Notifications/Application/DeploymentFailed.php @@ -4,18 +4,12 @@ namespace App\Notifications\Application; use App\Models\Application; use App\Models\ApplicationPreview; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class DeploymentFailed extends Notification implements ShouldQueue +class DeploymentFailed extends CustomEmailNotification { - use Queueable; - - public $tries = 1; - public Application $application; public ?ApplicationPreview $preview = null; @@ -34,6 +28,7 @@ class DeploymentFailed extends Notification implements ShouldQueue public function __construct(Application $application, string $deployment_uuid, ?ApplicationPreview $preview = null) { + $this->onQueue('high'); $this->application = $application; $this->deployment_uuid = $deployment_uuid; $this->preview = $preview; diff --git a/app/Notifications/Application/DeploymentSuccess.php b/app/Notifications/Application/DeploymentSuccess.php index 946a622ca..391601257 100644 --- a/app/Notifications/Application/DeploymentSuccess.php +++ b/app/Notifications/Application/DeploymentSuccess.php @@ -4,18 +4,12 @@ namespace App\Notifications\Application; use App\Models\Application; use App\Models\ApplicationPreview; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class DeploymentSuccess extends Notification implements ShouldQueue +class DeploymentSuccess extends CustomEmailNotification { - use Queueable; - - public $tries = 1; - public Application $application; public ?ApplicationPreview $preview = null; @@ -34,6 +28,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue public function __construct(Application $application, string $deployment_uuid, ?ApplicationPreview $preview = null) { + $this->onQueue('high'); $this->application = $application; $this->deployment_uuid = $deployment_uuid; $this->preview = $preview; diff --git a/app/Notifications/Application/StatusChanged.php b/app/Notifications/Application/StatusChanged.php index 852c6b526..c757495cb 100644 --- a/app/Notifications/Application/StatusChanged.php +++ b/app/Notifications/Application/StatusChanged.php @@ -3,18 +3,12 @@ namespace App\Notifications\Application; use App\Models\Application; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class StatusChanged extends Notification implements ShouldQueue +class StatusChanged extends CustomEmailNotification { - use Queueable; - - public $tries = 1; - public string $resource_name; public string $project_uuid; @@ -27,6 +21,7 @@ class StatusChanged extends Notification implements ShouldQueue public function __construct(public Application $resource) { + $this->onQueue('high'); $this->resource_name = data_get($resource, 'name'); $this->project_uuid = data_get($resource, 'environment.project.uuid'); $this->environment_name = data_get($resource, 'environment.name'); diff --git a/app/Notifications/Channels/DiscordChannel.php b/app/Notifications/Channels/DiscordChannel.php index 86276fec9..df7040f8f 100644 --- a/app/Notifications/Channels/DiscordChannel.php +++ b/app/Notifications/Channels/DiscordChannel.php @@ -17,6 +17,6 @@ class DiscordChannel if (! $webhookUrl) { return; } - dispatch(new SendMessageToDiscordJob($message, $webhookUrl))->onQueue('high'); + SendMessageToDiscordJob::dispatch($message, $webhookUrl); } } 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/app/Notifications/Channels/TelegramChannel.php b/app/Notifications/Channels/TelegramChannel.php index b3d4e384b..958c46c21 100644 --- a/app/Notifications/Channels/TelegramChannel.php +++ b/app/Notifications/Channels/TelegramChannel.php @@ -41,6 +41,6 @@ class TelegramChannel if (! $telegramToken || ! $chatId || ! $message) { return; } - dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId, $topicId))->onQueue('high'); + SendMessageToTelegramJob::dispatch($message, $buttons, $telegramToken, $chatId, $topicId); } } diff --git a/app/Notifications/Container/ContainerRestarted.php b/app/Notifications/Container/ContainerRestarted.php index 182a1f5fc..eb709535f 100644 --- a/app/Notifications/Container/ContainerRestarted.php +++ b/app/Notifications/Container/ContainerRestarted.php @@ -3,19 +3,16 @@ namespace App\Notifications\Container; use App\Models\Server; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class ContainerRestarted extends Notification implements ShouldQueue +class ContainerRestarted extends CustomEmailNotification { - use Queueable; - - public $tries = 1; - - public function __construct(public string $name, public Server $server, public ?string $url = null) {} + public function __construct(public string $name, public Server $server, public ?string $url = null) + { + $this->onQueue('high'); + } public function via(object $notifiable): array { diff --git a/app/Notifications/Container/ContainerStopped.php b/app/Notifications/Container/ContainerStopped.php index 33a55c65a..a73e984a0 100644 --- a/app/Notifications/Container/ContainerStopped.php +++ b/app/Notifications/Container/ContainerStopped.php @@ -3,19 +3,16 @@ namespace App\Notifications\Container; use App\Models\Server; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class ContainerStopped extends Notification implements ShouldQueue +class ContainerStopped extends CustomEmailNotification { - use Queueable; - - public $tries = 1; - - public function __construct(public string $name, public Server $server, public ?string $url = null) {} + public function __construct(public string $name, public Server $server, public ?string $url = null) + { + $this->onQueue('high'); + } public function via(object $notifiable): array { diff --git a/app/Notifications/CustomEmailNotification.php b/app/Notifications/CustomEmailNotification.php new file mode 100644 index 000000000..c3c89b30f --- /dev/null +++ b/app/Notifications/CustomEmailNotification.php @@ -0,0 +1,18 @@ +onQueue('high'); $this->name = $database->name; $this->frequency = $backup->frequency; } diff --git a/app/Notifications/Database/BackupSuccess.php b/app/Notifications/Database/BackupSuccess.php index 5128c8ed6..d8bab069b 100644 --- a/app/Notifications/Database/BackupSuccess.php +++ b/app/Notifications/Database/BackupSuccess.php @@ -3,26 +3,20 @@ namespace App\Notifications\Database; use App\Models\ScheduledDatabaseBackup; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class BackupSuccess extends Notification implements ShouldQueue +class BackupSuccess extends CustomEmailNotification { - use Queueable; - - public $backoff = 10; - - public $tries = 3; - public string $name; public string $frequency; public function __construct(ScheduledDatabaseBackup $backup, public $database, public $database_name) { + $this->onQueue('high'); + $this->name = $database->name; $this->frequency = $backup->frequency; } diff --git a/app/Notifications/Dto/DiscordMessage.php b/app/Notifications/Dto/DiscordMessage.php index 856753dca..278bfd1b6 100644 --- a/app/Notifications/Dto/DiscordMessage.php +++ b/app/Notifications/Dto/DiscordMessage.php @@ -46,7 +46,7 @@ class DiscordMessage public function toPayload(): array { - $footerText = 'Coolify v'.config('version'); + $footerText = 'Coolify v'.config('constants.coolify.version'); if (isCloud()) { $footerText = 'Coolify Cloud'; } diff --git a/app/Notifications/Internal/GeneralNotification.php b/app/Notifications/Internal/GeneralNotification.php index 48e7d8340..dcfde7b5b 100644 --- a/app/Notifications/Internal/GeneralNotification.php +++ b/app/Notifications/Internal/GeneralNotification.php @@ -15,7 +15,10 @@ class GeneralNotification extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public string $message) {} + public function __construct(public string $message) + { + $this->onQueue('high'); + } public function via(object $notifiable): array { diff --git a/app/Notifications/ScheduledTask/TaskFailed.php b/app/Notifications/ScheduledTask/TaskFailed.php index c3501a8eb..701f61277 100644 --- a/app/Notifications/ScheduledTask/TaskFailed.php +++ b/app/Notifications/ScheduledTask/TaskFailed.php @@ -3,24 +3,17 @@ namespace App\Notifications\ScheduledTask; use App\Models\ScheduledTask; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class TaskFailed extends Notification implements ShouldQueue +class TaskFailed extends CustomEmailNotification { - use Queueable; - - public $backoff = 10; - - public $tries = 2; - public ?string $url = null; public function __construct(public ScheduledTask $task, public string $output) { + $this->onQueue('high'); if ($task->application) { $this->url = $task->application->failedTaskLink($task->uuid); } elseif ($task->service) { diff --git a/app/Notifications/Server/DockerCleanup.php b/app/Notifications/Server/DockerCleanup.php index 7ea1b84c2..2d007a262 100644 --- a/app/Notifications/Server/DockerCleanup.php +++ b/app/Notifications/Server/DockerCleanup.php @@ -5,18 +5,15 @@ namespace App\Notifications\Server; use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Notifications\Notification; -class DockerCleanup extends Notification implements ShouldQueue +class DockerCleanup extends CustomEmailNotification { - use Queueable; - - public $tries = 1; - - public function __construct(public Server $server, public string $message) {} + public function __construct(public Server $server, public string $message) + { + $this->onQueue('high'); + } public function via(object $notifiable): array { diff --git a/app/Notifications/Server/ForceDisabled.php b/app/Notifications/Server/ForceDisabled.php index a26c803ee..eabf8b334 100644 --- a/app/Notifications/Server/ForceDisabled.php +++ b/app/Notifications/Server/ForceDisabled.php @@ -6,19 +6,16 @@ use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class ForceDisabled extends Notification implements ShouldQueue +class ForceDisabled extends CustomEmailNotification { - use Queueable; - - public $tries = 1; - - public function __construct(public Server $server) {} + public function __construct(public Server $server) + { + $this->onQueue('high'); + } public function via(object $notifiable): array { diff --git a/app/Notifications/Server/ForceEnabled.php b/app/Notifications/Server/ForceEnabled.php index 65b65a10c..0c21ed6b8 100644 --- a/app/Notifications/Server/ForceEnabled.php +++ b/app/Notifications/Server/ForceEnabled.php @@ -6,19 +6,16 @@ use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class ForceEnabled extends Notification implements ShouldQueue +class ForceEnabled extends CustomEmailNotification { - use Queueable; - - public $tries = 1; - - public function __construct(public Server $server) {} + public function __construct(public Server $server) + { + $this->onQueue('high'); + } public function via(object $notifiable): array { diff --git a/app/Notifications/Server/HighDiskUsage.php b/app/Notifications/Server/HighDiskUsage.php index e373abc03..7cec2e892 100644 --- a/app/Notifications/Server/HighDiskUsage.php +++ b/app/Notifications/Server/HighDiskUsage.php @@ -3,19 +3,16 @@ namespace App\Notifications\Server; use App\Models\Server; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class HighDiskUsage extends Notification implements ShouldQueue +class HighDiskUsage extends CustomEmailNotification { - use Queueable; - - public $tries = 1; - - public function __construct(public Server $server, public int $disk_usage, public int $server_disk_usage_notification_threshold) {} + public function __construct(public Server $server, public int $disk_usage, public int $server_disk_usage_notification_threshold) + { + $this->onQueue('high'); + } public function via(object $notifiable): array { diff --git a/app/Notifications/Server/Reachable.php b/app/Notifications/Server/Reachable.php index 9b54501d9..44189c3b5 100644 --- a/app/Notifications/Server/Reachable.php +++ b/app/Notifications/Server/Reachable.php @@ -6,22 +6,17 @@ use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class Reachable extends Notification implements ShouldQueue +class Reachable extends CustomEmailNotification { - use Queueable; - - public $tries = 1; - protected bool $isRateLimited = false; public function __construct(public Server $server) { + $this->onQueue('high'); $this->isRateLimited = isEmailRateLimited( limiterKey: 'server-reachable:'.$this->server->id, ); diff --git a/app/Notifications/Server/Unreachable.php b/app/Notifications/Server/Unreachable.php index 5bc568e82..6fb792bdc 100644 --- a/app/Notifications/Server/Unreachable.php +++ b/app/Notifications/Server/Unreachable.php @@ -6,22 +6,17 @@ use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class Unreachable extends Notification implements ShouldQueue +class Unreachable extends CustomEmailNotification { - use Queueable; - - public $tries = 1; - protected bool $isRateLimited = false; public function __construct(public Server $server) { + $this->onQueue('high'); $this->isRateLimited = isEmailRateLimited( limiterKey: 'server-unreachable:'.$this->server->id, ); diff --git a/app/Notifications/Test.php b/app/Notifications/Test.php index a43b1e153..64f9bb0a5 100644 --- a/app/Notifications/Test.php +++ b/app/Notifications/Test.php @@ -15,7 +15,10 @@ class Test extends Notification implements ShouldQueue public $tries = 5; - public function __construct(public ?string $emails = null) {} + public function __construct(public ?string $emails = null) + { + $this->onQueue('high'); + } public function via(object $notifiable): array { diff --git a/app/Notifications/TransactionalEmails/InvitationLink.php b/app/Notifications/TransactionalEmails/InvitationLink.php index 6da2a6fcc..30ace99dc 100644 --- a/app/Notifications/TransactionalEmails/InvitationLink.php +++ b/app/Notifications/TransactionalEmails/InvitationLink.php @@ -6,23 +6,20 @@ use App\Models\Team; use App\Models\TeamInvitation; use App\Models\User; use App\Notifications\Channels\TransactionalEmailChannel; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; +use App\Notifications\CustomEmailNotification; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class InvitationLink extends Notification implements ShouldQueue +class InvitationLink extends CustomEmailNotification { - use Queueable; - - public $tries = 5; - public function via(): array { return [TransactionalEmailChannel::class]; } - public function __construct(public User $user) {} + public function __construct(public User $user) + { + $this->onQueue('high'); + } public function toMail(): MailMessage { diff --git a/app/Notifications/TransactionalEmails/Test.php b/app/Notifications/TransactionalEmails/Test.php index 64883a58e..eeb32a254 100644 --- a/app/Notifications/TransactionalEmails/Test.php +++ b/app/Notifications/TransactionalEmails/Test.php @@ -3,18 +3,15 @@ namespace App\Notifications\TransactionalEmails; use App\Notifications\Channels\EmailChannel; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; +use App\Notifications\CustomEmailNotification; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class Test extends Notification implements ShouldQueue +class Test extends CustomEmailNotification { - use Queueable; - - public $tries = 5; - - public function __construct(public string $emails) {} + public function __construct(public string $emails) + { + $this->onQueue('high'); + } public function via(): array { diff --git a/bootstrap/getVersion.php b/bootstrap/getVersion.php index a8329a319..8fa27a332 100644 --- a/bootstrap/getVersion.php +++ b/bootstrap/getVersion.php @@ -1,4 +1,10 @@ id, - ))->onQueue('high'); + ); } elseif (next_queuable($server_id, $application_id)) { - dispatch(new ApplicationDeploymentJob( + ApplicationDeploymentJob::dispatch( application_deployment_queue_id: $deployment->id, - ))->onQueue('high'); + ); } } function force_start_deployment(ApplicationDeploymentQueue $deployment) @@ -59,9 +59,9 @@ function force_start_deployment(ApplicationDeploymentQueue $deployment) 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, ]); - dispatch(new ApplicationDeploymentJob( + ApplicationDeploymentJob::dispatch( application_deployment_queue_id: $deployment->id, - ))->onQueue('high'); + ); } function queue_next_deployment(Application $application) { @@ -72,9 +72,9 @@ function queue_next_deployment(Application $application) 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, ]); - dispatch(new ApplicationDeploymentJob( + ApplicationDeploymentJob::dispatch( application_deployment_queue_id: $next_found->id, - ))->onQueue('high'); + ); } } @@ -113,9 +113,9 @@ function next_after_cancel(?Server $server = null) 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, ]); - dispatch(new ApplicationDeploymentJob( + ApplicationDeploymentJob::dispatch( application_deployment_queue_id: $next->id, - ))->onQueue('high'); + ); } break; } 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 da99c5cbd..eda2133a7 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -192,7 +192,7 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica { $labels = collect([]); $labels->push('coolify.managed=true'); - $labels->push('coolify.version='.config('version')); + $labels->push('coolify.version='.config('constants.coolify.version')); $labels->push('coolify.'.$type.'Id='.$id); $labels->push("coolify.type=$type"); $labels->push('coolify.name='.$name); @@ -288,6 +288,10 @@ 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'; + } if (is_null($port) && ! is_null($onlyPort)) { $port = $onlyPort; } @@ -299,11 +303,11 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, $labels->push("caddy_{$loop}.try_files={path} /index.html /index.php"); if ($port) { - $labels->push("caddy_{$loop}.handle_path.{$loop}_reverse_proxy={{upstreams $port}}"); + $labels->push("caddy_{$loop}.{$handle}.{$loop}_reverse_proxy={{upstreams $port}}"); } else { - $labels->push("caddy_{$loop}.handle_path.{$loop}_reverse_proxy={{upstreams}}"); + $labels->push("caddy_{$loop}.{$handle}.{$loop}_reverse_proxy={{upstreams}}"); } - $labels->push("caddy_{$loop}.handle_path={$path}*"); + $labels->push("caddy_{$loop}.{$handle}={$path}*"); if ($is_gzip_enabled) { $labels->push("caddy_{$loop}.encode=zstd gzip"); } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index dc4924789..a3ef93dfc 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -90,8 +90,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"; } @@ -940,6 +963,15 @@ function generateEnvValue(string $command, Service|Application|null $service = n case 'REALBASE64_32': $generatedValue = base64_encode(Str::random(32)); break; + case 'HEX_32': + $generatedValue = bin2hex(Str::random(32)); + break; + case 'HEX_64': + $generatedValue = bin2hex(Str::random(64)); + break; + case 'HEX_128': + $generatedValue = bin2hex(Str::random(128)); + break; case 'USER': $generatedValue = Str::random(16); break; diff --git a/config/constants.php b/config/constants.php index 363cdd14f..f7d5a7831 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.370', + '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/config/version.php b/config/version.php deleted file mode 100644 index 64226ac05..000000000 --- a/config/version.php +++ /dev/null @@ -1,3 +0,0 @@ - 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_11_11_125366_add_index_to_activity_log.php b/database/migrations/2024_11_11_125366_add_index_to_activity_log.php new file mode 100644 index 000000000..0c281ff40 --- /dev/null +++ b/database/migrations/2024_11_11_125366_add_index_to_activity_log.php @@ -0,0 +1,28 @@ +getMessage()); + } + } + + public function down() + { + try { + DB::statement('DROP INDEX IF EXISTS idx_activity_type_uuid'); + DB::statement('ALTER TABLE activity_log ALTER COLUMN properties TYPE json USING properties::json'); + } catch (\Exception $e) { + Log::error('Error dropping index from activity_log: '.$e->getMessage()); + } + } +} 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 cdcd47f40..5d35331ec 100644 --- a/openapi.json +++ b/openapi.json @@ -1,5 +1,5 @@ { - "openapi": "3.0.0", + "openapi": "3.1.0", "info": { "title": "Coolify", "version": "0.1" @@ -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" @@ -5391,6 +5399,17 @@ "summary": "Update", "description": "Update Server.", "operationId": "update-server-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "Server UUID", + "required": true, + "schema": { + "type": "string" + } + } + ], "requestBody": { "description": "Server updated.", "required": true, @@ -5451,10 +5470,7 @@ "content": { "application\/json": { "schema": { - "type": "array", - "items": { - "$ref": "#\/components\/schemas\/Server" - } + "$ref": "#\/components\/schemas\/Server" } } } @@ -7441,13 +7457,8 @@ "type": "string", "description": "The swarm cluster configuration." }, - "delete_unused_volumes": { - "type": "boolean", - "description": "The flag to indicate if the unused volumes should be deleted." - }, - "delete_unused_networks": { - "type": "boolean", - "description": "The flag to indicate if the unused networks should be deleted." + "settings": { + "$ref": "#\/components\/schemas\/ServerSetting" } }, "type": "object" @@ -7556,6 +7567,14 @@ }, "updated_at": { "type": "string" + }, + "delete_unused_volumes": { + "type": "boolean", + "description": "The flag to indicate if the unused volumes should be deleted." + }, + "delete_unused_networks": { + "type": "boolean", + "description": "The flag to indicate if the unused networks should be deleted." } }, "type": "object" diff --git a/openapi.yaml b/openapi.yaml index 2b1ece41c..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' @@ -3671,6 +3677,14 @@ paths: summary: Update description: 'Update Server.' operationId: update-server-by-uuid + parameters: + - + name: uuid + in: path + description: 'Server UUID' + required: true + schema: + type: string requestBody: description: 'Server updated.' required: true @@ -3713,9 +3727,7 @@ paths: content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/Server' + $ref: '#/components/schemas/Server' '401': $ref: '#/components/responses/401' '400': @@ -4967,12 +4979,8 @@ components: swarm_cluster: type: string description: 'The swarm cluster configuration.' - delete_unused_volumes: - type: boolean - description: 'The flag to indicate if the unused volumes should be deleted.' - delete_unused_networks: - type: boolean - description: 'The flag to indicate if the unused networks should be deleted.' + settings: + $ref: '#/components/schemas/ServerSetting' type: object ServerSetting: description: 'Server Settings model' @@ -5045,6 +5053,12 @@ components: type: string updated_at: type: string + delete_unused_volumes: + type: boolean + description: 'The flag to indicate if the unused volumes should be deleted.' + delete_unused_networks: + type: boolean + description: 'The flag to indicate if the unused networks should be deleted.' type: object Service: description: 'Service model' 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/beszel.svg b/public/svgs/beszel.svg new file mode 100644 index 000000000..c6836479c --- /dev/null +++ b/public/svgs/beszel.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/convertx.png b/public/svgs/convertx.png new file mode 100644 index 000000000..7f4c41e2e Binary files /dev/null and b/public/svgs/convertx.png differ diff --git a/public/svgs/default.webp b/public/svgs/default.webp new file mode 100644 index 000000000..e47383469 Binary files /dev/null and b/public/svgs/default.webp differ diff --git a/public/svgs/macos.svg b/public/svgs/macos.svg new file mode 100644 index 000000000..483fa6a17 --- /dev/null +++ b/public/svgs/macos.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/maybe.svg b/public/svgs/maybe.svg new file mode 100644 index 000000000..9a8aa75cb --- /dev/null +++ b/public/svgs/maybe.svg @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/svgs/mealie.png b/public/svgs/mealie.png new file mode 100644 index 000000000..74a2d7b62 Binary files /dev/null and b/public/svgs/mealie.png differ 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/privatebin.svg b/public/svgs/privatebin.svg new file mode 100644 index 000000000..d63c65dbd --- /dev/null +++ b/public/svgs/privatebin.svg @@ -0,0 +1 @@ + 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/redlib.svg b/public/svgs/redlib.svg new file mode 100644 index 000000000..16f73b5dd --- /dev/null +++ b/public/svgs/redlib.svg @@ -0,0 +1,7 @@ + + + + + + + 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/public/svgs/windows.svg b/public/svgs/windows.svg new file mode 100644 index 000000000..2c7392e9c --- /dev/null +++ b/public/svgs/windows.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/css/app.css b/resources/css/app.css index e378d4640..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 { @@ -30,6 +30,14 @@ body { @apply text-black dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300; } +.input-sticky { + @apply text-black dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300 focus:ring-2 dark:focus:ring-coolgray-300 focus:ring-neutral-400 block w-full py-1.5 rounded border-0 text-sm ring-1 ring-inset; +} + +.input-sticky-active { + @apply border-2 border-coollabs text-black dark:text-white focus:bg-neutral-200 dark:focus:bg-coolgray-400 focus:border-coollabs; +} + /* Readonly */ .input { @apply dark:read-only:text-neutral-500 dark:read-only:ring-0 dark:read-only:bg-coolgray-100/40 placeholder:text-neutral-300 dark:placeholder:text-neutral-700 read-only:text-neutral-500 read-only:bg-neutral-200; @@ -314,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/js/terminal.js b/resources/js/terminal.js index 59c9a79a8..e70a8cd1a 100644 --- a/resources/js/terminal.js +++ b/resources/js/terminal.js @@ -60,7 +60,22 @@ export function initializeTerminalComponent() { }; }, + resetTerminal() { + if (this.term) { + this.$wire.dispatch('error', 'Terminal websocket connection lost.'); + this.term.reset(); + this.term.clear(); + this.pendingWrites = 0; + this.paused = false; + this.commandBuffer = ''; + // Force a refresh + this.$nextTick(() => { + this.resizeTerminal(); + this.term.focus(); + }); + } + }, setupTerminal() { const terminalElement = document.getElementById('terminal'); if (terminalElement) { @@ -69,9 +84,15 @@ export function initializeTerminalComponent() { rows: 30, fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"', cursorBlink: true, + rendererType: 'canvas', + convertEol: true, + disableStdin: false }); this.fitAddon = new FitAddon(); this.term.loadAddon(this.fitAddon); + this.$nextTick(() => { + this.resizeTerminal(); + }); } }, @@ -101,12 +122,19 @@ export function initializeTerminalComponent() { `${connectionString.protocol}://${connectionString.host}${connectionString.port}${connectionString.path}` this.socket = new WebSocket(url); + this.socket.onopen = () => { + console.log('[Terminal] WebSocket connection established. Cool cool cool cool cool cool.'); + }; + this.socket.onmessage = this.handleSocketMessage.bind(this); this.socket.onerror = (e) => { - console.error('WebSocket error:', e); + console.error('[Terminal] WebSocket error.'); }; this.socket.onclose = () => { - console.log('WebSocket connection closed'); + console.warn('[Terminal] WebSocket connection closed.'); + this.resetTerminal(); + this.message = '(connection closed)'; + this.terminalActive = false; this.reconnect(); }; } @@ -117,19 +145,18 @@ export function initializeTerminalComponent() { clearInterval(this.reconnectInterval); } this.reconnectInterval = setInterval(() => { - console.log('Attempting to reconnect...'); + console.warn('[Terminal] Attempting to reconnect...'); this.initializeWebSocket(); if (this.socket && this.socket.readyState === WebSocket.OPEN) { - console.log('Reconnected successfully'); + console.log('[Terminal] Reconnected successfully'); clearInterval(this.reconnectInterval); this.reconnectInterval = null; - window.location.reload(); + } }, 2000); }, handleSocketMessage(event) { - this.message = '(connection closed)'; if (event.data === 'pty-ready') { if (!this.term._initialized) { this.term.open(document.getElementById('terminal')); @@ -150,8 +177,17 @@ export function initializeTerminalComponent() { this.term.reset(); this.commandBuffer = ''; } else { - this.pendingWrites++; - this.term.write(event.data, this.flowControlCallback.bind(this)); + try { + this.pendingWrites++; + this.term.write(event.data, (err) => { + if (err) { + console.error('[Terminal] Write error:', err); + } + this.flowControlCallback(); + }); + } catch (error) { + console.error('[Terminal] Write operation failed:', error); + } } }, @@ -173,11 +209,15 @@ export function initializeTerminalComponent() { if (!this.term) return; this.term.onData((data) => { - this.socket.send(JSON.stringify({ message: data })); - if (data === '\r') { - this.commandBuffer = ''; + if (this.socket.readyState === WebSocket.OPEN) { + this.socket.send(JSON.stringify({ message: data })); + if (data === '\r') { + this.commandBuffer = ''; + } else { + this.commandBuffer += data; + } } else { - this.commandBuffer += data; + console.warn('[Terminal] WebSocket not ready, data not sent'); } }); 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/components/resources/breadcrumbs.blade.php b/resources/views/components/resources/breadcrumbs.blade.php index f9733f63a..f91e04037 100644 --- a/resources/views/components/resources/breadcrumbs.blade.php +++ b/resources/views/components/resources/breadcrumbs.blade.php @@ -8,7 +8,7 @@
  • {{ $this->parameters['environment_name'] }} + href="{{ route('project.resource.index', ['environment_name' => data_get($resource, 'environment.name'), 'project_uuid' => data_get($resource, 'environment.project.uuid')]) }}">{{ data_get($resource, 'environment.name') }}
      - v{{ config('version') }} - \ No newline at end of file + href="https://github.com/coollabsio/coolify/releases/tag/v{{ config('constants.coolify.version') }}" target="_blank"> + v{{ config('constants.coolify.version') }} + diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index 8c5017e03..5213479c8 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -34,11 +34,13 @@ @if (config('app.name') == 'Coolify Cloud') + @endif @auth + @endauth @section('body') @@ -94,6 +96,20 @@ enableStats: false, enableLogging: true, enabledTransports: ['ws', 'wss'], + disableStats: true, + // Add auto reconnection settings + enabledTransports: ['ws', 'wss'], + disabledTransports: ['sockjs', 'xhr_streaming', 'xhr_polling'], + // Attempt to reconnect on connection lost + autoReconnect: true, + // Wait 1 second before first reconnect attempt + reconnectionDelay: 1000, + // Maximum delay between reconnection attempts + maxReconnectionDelay: 1000, + // Multiply delay by this number for each reconnection attempt + reconnectionDelayGrowth: 1, + // Maximum number of reconnection attempts + maxAttempts: 15 }); @endauth let checkHealthInterval = null; diff --git a/resources/views/livewire/layout-popups.blade.php b/resources/views/livewire/layout-popups.blade.php index 8e75a2eee..41d249cb0 100644 --- a/resources/views/livewire/layout-popups.blade.php +++ b/resources/views/livewire/layout-popups.blade.php @@ -11,16 +11,22 @@ let checkNumber = 1; let checkPusherInterval = null; + let checkReconnectInterval = null; + if (!this.popups.realtime) { checkPusherInterval = setInterval(() => { - if (window.Echo && window.Echo.connector.pusher.connection.state !== 'connected') { - checkNumber++; - if (checkNumber > 5) { - this.popups.realtime = true; - console.error( - 'Coolify could not connect to its real-time service. This will cause unusual problems on the UI if not fixed! Please check the related documentation (https://coolify.io/docs/knowledge-base/cloudflare/tunnels) or get help on Discord (https://coollabs.io/discord).)' - ); - clearInterval(checkPusherInterval); + if (window.Echo) { + if (window.Echo.connector.pusher.connection.state === 'connected') { + this.popups.realtime = false; + } else { + checkNumber++; + if (checkNumber > 5) { + this.popups.realtime = true; + console.error( + 'Coolify could not connect to its real-time service. This will cause unusual problems on the UI if not fixed! Please check the related documentation (https://coolify.io/docs/knowledge-base/cloudflare/tunnels) or get help on Discord (https://coollabs.io/discord).)' + ); + } + } } }, 2000); diff --git a/resources/views/livewire/notifications/email.blade.php b/resources/views/livewire/notifications/email.blade.php index a2e5326c6..fba21d0b2 100644 --- a/resources/views/livewire/notifications/email.blade.php +++ b/resources/views/livewire/notifications/email.blade.php @@ -16,9 +16,10 @@ @endif @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 @@
      -
      +