Merge branch 'next' into feat/disable-default-redirect

This commit is contained in:
🏔️ Peak
2024-12-05 14:46:33 +01:00
committed by GitHub
190 changed files with 2581 additions and 952 deletions

View File

@@ -5,7 +5,7 @@
# About the Project # 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. 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 ### 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. * [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. * [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. * [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. * [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. * [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. * [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. * [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. * [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. * [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. * [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. * [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. * [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. * [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. * [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. * [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. * [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. * [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+) ## Github Sponsors ($40+)
@@ -90,7 +91,11 @@ Special thanks to our biggest sponsors!
<a href="https://github.com/urtho"><img src="https://github.com/urtho.png" width="60px" alt="Paweł Pierścionek" /></a> <a href="https://github.com/urtho"><img src="https://github.com/urtho.png" width="60px" alt="Paweł Pierścionek" /></a>
<a href="https://github.com/monocursive"><img src="https://github.com/monocursive.png" width="60px" alt="Michael Mazurczak" /></a> <a href="https://github.com/monocursive"><img src="https://github.com/monocursive.png" width="60px" alt="Michael Mazurczak" /></a>
<a href="https://formbricks.com/?utm_source=coolify.io"><img src="https://github.com/formbricks.png" width="60px" alt="Formbricks" /></a> <a href="https://formbricks.com/?utm_source=coolify.io"><img src="https://github.com/formbricks.png" width="60px" alt="Formbricks" /></a>
<a href="https://x.com/adithsuhas17?utm_source=coolify.io"><img src="https://github.com/adith-suhas-sv.png" width="60px" alt="Adith Suhas" /></a> <a href="https://startupfa.me?utm_source=coolify.io"><img src="https://github.com/startupfame.png" width="60px" alt="StartupFame" /></a>
<a href="https://jonasjaeger.com?utm_source=coolify.io"><img src="https://github.com/toxin20.png" width="60px" alt="Jonas Jaeger" /></a>
<a href="https://github.com/therealjp?utm_source=coolify.io"><img src="https://github.com/therealjp.png" width="60px" alt="JP" /></a>
<a href="https://evercam.io/?utm_source=coolify.io"><img src="https://github.com/evercam.png" width="60px" alt="Evercam" /></a>
<a href="https://web3.career/?utm_source=coolify.io"><img src="https://web3.career/favicon1.png" width="60px" alt="Web3 Career" /></a>
## Organizations ## Organizations
<a href="https://opencollective.com/coollabsio/organization/0/website"><img src="https://opencollective.com/coollabsio/organization/0/avatar.svg"></a> <a href="https://opencollective.com/coollabsio/organization/0/website"><img src="https://opencollective.com/coollabsio/organization/0/avatar.svg"></a>
@@ -142,10 +147,10 @@ By subscribing to the cloud version, you get the Coolify server for the same pri
# Core Maintainers # Core Maintainers
| Andras Bacsai | Peak | | Andras Bacsai | 🏔️ Peak |
|------------|------------| |------------|------------|
| <img src="https://github.com/andrasbacsai.png" width="200px" alt="Andras Bacsai" /> | <img src="https://github.com/peaklabs-dev.png" width="200px" alt="Peak Labs" /> | | <img src="https://github.com/andrasbacsai.png" width="200px" alt="Andras Bacsai" /> | <img src="https://github.com/peaklabs-dev.png" width="200px" alt="peaklabs-dev" /> |
| <a href="https://x.com/heyandras"><img src="https://raw.githubusercontent.com/gauravghongde/social-icons/master/SVG/Color/Twitter.svg" width="25px"></a> <a href="https://github.com/andrasbacsai"><img src="https://raw.githubusercontent.com/gauravghongde/social-icons/master/SVG/Color/Github.svg" width="25px"></a> | <a href="https://x.com/peaklabs_dev"><img src="https://raw.githubusercontent.com/gauravghongde/social-icons/master/SVG/Color/Twitter.svg" width="25px"></a> <a href="https://github.com/peaklabs-dev"><img src="https://raw.githubusercontent.com/gauravghongde/social-icons/master/SVG/Color/Github.svg" width="25px"></a> | | <a href="https://github.com/andrasbacsai"><img src="https://api.iconify.design/devicon:github.svg" width="25px"></a> <a href="https://x.com/heyandras"><img src="https://api.iconify.design/devicon:twitter.svg" width="25px"></a> <a href="https://bsky.app/profile/heyandras.dev"><img src="https://api.iconify.design/simple-icons:bluesky.svg" width="25px"></a> | <a href="https://github.com/peaklabs-dev"><img src="https://api.iconify.design/devicon:github.svg" width="25px"></a> <a href="https://x.com/peaklabs_dev"><img src="https://api.iconify.design/devicon:twitter.svg" width="25px"></a> <a href="https://bsky.app/profile/peaklabs.dev"><img src="https://api.iconify.design/simple-icons:bluesky.svg" width="25px"></a> |
# Repo Activity # Repo Activity

View File

@@ -10,6 +10,8 @@ class StopApplication
{ {
use AsAction; use AsAction;
public string $jobQueue = 'high';
public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true) public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true)
{ {
try { try {

View File

@@ -3,7 +3,6 @@
namespace App\Actions\CoolifyTask; namespace App\Actions\CoolifyTask;
use App\Data\CoolifyTaskArgs; use App\Data\CoolifyTaskArgs;
use App\Enums\ActivityTypes;
use App\Jobs\CoolifyTask; use App\Jobs\CoolifyTask;
use Spatie\Activitylog\Models\Activity; use Spatie\Activitylog\Models\Activity;
@@ -47,11 +46,7 @@ class PrepareCoolifyTask
call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish, call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish,
call_event_data: $this->remoteProcessArgs->call_event_data, call_event_data: $this->remoteProcessArgs->call_event_data,
); );
if ($this->remoteProcessArgs->type === ActivityTypes::COMMAND->value) { dispatch($job);
dispatch($job)->onQueue('high');
} else {
dispatch($job);
}
$this->activity->refresh(); $this->activity->refresh();
return $this->activity; return $this->activity;

View File

@@ -24,7 +24,7 @@ class StartClickhouse
$this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [ $this->commands = [
"echo 'Starting {$database->name}.'", "echo 'Starting database.'",
"mkdir -p $this->configuration_dir", "mkdir -p $this->configuration_dir",
]; ];

View File

@@ -16,6 +16,8 @@ class StartDatabase
{ {
use AsAction; use AsAction;
public string $jobQueue = 'high';
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database) public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
{ {
$server = $database->destination->server; $server = $database->destination->server;
@@ -49,7 +51,7 @@ class StartDatabase
break; break;
} }
if ($database->is_public && $database->public_port) { if ($database->is_public && $database->public_port) {
StartDatabaseProxy::dispatch($database)->onQueue('high'); StartDatabaseProxy::dispatch($database);
} }
return $activity; return $activity;

View File

@@ -18,6 +18,8 @@ class StartDatabaseProxy
{ {
use AsAction; use AsAction;
public string $jobQueue = 'high';
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|ServiceDatabase $database) public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|ServiceDatabase $database)
{ {
$internalPort = null; $internalPort = null;

View File

@@ -26,7 +26,7 @@ class StartDragonfly
$this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [ $this->commands = [
"echo 'Starting {$database->name}.'", "echo 'Starting database.'",
"mkdir -p $this->configuration_dir", "mkdir -p $this->configuration_dir",
]; ];

View File

@@ -27,7 +27,7 @@ class StartKeydb
$this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [ $this->commands = [
"echo 'Starting {$database->name}.'", "echo 'Starting database.'",
"mkdir -p $this->configuration_dir", "mkdir -p $this->configuration_dir",
]; ];

View File

@@ -24,7 +24,7 @@ class StartMariadb
$this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [ $this->commands = [
"echo 'Starting {$database->name}.'", "echo 'Starting database.'",
"mkdir -p $this->configuration_dir", "mkdir -p $this->configuration_dir",
]; ];

View File

@@ -30,7 +30,7 @@ class StartMongodb
} }
$this->commands = [ $this->commands = [
"echo 'Starting {$database->name}.'", "echo 'Starting database.'",
"mkdir -p $this->configuration_dir", "mkdir -p $this->configuration_dir",
]; ];

View File

@@ -24,7 +24,7 @@ class StartMysql
$this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [ $this->commands = [
"echo 'Starting {$database->name}.'", "echo 'Starting database.'",
"mkdir -p $this->configuration_dir", "mkdir -p $this->configuration_dir",
]; ];

View File

@@ -25,7 +25,7 @@ class StartPostgresql
$this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [ $this->commands = [
"echo 'Starting {$database->name}.'", "echo 'Starting database.'",
"mkdir -p $this->configuration_dir", "mkdir -p $this->configuration_dir",
"mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/", "mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/",
]; ];

View File

@@ -25,7 +25,7 @@ class StartRedis
$this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [ $this->commands = [
"echo 'Starting {$database->name}.'", "echo 'Starting database.'",
"mkdir -p $this->configuration_dir", "mkdir -p $this->configuration_dir",
]; ];

View File

@@ -18,6 +18,8 @@ class StopDatabaseProxy
{ {
use AsAction; use AsAction;
public string $jobQueue = 'high';
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|ServiceDatabase|StandaloneDragonfly|StandaloneClickhouse $database) public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|ServiceDatabase|StandaloneDragonfly|StandaloneClickhouse $database)
{ {
$server = data_get($database, 'destination.server'); $server = data_get($database, 'destination.server');

View File

@@ -7,7 +7,6 @@ use App\Actions\Shared\ComplexStatusCheck;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use App\Models\Server; use App\Models\Server;
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
use App\Notifications\Container\ContainerRestarted;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
@@ -16,6 +15,8 @@ class GetContainersStatus
{ {
use AsAction; use AsAction;
public string $jobQueue = 'high';
public $applications; public $applications;
public ?Collection $containers; public ?Collection $containers;
@@ -178,7 +179,7 @@ class GetContainersStatus
})->first(); })->first();
if (! $foundTcpProxy) { if (! $foundTcpProxy) {
StartDatabaseProxy::run($database); 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 { } else {

View File

@@ -30,7 +30,7 @@ class CheckProxy
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) { if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
return false; return false;
} }
['uptime' => $uptime, 'error' => $error] = $server->validateConnection(false); ['uptime' => $uptime, 'error' => $error] = $server->validateConnection();
if (! $uptime) { if (! $uptime) {
throw new \Exception($error); throw new \Exception($error);
} }

View File

@@ -9,6 +9,8 @@ class CleanupDocker
{ {
use AsAction; use AsAction;
public string $jobQueue = 'high';
public function handle(Server $server) public function handle(Server $server)
{ {
$settings = instanceSettings(); $settings = instanceSettings();

View File

@@ -51,7 +51,6 @@ class ServerCheck
$containerReplicates = null; $containerReplicates = null;
$this->isSentinel = true; $this->isSentinel = true;
} else { } else {
['containers' => $this->containers, 'containerReplicates' => $containerReplicates] = $this->server->getContainers(); ['containers' => $this->containers, 'containerReplicates' => $containerReplicates] = $this->server->getContainers();
// ServerStorageCheckJob::dispatch($this->server); // ServerStorageCheckJob::dispatch($this->server);
@@ -130,10 +129,10 @@ class ServerCheck
if ($foundLogDrainContainer) { if ($foundLogDrainContainer) {
$status = data_get($foundLogDrainContainer, 'State.Status'); $status = data_get($foundLogDrainContainer, 'State.Status');
if ($status !== 'running') { if ($status !== 'running') {
StartLogDrain::dispatch($this->server)->onQueue('high'); StartLogDrain::dispatch($this->server);
} }
} else { } else {
StartLogDrain::dispatch($this->server)->onQueue('high'); StartLogDrain::dispatch($this->server);
} }
} }
@@ -148,7 +147,6 @@ class ServerCheck
} else { } else {
$labels = Arr::undot(data_get($container, 'Config.Labels')); $labels = Arr::undot(data_get($container, 'Config.Labels'));
} }
} }
$managed = data_get($labels, 'coolify.managed'); $managed = data_get($labels, 'coolify.managed');
if (! $managed) { if (! $managed) {
@@ -259,7 +257,7 @@ class ServerCheck
})->first(); })->first();
if (! $foundTcpProxy) { if (! $foundTcpProxy) {
StartDatabaseProxy::run($database); 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));
} }
} }
} }

