Merge branch 'next' into main
This commit is contained in:
13
README.md
13
README.md
@@ -5,7 +5,7 @@
|
||||
|
||||
# About the Project
|
||||
|
||||
Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc.
|
||||
Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc.
|
||||
|
||||
It helps you manage your servers, applications, and databases on your own hardware; you only need an SSH connection. You can manage VPS, Bare Metal, Raspberry PIs, and anything else.
|
||||
|
||||
@@ -40,21 +40,20 @@ Special thanks to our biggest sponsors!
|
||||
|
||||
### Special Sponsors
|
||||
|
||||

|
||||

|
||||
|
||||
* [CCCareers](https://cccareers.org/) - A career development platform connecting coding bootcamp graduates with job opportunities in the tech industry.
|
||||
* [Hetzner](http://htznr.li/CoolifyXHetzner) - A German web hosting company offering affordable dedicated servers, cloud services, and web hosting solutions.
|
||||
* [Logto](https://logto.io/?ref=coolify) - An open-source authentication and authorization solution for building secure login systems and managing user identities.
|
||||
* [Tolgee](https://tolgee.io/?ref=coolify) - Developer & translator friendly web-based localization platform.
|
||||
* [BC Direct](https://bc.direct/?ref=coolify.io) - A digital marketing agency specializing in e-commerce solutions and online business growth strategies.
|
||||
* [QuantCDN](https://www.quantcdn.io/?ref=coolify.io) - A content delivery network (CDN) optimizing website performance through global content distribution.
|
||||
* [Arcjet](https://arcjet.com/?ref=coolify.io) - A cloud-based platform providing real-time protection against API abuse and bot attacks.
|
||||
* [SupaGuide](https://supa.guide/?ref=coolify.io) - A comprehensive resource hub offering guides and tutorials for web development using Supabase.
|
||||
* [Tigris](https://tigrisdata.com/?ref=coolify.io) - A fully managed serverless object storage service compatible with Amazon S3 API. Offers high performance, scalability, and built-in search capabilities for efficient data management.
|
||||
* [Fractal Networks](https://fractalnetworks.co/?ref=coolify.io) - A decentralized network infrastructure company focusing on secure and private communication solutions.
|
||||
* [Advin](https://coolify.ad.vin/?ref=coolify.io) - A digital advertising agency specializing in programmatic advertising and data-driven marketing strategies.
|
||||
* [Treive](https://trieve.ai/?ref=coolify.io) - An AI-powered search and discovery platform for enhancing information retrieval in large datasets.
|
||||
* [Blacksmith](https://blacksmith.sh/?ref=coolify.io) - A cloud-native platform for automating infrastructure provisioning and management across multiple cloud providers.
|
||||
* [Latitude](https://latitude.sh/?ref=coolify.io) - A cloud computing platform offering bare metal servers and cloud instances for developers and businesses.
|
||||
* [Brand Dev](https://brand.dev/?ref=coolify.io) - A web development agency specializing in creating custom digital experiences and brand identities.
|
||||
* [Jobscollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - A job search platform connecting professionals with remote work opportunities across various industries.
|
||||
* [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - A web hosting provider offering affordable hosting solutions, domain registration, and website building tools.
|
||||
@@ -63,6 +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.
|
||||
* [Saasykit](https://saasykit.com/?ref=coolify.io) - A Laravel-based boilerplate providing essential components and features for building SaaS applications quickly.
|
||||
* [Massivegrid](https://massivegrid.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes.
|
||||
* [LiquidWeb](https://liquidweb.com/?utm_source=coolify.io) - Fast web hosting provider.
|
||||
|
||||
|
||||
## Github Sponsors ($40+)
|
||||
@@ -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://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
|
||||
<a href="https://opencollective.com/coollabsio/organization/0/website"><img src="https://opencollective.com/coollabsio/organization/0/avatar.svg"></a>
|
||||
|
@@ -10,6 +10,8 @@ class StopApplication
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true)
|
||||
{
|
||||
try {
|
||||
|
@@ -3,7 +3,6 @@
|
||||
namespace App\Actions\CoolifyTask;
|
||||
|
||||
use App\Data\CoolifyTaskArgs;
|
||||
use App\Enums\ActivityTypes;
|
||||
use App\Jobs\CoolifyTask;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
@@ -47,11 +46,7 @@ class PrepareCoolifyTask
|
||||
call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish,
|
||||
call_event_data: $this->remoteProcessArgs->call_event_data,
|
||||
);
|
||||
if ($this->remoteProcessArgs->type === ActivityTypes::COMMAND->value) {
|
||||
dispatch($job)->onQueue('high');
|
||||
} else {
|
||||
dispatch($job);
|
||||
}
|
||||
dispatch($job);
|
||||
$this->activity->refresh();
|
||||
|
||||
return $this->activity;
|
||||
|
@@ -16,6 +16,8 @@ class StartDatabase
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
|
||||
{
|
||||
$server = $database->destination->server;
|
||||
@@ -49,7 +51,7 @@ class StartDatabase
|
||||
break;
|
||||
}
|
||||
if ($database->is_public && $database->public_port) {
|
||||
StartDatabaseProxy::dispatch($database)->onQueue('high');
|
||||
StartDatabaseProxy::dispatch($database);
|
||||
}
|
||||
|
||||
return $activity;
|
||||
|
@@ -18,6 +18,8 @@ class StartDatabaseProxy
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|ServiceDatabase $database)
|
||||
{
|
||||
$internalPort = null;
|
||||
|
@@ -18,6 +18,8 @@ class StopDatabaseProxy
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|ServiceDatabase|StandaloneDragonfly|StandaloneClickhouse $database)
|
||||
{
|
||||
$server = data_get($database, 'destination.server');
|
||||
|
@@ -7,7 +7,6 @@ use App\Actions\Shared\ComplexStatusCheck;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServiceDatabase;
|
||||
use App\Notifications\Container\ContainerRestarted;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
@@ -16,6 +15,8 @@ class GetContainersStatus
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public $applications;
|
||||
|
||||
public ?Collection $containers;
|
||||
|
@@ -30,7 +30,7 @@ class CheckProxy
|
||||
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
|
||||
return false;
|
||||
}
|
||||
['uptime' => $uptime, 'error' => $error] = $server->validateConnection(false);
|
||||
['uptime' => $uptime, 'error' => $error] = $server->validateConnection();
|
||||
if (! $uptime) {
|
||||
throw new \Exception($error);
|
||||
}
|
||||
|
@@ -9,6 +9,8 @@ class CleanupDocker
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(Server $server)
|
||||
{
|
||||
$settings = instanceSettings();
|
||||
|
@@ -130,10 +130,10 @@ class ServerCheck
|
||||
if ($foundLogDrainContainer) {
|
||||
$status = data_get($foundLogDrainContainer, 'State.Status');
|
||||
if ($status !== 'running') {
|
||||
StartLogDrain::dispatch($this->server)->onQueue('high');
|
||||
StartLogDrain::dispatch($this->server);
|
||||
}
|
||||
} else {
|
||||
StartLogDrain::dispatch($this->server)->onQueue('high');
|
||||
StartLogDrain::dispatch($this->server);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -9,6 +9,8 @@ class StartLogDrain
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(Server $server)
|
||||
{
|
||||
if ($server->settings->is_logdrain_newrelic_enabled) {
|
||||
|
@@ -29,7 +29,7 @@ class UpdateCoolify
|
||||
if (! $this->server) {
|
||||
return;
|
||||
}
|
||||
CleanupDocker::dispatch($this->server)->onQueue('high');
|
||||
CleanupDocker::dispatch($this->server);
|
||||
$this->latestVersion = get_latest_version_of_coolify();
|
||||
$this->currentVersion = config('version');
|
||||
if (! $manual_update) {
|
||||
|
@@ -9,6 +9,8 @@ class ValidateServer
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public ?string $uptime = null;
|
||||
|
||||
public ?string $error = null;
|
||||
|
@@ -9,6 +9,8 @@ class RestartService
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(Service $service)
|
||||
{
|
||||
StopService::run($service);
|
||||
|
@@ -10,6 +10,8 @@ class StartService
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(Service $service)
|
||||
{
|
||||
$service->saveComposeConfigs();
|
||||
|
@@ -10,6 +10,8 @@ class StopService
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true)
|
||||
{
|
||||
try {
|
||||
|
@@ -13,7 +13,6 @@ class CleanupRedis extends Command
|
||||
|
||||
public function handle()
|
||||
{
|
||||
echo "Cleanup Redis keys.\n";
|
||||
$prefix = config('database.redis.options.prefix');
|
||||
|
||||
$keys = Redis::connection()->keys('*:laravel*');
|
||||
|
@@ -30,7 +30,6 @@ class CleanupStuckedResources extends Command
|
||||
|
||||
public function handle()
|
||||
{
|
||||
echo "Running cleanup stucked resources.\n";
|
||||
$this->cleanup_stucked_resources();
|
||||
}
|
||||
|
||||
|
@@ -13,7 +13,7 @@ class Horizon extends Command
|
||||
public function handle()
|
||||
{
|
||||
if (config('constants.horizon.is_horizon_enabled')) {
|
||||
$this->info('Horizon is enabled. Starting.');
|
||||
$this->info('[x]: Horizon is enabled. Starting.');
|
||||
$this->call('horizon');
|
||||
exit(0);
|
||||
} else {
|
||||
|
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Actions\Server\StopSentinel;
|
||||
use App\Enums\ActivityTypes;
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Jobs\CheckHelperImageJob;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Environment;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
@@ -12,6 +12,7 @@ use App\Models\Server;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
@@ -25,6 +26,8 @@ class Init extends Command
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$this->optimize();
|
||||
|
||||
if (isCloud() && ! $this->option('force-cloud')) {
|
||||
echo "Skipping init as we are on cloud and --force-cloud option is not set\n";
|
||||
|
||||
@@ -39,7 +42,6 @@ class Init extends Command
|
||||
}
|
||||
|
||||
// Backward compatibility
|
||||
// $this->disable_metrics();
|
||||
$this->replace_slash_in_environment_name();
|
||||
$this->restore_coolify_db_backup();
|
||||
$this->update_user_emails();
|
||||
@@ -53,9 +55,18 @@ class Init extends Command
|
||||
} else {
|
||||
$this->cleanup_in_progress_application_deployments();
|
||||
}
|
||||
echo "[3]: Cleanup Redis keys.\n";
|
||||
$this->call('cleanup:redis');
|
||||
|
||||
echo "[4]: Cleanup stucked resources.\n";
|
||||
$this->call('cleanup:stucked-resources');
|
||||
|
||||
try {
|
||||
$this->pullHelperImage();
|
||||
} catch (\Throwable $e) {
|
||||
//
|
||||
}
|
||||
|
||||
if (isCloud()) {
|
||||
try {
|
||||
$this->pullTemplatesFromCDN();
|
||||
@@ -87,6 +98,11 @@ class Init extends Command
|
||||
}
|
||||
}
|
||||
|
||||
private function pullHelperImage()
|
||||
{
|
||||
CheckHelperImageJob::dispatch();
|
||||
}
|
||||
|
||||
private function pullTemplatesFromCDN()
|
||||
{
|
||||
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
|
||||
@@ -95,19 +111,13 @@ class Init extends Command
|
||||
File::put(base_path('templates/service-templates.json'), json_encode($services));
|
||||
}
|
||||
}
|
||||
// private function disable_metrics()
|
||||
// {
|
||||
// if (version_compare('4.0.0-beta.312', config('version'), '<=')) {
|
||||
// foreach ($this->servers as $server) {
|
||||
// if ($server->settings->is_metrics_enabled === true) {
|
||||
// $server->settings->update(['is_metrics_enabled' => false]);
|
||||
// }
|
||||
// if ($server->isFunctional()) {
|
||||
// StopSentinel::dispatch($server)->onQueue('high');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
private function optimize()
|
||||
{
|
||||
echo "[1]: Optimizing Laravel (caching config, routes, views).\n";
|
||||
Artisan::call('optimize:clear');
|
||||
Artisan::call('optimize');
|
||||
}
|
||||
|
||||
private function update_user_emails()
|
||||
{
|
||||
@@ -222,15 +232,15 @@ class Init extends Command
|
||||
$settings = instanceSettings();
|
||||
$do_not_track = data_get($settings, 'do_not_track');
|
||||
if ($do_not_track == true) {
|
||||
echo "Skipping alive as do_not_track is enabled\n";
|
||||
echo "[2]: Skipping sending live signal as do_not_track is enabled\n";
|
||||
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Http::get("https://undead.coolify.io/v4/alive?appId=$id&version=$version");
|
||||
echo "I am alive!\n";
|
||||
echo "[2]: Sending live signal!\n";
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in alive: {$e->getMessage()}\n";
|
||||
echo "[2]: Error in sending live signal: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -13,7 +13,7 @@ class Scheduler extends Command
|
||||
public function handle()
|
||||
{
|
||||
if (config('constants.horizon.is_scheduler_enabled')) {
|
||||
$this->info('Scheduler is enabled. Starting.');
|
||||
$this->info('[x]: Scheduler is enabled. Starting.');
|
||||
$this->call('schedule:work');
|
||||
exit(0);
|
||||
} else {
|
||||
|
@@ -96,7 +96,7 @@ class ServicesDelete extends Command
|
||||
if (! $confirmed) {
|
||||
break;
|
||||
}
|
||||
DeleteResourceJob::dispatch($toDelete)->onQueue('high');
|
||||
DeleteResourceJob::dispatch($toDelete);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,7 +122,7 @@ class ServicesDelete extends Command
|
||||
if (! $confirmed) {
|
||||
return;
|
||||
}
|
||||
DeleteResourceJob::dispatch($toDelete)->onQueue('high');
|
||||
DeleteResourceJob::dispatch($toDelete);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,7 +148,7 @@ class ServicesDelete extends Command
|
||||
if (! $confirmed) {
|
||||
return;
|
||||
}
|
||||
DeleteResourceJob::dispatch($toDelete)->onQueue('high');
|
||||
DeleteResourceJob::dispatch($toDelete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -50,7 +50,7 @@ class Kernel extends ConsoleKernel
|
||||
$this->instanceTimezone = config('app.timezone');
|
||||
}
|
||||
|
||||
$this->scheduleInstance->job(new CleanupStaleMultiplexedConnections)->hourly();
|
||||
// $this->scheduleInstance->job(new CleanupStaleMultiplexedConnections)->hourly();
|
||||
|
||||
if (isDev()) {
|
||||
// Instance Jobs
|
||||
@@ -154,7 +154,7 @@ class Kernel extends ConsoleKernel
|
||||
}
|
||||
|
||||
// Cleanup multiplexed connections every hour
|
||||
$this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer();
|
||||
// $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer();
|
||||
|
||||
// Temporary solution until we have better memory management for Sentinel
|
||||
if ($server->isSentinelEnabled()) {
|
||||
|
@@ -21,17 +21,14 @@ class SshMultiplexingHelper
|
||||
];
|
||||
}
|
||||
|
||||
public static function ensureMultiplexedConnection(Server $server)
|
||||
public static function ensureMultiplexedConnection(Server $server): bool
|
||||
{
|
||||
if (! self::isMultiplexingEnabled()) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
$sshConfig = self::serverSshConfiguration($server);
|
||||
$muxSocket = $sshConfig['muxFilename'];
|
||||
$sshKeyLocation = $sshConfig['sshKeyLocation'];
|
||||
|
||||
self::validateSshKey($sshKeyLocation);
|
||||
|
||||
$checkCommand = "ssh -O check -o ControlPath=$muxSocket ";
|
||||
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||
@@ -41,16 +38,17 @@ class SshMultiplexingHelper
|
||||
$process = Process::run($checkCommand);
|
||||
|
||||
if ($process->exitCode() !== 0) {
|
||||
self::establishNewMultiplexedConnection($server);
|
||||
return self::establishNewMultiplexedConnection($server);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function establishNewMultiplexedConnection(Server $server)
|
||||
public static function establishNewMultiplexedConnection(Server $server): bool
|
||||
{
|
||||
$sshConfig = self::serverSshConfiguration($server);
|
||||
$sshKeyLocation = $sshConfig['sshKeyLocation'];
|
||||
$muxSocket = $sshConfig['muxFilename'];
|
||||
|
||||
$connectionTimeout = config('constants.ssh.connection_timeout');
|
||||
$serverInterval = config('constants.ssh.server_interval');
|
||||
$muxPersistTime = config('constants.ssh.mux_persist_time');
|
||||
@@ -60,15 +58,14 @@ class SshMultiplexingHelper
|
||||
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||
$establishCommand .= ' -o ProxyCommand="cloudflared access ssh --hostname %h" ';
|
||||
}
|
||||
|
||||
$establishCommand .= self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval);
|
||||
$establishCommand .= "{$server->user}@{$server->ip}";
|
||||
|
||||
$establishProcess = Process::run($establishCommand);
|
||||
|
||||
if ($establishProcess->exitCode() !== 0) {
|
||||
throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function removeMuxFile(Server $server)
|
||||
@@ -97,9 +94,8 @@ class SshMultiplexingHelper
|
||||
if ($server->isIpv6()) {
|
||||
$scp_command .= '-6 ';
|
||||
}
|
||||
if (self::isMultiplexingEnabled()) {
|
||||
if (self::isMultiplexingEnabled() && self::ensureMultiplexedConnection($server)) {
|
||||
$scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
|
||||
self::ensureMultiplexedConnection($server);
|
||||
}
|
||||
|
||||
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||
@@ -120,6 +116,9 @@ class SshMultiplexingHelper
|
||||
|
||||
$sshConfig = self::serverSshConfiguration($server);
|
||||
$sshKeyLocation = $sshConfig['sshKeyLocation'];
|
||||
|
||||
self::validateSshKey($server->privateKey);
|
||||
|
||||
$muxSocket = $sshConfig['muxFilename'];
|
||||
|
||||
$timeout = config('constants.ssh.command_timeout');
|
||||
@@ -127,9 +126,8 @@ class SshMultiplexingHelper
|
||||
|
||||
$ssh_command = "timeout $timeout ssh ";
|
||||
|
||||
if (self::isMultiplexingEnabled()) {
|
||||
if (self::isMultiplexingEnabled() && self::ensureMultiplexedConnection($server)) {
|
||||
$ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
|
||||
self::ensureMultiplexedConnection($server);
|
||||
}
|
||||
|
||||
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||
@@ -154,13 +152,14 @@ class SshMultiplexingHelper
|
||||
return config('constants.ssh.mux_enabled') && ! config('constants.coolify.is_windows_docker_desktop');
|
||||
}
|
||||
|
||||
private static function validateSshKey(string $sshKeyLocation): void
|
||||
private static function validateSshKey(PrivateKey $privateKey): void
|
||||
{
|
||||
$checkKeyCommand = "ls $sshKeyLocation 2>/dev/null";
|
||||
$keyLocation = $privateKey->getKeyLocation();
|
||||
$checkKeyCommand = "ls $keyLocation 2>/dev/null";
|
||||
$keyCheckProcess = Process::run($checkKeyCommand);
|
||||
|
||||
if ($keyCheckProcess->exitCode() !== 0) {
|
||||
throw new \RuntimeException("SSH key file not accessible: $sshKeyLocation");
|
||||
$privateKey->storeInFileSystem();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1224,7 +1224,7 @@ class ApplicationsController extends Controller
|
||||
$service->name = "service-$service->uuid";
|
||||
$service->parse(isNew: true);
|
||||
if ($instantDeploy) {
|
||||
StartService::dispatch($service)->onQueue('high');
|
||||
StartService::dispatch($service);
|
||||
}
|
||||
|
||||
return response()->json(serializeApiResponse([
|
||||
@@ -1379,7 +1379,7 @@ class ApplicationsController extends Controller
|
||||
deleteVolumes: $request->query->get('delete_volumes', true),
|
||||
dockerCleanup: $request->query->get('docker_cleanup', true),
|
||||
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
|
||||
)->onQueue('high');
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Application deletion request queued.',
|
||||
@@ -2523,7 +2523,7 @@ class ApplicationsController extends Controller
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
StopApplication::dispatch($application)->onQueue('high');
|
||||
StopApplication::dispatch($application);
|
||||
|
||||
return response()->json(
|
||||
[
|
||||
|
@@ -497,9 +497,9 @@ class DatabasesController extends Controller
|
||||
$database->update($request->all());
|
||||
|
||||
if ($whatToDoWithDatabaseProxy === 'start') {
|
||||
StartDatabaseProxy::dispatch($database)->onQueue('high');
|
||||
StartDatabaseProxy::dispatch($database);
|
||||
} elseif ($whatToDoWithDatabaseProxy === 'stop') {
|
||||
StopDatabaseProxy::dispatch($database)->onQueue('high');
|
||||
StopDatabaseProxy::dispatch($database);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
@@ -1151,7 +1151,7 @@ class DatabasesController extends Controller
|
||||
}
|
||||
$database = create_standalone_postgresql($environment->id, $destination->uuid, $request->all());
|
||||
if ($instantDeploy) {
|
||||
StartDatabase::dispatch($database)->onQueue('high');
|
||||
StartDatabase::dispatch($database);
|
||||
}
|
||||
$database->refresh();
|
||||
$payload = [
|
||||
@@ -1206,7 +1206,7 @@ class DatabasesController extends Controller
|
||||
}
|
||||
$database = create_standalone_mariadb($environment->id, $destination->uuid, $request->all());
|
||||
if ($instantDeploy) {
|
||||
StartDatabase::dispatch($database)->onQueue('high');
|
||||
StartDatabase::dispatch($database);
|
||||
}
|
||||
|
||||
$database->refresh();
|
||||
@@ -1264,7 +1264,7 @@ class DatabasesController extends Controller
|
||||
}
|
||||
$database = create_standalone_mysql($environment->id, $destination->uuid, $request->all());
|
||||
if ($instantDeploy) {
|
||||
StartDatabase::dispatch($database)->onQueue('high');
|
||||
StartDatabase::dispatch($database);
|
||||
}
|
||||
|
||||
$database->refresh();
|
||||
@@ -1320,7 +1320,7 @@ class DatabasesController extends Controller
|
||||
}
|
||||
$database = create_standalone_redis($environment->id, $destination->uuid, $request->all());
|
||||
if ($instantDeploy) {
|
||||
StartDatabase::dispatch($database)->onQueue('high');
|
||||
StartDatabase::dispatch($database);
|
||||
}
|
||||
|
||||
$database->refresh();
|
||||
@@ -1357,7 +1357,7 @@ class DatabasesController extends Controller
|
||||
removeUnnecessaryFieldsFromRequest($request);
|
||||
$database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->all());
|
||||
if ($instantDeploy) {
|
||||
StartDatabase::dispatch($database)->onQueue('high');
|
||||
StartDatabase::dispatch($database);
|
||||
}
|
||||
|
||||
return response()->json(serializeApiResponse([
|
||||
@@ -1406,7 +1406,7 @@ class DatabasesController extends Controller
|
||||
}
|
||||
$database = create_standalone_keydb($environment->id, $destination->uuid, $request->all());
|
||||
if ($instantDeploy) {
|
||||
StartDatabase::dispatch($database)->onQueue('high');
|
||||
StartDatabase::dispatch($database);
|
||||
}
|
||||
|
||||
$database->refresh();
|
||||
@@ -1442,7 +1442,7 @@ class DatabasesController extends Controller
|
||||
removeUnnecessaryFieldsFromRequest($request);
|
||||
$database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->all());
|
||||
if ($instantDeploy) {
|
||||
StartDatabase::dispatch($database)->onQueue('high');
|
||||
StartDatabase::dispatch($database);
|
||||
}
|
||||
|
||||
$database->refresh();
|
||||
@@ -1500,7 +1500,7 @@ class DatabasesController extends Controller
|
||||
}
|
||||
$database = create_standalone_mongodb($environment->id, $destination->uuid, $request->all());
|
||||
if ($instantDeploy) {
|
||||
StartDatabase::dispatch($database)->onQueue('high');
|
||||
StartDatabase::dispatch($database);
|
||||
}
|
||||
|
||||
$database->refresh();
|
||||
@@ -1593,7 +1593,7 @@ class DatabasesController extends Controller
|
||||
deleteVolumes: $request->query->get('delete_volumes', true),
|
||||
dockerCleanup: $request->query->get('docker_cleanup', true),
|
||||
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
|
||||
)->onQueue('high');
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Database deletion request queued.',
|
||||
@@ -1666,7 +1666,7 @@ class DatabasesController extends Controller
|
||||
if (str($database->status)->contains('running')) {
|
||||
return response()->json(['message' => 'Database is already running.'], 400);
|
||||
}
|
||||
StartDatabase::dispatch($database)->onQueue('high');
|
||||
StartDatabase::dispatch($database);
|
||||
|
||||
return response()->json(
|
||||
[
|
||||
@@ -1742,7 +1742,7 @@ class DatabasesController extends Controller
|
||||
if (str($database->status)->contains('stopped') || str($database->status)->contains('exited')) {
|
||||
return response()->json(['message' => 'Database is already stopped.'], 400);
|
||||
}
|
||||
StopDatabase::dispatch($database)->onQueue('high');
|
||||
StopDatabase::dispatch($database);
|
||||
|
||||
return response()->json(
|
||||
[
|
||||
@@ -1815,7 +1815,7 @@ class DatabasesController extends Controller
|
||||
if (! $database) {
|
||||
return response()->json(['message' => 'Database not found.'], 404);
|
||||
}
|
||||
RestartDatabase::dispatch($database)->onQueue('high');
|
||||
RestartDatabase::dispatch($database);
|
||||
|
||||
return response()->json(
|
||||
[
|
||||
|
@@ -307,7 +307,7 @@ class DeployController extends Controller
|
||||
break;
|
||||
default:
|
||||
// Database resource
|
||||
StartDatabase::dispatch($resource)->onQueue('high');
|
||||
StartDatabase::dispatch($resource);
|
||||
$resource->update([
|
||||
'started_at' => now(),
|
||||
]);
|
||||
|
@@ -550,7 +550,7 @@ class ServersController extends Controller
|
||||
'is_build_server' => $request->is_build_server,
|
||||
]);
|
||||
if ($request->instant_validate) {
|
||||
ValidateServer::dispatch($server)->onQueue('high');
|
||||
ValidateServer::dispatch($server);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
@@ -675,7 +675,7 @@ class ServersController extends Controller
|
||||
]);
|
||||
}
|
||||
if ($request->instant_validate) {
|
||||
ValidateServer::dispatch($server)->onQueue('high');
|
||||
ValidateServer::dispatch($server);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
@@ -813,7 +813,7 @@ class ServersController extends Controller
|
||||
if (! $server) {
|
||||
return response()->json(['message' => 'Server not found.'], 404);
|
||||
}
|
||||
ValidateServer::dispatch($server)->onQueue('high');
|
||||
ValidateServer::dispatch($server);
|
||||
|
||||
return response()->json(['message' => 'Validation started.']);
|
||||
}
|
||||
|
@@ -342,7 +342,7 @@ class ServicesController extends Controller
|
||||
}
|
||||
$service->parse(isNew: true);
|
||||
if ($instantDeploy) {
|
||||
StartService::dispatch($service)->onQueue('high');
|
||||
StartService::dispatch($service);
|
||||
}
|
||||
$domains = $service->applications()->get()->pluck('fqdn')->sort();
|
||||
$domains = $domains->map(function ($domain) {
|
||||
@@ -487,7 +487,7 @@ class ServicesController extends Controller
|
||||
deleteVolumes: $request->query->get('delete_volumes', true),
|
||||
dockerCleanup: $request->query->get('docker_cleanup', true),
|
||||
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
|
||||
)->onQueue('high');
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Service deletion request queued.',
|
||||
@@ -1076,7 +1076,7 @@ class ServicesController extends Controller
|
||||
if (str($service->status())->contains('running')) {
|
||||
return response()->json(['message' => 'Service is already running.'], 400);
|
||||
}
|
||||
StartService::dispatch($service)->onQueue('high');
|
||||
StartService::dispatch($service);
|
||||
|
||||
return response()->json(
|
||||
[
|
||||
@@ -1154,7 +1154,7 @@ class ServicesController extends Controller
|
||||
if (str($service->status())->contains('stopped') || str($service->status())->contains('exited')) {
|
||||
return response()->json(['message' => 'Service is already stopped.'], 400);
|
||||
}
|
||||
StopService::dispatch($service)->onQueue('high');
|
||||
StopService::dispatch($service);
|
||||
|
||||
return response()->json(
|
||||
[
|
||||
@@ -1229,7 +1229,7 @@ class ServicesController extends Controller
|
||||
if (! $service) {
|
||||
return response()->json(['message' => 'Service not found.'], 404);
|
||||
}
|
||||
RestartService::dispatch($service)->onQueue('high');
|
||||
RestartService::dispatch($service);
|
||||
|
||||
return response()->json(
|
||||
[
|
||||
|
@@ -3,21 +3,26 @@
|
||||
namespace App\Http\Controllers\Webhook;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\ServerLimitCheckJob;
|
||||
use App\Jobs\SubscriptionInvoiceFailedJob;
|
||||
use App\Models\Subscription;
|
||||
use App\Models\Team;
|
||||
use App\Jobs\StripeProcessJob;
|
||||
use App\Models\Webhook;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Stripe extends Controller
|
||||
{
|
||||
protected $webhook;
|
||||
|
||||
public function events(Request $request)
|
||||
{
|
||||
try {
|
||||
$webhookSecret = config('subscription.stripe_webhook_secret');
|
||||
$signature = $request->header('Stripe-Signature');
|
||||
$event = \Stripe\Webhook::constructEvent(
|
||||
$request->getContent(),
|
||||
$signature,
|
||||
$webhookSecret
|
||||
);
|
||||
if (app()->isDownForMaintenance()) {
|
||||
$epoch = now()->valueOf();
|
||||
$data = [
|
||||
@@ -33,241 +38,17 @@ class Stripe extends Controller
|
||||
$json = json_encode($data);
|
||||
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Stripe::events_stripe", $json);
|
||||
|
||||
return;
|
||||
return response('Webhook received. Cool cool cool cool cool.', 200);
|
||||
}
|
||||
$webhookSecret = config('subscription.stripe_webhook_secret');
|
||||
$signature = $request->header('Stripe-Signature');
|
||||
$excludedPlans = config('subscription.stripe_excluded_plans');
|
||||
$event = \Stripe\Webhook::constructEvent(
|
||||
$request->getContent(),
|
||||
$signature,
|
||||
$webhookSecret
|
||||
);
|
||||
$webhook = Webhook::create([
|
||||
$this->webhook = Webhook::create([
|
||||
'type' => 'stripe',
|
||||
'payload' => $request->getContent(),
|
||||
]);
|
||||
$type = data_get($event, 'type');
|
||||
$data = data_get($event, 'data.object');
|
||||
switch ($type) {
|
||||
case 'radar.early_fraud_warning.created':
|
||||
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
|
||||
$id = data_get($data, 'id');
|
||||
$charge = data_get($data, 'charge');
|
||||
if ($charge) {
|
||||
$stripe->refunds->create(['charge' => $charge]);
|
||||
}
|
||||
$pi = data_get($data, 'payment_intent');
|
||||
$piData = $stripe->paymentIntents->retrieve($pi, []);
|
||||
$customerId = data_get($piData, 'customer');
|
||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
||||
if ($subscription) {
|
||||
$subscriptionId = data_get($subscription, 'stripe_subscription_id');
|
||||
$stripe->subscriptions->cancel($subscriptionId, []);
|
||||
$subscription->update([
|
||||
'stripe_invoice_paid' => false,
|
||||
]);
|
||||
send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}");
|
||||
} else {
|
||||
send_internal_notification("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
|
||||
StripeProcessJob::dispatch($event);
|
||||
|
||||
return response("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}", 400);
|
||||
}
|
||||
break;
|
||||
case 'checkout.session.completed':
|
||||
$clientReferenceId = data_get($data, 'client_reference_id');
|
||||
if (is_null($clientReferenceId)) {
|
||||
send_internal_notification('Checkout session completed without client reference id.');
|
||||
break;
|
||||
}
|
||||
$userId = Str::before($clientReferenceId, ':');
|
||||
$teamId = Str::after($clientReferenceId, ':');
|
||||
$subscriptionId = data_get($data, 'subscription');
|
||||
$customerId = data_get($data, 'customer');
|
||||
$team = Team::find($teamId);
|
||||
$found = $team->members->where('id', $userId)->first();
|
||||
if (! $found->isAdmin()) {
|
||||
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
|
||||
|
||||
return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.", 400);
|
||||
}
|
||||
$subscription = Subscription::where('team_id', $teamId)->first();
|
||||
if ($subscription) {
|
||||
// send_internal_notification('Old subscription activated for team: '.$teamId);
|
||||
$subscription->update([
|
||||
'stripe_subscription_id' => $subscriptionId,
|
||||
'stripe_customer_id' => $customerId,
|
||||
'stripe_invoice_paid' => true,
|
||||
]);
|
||||
} else {
|
||||
// send_internal_notification('New subscription for team: '.$teamId);
|
||||
Subscription::create([
|
||||
'team_id' => $teamId,
|
||||
'stripe_subscription_id' => $subscriptionId,
|
||||
'stripe_customer_id' => $customerId,
|
||||
'stripe_invoice_paid' => true,
|
||||
]);
|
||||
}
|
||||
break;
|
||||
case 'invoice.paid':
|
||||
$customerId = data_get($data, 'customer');
|
||||
$planId = data_get($data, 'lines.data.0.plan.id');
|
||||
if (Str::contains($excludedPlans, $planId)) {
|
||||
// send_internal_notification('Subscription excluded.');
|
||||
break;
|
||||
}
|
||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
||||
if ($subscription) {
|
||||
$subscription->update([
|
||||
'stripe_invoice_paid' => true,
|
||||
]);
|
||||
} else {
|
||||
return response("No subscription found for customer: {$customerId}", 400);
|
||||
}
|
||||
break;
|
||||
case 'invoice.payment_failed':
|
||||
$customerId = data_get($data, 'customer');
|
||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
||||
if (! $subscription) {
|
||||
// send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId);
|
||||
|
||||
return response('No subscription found in Coolify.');
|
||||
}
|
||||
$team = data_get($subscription, 'team');
|
||||
if (! $team) {
|
||||
// send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId);
|
||||
|
||||
return response('No team found in Coolify.');
|
||||
}
|
||||
if (! $subscription->stripe_invoice_paid) {
|
||||
SubscriptionInvoiceFailedJob::dispatch($team);
|
||||
// send_internal_notification('Invoice payment failed: '.$customerId);
|
||||
} else {
|
||||
// send_internal_notification('Invoice payment failed but already paid: '.$customerId);
|
||||
}
|
||||
break;
|
||||
case 'payment_intent.payment_failed':
|
||||
$customerId = data_get($data, 'customer');
|
||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
||||
if (! $subscription) {
|
||||
// send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId);
|
||||
|
||||
return response('No subscription found in Coolify.');
|
||||
}
|
||||
if ($subscription->stripe_invoice_paid) {
|
||||
// send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId);
|
||||
|
||||
return;
|
||||
}
|
||||
send_internal_notification('Subscription payment failed for customer: '.$customerId);
|
||||
break;
|
||||
case 'customer.subscription.created':
|
||||
$customerId = data_get($data, 'customer');
|
||||
$subscriptionId = data_get($data, 'id');
|
||||
$teamId = data_get($data, 'metadata.team_id');
|
||||
$userId = data_get($data, 'metadata.user_id');
|
||||
if (! $teamId || ! $userId) {
|
||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
||||
if ($subscription) {
|
||||
return response("Subscription already exists for customer: {$customerId}", 200);
|
||||
}
|
||||
|
||||
return response('No team id or user id found', 400);
|
||||
}
|
||||
$team = Team::find($teamId);
|
||||
$found = $team->members->where('id', $userId)->first();
|
||||
if (! $found->isAdmin()) {
|
||||
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
|
||||
|
||||
return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.", 400);
|
||||
}
|
||||
$subscription = Subscription::where('team_id', $teamId)->first();
|
||||
if ($subscription) {
|
||||
return response("Subscription already exists for team: {$teamId}", 200);
|
||||
} else {
|
||||
Subscription::create([
|
||||
'team_id' => $teamId,
|
||||
'stripe_subscription_id' => $subscriptionId,
|
||||
'stripe_customer_id' => $customerId,
|
||||
'stripe_invoice_paid' => false,
|
||||
]);
|
||||
|
||||
return response('Subscription created');
|
||||
}
|
||||
case 'customer.subscription.updated':
|
||||
$teamId = data_get($data, 'metadata.team_id');
|
||||
$userId = data_get($data, 'metadata.user_id');
|
||||
$customerId = data_get($data, 'customer');
|
||||
$status = data_get($data, 'status');
|
||||
$subscriptionId = data_get($data, 'items.data.0.subscription');
|
||||
$planId = data_get($data, 'items.data.0.plan.id');
|
||||
if (Str::contains($excludedPlans, $planId)) {
|
||||
// send_internal_notification('Subscription excluded.');
|
||||
break;
|
||||
}
|
||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
|
||||
if (! $subscription) {
|
||||
if ($status === 'incomplete_expired') {
|
||||
return response('Subscription incomplete expired', 200);
|
||||
}
|
||||
if ($teamId) {
|
||||
$subscription = Subscription::create([
|
||||
'team_id' => $teamId,
|
||||
'stripe_subscription_id' => $subscriptionId,
|
||||
'stripe_customer_id' => $customerId,
|
||||
'stripe_invoice_paid' => false,
|
||||
]);
|
||||
} else {
|
||||
return response('No subscription and team id found', 400);
|
||||
}
|
||||
}
|
||||
$cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
|
||||
$feedback = data_get($data, 'cancellation_details.feedback');
|
||||
$comment = data_get($data, 'cancellation_details.comment');
|
||||
$lookup_key = data_get($data, 'items.data.0.price.lookup_key');
|
||||
if (str($lookup_key)->contains('dynamic')) {
|
||||
$quantity = data_get($data, 'items.data.0.quantity', 2);
|
||||
$team = data_get($subscription, 'team');
|
||||
if ($team) {
|
||||
$team->update([
|
||||
'custom_server_limit' => $quantity,
|
||||
]);
|
||||
}
|
||||
ServerLimitCheckJob::dispatch($team);
|
||||
}
|
||||
$subscription->update([
|
||||
'stripe_feedback' => $feedback,
|
||||
'stripe_comment' => $comment,
|
||||
'stripe_plan_id' => $planId,
|
||||
'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
|
||||
]);
|
||||
if ($status === 'paused' || $status === 'incomplete_expired') {
|
||||
$subscription->update([
|
||||
'stripe_invoice_paid' => false,
|
||||
]);
|
||||
}
|
||||
if ($feedback) {
|
||||
$reason = "Cancellation feedback for {$customerId}: '".$feedback."'";
|
||||
if ($comment) {
|
||||
$reason .= ' with comment: \''.$comment."'";
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'customer.subscription.deleted':
|
||||
// End subscription
|
||||
$customerId = data_get($data, 'customer');
|
||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
|
||||
$team = data_get($subscription, 'team');
|
||||
$team?->subscriptionEnded();
|
||||
break;
|
||||
default:
|
||||
// Unhandled event type
|
||||
}
|
||||
return response('Webhook received. Cool cool cool cool cool.', 200);
|
||||
} catch (Exception $e) {
|
||||
if ($type !== 'payment_intent.payment_failed') {
|
||||
send_internal_notification("Subscription webhook ($type) failed: ".$e->getMessage());
|
||||
}
|
||||
$webhook->update([
|
||||
$this->webhook->update([
|
||||
'status' => 'failed',
|
||||
'failure_reason' => $e->getMessage(),
|
||||
]);
|
||||
|
@@ -166,6 +166,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public function __construct(int $application_deployment_queue_id)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
|
||||
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
|
||||
$this->application = Application::find($this->application_deployment_queue->application_id);
|
||||
$this->build_pack = data_get($this->application, 'build_pack');
|
||||
@@ -349,8 +351,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
private function post_deployment()
|
||||
{
|
||||
if ($this->server->isProxyShouldRun()) {
|
||||
GetContainersStatus::dispatch($this->server)->onQueue('high');
|
||||
// dispatch(new ContainerStatusJob($this->server));
|
||||
GetContainersStatus::dispatch($this->server);
|
||||
}
|
||||
$this->next(ApplicationDeploymentStatus::FINISHED->value);
|
||||
if ($this->pull_request_id !== 0) {
|
||||
|
@@ -25,7 +25,9 @@ class ApplicationPullRequestUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
public ApplicationPreview $preview,
|
||||
public ProcessStatus $status,
|
||||
public ?string $deployment_uuid = null
|
||||
) {}
|
||||
) {
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
|
@@ -23,7 +23,10 @@ class CoolifyTask implements ShouldBeEncrypted, ShouldQueue
|
||||
public bool $ignore_errors,
|
||||
public $call_event_on_finish,
|
||||
public $call_event_data,
|
||||
) {}
|
||||
) {
|
||||
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
|
@@ -60,6 +60,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public function __construct($backup)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
$this->backup = $backup;
|
||||
}
|
||||
|
||||
@@ -197,8 +198,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$databaseType = $this->database->type();
|
||||
$databasesToBackup = data_get($this->backup, 'databases_to_backup');
|
||||
}
|
||||
|
||||
if (is_null($databasesToBackup)) {
|
||||
if (blank($databasesToBackup)) {
|
||||
if (str($databaseType)->contains('postgres')) {
|
||||
$databasesToBackup = [$this->database->postgres_db];
|
||||
} elseif (str($databaseType)->contains('mongodb')) {
|
||||
@@ -319,12 +319,10 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
'filename' => null,
|
||||
]);
|
||||
}
|
||||
send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
|
||||
$this->team?->notify(new BackupFailed($this->backup, $this->database, $this->backup_output, $database));
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
|
||||
throw $e;
|
||||
} finally {
|
||||
if ($this->team) {
|
||||
|
@@ -35,7 +35,9 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
||||
public bool $deleteVolumes = true,
|
||||
public bool $dockerCleanup = true,
|
||||
public bool $deleteConnectedNetworks = true
|
||||
) {}
|
||||
) {
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
@@ -87,7 +89,6 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->resource?->delete_connected_networks($this->resource->uuid);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
|
||||
throw $e;
|
||||
} finally {
|
||||
$this->resource->forceDelete();
|
||||
|
@@ -16,7 +16,10 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public $timeout = 1000;
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
|
@@ -17,7 +17,10 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public $timeout = 10;
|
||||
|
||||
public function __construct() {}
|
||||
public function __construct()
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
|
@@ -360,7 +360,7 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
private function checkLogDrainContainer()
|
||||
{
|
||||
if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) {
|
||||
StartLogDrain::dispatch($this->server)->onQueue('high');
|
||||
StartLogDrain::dispatch($this->server);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -40,6 +40,8 @@ class ScheduledTaskJob implements ShouldQueue
|
||||
|
||||
public function __construct($task)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
|
||||
$this->task = $task;
|
||||
if ($service = $task->service()->first()) {
|
||||
$this->resource = $service;
|
||||
|
@@ -32,7 +32,9 @@ class SendMessageToDiscordJob implements ShouldBeEncrypted, ShouldQueue
|
||||
public function __construct(
|
||||
public DiscordMessage $message,
|
||||
public string $webhookUrl
|
||||
) {}
|
||||
) {
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
|
@@ -33,7 +33,9 @@ class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue
|
||||
public string $token,
|
||||
public string $chatId,
|
||||
public ?string $topicId = null,
|
||||
) {}
|
||||
) {
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
@@ -70,7 +72,7 @@ class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
$response = Http::post($url, $payload);
|
||||
if ($response->failed()) {
|
||||
throw new \Exception('Telegram notification failed with '.$response->status().' status code.'.$response->body());
|
||||
throw new \RuntimeException('Telegram notification failed with '.$response->status().' status code.'.$response->body());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -94,10 +94,10 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if ($foundLogDrainContainer) {
|
||||
$status = data_get($foundLogDrainContainer, 'State.Status');
|
||||
if ($status !== 'running') {
|
||||
StartLogDrain::dispatch($this->server)->onQueue('high');
|
||||
StartLogDrain::dispatch($this->server);
|
||||
}
|
||||
} else {
|
||||
StartLogDrain::dispatch($this->server)->onQueue('high');
|
||||
StartLogDrain::dispatch($this->server);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -16,7 +16,10 @@ class ServerFilesFromServerJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function __construct(public ServiceApplication|ServiceDatabase|Application $resource) {}
|
||||
public function __construct(public ServiceApplication|ServiceDatabase|Application $resource)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
|
@@ -25,7 +25,7 @@ class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
return isDev() ? 1 : 3;
|
||||
}
|
||||
|
||||
public function __construct(public Server $server, public ?int $percentage = null) {}
|
||||
public function __construct(public Server $server, public int|string|null $percentage = null) {}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
|
@@ -14,7 +14,10 @@ class ServerStorageSaveJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function __construct(public LocalFileVolume $localFileVolume) {}
|
||||
public function __construct(public LocalFileVolume $localFileVolume)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
|
242
app/Jobs/StripeProcessJob.php
Normal file
242
app/Jobs/StripeProcessJob.php
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@@ -15,7 +15,10 @@ class SubscriptionInvoiceFailedJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function __construct(protected Team $team) {}
|
||||
public function __construct(protected Team $team)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
|
@@ -18,6 +18,11 @@ class UpdateCoolifyJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public $timeout = 600;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
|
@@ -3,7 +3,6 @@
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use Illuminate\Container\Attributes\Auth as AttributesAuth;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
@@ -32,7 +31,7 @@ class NavbarDeleteTeam extends Component
|
||||
$currentTeam->delete();
|
||||
|
||||
$currentTeam->members->each(function ($user) use ($currentTeam) {
|
||||
if ($user->id === AttributesAuth::id()) {
|
||||
if ($user->id === Auth::id()) {
|
||||
return;
|
||||
}
|
||||
$user->teams()->detach($currentTeam);
|
||||
|
@@ -73,6 +73,9 @@ class Email extends Component
|
||||
#[Validate(['nullable', 'string'])]
|
||||
public ?string $resendApiKey = null;
|
||||
|
||||
#[Validate(['required', 'email'])]
|
||||
public string $testEmailAddress = '';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
@@ -132,14 +135,21 @@ class Email extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function sendTestNotification()
|
||||
public function sendTestEmail()
|
||||
{
|
||||
try {
|
||||
$this->validate([
|
||||
'testEmailAddress' => 'required|email',
|
||||
], [
|
||||
'testEmailAddress.required' => 'Test email address is required.',
|
||||
'testEmailAddress.email' => 'Please enter a valid email address.',
|
||||
]);
|
||||
|
||||
$executed = RateLimiter::attempt(
|
||||
'test-email:'.$this->team->id,
|
||||
$perMinute = 0,
|
||||
function () {
|
||||
$this->team?->notify(new Test($this->emails));
|
||||
$this->team?->notify(new Test($this->testEmailAddress));
|
||||
$this->dispatch('success', 'Test Email sent.');
|
||||
},
|
||||
$decaySeconds = 10,
|
||||
|
@@ -36,7 +36,11 @@ class Heading extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->parameters = [
|
||||
'project_uuid' => $this->application->project()->uuid,
|
||||
'environment_name' => $this->application->environment->name,
|
||||
'application_uuid' => $this->application->uuid,
|
||||
];
|
||||
$lastDeployment = $this->application->get_last_successful_deployment();
|
||||
$this->lastDeploymentInfo = data_get_str($lastDeployment, 'commit')->limit(7).' '.data_get($lastDeployment, 'commit_message');
|
||||
$this->lastDeploymentLink = $this->application->gitCommitLink(data_get($lastDeployment, 'commit'));
|
||||
@@ -45,13 +49,11 @@ class Heading extends Component
|
||||
public function check_status($showNotification = false)
|
||||
{
|
||||
if ($this->application->destination->server->isFunctional()) {
|
||||
GetContainersStatus::dispatch($this->application->destination->server)->onQueue('high');
|
||||
GetContainersStatus::dispatch($this->application->destination->server);
|
||||
}
|
||||
if ($showNotification) {
|
||||
$this->dispatch('success', 'Success', 'Application status updated.');
|
||||
}
|
||||
// Removed because it caused flickering
|
||||
// $this->dispatch('configurationChanged');
|
||||
}
|
||||
|
||||
public function force_deploy_without_cache()
|
||||
|
@@ -21,8 +21,8 @@ class CreateScheduledBackup extends Component
|
||||
|
||||
public bool $enabled = true;
|
||||
|
||||
#[Validate(['required', 'integer'])]
|
||||
public int $s3StorageId;
|
||||
#[Validate(['nullable', 'integer'])]
|
||||
public ?int $s3StorageId = null;
|
||||
|
||||
public Collection $definedS3s;
|
||||
|
||||
@@ -49,6 +49,7 @@ class CreateScheduledBackup extends Component
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'enabled' => true,
|
||||
'frequency' => $this->frequency,
|
||||
@@ -58,6 +59,7 @@ class CreateScheduledBackup extends Component
|
||||
'database_type' => $this->database->getMorphClass(),
|
||||
'team_id' => currentTeam()->id,
|
||||
];
|
||||
|
||||
if ($this->database->type() === 'standalone-postgresql') {
|
||||
$payload['databases_to_backup'] = $this->database->postgres_db;
|
||||
} elseif ($this->database->type() === 'standalone-mysql') {
|
||||
@@ -72,11 +74,11 @@ class CreateScheduledBackup extends Component
|
||||
} else {
|
||||
$this->dispatch('refreshScheduledBackups');
|
||||
}
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
$this->frequency = '';
|
||||
$this->saveToS3 = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -91,9 +91,12 @@ class Select extends Component
|
||||
{
|
||||
$services = get_service_templates(true);
|
||||
$services = collect($services)->map(function ($service, $key) {
|
||||
$logo = data_get($service, 'logo', 'svgs/coolify.png');
|
||||
|
||||
return [
|
||||
'name' => str($key)->headline(),
|
||||
'logo' => asset(data_get($service, 'logo', 'svgs/coolify.png')),
|
||||
'logo' => asset($logo),
|
||||
'logo_github_url' => 'https://raw.githubusercontent.com/coollabsio/coolify/refs/heads/main/public/a'.$logo,
|
||||
] + (array) $service;
|
||||
})->all();
|
||||
$gitBasedApplications = [
|
||||
|
@@ -37,6 +37,7 @@ class Tags extends Component
|
||||
$this->validate();
|
||||
$tags = str($this->newTags)->trim()->explode(' ');
|
||||
foreach ($tags as $tag) {
|
||||
$tag = strip_tags($tag);
|
||||
if (strlen($tag) < 2) {
|
||||
$this->dispatch('error', 'Invalid tag.', "Tag <span class='dark:text-warning'>$tag</span> is invalid. Min length is 2.");
|
||||
|
||||
@@ -65,6 +66,7 @@ class Tags extends Component
|
||||
public function addTag(string $id, string $name)
|
||||
{
|
||||
try {
|
||||
$name = strip_tags($name);
|
||||
if ($this->resource->tags()->where('id', $id)->exists()) {
|
||||
$this->dispatch('error', 'Duplicate tags.', "Tag <span class='dark:text-warning'>$name</span> already added.");
|
||||
|
||||
|
@@ -6,64 +6,60 @@ use App\Enums\ProxyTypes;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
class ByIp extends Component
|
||||
{
|
||||
#[Locked]
|
||||
public $private_keys;
|
||||
|
||||
#[Locked]
|
||||
public $limit_reached;
|
||||
|
||||
#[Validate('nullable|integer', as: 'Private Key')]
|
||||
public ?int $private_key_id = null;
|
||||
|
||||
#[Validate('nullable|string', as: 'Private Key Name')]
|
||||
public $new_private_key_name;
|
||||
|
||||
#[Validate('nullable|string', as: 'Private Key Description')]
|
||||
public $new_private_key_description;
|
||||
|
||||
#[Validate('nullable|string', as: 'Private Key Value')]
|
||||
public $new_private_key_value;
|
||||
|
||||
#[Validate('required|string', as: 'Name')]
|
||||
public string $name;
|
||||
|
||||
#[Validate('nullable|string', as: 'Description')]
|
||||
public ?string $description = null;
|
||||
|
||||
#[Validate('required|string', as: 'IP Address/Domain')]
|
||||
public string $ip;
|
||||
|
||||
#[Validate('required|string', as: 'User')]
|
||||
public string $user = 'root';
|
||||
|
||||
#[Validate('required|integer|between:1,65535', as: 'Port')]
|
||||
public int $port = 22;
|
||||
|
||||
#[Validate('required|boolean', as: 'Swarm Manager')]
|
||||
public bool $is_swarm_manager = false;
|
||||
|
||||
#[Validate('required|boolean', as: 'Swarm Worker')]
|
||||
public bool $is_swarm_worker = false;
|
||||
|
||||
#[Validate('nullable|integer', as: 'Swarm Cluster')]
|
||||
public $selected_swarm_cluster = null;
|
||||
|
||||
#[Validate('required|boolean', as: 'Build Server')]
|
||||
public bool $is_build_server = false;
|
||||
|
||||
#[Locked]
|
||||
public Collection $swarm_managers;
|
||||
|
||||
protected $rules = [
|
||||
'name' => 'required|string',
|
||||
'description' => 'nullable|string',
|
||||
'ip' => 'required',
|
||||
'user' => 'required|string',
|
||||
'port' => 'required|integer',
|
||||
'is_swarm_manager' => 'required|boolean',
|
||||
'is_swarm_worker' => 'required|boolean',
|
||||
'is_build_server' => 'required|boolean',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
'name' => 'Name',
|
||||
'description' => 'Description',
|
||||
'ip' => 'IP Address/Domain',
|
||||
'user' => 'User',
|
||||
'port' => 'Port',
|
||||
'is_swarm_manager' => 'Swarm Manager',
|
||||
'is_swarm_worker' => 'Swarm Worker',
|
||||
'is_build_server' => 'Build Server',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->name = generate_random_name();
|
||||
@@ -88,6 +84,12 @@ class ByIp extends Component
|
||||
{
|
||||
$this->validate();
|
||||
try {
|
||||
if (Server::where('team_id', currentTeam()->id)
|
||||
->where('ip', $this->ip)
|
||||
->exists()) {
|
||||
return $this->dispatch('error', 'This IP/Domain is already in use by another server in your team.');
|
||||
}
|
||||
|
||||
if (is_null($this->private_key_id)) {
|
||||
return $this->dispatch('error', 'You must select a private key');
|
||||
}
|
||||
|
@@ -107,6 +107,15 @@ class Show extends Component
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
if (Server::where('team_id', currentTeam()->id)
|
||||
->where('ip', $this->ip)
|
||||
->where('id', '!=', $this->server->id)
|
||||
->exists()) {
|
||||
$this->ip = $this->server->ip;
|
||||
throw new \Exception('This IP/Domain is already in use by another server in your team.');
|
||||
}
|
||||
|
||||
$this->server->name = $this->name;
|
||||
$this->server->description = $this->description;
|
||||
$this->server->ip = $this->ip;
|
||||
|
@@ -218,10 +218,12 @@ class PrivateKey extends BaseModel
|
||||
|
||||
private static function fingerprintExists($fingerprint, $excludeId = null)
|
||||
{
|
||||
$query = self::where('fingerprint', $fingerprint);
|
||||
$query = self::query()
|
||||
->where('fingerprint', $fingerprint)
|
||||
->where('id', '!=', $excludeId);
|
||||
|
||||
if (! is_null($excludeId)) {
|
||||
$query->where('id', '!=', $excludeId);
|
||||
if (currentTeam()) {
|
||||
$query->where('team_id', currentTeam()->id);
|
||||
}
|
||||
|
||||
return $query->exists();
|
||||
|
@@ -988,7 +988,7 @@ $schema://$host {
|
||||
|
||||
public function status(): bool
|
||||
{
|
||||
['uptime' => $uptime] = $this->validateConnection(false);
|
||||
['uptime' => $uptime] = $this->validateConnection();
|
||||
if ($uptime === false) {
|
||||
foreach ($this->applications() as $application) {
|
||||
$application->status = 'exited';
|
||||
@@ -1051,18 +1051,14 @@ $schema://$host {
|
||||
$this->team->notify(new Unreachable($this));
|
||||
}
|
||||
|
||||
public function validateConnection(bool $isManualCheck = true, bool $justCheckingNewKey = false)
|
||||
public function validateConnection(bool $justCheckingNewKey = false)
|
||||
{
|
||||
config()->set('constants.ssh.mux_enabled', ! $isManualCheck);
|
||||
config()->set('constants.ssh.mux_enabled', false);
|
||||
|
||||
if ($this->skipServer()) {
|
||||
return ['uptime' => false, 'error' => 'Server skipped.'];
|
||||
}
|
||||
try {
|
||||
// Make sure the private key is stored
|
||||
if ($this->privateKey) {
|
||||
$this->privateKey->storeInFileSystem();
|
||||
}
|
||||
instant_remote_process(['ls /'], $this);
|
||||
if ($this->settings->is_reachable === false) {
|
||||
$this->settings->is_reachable = true;
|
||||
|
@@ -35,6 +35,7 @@ class DeploymentFailed extends Notification implements ShouldQueue
|
||||
|
||||
public function __construct(Application $application, string $deployment_uuid, ?ApplicationPreview $preview = null)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
$this->application = $application;
|
||||
$this->deployment_uuid = $deployment_uuid;
|
||||
$this->preview = $preview;
|
||||
|
@@ -34,6 +34,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
|
||||
|
||||
public function __construct(Application $application, string $deployment_uuid, ?ApplicationPreview $preview = null)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
$this->application = $application;
|
||||
$this->deployment_uuid = $deployment_uuid;
|
||||
$this->preview = $preview;
|
||||
|
@@ -27,6 +27,7 @@ class StatusChanged extends Notification implements ShouldQueue
|
||||
|
||||
public function __construct(public Application $resource)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
$this->resource_name = data_get($resource, 'name');
|
||||
$this->project_uuid = data_get($resource, 'environment.project.uuid');
|
||||
$this->environment_name = data_get($resource, 'environment.name');
|
||||
|
@@ -17,6 +17,6 @@ class DiscordChannel
|
||||
if (! $webhookUrl) {
|
||||
return;
|
||||
}
|
||||
dispatch(new SendMessageToDiscordJob($message, $webhookUrl))->onQueue('high');
|
||||
SendMessageToDiscordJob::dispatch($message, $webhookUrl);
|
||||
}
|
||||
}
|
||||
|
@@ -41,6 +41,6 @@ class TelegramChannel
|
||||
if (! $telegramToken || ! $chatId || ! $message) {
|
||||
return;
|
||||
}
|
||||
dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId, $topicId))->onQueue('high');
|
||||
SendMessageToTelegramJob::dispatch($message, $buttons, $telegramToken, $chatId, $topicId);
|
||||
}
|
||||
}
|
||||
|
@@ -17,6 +17,7 @@ class ContainerRestarted extends Notification implements ShouldQueue
|
||||
|
||||
public function __construct(public string $name, public Server $server, public ?string $url = null)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
|
@@ -17,6 +17,7 @@ class ContainerStopped extends Notification implements ShouldQueue
|
||||
|
||||
public function __construct(public string $name, public Server $server, public ?string $url = null)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
|
@@ -23,6 +23,7 @@ class BackupFailed extends Notification implements ShouldQueue
|
||||
|
||||
public function __construct(ScheduledDatabaseBackup $backup, public $database, public $output, public $database_name)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
$this->name = $database->name;
|
||||
$this->frequency = $backup->frequency;
|
||||
}
|
||||
|
@@ -23,6 +23,7 @@ class BackupSuccess extends Notification implements ShouldQueue
|
||||
|
||||
public function __construct(ScheduledDatabaseBackup $backup, public $database, public $database_name)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
$this->name = $database->name;
|
||||
$this->frequency = $backup->frequency;
|
||||
}
|
||||
|
@@ -19,6 +19,7 @@ class GeneralNotification extends Notification implements ShouldQueue
|
||||
|
||||
public function __construct(public string $message)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
|
@@ -21,6 +21,7 @@ class TaskFailed extends Notification implements ShouldQueue
|
||||
|
||||
public function __construct(public ScheduledTask $task, public string $output)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
if ($task->application) {
|
||||
$this->url = $task->application->failedTaskLink($task->uuid);
|
||||
} elseif ($task->service) {
|
||||
|
@@ -19,6 +19,7 @@ class DockerCleanup extends Notification implements ShouldQueue
|
||||
|
||||
public function __construct(public Server $server, public string $message)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
|
@@ -22,6 +22,7 @@ class ForceDisabled extends Notification implements ShouldQueue
|
||||
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
|
@@ -22,6 +22,7 @@ class ForceEnabled extends Notification implements ShouldQueue
|
||||
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
|
@@ -18,6 +18,7 @@ class HighDiskUsage extends Notification implements ShouldQueue
|
||||
|
||||
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
|
||||
|
@@ -24,6 +24,7 @@ class Reachable extends Notification implements ShouldQueue
|
||||
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
$this->isRateLimited = isEmailRateLimited(
|
||||
limiterKey: 'server-reachable:' . $this->server->id,
|
||||
);
|
||||
|
@@ -24,6 +24,7 @@ class Unreachable extends Notification implements ShouldQueue
|
||||
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
$this->isRateLimited = isEmailRateLimited(
|
||||
limiterKey: 'server-unreachable:' . $this->server->id,
|
||||
);
|
||||
|
@@ -18,6 +18,7 @@ class Test extends Notification implements ShouldQueue
|
||||
|
||||
public function __construct(public ?string $emails = null)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
|
@@ -22,7 +22,10 @@ class InvitationLink extends Notification implements ShouldQueue
|
||||
return [TransactionalEmailChannel::class];
|
||||
}
|
||||
|
||||
public function __construct(public User $user) {}
|
||||
public function __construct(public User $user)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
public function toMail(): MailMessage
|
||||
{
|
||||
|
@@ -14,7 +14,10 @@ class Test extends Notification implements ShouldQueue
|
||||
|
||||
public $tries = 5;
|
||||
|
||||
public function __construct(public string $emails) {}
|
||||
public function __construct(public string $emails)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
public function via(): array
|
||||
{
|
||||
|
@@ -17,11 +17,14 @@ class Checkbox extends Component
|
||||
public ?string $value = null,
|
||||
public ?string $label = null,
|
||||
public ?string $helper = null,
|
||||
public string|bool|null $checked = false,
|
||||
public string|bool $instantSave = false,
|
||||
public bool $disabled = false,
|
||||
public string $defaultClass = 'dark:border-neutral-700 text-coolgray-400 focus:ring-warning dark:bg-coolgray-100 rounded cursor-pointer dark:disabled:bg-base dark:disabled:cursor-not-allowed',
|
||||
) {
|
||||
//
|
||||
if ($this->disabled) {
|
||||
$this->defaultClass .= ' opacity-40';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -44,13 +44,13 @@ function queue_application_deployment(Application $application, string $deployme
|
||||
]);
|
||||
|
||||
if ($no_questions_asked) {
|
||||
dispatch(new ApplicationDeploymentJob(
|
||||
ApplicationDeploymentJob::dispatch(
|
||||
application_deployment_queue_id: $deployment->id,
|
||||
))->onQueue('high');
|
||||
);
|
||||
} elseif (next_queuable($server_id, $application_id)) {
|
||||
dispatch(new ApplicationDeploymentJob(
|
||||
ApplicationDeploymentJob::dispatch(
|
||||
application_deployment_queue_id: $deployment->id,
|
||||
))->onQueue('high');
|
||||
);
|
||||
}
|
||||
}
|
||||
function force_start_deployment(ApplicationDeploymentQueue $deployment)
|
||||
@@ -59,9 +59,9 @@ function force_start_deployment(ApplicationDeploymentQueue $deployment)
|
||||
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
|
||||
]);
|
||||
|
||||
dispatch(new ApplicationDeploymentJob(
|
||||
ApplicationDeploymentJob::dispatch(
|
||||
application_deployment_queue_id: $deployment->id,
|
||||
))->onQueue('high');
|
||||
);
|
||||
}
|
||||
function queue_next_deployment(Application $application)
|
||||
{
|
||||
@@ -72,9 +72,9 @@ function queue_next_deployment(Application $application)
|
||||
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
|
||||
]);
|
||||
|
||||
dispatch(new ApplicationDeploymentJob(
|
||||
ApplicationDeploymentJob::dispatch(
|
||||
application_deployment_queue_id: $next_found->id,
|
||||
))->onQueue('high');
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,9 +113,9 @@ function next_after_cancel(?Server $server = null)
|
||||
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
|
||||
]);
|
||||
|
||||
dispatch(new ApplicationDeploymentJob(
|
||||
ApplicationDeploymentJob::dispatch(
|
||||
application_deployment_queue_id: $next->id,
|
||||
))->onQueue('high');
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@@ -227,13 +227,17 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
|
||||
$MINIO_BROWSER_REDIRECT_URL = $variables->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
|
||||
$MINIO_SERVER_URL = $variables->where('key', 'MINIO_SERVER_URL')->first();
|
||||
|
||||
if (str($MINIO_BROWSER_REDIRECT_URL->value)->isEmpty()) {
|
||||
$MINIO_BROWSER_REDIRECT_URL?->update([
|
||||
if (is_null($MINIO_BROWSER_REDIRECT_URL) || is_null($MINIO_SERVER_URL)) {
|
||||
return collect([]);
|
||||
}
|
||||
|
||||
if (str($MINIO_BROWSER_REDIRECT_URL->value ?? '')->isEmpty()) {
|
||||
$MINIO_BROWSER_REDIRECT_URL->update([
|
||||
'value' => generateFqdn($server, 'console-'.$uuid, true),
|
||||
]);
|
||||
}
|
||||
if (str($MINIO_SERVER_URL->value)->isEmpty()) {
|
||||
$MINIO_SERVER_URL?->update([
|
||||
if (str($MINIO_SERVER_URL->value ?? '')->isEmpty()) {
|
||||
$MINIO_SERVER_URL->update([
|
||||
'value' => generateFqdn($server, 'minio-'.$uuid, true),
|
||||
]);
|
||||
}
|
||||
@@ -246,13 +250,17 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
|
||||
$LOGTO_ENDPOINT = $variables->where('key', 'LOGTO_ENDPOINT')->first();
|
||||
$LOGTO_ADMIN_ENDPOINT = $variables->where('key', 'LOGTO_ADMIN_ENDPOINT')->first();
|
||||
|
||||
if (str($LOGTO_ENDPOINT?->value)->isEmpty()) {
|
||||
$LOGTO_ENDPOINT?->update([
|
||||
if (is_null($LOGTO_ENDPOINT) || is_null($LOGTO_ADMIN_ENDPOINT)) {
|
||||
return collect([]);
|
||||
}
|
||||
|
||||
if (str($LOGTO_ENDPOINT->value ?? '')->isEmpty()) {
|
||||
$LOGTO_ENDPOINT->update([
|
||||
'value' => generateFqdn($server, 'logto-'.$uuid),
|
||||
]);
|
||||
}
|
||||
if (str($LOGTO_ADMIN_ENDPOINT?->value)->isEmpty()) {
|
||||
$LOGTO_ADMIN_ENDPOINT?->update([
|
||||
if (str($LOGTO_ADMIN_ENDPOINT->value ?? '')->isEmpty()) {
|
||||
$LOGTO_ADMIN_ENDPOINT->update([
|
||||
'value' => generateFqdn($server, 'logto-admin-'.$uuid),
|
||||
]);
|
||||
}
|
||||
|
@@ -4081,7 +4081,7 @@ function defaultNginxConfiguration(): string
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/index.html =404;
|
||||
try_files $uri $uri.html $uri/index.html $uri/index.htm $uri/ /index.html /index.htm =404;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
@@ -42,6 +42,7 @@
|
||||
"socialiteproviders/microsoft-azure": "^5.1",
|
||||
"spatie/laravel-activitylog": "^4.7.3",
|
||||
"spatie/laravel-data": "^4.11",
|
||||
"spatie/laravel-ray": "^1.37",
|
||||
"spatie/laravel-schemaless-attributes": "^2.4",
|
||||
"spatie/url": "^2.2",
|
||||
"stripe/stripe-php": "^16.2.0",
|
||||
@@ -63,7 +64,6 @@
|
||||
"phpunit/phpunit": "^11.4",
|
||||
"serversideup/spin": "^2.3",
|
||||
"spatie/laravel-ignition": "^2.1.0",
|
||||
"spatie/laravel-ray": "^1.37",
|
||||
"symfony/http-client": "^7.1"
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
|
1686
composer.lock
generated
1686
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -2,9 +2,9 @@
|
||||
|
||||
return [
|
||||
'coolify' => [
|
||||
'version' => '4.0.0-beta.368',
|
||||
'version' => '4.0.0-beta.372',
|
||||
'self_hosted' => env('SELF_HOSTED', true),
|
||||
'autoupdate' => env('AUTOUPDATE', false),
|
||||
'autoupdate' => env('AUTOUPDATE'),
|
||||
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
|
||||
'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper'),
|
||||
'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false),
|
||||
|
@@ -1,3 +1,3 @@
|
||||
<?php
|
||||
|
||||
return '4.0.0-beta.368';
|
||||
return '4.0.0-beta.372';
|
||||
|
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class AddIndexToActivityLog extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
try {
|
||||
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()
|
||||
{
|
||||
try {
|
||||
DB::statement('DROP INDEX IF EXISTS idx_activity_type_uuid');
|
||||
DB::statement('ALTER TABLE activity_log ALTER COLUMN properties TYPE json USING properties::json');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error dropping index from activity_log: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
@@ -22,9 +22,9 @@ class ProductionSeeder extends Seeder
|
||||
public function run(): void
|
||||
{
|
||||
if (isCloud()) {
|
||||
echo "Running in cloud mode.\n";
|
||||
echo "[x]: Running in cloud mode.\n";
|
||||
} else {
|
||||
echo "Running in self-hosted mode.\n";
|
||||
echo "[x]: Running in self-hosted mode.\n";
|
||||
}
|
||||
|
||||
// Fix for 4.0.0-beta.37
|
||||
|
@@ -1,4 +1,5 @@
|
||||
# Versions
|
||||
|
||||
# https://hub.docker.com/_/alpine
|
||||
ARG BASE_IMAGE=alpine:3.20
|
||||
# https://download.docker.com/linux/static/stable/
|
||||
|
@@ -1,5 +1,6 @@
|
||||
FROM quay.io/soketi/soketi:1.6-16-alpine
|
||||
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
# https://github.com/cloudflare/cloudflared/releases
|
||||
ARG CLOUDFLARED_VERSION=2024.4.1
|
||||
|
@@ -57,13 +57,6 @@ RUN composer dump-autoload
|
||||
COPY --from=static-assets --chown=9999:9999 /app/public/build ./public/build
|
||||
COPY --chmod=755 docker/prod/etc/s6-overlay/ /etc/s6-overlay/
|
||||
|
||||
RUN php artisan route:clear
|
||||
RUN php artisan view:clear
|
||||
RUN php artisan config:clear
|
||||
RUN php artisan route:cache
|
||||
RUN php artisan view:cache
|
||||
RUN php artisan config:cache
|
||||
|
||||
RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc
|
||||
RUN echo "alias a='php artisan'" >>/etc/bash.bashrc
|
||||
RUN echo "alias logs='tail -f storage/logs/laravel.log'" >>/etc/bash.bashrc
|
||||
@@ -86,4 +79,4 @@ RUN { \
|
||||
} > /etc/php/current_version/cli/conf.d/upload-limits.ini
|
||||
|
||||
COPY --from=minio-client /usr/bin/mc /usr/bin/mc
|
||||
RUN chmod +x /usr/bin/mc
|
||||
RUN chmod +x /usr/bin/mc
|
||||
|
@@ -9,7 +9,7 @@ CDN="https://cdn.coollabs.io/coolify-nightly"
|
||||
DATE=$(date +"%Y%m%d-%H%M%S")
|
||||
|
||||
VERSION="1.6"
|
||||
DOCKER_VERSION="27.3"
|
||||
DOCKER_VERSION="27.0"
|
||||
# TODO: Ask for a user
|
||||
CURRENT_USER=$USER
|
||||
|
||||
|
@@ -5,6 +5,7 @@
|
||||
'disabled' => false,
|
||||
'instantSave' => false,
|
||||
'value' => null,
|
||||
'checked' => false,
|
||||
'hideLabel' => false,
|
||||
'fullWidth' => false,
|
||||
])
|
||||
@@ -14,7 +15,9 @@
|
||||
'w-full' => $fullWidth,
|
||||
])>
|
||||
@if (!$hideLabel)
|
||||
<label @class(['flex gap-4 items-center px-0 min-w-fit label w-full cursor-pointer', 'opacity-40' => $disabled])>
|
||||
<label @class([
|
||||
'flex gap-4 items-center px-0 min-w-fit label w-full cursor-pointer',
|
||||
])>
|
||||
<span class="flex flex-grow gap-2">
|
||||
@if ($label)
|
||||
{!! $label !!}
|
||||
@@ -26,9 +29,10 @@
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
<input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }}
|
||||
@if ($instantSave) wire:loading.attr="disabled" wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
|
||||
wire:model={{ $id }} @else wire:model={{ $value ?? $id }} @endif />
|
||||
<input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }}
|
||||
@if ($instantSave) wire:loading.attr="disabled" wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
|
||||
@if ($checked) checked @endif
|
||||
wire:model={{ $id }} @else wire:model={{ $value ?? $id }} @endif />
|
||||
@if (!$hideLabel)
|
||||
</label>
|
||||
@endif
|
||||
|
@@ -9,7 +9,7 @@
|
||||
'closeOutside' => true,
|
||||
])
|
||||
<div x-data="{ modalOpen: false }" :class="{ 'z-40': modalOpen }" @keydown.window.escape="modalOpen=false"
|
||||
class="relative w-auto h-auto">
|
||||
class="relative w-auto h-auto" wire:ignore>
|
||||
@if ($content)
|
||||
<div @click="modalOpen=true">
|
||||
{{ $content }}
|
||||
@@ -27,7 +27,7 @@
|
||||
@endif
|
||||
<template x-teleport="body">
|
||||
<div x-show="modalOpen"
|
||||
class="fixed top-0 left-0 lg:px-0 px-4 z-[99] flex items-center justify-center w-screen h-screen" x-cloak>
|
||||
class="fixed top-0 left-0 lg:px-0 px-4 z-[99] flex items-center justify-center w-screen h-screen">
|
||||
<div x-show="modalOpen" x-transition:enter="ease-out duration-100" x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
||||
|
@@ -8,7 +8,7 @@
|
||||
<li class="inline-flex items-center">
|
||||
<div class="flex items-center">
|
||||
<a class="text-xs truncate lg:text-sm"
|
||||
href="{{ route('project.show', ['project_uuid' => $this->parameters['project_uuid']]) }}">
|
||||
href="{{ route('project.show', ['project_uuid' => data_get($resource, 'environment.project.uuid')]) }}">
|
||||
{{ data_get($resource, 'environment.project.name', 'Undefined Name') }}</a>
|
||||
<svg aria-hidden="true" class="w-4 h-4 mx-1 font-bold dark:text-warning" fill="currentColor"
|
||||
viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -21,7 +21,7 @@
|
||||
<li>
|
||||
<div class="flex items-center">
|
||||
<a class="text-xs truncate lg:text-sm"
|
||||
href="{{ route('project.resource.index', ['environment_name' => $this->parameters['environment_name'], 'project_uuid' => $this->parameters['project_uuid']]) }}">{{ $this->parameters['environment_name'] }}</a>
|
||||
href="{{ route('project.resource.index', ['environment_name' => data_get($resource, 'environment.name'), 'project_uuid' => data_get($resource, 'environment.project.uuid')]) }}">{{ data_get($resource, 'environment.name') }}</a>
|
||||
<svg aria-hidden="true" class="w-4 h-4 mx-1 font-bold dark:text-warning" fill="currentColor"
|
||||
viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd"
|
||||
|
@@ -9,12 +9,12 @@
|
||||
<a class="menu-item {{ $activeMenu === 'private-key' ? 'menu-item-active' : '' }}"
|
||||
href="{{ route('server.private-key', ['server_uuid' => $server->uuid]) }}">Private Key
|
||||
</a>
|
||||
@if (!$server->isLocalhost())
|
||||
<a class="menu-item {{ $activeMenu === 'cloudflare-tunnels' ? 'menu-item-active' : '' }}"
|
||||
href="{{ route('server.cloudflare-tunnels', ['server_uuid' => $server->uuid]) }}">Cloudflare
|
||||
Tunnels</a>
|
||||
@endif
|
||||
@if ($server->isFunctional())
|
||||
@if (!$server->isLocalhost())
|
||||
<a class="menu-item {{ $activeMenu === 'cloudflare-tunnels' ? 'menu-item-active' : '' }}"
|
||||
href="{{ route('server.cloudflare-tunnels', ['server_uuid' => $server->uuid]) }}">Cloudflare
|
||||
Tunnels</a>
|
||||
@endif
|
||||
<a class="menu-item {{ $activeMenu === 'destinations' ? 'menu-item-active' : '' }}"
|
||||
href="{{ route('server.destinations', ['server_uuid' => $server->uuid]) }}">Destinations
|
||||
</a>
|
||||
|
@@ -34,11 +34,13 @@
|
||||
</style>
|
||||
@if (config('app.name') == 'Coolify Cloud')
|
||||
<script defer data-domain="app.coolify.io" src="https://analytics.coollabs.io/js/plausible.js"></script>
|
||||
<script src="https://js.sentry-cdn.com/0f8593910512b5cdd48c6da78d4093be.min.js" crossorigin="anonymous"></script>
|
||||
@endif
|
||||
@auth
|
||||
<script type="text/javascript" src="{{ URL::asset('js/echo.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ URL::asset('js/pusher.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ URL::asset('js/apexcharts.js') }}"></script>
|
||||
|
||||
@endauth
|
||||
</head>
|
||||
@section('body')
|
||||
|
@@ -16,9 +16,9 @@
|
||||
@endif
|
||||
@if (isEmailEnabled($team) && auth()->user()->isAdminFromSession() && isTestEmailEnabled($team))
|
||||
<x-modal-input buttonTitle="Send Test Email" title="Send Test Email">
|
||||
<form wire:submit='submit' class="flex flex-col w-full gap-2">
|
||||
<x-forms.input placeholder="test@example.com" id="emails" label="Recipients" required />
|
||||
<x-forms.button wire:click="sendTestNotification" @click="modalOpen=false">
|
||||
<form wire:submit.prevent="sendTestEmail" class="flex flex-col w-full gap-2">
|
||||
<x-forms.input wire:model="testEmailAddress" placeholder="test@example.com" id="testEmailAddress" label="Recipients" required />
|
||||
<x-forms.button type="submit" @click="modalOpen=false">
|
||||
Send Email
|
||||
</x-forms.button>
|
||||
</form>
|
||||
|
@@ -8,7 +8,7 @@
|
||||
@forelse ($private_keys as $key)
|
||||
@if ($private_key_id == $key->id)
|
||||
<div class="gap-2 py-4 cursor-pointer group hover:bg-coollabs bg-coolgray-200 box"
|
||||
wire:click.defer="setPrivateKey('{{ $key->id }}')" wire:key="{{ $key->id }}">
|
||||
wire:click="setPrivateKey('{{ $key->id }}')" wire:key="{{ $key->id }}">
|
||||
<div class="flex flex-col mx-6">
|
||||
<div class="box-title">
|
||||
{{ $key->name }}
|
||||
@@ -21,7 +21,7 @@
|
||||
</div>
|
||||
@else
|
||||
<div class="gap-2 py-4 cursor-pointer group hover:bg-coollabs bg-coolgray-200 box"
|
||||
wire:click.defer="setPrivateKey('{{ $key->id }}')" wire:key="{{ $key->id }}">
|
||||
wire:click="setPrivateKey('{{ $key->id }}')" wire:key="{{ $key->id }}">
|
||||
<div class="flex flex-col mx-6">
|
||||
<div class="box-title">
|
||||
{{ $key->name }}
|
||||
|
@@ -101,7 +101,11 @@
|
||||
<x-slot:logo>
|
||||
<template x-if="service.logo">
|
||||
<img class="w-[4.5rem] aspect-square h-[4.5rem] p-2 transition-all duration-200 opacity-30 grayscale group-hover:grayscale-0 group-hover:opacity-100"
|
||||
:src='service.logo'>
|
||||
:src='service.logo'
|
||||
x-on:error.window="$event.target.src = service.logo_github_url"
|
||||
onerror="this.onerror=null; this.src=this.getAttribute('data-fallback');"
|
||||
x-on:error="$event.target.src = '/svgs/coolify.png'"
|
||||
:data-fallback='service.logo_github_url' />
|
||||
</template>
|
||||
</x-slot:logo>
|
||||
<x-slot:documentation>
|
||||
@@ -205,7 +209,7 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@if ($current_step === 'servers')
|
||||
<h2>Select a server</h2>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user