Merge branch 'next' into new-dockerfiles

This commit is contained in:
🏔️ Peak
2024-11-22 17:50:43 +01:00
committed by GitHub
71 changed files with 492 additions and 362 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,20 @@ 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/152bd1e0-e0c1-4d47-8a4f-0eb3700d2e61)
* [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.
* [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 +62,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+)
@@ -91,6 +91,11 @@ Special thanks to our biggest sponsors!
<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://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>

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

@@ -46,7 +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,
); );
dispatch($job)->onQueue('high'); dispatch($job);
$this->activity->refresh(); $this->activity->refresh();
return $this->activity; return $this->activity;

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

@@ -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;

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

@@ -130,10 +130,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);
} }
} }

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,7 +29,7 @@ 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('version');
if (! $manual_update) { if (! $manual_update) {

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

@@ -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

@@ -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.',
@@ -2523,7 +2523,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(
[ [

View File

@@ -497,9 +497,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([
@@ -1151,7 +1151,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 +1206,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();
@@ -1264,7 +1264,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 +1320,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 +1357,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 +1406,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 +1442,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();
@@ -1500,7 +1500,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();
@@ -1593,7 +1593,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.',
@@ -1666,7 +1666,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(
[ [
@@ -1742,7 +1742,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(
[ [
@@ -1815,7 +1815,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

@@ -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([
@@ -675,7 +675,7 @@ 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([
@@ -813,7 +813,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

@@ -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

@@ -166,6 +166,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');
@@ -349,8 +351,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) {

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

@@ -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,6 +60,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public function __construct($backup) public function __construct($backup)
{ {
$this->onQueue('high');
$this->backup = $backup; $this->backup = $backup;
} }
@@ -198,7 +199,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$databasesToBackup = data_get($this->backup, 'databases_to_backup'); $databasesToBackup = data_get($this->backup, 'databases_to_backup');
} }
if (is_null($databasesToBackup)) { if (filled($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')) {
@@ -319,12 +320,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.

View File

@@ -94,10 +94,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,242 @@
<?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
{
$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 \Exception("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 \Exception("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 \Exception("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 \Exception("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 \Exception("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 \Exception("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 \Exception("Subscription already exists for customer: {$customerId}");
}
throw new \Exception('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 \Exception("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 \Exception("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 \Exception('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 \Exception('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:
// Unhandled event type
}
}
}

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

@@ -73,6 +73,9 @@ class Email extends Component
#[Validate(['nullable', 'string'])] #[Validate(['nullable', 'string'])]
public ?string $resendApiKey = null; public ?string $resendApiKey = null;
#[Validate(['required', 'email'])]
public string $testEmailAddress = '';
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

@@ -45,13 +45,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()

View File

@@ -988,7 +988,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';
@@ -1051,18 +1051,14 @@ $schema://$host {
$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

@@ -34,6 +34,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

@@ -34,6 +34,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

@@ -27,6 +27,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

@@ -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

@@ -15,7 +15,10 @@ class ContainerRestarted extends Notification implements ShouldQueue
public $tries = 1; public $tries = 1;
public function __construct(public string $name, public Server $server, public ?string $url = null) {} public function __construct(public string $name, public Server $server, public ?string $url = null)
{
$this->onQueue('high');
}
public function via(object $notifiable): array public function via(object $notifiable): array
{ {

View File

@@ -15,7 +15,10 @@ class ContainerStopped extends Notification implements ShouldQueue
public $tries = 1; public $tries = 1;
public function __construct(public string $name, public Server $server, public ?string $url = null) {} public function __construct(public string $name, public Server $server, public ?string $url = null)
{
$this->onQueue('high');
}
public function via(object $notifiable): array public function via(object $notifiable): array
{ {

View File

@@ -23,6 +23,7 @@ class BackupFailed extends Notification implements ShouldQueue
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

@@ -23,6 +23,7 @@ class BackupSuccess extends Notification implements ShouldQueue
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

@@ -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

@@ -21,6 +21,7 @@ class TaskFailed extends Notification implements ShouldQueue
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

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

View File

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

View File

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

View File

@@ -15,7 +15,10 @@ class HighDiskUsage extends Notification implements ShouldQueue
public $tries = 1; public $tries = 1;
public function __construct(public Server $server, public int $disk_usage, public int $server_disk_usage_notification_threshold) {} public function __construct(public Server $server, public int $disk_usage, public int $server_disk_usage_notification_threshold)
{
$this->onQueue('high');
}
public function via(object $notifiable): array public function via(object $notifiable): array
{ {

View File

@@ -22,6 +22,7 @@ class Reachable extends Notification implements ShouldQueue
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

@@ -22,6 +22,7 @@ class Unreachable extends Notification implements ShouldQueue
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
{ {

View File

@@ -22,7 +22,10 @@ class InvitationLink extends Notification implements ShouldQueue
return [TransactionalEmailChannel::class]; return [TransactionalEmailChannel::class];
} }
public function __construct(public User $user) {} public function __construct(public User $user)
{
$this->onQueue('high');
}
public function toMail(): MailMessage public function toMail(): MailMessage
{ {

View File

@@ -14,7 +14,10 @@ class Test extends Notification implements ShouldQueue
public $tries = 5; public $tries = 5;
public function __construct(public string $emails) {} public function __construct(public string $emails)
{
$this->onQueue('high');
}
public function via(): array public function via(): array
{ {

View File

@@ -44,13 +44,13 @@ function queue_application_deployment(Application $application, string $deployme
]); ]);
if ($no_questions_asked) { if ($no_questions_asked) {
dispatch(new ApplicationDeploymentJob( ApplicationDeploymentJob::dispatch(
application_deployment_queue_id: $deployment->id, application_deployment_queue_id: $deployment->id,
))->onQueue('high'); );
} elseif (next_queuable($server_id, $application_id)) { } elseif (next_queuable($server_id, $application_id)) {
dispatch(new ApplicationDeploymentJob( ApplicationDeploymentJob::dispatch(
application_deployment_queue_id: $deployment->id, application_deployment_queue_id: $deployment->id,
))->onQueue('high'); );
} }
} }
function force_start_deployment(ApplicationDeploymentQueue $deployment) function force_start_deployment(ApplicationDeploymentQueue $deployment)
@@ -59,9 +59,9 @@ function force_start_deployment(ApplicationDeploymentQueue $deployment)
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]); ]);
dispatch(new ApplicationDeploymentJob( ApplicationDeploymentJob::dispatch(
application_deployment_queue_id: $deployment->id, application_deployment_queue_id: $deployment->id,
))->onQueue('high'); );
} }
function queue_next_deployment(Application $application) function queue_next_deployment(Application $application)
{ {
@@ -72,9 +72,9 @@ function queue_next_deployment(Application $application)
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]); ]);
dispatch(new ApplicationDeploymentJob( ApplicationDeploymentJob::dispatch(
application_deployment_queue_id: $next_found->id, application_deployment_queue_id: $next_found->id,
))->onQueue('high'); );
} }
} }
@@ -113,9 +113,9 @@ function next_after_cancel(?Server $server = null)
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]); ]);
dispatch(new ApplicationDeploymentJob( ApplicationDeploymentJob::dispatch(
application_deployment_queue_id: $next->id, application_deployment_queue_id: $next->id,
))->onQueue('high'); );
} }
break; break;
} }

View File

@@ -2,7 +2,7 @@
return [ return [
'coolify' => [ 'coolify' => [
'version' => '4.0.0-beta.371', 'version' => '4.0.0-beta.372',
'self_hosted' => env('SELF_HOSTED', true), 'self_hosted' => env('SELF_HOSTED', true),
'autoupdate' => env('AUTOUPDATE'), 'autoupdate' => env('AUTOUPDATE'),
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.371'; return '4.0.0-beta.372';

View File

@@ -2,19 +2,27 @@
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class AddIndexToActivityLog extends Migration class AddIndexToActivityLog extends Migration
{ {
public function up() public function up()
{ {
DB::statement('ALTER TABLE activity_log ALTER COLUMN properties TYPE jsonb USING properties::jsonb'); try {
DB::statement('CREATE INDEX idx_activity_type_uuid ON activity_log USING GIN (properties jsonb_path_ops)'); DB::statement('ALTER TABLE activity_log ALTER COLUMN properties TYPE jsonb USING properties::jsonb');
DB::statement('CREATE INDEX idx_activity_type_uuid ON activity_log USING GIN (properties jsonb_path_ops)');
} catch (\Exception $e) {
Log::error('Error adding index to activity_log: '.$e->getMessage());
}
} }
public function down() public function down()
{ {
DB::statement('DROP INDEX IF EXISTS idx_activity_type_uuid'); try {
DB::statement('ALTER TABLE activity_log ALTER COLUMN properties TYPE json USING properties::json'); DB::statement('DROP INDEX IF EXISTS idx_activity_type_uuid');
DB::statement('ALTER TABLE activity_log ALTER COLUMN properties TYPE json USING properties::json');
} catch (\Exception $e) {
Log::error('Error dropping index from activity_log: '.$e->getMessage());
}
} }
} }

View File

@@ -16,9 +16,9 @@
@endif @endif
@if (isEmailEnabled($team) && auth()->user()->isAdminFromSession() && isTestEmailEnabled($team)) @if (isEmailEnabled($team) && auth()->user()->isAdminFromSession() && isTestEmailEnabled($team))
<x-modal-input buttonTitle="Send Test Email" title="Send Test Email"> <x-modal-input buttonTitle="Send Test Email" title="Send Test Email">
<form wire:submit='submit' class="flex flex-col w-full gap-2"> <form wire:submit.prevent="sendTestEmail" class="flex flex-col w-full gap-2">
<x-forms.input placeholder="test@example.com" id="emails" label="Recipients" required /> <x-forms.input wire:model="testEmailAddress" placeholder="test@example.com" id="testEmailAddress" label="Recipients" required />
<x-forms.button wire:click="sendTestNotification" @click="modalOpen=false"> <x-forms.button type="submit" @click="modalOpen=false">
Send Email Send Email
</x-forms.button> </x-forms.button>
</form> </form>

File diff suppressed because one or more lines are too long

View File

@@ -1,10 +1,10 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.371" "version": "4.0.0-beta.372"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.372" "version": "4.0.0-beta.373"
}, },
"helper": { "helper": {
"version": "1.0.4" "version": "1.0.4"