View File

@@ -9,6 +9,8 @@ class StartLogDrain
{ {
use AsAction; use AsAction;
public string $jobQueue = 'high';
public function handle(Server $server) public function handle(Server $server)
{ {
if ($server->settings->is_logdrain_newrelic_enabled) { if ($server->settings->is_logdrain_newrelic_enabled) {

View File

@@ -29,9 +29,9 @@ class UpdateCoolify
if (! $this->server) { if (! $this->server) {
return; return;
} }
CleanupDocker::dispatch($this->server)->onQueue('high'); CleanupDocker::dispatch($this->server);
$this->latestVersion = get_latest_version_of_coolify(); $this->latestVersion = get_latest_version_of_coolify();
$this->currentVersion = config('version'); $this->currentVersion = config('constants.coolify.version');
if (! $manual_update) { if (! $manual_update) {
if (! $settings->is_auto_update_enabled) { if (! $settings->is_auto_update_enabled) {
return; return;

View File

@@ -9,6 +9,8 @@ class ValidateServer
{ {
use AsAction; use AsAction;
public string $jobQueue = 'high';
public ?string $uptime = null; public ?string $uptime = null;
public ?string $error = null; public ?string $error = null;

View File

@@ -9,6 +9,8 @@ class RestartService
{ {
use AsAction; use AsAction;
public string $jobQueue = 'high';
public function handle(Service $service) public function handle(Service $service)
{ {
StopService::run($service); StopService::run($service);

View File

@@ -10,6 +10,8 @@ class StartService
{ {
use AsAction; use AsAction;
public string $jobQueue = 'high';
public function handle(Service $service) public function handle(Service $service)
{ {
$service->saveComposeConfigs(); $service->saveComposeConfigs();

View File

@@ -10,6 +10,8 @@ class StopService
{ {
use AsAction; use AsAction;
public string $jobQueue = 'high';
public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true) public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true)
{ {
try { try {

View File

@@ -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 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'))) { 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([ $team->subscription->update([
'stripe_invoice_paid' => false, 'stripe_invoice_paid' => false,
@@ -61,9 +61,9 @@ class CloudCleanupSubscriptions extends Command
$this->info('Subscription id: '.data_get($team, 'subscription.stripe_subscription_id')); $this->info('Subscription id: '.data_get($team, 'subscription.stripe_subscription_id'));
$confirm = $this->confirm('Do you want to cancel the subscription?', true); $confirm = $this->confirm('Do you want to cancel the subscription?', true);
if (! $confirm) { if (! $confirm) {
$this->info("Skipping team {$team->id} {$team->name}"); $this->info("Skipping team {$team->id}");
} else { } else {
$this->info("Cancelling subscription for team {$team->id} {$team->name}"); $this->info("Cancelling subscription for team {$team->id}");
$team->subscription->update([ $team->subscription->update([
'stripe_invoice_paid' => false, 'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => false, 'stripe_trial_already_ended' => false,

View File

@@ -187,7 +187,7 @@ class Emails extends Command
'team_id' => 0, 'team_id' => 0,
]); ]);
} }
$this->mail = (new BackupSuccess($backup, $db))->toMail(); // $this->mail = (new BackupSuccess($backup->frequency, $db->name))->toMail();
$this->sendEmail(); $this->sendEmail();
break; break;
// case 'invitation-link': // case 'invitation-link':

View File

@@ -200,7 +200,7 @@ class Init extends Command
private function restore_coolify_db_backup() 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 { try {
$database = StandalonePostgresql::withTrashed()->find(0); $database = StandalonePostgresql::withTrashed()->find(0);
if ($database && $database->trashed()) { if ($database && $database->trashed()) {
@@ -228,7 +228,7 @@ class Init extends Command
private function send_alive_signal() private function send_alive_signal()
{ {
$id = config('app.id'); $id = config('app.id');
$version = config('version'); $version = config('constants.coolify.version');
$settings = instanceSettings(); $settings = instanceSettings();
$do_not_track = data_get($settings, 'do_not_track'); $do_not_track = data_get($settings, 'do_not_track');
if ($do_not_track == true) { if ($do_not_track == true) {
@@ -264,7 +264,7 @@ class Init extends Command
private function replace_slash_in_environment_name() 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(); $environments = Environment::all();
foreach ($environments as $environment) { foreach ($environments as $environment) {
if (str_contains($environment->name, '/')) { if (str_contains($environment->name, '/')) {

View File

@@ -96,7 +96,7 @@ class ServicesDelete extends Command
if (! $confirmed) { if (! $confirmed) {
break; break;
} }
DeleteResourceJob::dispatch($toDelete)->onQueue('high'); DeleteResourceJob::dispatch($toDelete);
} }
} }
} }
@@ -122,7 +122,7 @@ class ServicesDelete extends Command
if (! $confirmed) { if (! $confirmed) {
return; return;
} }
DeleteResourceJob::dispatch($toDelete)->onQueue('high'); DeleteResourceJob::dispatch($toDelete);
} }
} }
} }
@@ -148,7 +148,7 @@ class ServicesDelete extends Command
if (! $confirmed) { if (! $confirmed) {
return; return;
} }
DeleteResourceJob::dispatch($toDelete)->onQueue('high'); DeleteResourceJob::dispatch($toDelete);
} }
} }
} }

View File

@@ -20,7 +20,10 @@ class ServicesGenerate extends Command
public function handle(): int 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 { ->mapWithKeys(function ($file): array {
$file = basename($file); $file = basename($file);
$parsed = $this->processFile($file); $parsed = $this->processFile($file);
@@ -68,7 +71,7 @@ class ServicesGenerate extends Command
'slogan' => $data->get('slogan', str($file)->headline()), 'slogan' => $data->get('slogan', str($file)->headline()),
'compose' => $compose, 'compose' => $compose,
'tags' => $tags, 'tags' => $tags,
'logo' => $data->get('logo', 'svgs/coolify.png'), 'logo' => $data->get('logo', 'svgs/default.webp'),
'minversion' => $data->get('minversion', '0.0.0'), 'minversion' => $data->get('minversion', '0.0.0'),
]; ];

View File

@@ -50,7 +50,7 @@ class Kernel extends ConsoleKernel
$this->instanceTimezone = config('app.timezone'); $this->instanceTimezone = config('app.timezone');
} }
$this->scheduleInstance->job(new CleanupStaleMultiplexedConnections)->hourly(); // $this->scheduleInstance->job(new CleanupStaleMultiplexedConnections)->hourly();
if (isDev()) { if (isDev()) {
// Instance Jobs // Instance Jobs
@@ -132,7 +132,7 @@ class Kernel extends ConsoleKernel
} }
foreach ($servers as $server) { foreach ($servers as $server) {
$serverTimezone = $server->settings->server_timezone; $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
// Sentinel check // Sentinel check
$lastSentinelUpdate = $server->sentinel_updated_at; $lastSentinelUpdate = $server->sentinel_updated_at;
@@ -141,8 +141,12 @@ class Kernel extends ConsoleKernel
if (validate_timezone($serverTimezone) === false) { if (validate_timezone($serverTimezone) === false) {
$serverTimezone = config('app.timezone'); $serverTimezone = config('app.timezone');
} }
$this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyMinute()->onOneServer(); if (isCloud()) {
// $this->scheduleInstance->job(new \App\Jobs\ServerCheckNewJob($server))->everyMinute()->onOneServer(); $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 // Check storage usage every 10 minutes if Sentinel does not activated
$this->scheduleInstance->job(new ServerStorageCheckJob($server))->everyTenMinutes()->onOneServer(); $this->scheduleInstance->job(new ServerStorageCheckJob($server))->everyTenMinutes()->onOneServer();
@@ -154,7 +158,7 @@ class Kernel extends ConsoleKernel
} }
// Cleanup multiplexed connections every hour // 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 // Temporary solution until we have better memory management for Sentinel
if ($server->isSentinelEnabled()) { if ($server->isSentinelEnabled()) {

View File

@@ -18,7 +18,7 @@ class DatabaseProxyStopped implements ShouldBroadcast
public function __construct($teamId = null) public function __construct($teamId = null)
{ {
if (is_null($teamId)) { if (is_null($teamId)) {
$teamId = Auth::user()->currentTeam()->id ?? null; $teamId = Auth::user()?->currentTeam()?->id ?? null;
} }
if (is_null($teamId)) { if (is_null($teamId)) {
throw new \Exception('Team id is null'); throw new \Exception('Team id is null');

View File

@@ -21,17 +21,14 @@ class SshMultiplexingHelper
]; ];
} }
public static function ensureMultiplexedConnection(Server $server) public static function ensureMultiplexedConnection(Server $server): bool
{ {
if (! self::isMultiplexingEnabled()) { if (! self::isMultiplexingEnabled()) {
return; return false;
} }
$sshConfig = self::serverSshConfiguration($server); $sshConfig = self::serverSshConfiguration($server);
$muxSocket = $sshConfig['muxFilename']; $muxSocket = $sshConfig['muxFilename'];
$sshKeyLocation = $sshConfig['sshKeyLocation'];
self::validateSshKey($sshKeyLocation);
$checkCommand = "ssh -O check -o ControlPath=$muxSocket "; $checkCommand = "ssh -O check -o ControlPath=$muxSocket ";
if (data_get($server, 'settings.is_cloudflare_tunnel')) { if (data_get($server, 'settings.is_cloudflare_tunnel')) {
@@ -41,16 +38,17 @@ class SshMultiplexingHelper
$process = Process::run($checkCommand); $process = Process::run($checkCommand);
if ($process->exitCode() !== 0) { 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); $sshConfig = self::serverSshConfiguration($server);
$sshKeyLocation = $sshConfig['sshKeyLocation']; $sshKeyLocation = $sshConfig['sshKeyLocation'];
$muxSocket = $sshConfig['muxFilename']; $muxSocket = $sshConfig['muxFilename'];
$connectionTimeout = config('constants.ssh.connection_timeout'); $connectionTimeout = config('constants.ssh.connection_timeout');
$serverInterval = config('constants.ssh.server_interval'); $serverInterval = config('constants.ssh.server_interval');
$muxPersistTime = config('constants.ssh.mux_persist_time'); $muxPersistTime = config('constants.ssh.mux_persist_time');
@@ -60,15 +58,14 @@ class SshMultiplexingHelper
if (data_get($server, 'settings.is_cloudflare_tunnel')) { if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$establishCommand .= ' -o ProxyCommand="cloudflared access ssh --hostname %h" '; $establishCommand .= ' -o ProxyCommand="cloudflared access ssh --hostname %h" ';
} }
$establishCommand .= self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval); $establishCommand .= self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval);
$establishCommand .= "{$server->user}@{$server->ip}"; $establishCommand .= "{$server->user}@{$server->ip}";
$establishProcess = Process::run($establishCommand); $establishProcess = Process::run($establishCommand);
if ($establishProcess->exitCode() !== 0) { if ($establishProcess->exitCode() !== 0) {
throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput()); return false;
} }
return true;
} }
public static function removeMuxFile(Server $server) public static function removeMuxFile(Server $server)
@@ -97,9 +94,8 @@ class SshMultiplexingHelper
if ($server->isIpv6()) { if ($server->isIpv6()) {
$scp_command .= '-6 '; $scp_command .= '-6 ';
} }
if (self::isMultiplexingEnabled()) { if (self::isMultiplexingEnabled() && self::ensureMultiplexedConnection($server)) {
$scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; $scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
self::ensureMultiplexedConnection($server);
} }
if (data_get($server, 'settings.is_cloudflare_tunnel')) { if (data_get($server, 'settings.is_cloudflare_tunnel')) {
@@ -120,6 +116,9 @@ class SshMultiplexingHelper
$sshConfig = self::serverSshConfiguration($server); $sshConfig = self::serverSshConfiguration($server);
$sshKeyLocation = $sshConfig['sshKeyLocation']; $sshKeyLocation = $sshConfig['sshKeyLocation'];
self::validateSshKey($server->privateKey);
$muxSocket = $sshConfig['muxFilename']; $muxSocket = $sshConfig['muxFilename'];
$timeout = config('constants.ssh.command_timeout'); $timeout = config('constants.ssh.command_timeout');
@@ -127,9 +126,8 @@ class SshMultiplexingHelper
$ssh_command = "timeout $timeout ssh "; $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} "; $ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
self::ensureMultiplexedConnection($server);
} }
if (data_get($server, 'settings.is_cloudflare_tunnel')) { 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'); 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); $keyCheckProcess = Process::run($checkKeyCommand);
if ($keyCheckProcess->exitCode() !== 0) { if ($keyCheckProcess->exitCode() !== 0) {
throw new \RuntimeException("SSH key file not accessible: $sshKeyLocation"); $privateKey->storeInFileSystem();
} }
} }

View File

@@ -1224,7 +1224,7 @@ class ApplicationsController extends Controller
$service->name = "service-$service->uuid"; $service->name = "service-$service->uuid";
$service->parse(isNew: true); $service->parse(isNew: true);
if ($instantDeploy) { if ($instantDeploy) {
StartService::dispatch($service)->onQueue('high'); StartService::dispatch($service);
} }
return response()->json(serializeApiResponse([ return response()->json(serializeApiResponse([
@@ -1379,7 +1379,7 @@ class ApplicationsController extends Controller
deleteVolumes: $request->query->get('delete_volumes', true), deleteVolumes: $request->query->get('delete_volumes', true),
dockerCleanup: $request->query->get('docker_cleanup', true), dockerCleanup: $request->query->get('docker_cleanup', true),
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
)->onQueue('high'); );
return response()->json([ return response()->json([
'message' => 'Application deletion request queued.', 'message' => 'Application deletion request queued.',
@@ -1591,16 +1591,32 @@ class ApplicationsController extends Controller
} }
$domains = $request->domains; $domains = $request->domains;
if ($request->has('domains') && $server->isProxyShouldRun()) { if ($request->has('domains') && $server->isProxyShouldRun()) {
$errors = []; $uuid = $request->uuid;
$fqdn = $request->domains; $fqdn = $request->domains;
$fqdn = str($fqdn)->replaceEnd(',', '')->trim(); $fqdn = str($fqdn)->replaceEnd(',', '')->trim();
$fqdn = str($fqdn)->replaceStart(',', '')->trim(); $fqdn = str($fqdn)->replaceStart(',', '')->trim();
$application->fqdn = $fqdn; $errors = [];
if (! $application->settings->is_container_label_readonly_enabled) { $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
$customLabels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); $domain = trim($domain);
$application->custom_labels = base64_encode($customLabels); 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(); $dockerComposeDomainsJson = collect();
@@ -2523,7 +2539,7 @@ class ApplicationsController extends Controller
if (! $application) { if (! $application) {
return response()->json(['message' => 'Application not found.'], 404); return response()->json(['message' => 'Application not found.'], 404);
} }
StopApplication::dispatch($application)->onQueue('high'); StopApplication::dispatch($application);
return response()->json( 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);
}
}
}
}

View File

@@ -211,8 +211,9 @@ class DatabasesController extends Controller
'mongo_conf' => ['type' => 'string', 'description' => 'Mongo conf'], 'mongo_conf' => ['type' => 'string', 'description' => 'Mongo conf'],
'mongo_initdb_root_username' => ['type' => 'string', 'description' => 'Mongo initdb root username'], 'mongo_initdb_root_username' => ['type' => 'string', 'description' => 'Mongo initdb root username'],
'mongo_initdb_root_password' => ['type' => 'string', 'description' => 'Mongo initdb root password'], '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_root_password' => ['type' => 'string', 'description' => 'MySQL root password'],
'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'],
'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'], 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'],
'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'], 'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'],
'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'], 'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'],
@@ -241,7 +242,7 @@ class DatabasesController extends Controller
)] )]
public function update_by_uuid(Request $request) 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(); $teamId = getTeamIdFromToken();
if (is_null($teamId)) { if (is_null($teamId)) {
return invalidTokenResponse(); return invalidTokenResponse();
@@ -413,12 +414,12 @@ class DatabasesController extends Controller
} }
break; break;
case 'standalone-mongodb': 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(), [ $validator = customApiValidator($request->all(), [
'mongo_conf' => 'string', 'mongo_conf' => 'string',
'mongo_initdb_root_username' => 'string', 'mongo_initdb_root_username' => 'string',
'mongo_initdb_root_password' => 'string', 'mongo_initdb_root_password' => 'string',
'mongo_initdb_init_database' => 'string', 'mongo_initdb_database' => 'string',
]); ]);
if ($request->has('mongo_conf')) { if ($request->has('mongo_conf')) {
if (! isBase64Encoded($request->mongo_conf)) { if (! isBase64Encoded($request->mongo_conf)) {
@@ -443,9 +444,10 @@ class DatabasesController extends Controller
break; break;
case 'standalone-mysql': 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(), [ $validator = customApiValidator($request->all(), [
'mysql_root_password' => 'string', 'mysql_root_password' => 'string',
'mysql_password' => 'string',
'mysql_user' => 'string', 'mysql_user' => 'string',
'mysql_database' => 'string', 'mysql_database' => 'string',
'mysql_conf' => 'string', 'mysql_conf' => 'string',
@@ -497,9 +499,9 @@ class DatabasesController extends Controller
$database->update($request->all()); $database->update($request->all());
if ($whatToDoWithDatabaseProxy === 'start') { if ($whatToDoWithDatabaseProxy === 'start') {
StartDatabaseProxy::dispatch($database)->onQueue('high'); StartDatabaseProxy::dispatch($database);
} elseif ($whatToDoWithDatabaseProxy === 'stop') { } elseif ($whatToDoWithDatabaseProxy === 'stop') {
StopDatabaseProxy::dispatch($database)->onQueue('high'); StopDatabaseProxy::dispatch($database);
} }
return response()->json([ return response()->json([
@@ -909,6 +911,7 @@ class DatabasesController extends Controller
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], '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_root_password' => ['type' => 'string', 'description' => 'MySQL root password'],
'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'],
'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'], 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'],
'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'], 'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'],
'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'], 'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'],
@@ -1013,7 +1016,7 @@ class DatabasesController extends Controller
public function create_database(Request $request, NewDatabaseTypes $type) 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(); $teamId = getTeamIdFromToken();
if (is_null($teamId)) { if (is_null($teamId)) {
@@ -1151,7 +1154,7 @@ class DatabasesController extends Controller
} }
$database = create_standalone_postgresql($environment->id, $destination->uuid, $request->all()); $database = create_standalone_postgresql($environment->id, $destination->uuid, $request->all());
if ($instantDeploy) { if ($instantDeploy) {
StartDatabase::dispatch($database)->onQueue('high'); StartDatabase::dispatch($database);
} }
$database->refresh(); $database->refresh();
$payload = [ $payload = [
@@ -1206,7 +1209,7 @@ class DatabasesController extends Controller
} }
$database = create_standalone_mariadb($environment->id, $destination->uuid, $request->all()); $database = create_standalone_mariadb($environment->id, $destination->uuid, $request->all());
if ($instantDeploy) { if ($instantDeploy) {
StartDatabase::dispatch($database)->onQueue('high'); StartDatabase::dispatch($database);
} }
$database->refresh(); $database->refresh();
@@ -1220,9 +1223,10 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201); return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::MYSQL) { } 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(), [ $validator = customApiValidator($request->all(), [
'mysql_root_password' => 'string', 'mysql_root_password' => 'string',
'mysql_password' => 'string',
'mysql_user' => 'string', 'mysql_user' => 'string',
'mysql_database' => 'string', 'mysql_database' => 'string',
'mysql_conf' => 'string', 'mysql_conf' => 'string',
@@ -1264,7 +1268,7 @@ class DatabasesController extends Controller
} }
$database = create_standalone_mysql($environment->id, $destination->uuid, $request->all()); $database = create_standalone_mysql($environment->id, $destination->uuid, $request->all());
if ($instantDeploy) { if ($instantDeploy) {
StartDatabase::dispatch($database)->onQueue('high'); StartDatabase::dispatch($database);
} }
$database->refresh(); $database->refresh();
@@ -1320,7 +1324,7 @@ class DatabasesController extends Controller
} }
$database = create_standalone_redis($environment->id, $destination->uuid, $request->all()); $database = create_standalone_redis($environment->id, $destination->uuid, $request->all());
if ($instantDeploy) { if ($instantDeploy) {
StartDatabase::dispatch($database)->onQueue('high'); StartDatabase::dispatch($database);
} }
$database->refresh(); $database->refresh();
@@ -1357,7 +1361,7 @@ class DatabasesController extends Controller
removeUnnecessaryFieldsFromRequest($request); removeUnnecessaryFieldsFromRequest($request);
$database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->all()); $database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->all());
if ($instantDeploy) { if ($instantDeploy) {
StartDatabase::dispatch($database)->onQueue('high'); StartDatabase::dispatch($database);
} }
return response()->json(serializeApiResponse([ return response()->json(serializeApiResponse([
@@ -1406,7 +1410,7 @@ class DatabasesController extends Controller
} }
$database = create_standalone_keydb($environment->id, $destination->uuid, $request->all()); $database = create_standalone_keydb($environment->id, $destination->uuid, $request->all());
if ($instantDeploy) { if ($instantDeploy) {
StartDatabase::dispatch($database)->onQueue('high'); StartDatabase::dispatch($database);
} }
$database->refresh(); $database->refresh();
@@ -1442,7 +1446,7 @@ class DatabasesController extends Controller
removeUnnecessaryFieldsFromRequest($request); removeUnnecessaryFieldsFromRequest($request);
$database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->all()); $database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->all());
if ($instantDeploy) { if ($instantDeploy) {
StartDatabase::dispatch($database)->onQueue('high'); StartDatabase::dispatch($database);
} }
$database->refresh(); $database->refresh();
@@ -1456,12 +1460,12 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201); return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::MONGODB) { } 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(), [ $validator = customApiValidator($request->all(), [
'mongo_conf' => 'string', 'mongo_conf' => 'string',
'mongo_initdb_root_username' => 'string', 'mongo_initdb_root_username' => 'string',
'mongo_initdb_root_password' => 'string', 'mongo_initdb_root_password' => 'string',
'mongo_initdb_init_database' => 'string', 'mongo_initdb_database' => 'string',
]); ]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields); $extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) { if ($validator->fails() || ! empty($extraFields)) {
@@ -1500,7 +1504,7 @@ class DatabasesController extends Controller
} }
$database = create_standalone_mongodb($environment->id, $destination->uuid, $request->all()); $database = create_standalone_mongodb($environment->id, $destination->uuid, $request->all());
if ($instantDeploy) { if ($instantDeploy) {
StartDatabase::dispatch($database)->onQueue('high'); StartDatabase::dispatch($database);
} }
$database->refresh(); $database->refresh();
@@ -1557,7 +1561,8 @@ class DatabasesController extends Controller
] ]
) )
), ),
]), ]
),
new OA\Response( new OA\Response(
response: 401, response: 401,
ref: '#/components/responses/401', ref: '#/components/responses/401',
@@ -1593,7 +1598,7 @@ class DatabasesController extends Controller
deleteVolumes: $request->query->get('delete_volumes', true), deleteVolumes: $request->query->get('delete_volumes', true),
dockerCleanup: $request->query->get('docker_cleanup', true), dockerCleanup: $request->query->get('docker_cleanup', true),
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
)->onQueue('high'); );
return response()->json([ return response()->json([
'message' => 'Database deletion request queued.', 'message' => 'Database deletion request queued.',
@@ -1632,9 +1637,11 @@ class DatabasesController extends Controller
type: 'object', type: 'object',
properties: [ properties: [
'message' => ['type' => 'string', 'example' => 'Database starting request queued.'], 'message' => ['type' => 'string', 'example' => 'Database starting request queued.'],
]) ]
)
), ),
]), ]
),
new OA\Response( new OA\Response(
response: 401, response: 401,
ref: '#/components/responses/401', ref: '#/components/responses/401',
@@ -1666,7 +1673,7 @@ class DatabasesController extends Controller
if (str($database->status)->contains('running')) { if (str($database->status)->contains('running')) {
return response()->json(['message' => 'Database is already running.'], 400); return response()->json(['message' => 'Database is already running.'], 400);
} }
StartDatabase::dispatch($database)->onQueue('high'); StartDatabase::dispatch($database);
return response()->json( return response()->json(
[ [
@@ -1708,9 +1715,11 @@ class DatabasesController extends Controller
type: 'object', type: 'object',
properties: [ properties: [
'message' => ['type' => 'string', 'example' => 'Database stopping request queued.'], 'message' => ['type' => 'string', 'example' => 'Database stopping request queued.'],
]) ]
)
), ),
]), ]
),
new OA\Response( new OA\Response(
response: 401, response: 401,
ref: '#/components/responses/401', ref: '#/components/responses/401',
@@ -1742,7 +1751,7 @@ class DatabasesController extends Controller
if (str($database->status)->contains('stopped') || str($database->status)->contains('exited')) { if (str($database->status)->contains('stopped') || str($database->status)->contains('exited')) {
return response()->json(['message' => 'Database is already stopped.'], 400); return response()->json(['message' => 'Database is already stopped.'], 400);
} }
StopDatabase::dispatch($database)->onQueue('high'); StopDatabase::dispatch($database);
return response()->json( return response()->json(
[ [
@@ -1784,9 +1793,11 @@ class DatabasesController extends Controller
type: 'object', type: 'object',
properties: [ properties: [
'message' => ['type' => 'string', 'example' => 'Database restaring request queued.'], 'message' => ['type' => 'string', 'example' => 'Database restaring request queued.'],
]) ]
)
), ),
]), ]
),
new OA\Response( new OA\Response(
response: 401, response: 401,
ref: '#/components/responses/401', ref: '#/components/responses/401',
@@ -1815,7 +1826,7 @@ class DatabasesController extends Controller
if (! $database) { if (! $database) {
return response()->json(['message' => 'Database not found.'], 404); return response()->json(['message' => 'Database not found.'], 404);
} }
RestartDatabase::dispatch($database)->onQueue('high'); RestartDatabase::dispatch($database);
return response()->json( return response()->json(
[ [

View File

@@ -307,7 +307,7 @@ class DeployController extends Controller
break; break;
default: default:
// Database resource // Database resource
StartDatabase::dispatch($resource)->onQueue('high'); StartDatabase::dispatch($resource);
$resource->update([ $resource->update([
'started_at' => now(), 'started_at' => now(),
]); ]);

View File

@@ -37,7 +37,7 @@ class OtherController extends Controller
)] )]
public function version(Request $request) public function version(Request $request)
{ {
return response(config('version')); return response(config('constants.coolify.version'));
} }
#[OA\Get( #[OA\Get(

View File

@@ -550,7 +550,7 @@ class ServersController extends Controller
'is_build_server' => $request->is_build_server, 'is_build_server' => $request->is_build_server,
]); ]);
if ($request->instant_validate) { if ($request->instant_validate) {
ValidateServer::dispatch($server)->onQueue('high'); ValidateServer::dispatch($server);
} }
return response()->json([ return response()->json([
@@ -567,6 +567,9 @@ class ServersController extends Controller
['bearerAuth' => []], ['bearerAuth' => []],
], ],
tags: ['Servers'], 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( requestBody: new OA\RequestBody(
required: true, required: true,
description: 'Server updated.', description: 'Server updated.',
@@ -596,8 +599,7 @@ class ServersController extends Controller
new OA\MediaType( new OA\MediaType(
mediaType: 'application/json', mediaType: 'application/json',
schema: new OA\Schema( schema: new OA\Schema(
type: 'array', ref: '#/components/schemas/Server'
items: new OA\Items(ref: '#/components/schemas/Server')
) )
), ),
]), ]),
@@ -675,11 +677,11 @@ class ServersController extends Controller
]); ]);
} }
if ($request->instant_validate) { if ($request->instant_validate) {
ValidateServer::dispatch($server)->onQueue('high'); ValidateServer::dispatch($server);
} }
return response()->json([ return response()->json([
'uuid' => $server->uuid,
])->setStatusCode(201); ])->setStatusCode(201);
} }
@@ -813,7 +815,7 @@ class ServersController extends Controller
if (! $server) { if (! $server) {
return response()->json(['message' => 'Server not found.'], 404); return response()->json(['message' => 'Server not found.'], 404);
} }
ValidateServer::dispatch($server)->onQueue('high'); ValidateServer::dispatch($server);
return response()->json(['message' => 'Validation started.']); return response()->json(['message' => 'Validation started.']);
} }

View File

@@ -342,7 +342,7 @@ class ServicesController extends Controller
} }
$service->parse(isNew: true); $service->parse(isNew: true);
if ($instantDeploy) { if ($instantDeploy) {
StartService::dispatch($service)->onQueue('high'); StartService::dispatch($service);
} }
$domains = $service->applications()->get()->pluck('fqdn')->sort(); $domains = $service->applications()->get()->pluck('fqdn')->sort();
$domains = $domains->map(function ($domain) { $domains = $domains->map(function ($domain) {
@@ -487,7 +487,7 @@ class ServicesController extends Controller
deleteVolumes: $request->query->get('delete_volumes', true), deleteVolumes: $request->query->get('delete_volumes', true),
dockerCleanup: $request->query->get('docker_cleanup', true), dockerCleanup: $request->query->get('docker_cleanup', true),
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
)->onQueue('high'); );
return response()->json([ return response()->json([
'message' => 'Service deletion request queued.', 'message' => 'Service deletion request queued.',
@@ -1076,7 +1076,7 @@ class ServicesController extends Controller
if (str($service->status())->contains('running')) { if (str($service->status())->contains('running')) {
return response()->json(['message' => 'Service is already running.'], 400); return response()->json(['message' => 'Service is already running.'], 400);
} }
StartService::dispatch($service)->onQueue('high'); StartService::dispatch($service);
return response()->json( return response()->json(
[ [
@@ -1154,7 +1154,7 @@ class ServicesController extends Controller
if (str($service->status())->contains('stopped') || str($service->status())->contains('exited')) { if (str($service->status())->contains('stopped') || str($service->status())->contains('exited')) {
return response()->json(['message' => 'Service is already stopped.'], 400); return response()->json(['message' => 'Service is already stopped.'], 400);
} }
StopService::dispatch($service)->onQueue('high'); StopService::dispatch($service);
return response()->json( return response()->json(
[ [
@@ -1229,7 +1229,7 @@ class ServicesController extends Controller
if (! $service) { if (! $service) {
return response()->json(['message' => 'Service not found.'], 404); return response()->json(['message' => 'Service not found.'], 404);
} }
RestartService::dispatch($service)->onQueue('high'); RestartService::dispatch($service);
return response()->json( return response()->json(
[ [

View File

@@ -463,7 +463,7 @@ class Github extends Controller
$private_key = data_get($data, 'pem'); $private_key = data_get($data, 'pem');
$webhook_secret = data_get($data, 'webhook_secret'); $webhook_secret = data_get($data, 'webhook_secret');
$private_key = PrivateKey::create([ $private_key = PrivateKey::create([
'name' => $slug, 'name' => "github-app-{$slug}",
'private_key' => $private_key, 'private_key' => $private_key,
'team_id' => $github_app->team_id, 'team_id' => $github_app->team_id,
'is_git_related' => true, 'is_git_related' => true,

View File

@@ -3,21 +3,26 @@
namespace App\Http\Controllers\Webhook; namespace App\Http\Controllers\Webhook;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Jobs\ServerLimitCheckJob; use App\Jobs\StripeProcessJob;
use App\Jobs\SubscriptionInvoiceFailedJob;
use App\Models\Subscription;
use App\Models\Team;
use App\Models\Webhook; use App\Models\Webhook;
use Exception; use Exception;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class Stripe extends Controller class Stripe extends Controller
{ {
protected $webhook;
public function events(Request $request) public function events(Request $request)
{ {
try { try {
$webhookSecret = config('subscription.stripe_webhook_secret');
$signature = $request->header('Stripe-Signature');
$event = \Stripe\Webhook::constructEvent(
$request->getContent(),
$signature,
$webhookSecret
);
if (app()->isDownForMaintenance()) { if (app()->isDownForMaintenance()) {
$epoch = now()->valueOf(); $epoch = now()->valueOf();
$data = [ $data = [
@@ -33,241 +38,17 @@ class Stripe extends Controller
$json = json_encode($data); $json = json_encode($data);
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Stripe::events_stripe", $json); 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'); $this->webhook = Webhook::create([
$signature = $request->header('Stripe-Signature');
$excludedPlans = config('subscription.stripe_excluded_plans');
$event = \Stripe\Webhook::constructEvent(
$request->getContent(),
$signature,
$webhookSecret
);
$webhook = Webhook::create([
'type' => 'stripe', 'type' => 'stripe',
'payload' => $request->getContent(), 'payload' => $request->getContent(),
]); ]);
$type = data_get($event, 'type'); StripeProcessJob::dispatch($event);
$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}");
return response("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}", 400); return response('Webhook received. Cool cool cool cool cool.', 200);
}
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
}
} catch (Exception $e) { } catch (Exception $e) {
if ($type !== 'payment_intent.payment_failed') { $this->webhook->update([
send_internal_notification("Subscription webhook ($type) failed: ".$e->getMessage());
}
$webhook->update([
'status' => 'failed', 'status' => 'failed',
'failure_reason' => $e->getMessage(), 'failure_reason' => $e->getMessage(),
]); ]);

View File

@@ -140,6 +140,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private ?string $buildTarget = null; private ?string $buildTarget = null;
private bool $disableBuildCache = false;
private Collection $saved_outputs; private Collection $saved_outputs;
private ?string $full_healthcheck_url = null; private ?string $full_healthcheck_url = null;
@@ -166,6 +168,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(int $application_deployment_queue_id) public function __construct(int $application_deployment_queue_id)
{ {
$this->onQueue('high');
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id); $this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
$this->application = Application::find($this->application_deployment_queue->application_id); $this->application = Application::find($this->application_deployment_queue->application_id);
$this->build_pack = data_get($this->application, 'build_pack'); $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->pull_request_id = $this->application_deployment_queue->pull_request_id;
$this->commit = $this->application_deployment_queue->commit; $this->commit = $this->application_deployment_queue->commit;
$this->rollback = $this->application_deployment_queue->rollback; $this->rollback = $this->application_deployment_queue->rollback;
$this->disableBuildCache = $this->application->settings->disable_build_cache;
$this->force_rebuild = $this->application_deployment_queue->force_rebuild; $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->application_deployment_queue->restart_only;
$this->restart_only = $this->restart_only && $this->application->build_pack !== 'dockerimage' && $this->application->build_pack !== 'dockerfile'; $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; $this->only_this_server = $this->application_deployment_queue->only_this_server;
@@ -349,8 +357,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function post_deployment() private function post_deployment()
{ {
if ($this->server->isProxyShouldRun()) { if ($this->server->isProxyShouldRun()) {
GetContainersStatus::dispatch($this->server)->onQueue('high'); GetContainersStatus::dispatch($this->server);
// dispatch(new ContainerStatusJob($this->server));
} }
$this->next(ApplicationDeploymentStatus::FINISHED->value); $this->next(ApplicationDeploymentStatus::FINISHED->value);
if ($this->pull_request_id !== 0) { 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')); $composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id'));
$this->save_environment_variables(); $this->save_environment_variables();
if (! is_null($this->env_filename)) { if (! is_null($this->env_filename)) {
$services = collect($composeFile['services']); $services = collect(data_get($composeFile, 'services', []));
$services = $services->map(function ($service, $name) { $services = $services->map(function ($service, $name) {
$service['env_file'] = [$this->env_filename]; $service['env_file'] = [$this->env_filename];
@@ -1975,6 +1982,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->build_args = $this->build_args->implode(' '); $this->build_args = $this->build_args->implode(' ');
$this->application_deployment_queue->addLogEntry('----------------------------------------'); $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') { if ($this->application->build_pack === 'static') {
$this->application_deployment_queue->addLogEntry('Static deployment. Copying static assets to the image.'); $this->application_deployment_queue->addLogEntry('Static deployment. Copying static assets to the image.');
} else { } else {
@@ -2399,7 +2409,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
if (! $this->only_this_server) { if (! $this->only_this_server) {
$this->deploy_to_additional_destinations(); $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));
} }
} }

View File

@@ -25,7 +25,9 @@ class ApplicationPullRequestUpdateJob implements ShouldBeEncrypted, ShouldQueue
public ApplicationPreview $preview, public ApplicationPreview $preview,
public ProcessStatus $status, public ProcessStatus $status,
public ?string $deployment_uuid = null public ?string $deployment_uuid = null
) {} ) {
$this->onQueue('high');
}
public function handle() public function handle()
{ {

View File

@@ -27,7 +27,7 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
$versions = $response->json(); $versions = $response->json();
$latest_version = data_get($versions, 'coolify.v4.version'); $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, '>')) { if (version_compare($latest_version, $current_version, '>')) {
// New version available // New version available

View File

@@ -23,7 +23,10 @@ class CoolifyTask implements ShouldBeEncrypted, ShouldQueue
public bool $ignore_errors, public bool $ignore_errors,
public $call_event_on_finish, public $call_event_on_finish,
public $call_event_data, public $call_event_data,
) {} ) {
$this->onQueue('high');
}
/** /**
* Execute the job. * Execute the job.

View File

@@ -60,12 +60,15 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public function __construct($backup) public function __construct($backup)
{ {
$this->onQueue('high');
$this->backup = $backup; $this->backup = $backup;
} }
public function handle(): void public function handle(): void
{ {
try { try {
$databasesToBackup = null;
$this->team = Team::find($this->backup->team_id); $this->team = Team::find($this->backup->team_id);
if (! $this->team) { if (! $this->team) {
$this->backup->delete(); $this->backup->delete();
@@ -197,8 +200,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$databaseType = $this->database->type(); $databaseType = $this->database->type();
$databasesToBackup = data_get($this->backup, 'databases_to_backup'); $databasesToBackup = data_get($this->backup, 'databases_to_backup');
} }
if (blank($databasesToBackup)) {
if (is_null($databasesToBackup)) {
if (str($databaseType)->contains('postgres')) { if (str($databaseType)->contains('postgres')) {
$databasesToBackup = [$this->database->postgres_db]; $databasesToBackup = [$this->database->postgres_db];
} elseif (str($databaseType)->contains('mongodb')) { } elseif (str($databaseType)->contains('mongodb')) {
@@ -304,7 +306,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
if ($this->backup->save_s3) { if ($this->backup->save_s3) {
$this->upload_to_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([ $this->backup_log->update([
'status' => 'success', 'status' => 'success',
'message' => $this->backup_output, 'message' => $this->backup_output,
@@ -319,12 +321,10 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
'filename' => null, 'filename' => null,
]); ]);
} }
send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
$this->team?->notify(new BackupFailed($this->backup, $this->database, $this->backup_output, $database)); $this->team?->notify(new BackupFailed($this->backup, $this->database, $this->backup_output, $database));
} }
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
throw $e; throw $e;
} finally { } finally {
if ($this->team) { if ($this->team) {

View File

@@ -35,7 +35,9 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
public bool $deleteVolumes = true, public bool $deleteVolumes = true,
public bool $dockerCleanup = true, public bool $dockerCleanup = true,
public bool $deleteConnectedNetworks = true public bool $deleteConnectedNetworks = true
) {} ) {
$this->onQueue('high');
}
public function handle() public function handle()
{ {
@@ -87,7 +89,6 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
$this->resource?->delete_connected_networks($this->resource->uuid); $this->resource?->delete_connected_networks($this->resource->uuid);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
throw $e; throw $e;
} finally { } finally {
$this->resource->forceDelete(); $this->resource->forceDelete();

View File

@@ -16,7 +16,10 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 1000; public $timeout = 1000;
public function __construct(public Server $server) {} public function __construct(public Server $server)
{
$this->onQueue('high');
}
public function handle(): void public function handle(): void
{ {

View File

@@ -17,7 +17,10 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue
public $timeout = 10; public $timeout = 10;
public function __construct() {} public function __construct()
{
$this->onQueue('high');
}
public function handle(): void public function handle(): void
{ {

View File

@@ -360,7 +360,7 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
private function checkLogDrainContainer() private function checkLogDrainContainer()
{ {
if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) { if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) {
StartLogDrain::dispatch($this->server)->onQueue('high'); StartLogDrain::dispatch($this->server);
} }
} }
} }

View File

@@ -40,6 +40,8 @@ class ScheduledTaskJob implements ShouldQueue
public function __construct($task) public function __construct($task)
{ {
$this->onQueue('high');
$this->task = $task; $this->task = $task;
if ($service = $task->service()->first()) { if ($service = $task->service()->first()) {
$this->resource = $service; $this->resource = $service;

View File

@@ -32,7 +32,9 @@ class SendMessageToDiscordJob implements ShouldBeEncrypted, ShouldQueue
public function __construct( public function __construct(
public DiscordMessage $message, public DiscordMessage $message,
public string $webhookUrl public string $webhookUrl
) {} ) {
$this->onQueue('high');
}
/** /**
* Execute the job. * Execute the job.

View File

@@ -33,7 +33,9 @@ class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue
public string $token, public string $token,
public string $chatId, public string $chatId,
public ?string $topicId = null, public ?string $topicId = null,
) {} ) {
$this->onQueue('high');
}
/** /**
* Execute the job. * Execute the job.
@@ -70,7 +72,7 @@ class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue
} }
$response = Http::post($url, $payload); $response = Http::post($url, $payload);
if ($response->failed()) { 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());
} }
} }
} }

View File

@@ -31,7 +31,12 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()]; 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() public function handle()
{ {
@@ -94,10 +99,10 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
if ($foundLogDrainContainer) { if ($foundLogDrainContainer) {
$status = data_get($foundLogDrainContainer, 'State.Status'); $status = data_get($foundLogDrainContainer, 'State.Status');
if ($status !== 'running') { if ($status !== 'running') {
StartLogDrain::dispatch($this->server)->onQueue('high'); StartLogDrain::dispatch($this->server);
} }
} else { } else {
StartLogDrain::dispatch($this->server)->onQueue('high'); StartLogDrain::dispatch($this->server);
} }
} }
} }

View File

@@ -16,7 +16,10 @@ class ServerFilesFromServerJob implements ShouldBeEncrypted, ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; 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() public function handle()
{ {

View File

@@ -14,7 +14,10 @@ class ServerStorageSaveJob implements ShouldBeEncrypted, ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public LocalFileVolume $localFileVolume) {} public function __construct(public LocalFileVolume $localFileVolume)
{
$this->onQueue('high');
}
public function handle() public function handle()
{ {

View File

@@ -0,0 +1,246 @@
<?php
namespace App\Jobs;
use App\Models\Subscription;
use App\Models\Team;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Str;
class StripeProcessJob implements ShouldQueue
{
use Queueable;
public $type;
public $webhook;
public $tries = 3;
public function __construct(public $event)
{
$this->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());
}
}
}

View File

@@ -15,7 +15,10 @@ class SubscriptionInvoiceFailedJob implements ShouldBeEncrypted, ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(protected Team $team) {} public function __construct(protected Team $team)
{
$this->onQueue('high');
}
public function handle() public function handle()
{ {

View File

@@ -18,6 +18,11 @@ class UpdateCoolifyJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 600; public $timeout = 600;
public function __construct()
{
$this->onQueue('high');
}
public function handle(): void public function handle(): void
{ {
try { try {

View File

@@ -172,13 +172,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function getProxyType() public function getProxyType()
{ {
// Set Default Proxy Type
$this->selectProxy(ProxyTypes::TRAEFIK->value); $this->selectProxy(ProxyTypes::TRAEFIK->value);
// $proxyTypeSet = $this->createdServer->proxy->type;
// if (!$proxyTypeSet) {
// $this->currentState = 'select-proxy';
// return;
// }
$this->getProjects(); $this->getProjects();
} }
@@ -189,7 +183,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
return; 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->privateKey = $this->createdPrivateKey->private_key;
$this->currentState = 'create-server'; $this->currentState = 'create-server';
} }

View File

@@ -35,7 +35,7 @@ class Docker extends Component
$this->network = new Cuid2; $this->network = new Cuid2;
$this->servers = Server::isUsable()->get(); $this->servers = Server::isUsable()->get();
if ($server_id) { 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; $this->serverId = $this->selectedServer->id;
} else { } else {
$this->selectedServer = $this->servers->first(); $this->selectedServer = $this->servers->first();

View File

@@ -3,7 +3,6 @@
namespace App\Livewire; namespace App\Livewire;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use Illuminate\Container\Attributes\Auth as AttributesAuth;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
@@ -32,7 +31,7 @@ class NavbarDeleteTeam extends Component
$currentTeam->delete(); $currentTeam->delete();
$currentTeam->members->each(function ($user) use ($currentTeam) { $currentTeam->members->each(function ($user) use ($currentTeam) {
if ($user->id === AttributesAuth::id()) { if ($user->id === Auth::id()) {
return; return;
} }
$user->teams()->detach($currentTeam); $user->teams()->detach($currentTeam);

View File

@@ -37,7 +37,7 @@ class Email extends Component
#[Validate(['nullable', 'numeric'])] #[Validate(['nullable', 'numeric'])]
public ?int $smtpPort = null; public ?int $smtpPort = null;
#[Validate(['nullable', 'string'])] #[Validate(['nullable', 'string', 'in:tls,ssl,none'])]
public ?string $smtpEncryption = null; public ?string $smtpEncryption = null;
#[Validate(['nullable', 'string'])] #[Validate(['nullable', 'string'])]
@@ -73,6 +73,9 @@ class Email extends Component
#[Validate(['nullable', 'string'])] #[Validate(['nullable', 'string'])]
public ?string $resendApiKey = null; public ?string $resendApiKey = null;
#[Validate(['nullable', 'email'])]
public ?string $testEmailAddress = null;
public function mount() public function mount()
{ {
try { try {
@@ -132,14 +135,21 @@ class Email extends Component
} }
} }
public function sendTestNotification() public function sendTestEmail()
{ {
try { try {
$this->validate([
'testEmailAddress' => 'required|email',
], [
'testEmailAddress.required' => 'Test email address is required.',
'testEmailAddress.email' => 'Please enter a valid email address.',
]);
$executed = RateLimiter::attempt( $executed = RateLimiter::attempt(
'test-email:'.$this->team->id, 'test-email:'.$this->team->id,
$perMinute = 0, $perMinute = 0,
function () { function () {
$this->team?->notify(new Test($this->emails)); $this->team?->notify(new Test($this->testEmailAddress));
$this->dispatch('success', 'Test Email sent.'); $this->dispatch('success', 'Test Email sent.');
}, },
$decaySeconds = 10, $decaySeconds = 10,

View File

@@ -25,6 +25,9 @@ class Advanced extends Component
#[Validate(['boolean'])] #[Validate(['boolean'])]
public bool $isAutoDeployEnabled = true; public bool $isAutoDeployEnabled = true;
#[Validate(['boolean'])]
public bool $disableBuildCache = false;
#[Validate(['boolean'])] #[Validate(['boolean'])]
public bool $isLogDrainEnabled = false; public bool $isLogDrainEnabled = false;
@@ -95,6 +98,7 @@ class Advanced extends Component
$this->application->settings->is_stripprefix_enabled = $this->isStripprefixEnabled; $this->application->settings->is_stripprefix_enabled = $this->isStripprefixEnabled;
$this->application->settings->is_raw_compose_deployment_enabled = $this->isRawComposeDeploymentEnabled; $this->application->settings->is_raw_compose_deployment_enabled = $this->isRawComposeDeploymentEnabled;
$this->application->settings->connect_to_docker_network = $this->isConnectToDockerNetworkEnabled; $this->application->settings->connect_to_docker_network = $this->isConnectToDockerNetworkEnabled;
$this->application->settings->disable_build_cache = $this->disableBuildCache;
$this->application->settings->save(); $this->application->settings->save();
} else { } else {
$this->isForceHttpsEnabled = $this->application->isForceHttpsEnabled(); $this->isForceHttpsEnabled = $this->application->isForceHttpsEnabled();
@@ -116,6 +120,7 @@ class Advanced extends Component
$this->customInternalName = $this->application->settings->custom_internal_name; $this->customInternalName = $this->application->settings->custom_internal_name;
$this->isRawComposeDeploymentEnabled = $this->application->settings->is_raw_compose_deployment_enabled; $this->isRawComposeDeploymentEnabled = $this->application->settings->is_raw_compose_deployment_enabled;
$this->isConnectToDockerNetworkEnabled = $this->application->settings->connect_to_docker_network; $this->isConnectToDockerNetworkEnabled = $this->application->settings->connect_to_docker_network;
$this->disableBuildCache = $this->application->settings->disable_build_cache;
} }
} }

View File

@@ -16,24 +16,30 @@ class Configuration extends Component
public function mount() public function mount()
{ {
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); $project = currentTeam()
if (! $project) { ->projects()
return redirect()->route('dashboard'); ->select('id', 'uuid', 'team_id')
} ->where('uuid', request()->route('project_uuid'))
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']); ->firstOrFail();
if (! $environment) { $environment = $project->environments()
return redirect()->route('dashboard'); ->select('id', 'name', 'project_id')
} ->where('name', request()->route('environment_name'))
$application = $environment->applications->where('uuid', request()->route('application_uuid'))->first(); ->firstOrFail();
if (! $application) { $application = $environment->applications()
return redirect()->route('dashboard'); ->with(['destination'])
} ->where('uuid', request()->route('application_uuid'))
->firstOrFail();
$this->application = $application; $this->application = $application;
$mainServer = $this->application->destination->server; if ($application->destination && $application->destination->server) {
$servers = Server::ownedByCurrentTeam()->get(); $mainServer = $application->destination->server;
$this->servers = $servers->filter(function ($server) use ($mainServer) { $this->servers = Server::ownedByCurrentTeam()
return $server->id != $mainServer->id; ->select('id', 'name')
}); ->where('id', '!=', $mainServer->id)
->get();
} else {
$this->servers = collect();
}
} }
public function render() public function render()

View File

@@ -36,7 +36,11 @@ class Heading extends Component
public function mount() 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(); $lastDeployment = $this->application->get_last_successful_deployment();
$this->lastDeploymentInfo = data_get_str($lastDeployment, 'commit')->limit(7).' '.data_get($lastDeployment, 'commit_message'); $this->lastDeploymentInfo = data_get_str($lastDeployment, 'commit')->limit(7).' '.data_get($lastDeployment, 'commit_message');
$this->lastDeploymentLink = $this->application->gitCommitLink(data_get($lastDeployment, 'commit')); $this->lastDeploymentLink = $this->application->gitCommitLink(data_get($lastDeployment, 'commit'));
@@ -45,13 +49,11 @@ class Heading extends Component
public function check_status($showNotification = false) public function check_status($showNotification = false)
{ {
if ($this->application->destination->server->isFunctional()) { if ($this->application->destination->server->isFunctional()) {
GetContainersStatus::dispatch($this->application->destination->server)->onQueue('high'); GetContainersStatus::dispatch($this->application->destination->server);
} }
if ($showNotification) { if ($showNotification) {
$this->dispatch('success', 'Success', 'Application status updated.'); $this->dispatch('success', 'Success', 'Application status updated.');
} }
// Removed because it caused flickering
// $this->dispatch('configurationChanged');
} }
public function force_deploy_without_cache() public function force_deploy_without_cache()

File diff suppressed because one or more lines are too long

View File

@@ -168,18 +168,42 @@ class ExecuteContainerCommand extends Component
return; return;
} }
try { 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); $container = collect($this->containers)->firstWhere('container.Names', $this->selected_container);
if (is_null($container)) { if (is_null($container)) {
throw new \RuntimeException('Container not found.'); 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()) { if ($server->isForceDisabled()) {
throw new \RuntimeException('Server is disabled.'); 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( $this->dispatch(
'send-terminal-command', 'send-terminal-command',
isset($container), true,
data_get($container, 'container.Names'), data_get($container, 'container.Names'),
data_get($container, 'server.uuid') data_get($container, 'server.uuid')
); );

View File

@@ -29,11 +29,20 @@ class Terminal extends Component
$server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail(); $server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail();
if ($isContainer) { 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); $status = getContainerStatus($server, $identifier);
if ($status !== 'running') { if ($status !== 'running') {
return; 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 { } 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'); $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');
} }

View File

@@ -22,7 +22,7 @@ class Advanced extends Component
#[Validate('boolean')] #[Validate('boolean')]
public bool $forceDockerCleanup = false; public bool $forceDockerCleanup = false;
#[Validate('string')] #[Validate(['string', 'required'])]
public string $dockerCleanupFrequency = '*/10 * * * *'; public string $dockerCleanupFrequency = '*/10 * * * *';
#[Validate(['integer', 'min:1', 'max:99'])] #[Validate(['integer', 'min:1', 'max:99'])]
@@ -78,7 +78,6 @@ class Advanced extends Component
try { try {
$this->syncData(true); $this->syncData(true);
$this->dispatch('success', 'Server updated.'); $this->dispatch('success', 'Server updated.');
// $this->dispatch('refreshServerShow');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -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) { if ($toModel) {
$this->customValidation(); $this->customValidation();
$this->server->settings->is_logdrain_newrelic_enabled = $this->isLogDrainNewRelicEnabled; if ($type === 'newrelic') {
$this->server->settings->is_logdrain_axiom_enabled = $this->isLogDrainAxiomEnabled; $this->syncDataNewRelic($toModel);
$this->server->settings->is_logdrain_custom_enabled = $this->isLogDrainCustomEnabled; } elseif ($type === 'axiom') {
$this->syncDataAxiom($toModel);
$this->server->settings->logdrain_newrelic_license_key = $this->logDrainNewRelicLicenseKey; } elseif ($type === 'custom') {
$this->server->settings->logdrain_newrelic_base_uri = $this->logDrainNewRelicBaseUri; $this->syncDataCustom($toModel);
$this->server->settings->logdrain_axiom_dataset_name = $this->logDrainAxiomDatasetName; } else {
$this->server->settings->logdrain_axiom_api_key = $this->logDrainAxiomApiKey; $this->syncDataNewRelic($toModel);
$this->server->settings->logdrain_custom_config = $this->logDrainCustomConfig; $this->syncDataAxiom($toModel);
$this->server->settings->logdrain_custom_config_parser = $this->logDrainCustomConfigParser; $this->syncDataCustom($toModel);
}
$this->server->settings->save(); $this->server->settings->save();
} else { } else {
$this->isLogDrainNewRelicEnabled = $this->server->settings->is_logdrain_newrelic_enabled; if ($type === 'newrelic') {
$this->isLogDrainAxiomEnabled = $this->server->settings->is_logdrain_axiom_enabled; $this->syncDataNewRelic($toModel);
$this->isLogDrainCustomEnabled = $this->server->settings->is_logdrain_custom_enabled; } elseif ($type === 'axiom') {
$this->syncDataAxiom($toModel);
$this->logDrainNewRelicLicenseKey = $this->server->settings->logdrain_newrelic_license_key; } elseif ($type === 'custom') {
$this->logDrainNewRelicBaseUri = $this->server->settings->logdrain_newrelic_base_uri; $this->syncDataCustom($toModel);
$this->logDrainAxiomDatasetName = $this->server->settings->logdrain_axiom_dataset_name; } else {
$this->logDrainAxiomApiKey = $this->server->settings->logdrain_axiom_api_key; $this->syncDataNewRelic($toModel);
$this->logDrainCustomConfig = $this->server->settings->logdrain_custom_config; $this->syncDataAxiom($toModel);
$this->logDrainCustomConfigParser = $this->server->settings->logdrain_custom_config_parser; $this->syncDataCustom($toModel);
}
} }
} }
@@ -136,7 +176,7 @@ class LogDrains extends Component
public function submit(string $type) public function submit(string $type)
{ {
try { try {
$this->syncData(true); $this->syncData(true, $type);
$this->dispatch('success', 'Settings saved.'); $this->dispatch('success', 'Settings saved.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);

View File

@@ -5,7 +5,7 @@ namespace App\Livewire\Server;
use App\Actions\Server\StartSentinel; use App\Actions\Server\StartSentinel;
use App\Actions\Server\StopSentinel; use App\Actions\Server\StopSentinel;
use App\Models\Server; use App\Models\Server;
use Livewire\Attributes\Locked; use Livewire\Attributes\Computed;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
@@ -79,9 +79,6 @@ class Show extends Component
#[Validate(['required'])] #[Validate(['required'])]
public string $serverTimezone; public string $serverTimezone;
#[Locked]
public array $timezones;
public function getListeners() public function getListeners()
{ {
$teamId = auth()->user()->currentTeam()->id; $teamId = auth()->user()->currentTeam()->id;
@@ -96,13 +93,21 @@ class Show extends Component
{ {
try { try {
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
$this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
$this->syncData(); $this->syncData();
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
} }
#[Computed]
public function timezones(): array
{
return collect(timezone_identifiers_list())
->sort()
->values()
->toArray();
}
public function syncData(bool $toModel = false) public function syncData(bool $toModel = false)
{ {
if ($toModel) { if ($toModel) {

View File

@@ -7,7 +7,7 @@ use App\Models\InstanceSettings;
use App\Models\Server; use App\Models\Server;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\Locked; use Livewire\Attributes\Computed;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
@@ -17,9 +17,6 @@ class Index extends Component
protected Server $server; protected Server $server;
#[Locked]
public $timezones;
#[Validate('boolean')] #[Validate('boolean')]
public bool $is_auto_update_enabled; public bool $is_auto_update_enabled;
@@ -53,7 +50,7 @@ class Index extends Component
#[Validate('string')] #[Validate('string')]
public string $auto_update_frequency; public string $auto_update_frequency;
#[Validate('string')] #[Validate('string|required')]
public string $update_check_frequency; public string $update_check_frequency;
#[Validate('required|string|timezone')] #[Validate('required|string|timezone')]
@@ -101,14 +98,29 @@ class Index extends Component
$this->is_api_enabled = $this->settings->is_api_enabled; $this->is_api_enabled = $this->settings->is_api_enabled;
$this->auto_update_frequency = $this->settings->auto_update_frequency; $this->auto_update_frequency = $this->settings->auto_update_frequency;
$this->update_check_frequency = $this->settings->update_check_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->instance_timezone = $this->settings->instance_timezone;
$this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation; $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) 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->fqdn = $this->fqdn;
$this->settings->resale_license = $this->resale_license; $this->settings->resale_license = $this->resale_license;
$this->settings->public_port_min = $this->public_port_min; $this->settings->public_port_min = $this->public_port_min;

View File

@@ -19,7 +19,7 @@ class SettingsEmail extends Component
#[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])] #[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])]
public ?int $smtpPort = null; public ?int $smtpPort = null;
#[Validate(['nullable', 'string'])] #[Validate(['nullable', 'string', 'in:tls,ssl,none'])]
public ?string $smtpEncryption = null; public ?string $smtpEncryption = null;
#[Validate(['nullable', 'string'])] #[Validate(['nullable', 'string'])]

View File

@@ -4,6 +4,11 @@ namespace App\Livewire\Source\Github;
use App\Jobs\GithubAppPermissionJob; use App\Jobs\GithubAppPermissionJob;
use App\Models\GithubApp; 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; use Livewire\Component;
class Change extends Component class Change extends Component
@@ -51,12 +56,20 @@ class Change extends Component
'github_app.administration' => 'nullable|string', 'github_app.administration' => 'nullable|string',
]; ];
public function boot()
{
if ($this->github_app) {
$this->github_app->makeVisible(['client_secret', 'webhook_secret']);
}
}
public function checkPermissions() public function checkPermissions()
{ {
GithubAppPermissionJob::dispatchSync($this->github_app); GithubAppPermissionJob::dispatchSync($this->github_app);
$this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret'); $this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->dispatch('success', 'Github App permissions updated.'); $this->dispatch('success', 'Github App permissions updated.');
} }
// public function check() // public function check()
// { // {
@@ -90,15 +103,16 @@ class Change extends Component
// ray($runners_by_repository); // ray($runners_by_repository);
// } // }
public function mount() public function mount()
{ {
try { try {
$github_app_uuid = request()->github_app_uuid; $github_app_uuid = request()->github_app_uuid;
$this->github_app = GithubApp::ownedByCurrentTeam()->whereUuid($github_app_uuid)->firstOrFail(); $this->github_app = GithubApp::ownedByCurrentTeam()->whereUuid($github_app_uuid)->firstOrFail();
$this->github_app->makeVisible(['client_secret', 'webhook_secret']);
$this->applications = $this->github_app->applications; $this->applications = $this->github_app->applications;
$settings = instanceSettings(); $settings = instanceSettings();
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->name = str($this->github_app->name)->kebab(); $this->name = str($this->github_app->name)->kebab();
$this->fqdn = $settings->fqdn; $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() public function submit()
{ {
try { try {

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use App\Enums\ApplicationDeploymentStatus; use App\Enums\ApplicationDeploymentStatus;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Process\InvokedProcess; use Illuminate\Process\InvokedProcess;
@@ -104,7 +105,7 @@ use Visus\Cuid2\Cuid2;
class Application extends BaseModel class Application extends BaseModel
{ {
use SoftDeletes; use HasFactory, SoftDeletes;
private static $parserVersion = '4'; private static $parserVersion = '4';

View File

@@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Visus\Cuid2\Cuid2; 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')),
);
}
} }

View File

@@ -71,7 +71,7 @@ class EnvironmentVariable extends Model
} }
} }
$environment_variable->update([ $environment_variable->update([
'version' => config('version'), 'version' => config('constants.coolify.version'),
]); ]);
}); });
static::saving(function (EnvironmentVariable $environmentVariable) { static::saving(function (EnvironmentVariable $environmentVariable) {

View File

@@ -11,6 +11,7 @@ use App\Notifications\Server\Reachable;
use App\Notifications\Server\Unreachable; use App\Notifications\Server\Unreachable;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@@ -42,14 +43,13 @@ use Symfony\Component\Yaml\Yaml;
'validation_logs' => ['type' => 'string', 'description' => 'The validation logs.'], '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.'], '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.'], '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.'], 'settings' => ['$ref' => '#/components/schemas/ServerSetting'],
'delete_unused_networks' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused networks should be deleted.'],
] ]
)] )]
class Server extends BaseModel class Server extends BaseModel
{ {
use SchemalessAttributesTrait, SoftDeletes; use HasFactory, SchemalessAttributesTrait, SoftDeletes;
public static $batch_counter = 0; public static $batch_counter = 0;
@@ -606,7 +606,8 @@ $schema://$host {
} }
$memory = json_decode($memory, true); $memory = json_decode($memory, true);
$parsedCollection = collect($memory)->map(function ($metric) { $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(); return $parsedCollection->toArray();
@@ -809,7 +810,7 @@ $schema://$host {
{ {
return Attribute::make( return Attribute::make(
get: function ($value) { 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 public function status(): bool
{ {
['uptime' => $uptime] = $this->validateConnection(false); ['uptime' => $uptime] = $this->validateConnection();
if ($uptime === false) { if ($uptime === false) {
foreach ($this->applications() as $application) { foreach ($this->applications() as $application) {
$application->status = 'exited'; $application->status = 'exited';
@@ -1035,7 +1036,7 @@ $schema://$host {
$this->unreachable_notification_sent = false; $this->unreachable_notification_sent = false;
$this->save(); $this->save();
$this->refresh(); $this->refresh();
$this->team->notify(new Reachable($this)); // $this->team->notify(new Reachable($this));
} }
public function sendUnreachableNotification() public function sendUnreachableNotification()
@@ -1043,21 +1044,17 @@ $schema://$host {
$this->unreachable_notification_sent = true; $this->unreachable_notification_sent = true;
$this->save(); $this->save();
$this->refresh(); $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()) { if ($this->skipServer()) {
return ['uptime' => false, 'error' => 'Server skipped.']; return ['uptime' => false, 'error' => 'Server skipped.'];
} }
try { try {
// Make sure the private key is stored
if ($this->privateKey) {
$this->privateKey->storeInFileSystem();
}
instant_remote_process(['ls /'], $this); instant_remote_process(['ls /'], $this);
if ($this->settings->is_reachable === false) { if ($this->settings->is_reachable === false) {
$this->settings->is_reachable = true; $this->settings->is_reachable = true;

View File

@@ -45,6 +45,8 @@ use OpenApi\Attributes as OA;
'wildcard_domain' => ['type' => 'string'], 'wildcard_domain' => ['type' => 'string'],
'created_at' => ['type' => 'string'], 'created_at' => ['type' => 'string'],
'updated_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 class ServerSetting extends Model

View File

@@ -4,18 +4,12 @@ namespace App\Notifications\Application;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; 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 Application $application;
public ?ApplicationPreview $preview = null; 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) public function __construct(Application $application, string $deployment_uuid, ?ApplicationPreview $preview = null)
{ {
$this->onQueue('high');
$this->application = $application; $this->application = $application;
$this->deployment_uuid = $deployment_uuid; $this->deployment_uuid = $deployment_uuid;
$this->preview = $preview; $this->preview = $preview;

View File

@@ -4,18 +4,12 @@ namespace App\Notifications\Application;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; 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 Application $application;
public ?ApplicationPreview $preview = null; 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) public function __construct(Application $application, string $deployment_uuid, ?ApplicationPreview $preview = null)
{ {
$this->onQueue('high');
$this->application = $application; $this->application = $application;
$this->deployment_uuid = $deployment_uuid; $this->deployment_uuid = $deployment_uuid;
$this->preview = $preview; $this->preview = $preview;

View File

@@ -3,18 +3,12 @@
namespace App\Notifications\Application; namespace App\Notifications\Application;
use App\Models\Application; use App\Models\Application;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; 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 $resource_name;
public string $project_uuid; public string $project_uuid;
@@ -27,6 +21,7 @@ class StatusChanged extends Notification implements ShouldQueue
public function __construct(public Application $resource) public function __construct(public Application $resource)
{ {
$this->onQueue('high');
$this->resource_name = data_get($resource, 'name'); $this->resource_name = data_get($resource, 'name');
$this->project_uuid = data_get($resource, 'environment.project.uuid'); $this->project_uuid = data_get($resource, 'environment.project.uuid');
$this->environment_name = data_get($resource, 'environment.name'); $this->environment_name = data_get($resource, 'environment.name');

View File

@@ -17,6 +17,6 @@ class DiscordChannel
if (! $webhookUrl) { if (! $webhookUrl) {
return; return;
} }
dispatch(new SendMessageToDiscordJob($message, $webhookUrl))->onQueue('high'); SendMessageToDiscordJob::dispatch($message, $webhookUrl);
} }
} }

View File

@@ -66,11 +66,12 @@ class EmailChannel
'transport' => 'smtp', 'transport' => 'smtp',
'host' => data_get($notifiable, 'smtp_host'), 'host' => data_get($notifiable, 'smtp_host'),
'port' => data_get($notifiable, 'smtp_port'), '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'), 'username' => data_get($notifiable, 'smtp_username'),
'password' => data_get($notifiable, 'smtp_password'), 'password' => data_get($notifiable, 'smtp_password'),
'timeout' => data_get($notifiable, 'smtp_timeout'), 'timeout' => data_get($notifiable, 'smtp_timeout'),
'local_domain' => null, 'local_domain' => null,
'auto_tls' => data_get($notifiable, 'smtp_encryption') === 'none' ? '0' : '',
]); ]);
} }
} }

View File

@@ -41,6 +41,6 @@ class TelegramChannel
if (! $telegramToken || ! $chatId || ! $message) { if (! $telegramToken || ! $chatId || ! $message) {
return; return;
} }
dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId, $topicId))->onQueue('high'); SendMessageToTelegramJob::dispatch($message, $buttons, $telegramToken, $chatId, $topicId);
} }
} }

View File

@@ -3,19 +3,16 @@
namespace App\Notifications\Container; namespace App\Notifications\Container;
use App\Models\Server; use App\Models\Server;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ContainerRestarted extends Notification implements ShouldQueue class ContainerRestarted extends CustomEmailNotification
{ {
use Queueable; public function __construct(public string $name, public Server $server, public ?string $url = null)
{
public $tries = 1; $this->onQueue('high');
}
public function __construct(public string $name, public Server $server, public ?string $url = null) {}
public function via(object $notifiable): array public function via(object $notifiable): array
{ {

View File

@@ -3,19 +3,16 @@
namespace App\Notifications\Container; namespace App\Notifications\Container;
use App\Models\Server; use App\Models\Server;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ContainerStopped extends Notification implements ShouldQueue class ContainerStopped extends CustomEmailNotification
{ {
use Queueable; public function __construct(public string $name, public Server $server, public ?string $url = null)
{
public $tries = 1; $this->onQueue('high');
}
public function __construct(public string $name, public Server $server, public ?string $url = null) {}
public function via(object $notifiable): array public function via(object $notifiable): array
{ {

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
class CustomEmailNotification extends Notification implements ShouldQueue
{
use Queueable;
public $backoff = [10, 20, 30, 40, 50];
public $tries = 5;
public $maxExceptions = 5;
}

View File

@@ -3,26 +3,19 @@
namespace App\Notifications\Database; namespace App\Notifications\Database;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class BackupFailed extends Notification implements ShouldQueue class BackupFailed extends CustomEmailNotification
{ {
use Queueable;
public $backoff = 10;
public $tries = 2;
public string $name; public string $name;
public string $frequency; public string $frequency;
public function __construct(ScheduledDatabaseBackup $backup, public $database, public $output, public $database_name) public function __construct(ScheduledDatabaseBackup $backup, public $database, public $output, public $database_name)
{ {
$this->onQueue('high');
$this->name = $database->name; $this->name = $database->name;
$this->frequency = $backup->frequency; $this->frequency = $backup->frequency;
} }

View File

@@ -3,26 +3,20 @@
namespace App\Notifications\Database; namespace App\Notifications\Database;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; 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 $name;
public string $frequency; public string $frequency;
public function __construct(ScheduledDatabaseBackup $backup, public $database, public $database_name) public function __construct(ScheduledDatabaseBackup $backup, public $database, public $database_name)
{ {
$this->onQueue('high');
$this->name = $database->name; $this->name = $database->name;
$this->frequency = $backup->frequency; $this->frequency = $backup->frequency;
} }

View File

@@ -46,7 +46,7 @@ class DiscordMessage
public function toPayload(): array public function toPayload(): array
{ {
$footerText = 'Coolify v'.config('version'); $footerText = 'Coolify v'.config('constants.coolify.version');
if (isCloud()) { if (isCloud()) {
$footerText = 'Coolify Cloud'; $footerText = 'Coolify Cloud';
} }

View File

@@ -15,7 +15,10 @@ class GeneralNotification extends Notification implements ShouldQueue
public $tries = 1; public $tries = 1;
public function __construct(public string $message) {} public function __construct(public string $message)
{
$this->onQueue('high');
}
public function via(object $notifiable): array public function via(object $notifiable): array
{ {

View File

@@ -3,24 +3,17 @@
namespace App\Notifications\ScheduledTask; namespace App\Notifications\ScheduledTask;
use App\Models\ScheduledTask; use App\Models\ScheduledTask;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; 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 ?string $url = null;
public function __construct(public ScheduledTask $task, public string $output) public function __construct(public ScheduledTask $task, public string $output)
{ {
$this->onQueue('high');
if ($task->application) { if ($task->application) {
$this->url = $task->application->failedTaskLink($task->uuid); $this->url = $task->application->failedTaskLink($task->uuid);
} elseif ($task->service) { } elseif ($task->service) {

View File

@@ -5,18 +5,15 @@ namespace App\Notifications\Server;
use App\Models\Server; use App\Models\Server;
use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\TelegramChannel; use App\Notifications\Channels\TelegramChannel;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; 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 function __construct(public Server $server, public string $message)
{
public $tries = 1; $this->onQueue('high');
}
public function __construct(public Server $server, public string $message) {}
public function via(object $notifiable): array public function via(object $notifiable): array
{ {

View File

@@ -6,19 +6,16 @@ use App\Models\Server;
use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel; use App\Notifications\Channels\TelegramChannel;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ForceDisabled extends Notification implements ShouldQueue class ForceDisabled extends CustomEmailNotification
{ {
use Queueable; public function __construct(public Server $server)
{
public $tries = 1; $this->onQueue('high');
}
public function __construct(public Server $server) {}
public function via(object $notifiable): array public function via(object $notifiable): array
{ {

View File

@@ -6,19 +6,16 @@ use App\Models\Server;
use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel; use App\Notifications\Channels\TelegramChannel;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ForceEnabled extends Notification implements ShouldQueue class ForceEnabled extends CustomEmailNotification
{ {
use Queueable; public function __construct(public Server $server)
{
public $tries = 1; $this->onQueue('high');
}
public function __construct(public Server $server) {}
public function via(object $notifiable): array public function via(object $notifiable): array
{ {

View File

@@ -3,19 +3,16 @@
namespace App\Notifications\Server; namespace App\Notifications\Server;
use App\Models\Server; use App\Models\Server;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class HighDiskUsage extends Notification implements ShouldQueue class HighDiskUsage extends CustomEmailNotification
{ {
use Queueable; public function __construct(public Server $server, public int $disk_usage, public int $server_disk_usage_notification_threshold)
{
public $tries = 1; $this->onQueue('high');
}
public function __construct(public Server $server, public int $disk_usage, public int $server_disk_usage_notification_threshold) {}
public function via(object $notifiable): array public function via(object $notifiable): array
{ {

View File

@@ -6,22 +6,17 @@ use App\Models\Server;
use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel; use App\Notifications\Channels\TelegramChannel;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; 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; protected bool $isRateLimited = false;
public function __construct(public Server $server) public function __construct(public Server $server)
{ {
$this->onQueue('high');
$this->isRateLimited = isEmailRateLimited( $this->isRateLimited = isEmailRateLimited(
limiterKey: 'server-reachable:'.$this->server->id, limiterKey: 'server-reachable:'.$this->server->id,
); );

View File

@@ -6,22 +6,17 @@ use App\Models\Server;
use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel; use App\Notifications\Channels\TelegramChannel;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; 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; protected bool $isRateLimited = false;
public function __construct(public Server $server) public function __construct(public Server $server)
{ {
$this->onQueue('high');
$this->isRateLimited = isEmailRateLimited( $this->isRateLimited = isEmailRateLimited(
limiterKey: 'server-unreachable:'.$this->server->id, limiterKey: 'server-unreachable:'.$this->server->id,
); );

View File

@@ -15,7 +15,10 @@ class Test extends Notification implements ShouldQueue
public $tries = 5; 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 public function via(object $notifiable): array
{ {

Some files were not shown because too many files have changed in this diff Show More