Merge branch 'next' into fix-environement-route

This commit is contained in:
Andras Bacsai
2024-12-17 12:17:50 +01:00
committed by GitHub
426 changed files with 11051 additions and 5406 deletions

View File

@@ -3,7 +3,6 @@
/public/build
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.env.backup
@@ -25,3 +24,15 @@ yarn-error.log
.ignition.json
.env.dusk.local
docker/coolify-realtime/node_modules
/storage/*.key
/storage/app/backups
/storage/app/ssh/keys
/storage/app/ssh/mux
/storage/app/tmp
/storage/app/debugbar
/storage/logs
/storage/pail

View File

@@ -47,7 +47,7 @@ jobs:
uses: docker/build-push-action@v6
with:
context: .
file: docker/prod/Dockerfile
file: docker/production/Dockerfile
platforms: linux/amd64
push: true
tags: |
@@ -82,7 +82,7 @@ jobs:
uses: docker/build-push-action@v6
with:
context: .
file: docker/prod/Dockerfile
file: docker/production/Dockerfile
platforms: linux/aarch64
push: true
tags: |

View File

@@ -42,7 +42,7 @@ jobs:
uses: docker/build-push-action@v6
with:
context: .
file: docker/prod/Dockerfile
file: docker/production/Dockerfile
platforms: linux/amd64
push: true
tags: |
@@ -75,7 +75,7 @@ jobs:
uses: docker/build-push-action@v6
with:
context: .
file: docker/prod/Dockerfile
file: docker/production/Dockerfile
platforms: linux/aarch64
push: true
tags: |

View File

@@ -40,7 +40,8 @@ Special thanks to our biggest sponsors!
### Special Sponsors
![image](https://github.com/user-attachments/assets/152bd1e0-e0c1-4d47-8a4f-0eb3700d2e61)
![image](https://github.com/user-attachments/assets/6022bc9c-8435-4d14-9497-8be230ed8cb1)
* [CCCareers](https://cccareers.org/) - A career development platform connecting coding bootcamp graduates with job opportunities in the tech industry.
* [Hetzner](http://htznr.li/CoolifyXHetzner) - A German web hosting company offering affordable dedicated servers, cloud services, and web hosting solutions.
@@ -50,8 +51,11 @@ Special thanks to our biggest sponsors!
* [QuantCDN](https://www.quantcdn.io/?ref=coolify.io) - A content delivery network (CDN) optimizing website performance through global content distribution.
* [Arcjet](https://arcjet.com/?ref=coolify.io) - A cloud-based platform providing real-time protection against API abuse and bot attacks.
* [SupaGuide](https://supa.guide/?ref=coolify.io) - A comprehensive resource hub offering guides and tutorials for web development using Supabase.
* [GoldenVM](https://billing.goldenvm.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes.
* [Tigris](https://tigrisdata.com/?ref=coolify.io) - A fully managed serverless object storage service compatible with Amazon S3 API. Offers high performance, scalability, and built-in search capabilities for efficient data management.
* [Advin](https://coolify.ad.vin/?ref=coolify.io) - A digital advertising agency specializing in programmatic advertising and data-driven marketing strategies.
* [Cloudify.ro](https://cloudify.ro/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes.
* [Syntaxfm](https://syntax.fm/?ref=coolify.io) - Podcast for web developers.
* [PFGlabs](https://pfglabs.com/?ref=coolify.io) - Build real project with Golang.
* [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.
* [Brand Dev](https://brand.dev/?ref=coolify.io) - A web development agency specializing in creating custom digital experiences and brand identities.
@@ -90,8 +94,10 @@ Special thanks to our biggest sponsors!
<a href="https://github.com/urtho"><img src="https://github.com/urtho.png" width="60px" alt="Paweł Pierścionek" /></a>
<a href="https://github.com/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://bsky.app/profile/jyc.dev"><img src="https://github.com/jycouet.png" width="60px" alt="jyc.dev" /></a>
<a href="https://bitlaunch.io/?utm_source=coolify.io"><img src="https://github.com/bitlaunchio.png" width="60px" alt="BitLaunch" /></a>
<a href="https://internetgarden.co/?utm_source=coolify.io"><img src="./other/logos/internetgarden.ico" width="60px" alt="Internet Garden" /></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>
@@ -147,10 +153,10 @@ By subscribing to the cloud version, you get the Coolify server for the same pri
# Core Maintainers
| Andras Bacsai | Peak |
| Andras Bacsai | 🏔️ Peak |
|------------|------------|
| <img src="https://github.com/andrasbacsai.png" width="200px" alt="Andras Bacsai" /> | <img src="https://github.com/peaklabs-dev.png" width="200px" alt="Peak Labs" /> |
| <a href="https://x.com/heyandras"><img src="https://raw.githubusercontent.com/gauravghongde/social-icons/master/SVG/Color/Twitter.svg" width="25px"></a> <a href="https://github.com/andrasbacsai"><img src="https://raw.githubusercontent.com/gauravghongde/social-icons/master/SVG/Color/Github.svg" width="25px"></a> | <a href="https://x.com/peaklabs_dev"><img src="https://raw.githubusercontent.com/gauravghongde/social-icons/master/SVG/Color/Twitter.svg" width="25px"></a> <a href="https://github.com/peaklabs-dev"><img src="https://raw.githubusercontent.com/gauravghongde/social-icons/master/SVG/Color/Github.svg" width="25px"></a> |
| <img src="https://github.com/andrasbacsai.png" width="200px" alt="Andras Bacsai" /> | <img src="https://github.com/peaklabs-dev.png" width="200px" alt="peaklabs-dev" /> |
| <a href="https://github.com/andrasbacsai"><img src="https://api.iconify.design/devicon:github.svg" width="25px"></a> <a href="https://x.com/heyandras"><img src="https://api.iconify.design/devicon:twitter.svg" width="25px"></a> <a href="https://bsky.app/profile/heyandras.dev"><img src="https://api.iconify.design/simple-icons:bluesky.svg" width="25px"></a> | <a href="https://github.com/peaklabs-dev"><img src="https://api.iconify.design/devicon:github.svg" width="25px"></a> <a href="https://x.com/peaklabs_dev"><img src="https://api.iconify.design/devicon:twitter.svg" width="25px"></a> <a href="https://bsky.app/profile/peaklabs.dev"><img src="https://api.iconify.design/simple-icons:bluesky.svg" width="25px"></a> |
# Repo Activity

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -179,7 +179,7 @@ class GetContainersStatus
})->first();
if (! $foundTcpProxy) {
StartDatabaseProxy::run($database);
// $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
// $this->server->team?->notify(new ContainerRestarted("TCP Proxy for database", $this->server));
}
}
} else {

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Proxy;
use App\Enums\ProxyTypes;
use App\Events\ProxyStarted;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -37,11 +38,16 @@ class StartProxy
"echo 'Successfully started coolify-proxy.'",
]);
} else {
$caddfile = 'import /dynamic/*.caddy';
if (isDev()) {
if ($proxyType === ProxyTypes::CADDY->value) {
$proxy_path = '/data/coolify/proxy/caddy';
}
}
$caddyfile = 'import /dynamic/*.caddy';
$commands = $commands->merge([
"mkdir -p $proxy_path/dynamic",
"cd $proxy_path",
"echo '$caddfile' > $proxy_path/dynamic/Caddyfile",
"echo '$caddyfile' > $proxy_path/dynamic/Caddyfile",
"echo 'Creating required Docker Compose file.'",
"echo 'Pulling docker image.'",
'docker compose pull',

View File

@@ -51,7 +51,6 @@ class ServerCheck
$containerReplicates = null;
$this->isSentinel = true;
} else {
['containers' => $this->containers, 'containerReplicates' => $containerReplicates] = $this->server->getContainers();
// ServerStorageCheckJob::dispatch($this->server);
@@ -148,7 +147,6 @@ class ServerCheck
} else {
$labels = Arr::undot(data_get($container, 'Config.Labels'));
}
}
$managed = data_get($labels, 'coolify.managed');
if (! $managed) {
@@ -259,7 +257,7 @@ class ServerCheck
})->first();
if (! $foundTcpProxy) {
StartDatabaseProxy::run($database);
// $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
// $this->server->team?->notify(new ContainerRestarted("TCP Proxy for database", $this->server));
}
}
}

View File

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

View File

@@ -18,7 +18,6 @@ class CleanupUnreachableServers extends Command
if ($servers->count() > 0) {
foreach ($servers as $server) {
echo "Cleanup unreachable server ($server->id) with name $server->name";
// send_internal_notification("Server $server->name is unreachable for 7 days. Cleaning up...");
$server->update([
'ip' => '1.2.3.4',
]);

View File

@@ -36,7 +36,7 @@ class CloudCleanupSubscriptions extends Command
}
// If the team has no subscription id and the invoice is paid, we need to reset the invoice paid status
if (! (data_get($team, 'subscription.stripe_subscription_id'))) {
$this->info("Resetting invoice paid status for team {$team->id} {$team->name}");
$this->info("Resetting invoice paid status for team {$team->id}");
$team->subscription->update([
'stripe_invoice_paid' => false,
@@ -61,9 +61,9 @@ class CloudCleanupSubscriptions extends Command
$this->info('Subscription id: '.data_get($team, 'subscription.stripe_subscription_id'));
$confirm = $this->confirm('Do you want to cancel the subscription?', true);
if (! $confirm) {
$this->info("Skipping team {$team->id} {$team->name}");
$this->info("Skipping team {$team->id}");
} else {
$this->info("Cancelling subscription for team {$team->id} {$team->name}");
$this->info("Cancelling subscription for team {$team->id}");
$team->subscription->update([
'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => false,

View File

@@ -76,7 +76,5 @@ class Dev extends Command
} else {
echo "Instance already initialized.\n";
}
// Set permissions
Process::run(['chmod', '-R', 'o+rwx', '.']);
}
}

View File

@@ -2,14 +2,12 @@
namespace App\Console\Commands;
use App\Jobs\SendConfirmationForWaitlistJob;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\StandalonePostgresql;
use App\Models\Team;
use App\Models\Waitlist;
use App\Notifications\Application\DeploymentFailed;
use App\Notifications\Application\DeploymentSuccess;
use App\Notifications\Application\StatusChanged;
@@ -64,8 +62,6 @@ class Emails extends Command
'backup-success' => 'Database - Backup Success',
'backup-failed' => 'Database - Backup Failed',
// 'invitation-link' => 'Invitation Link',
'waitlist-invitation-link' => 'Waitlist Invitation Link',
'waitlist-confirmation' => 'Waitlist Confirmation',
'realusers-before-trial' => 'REAL - Registered Users Before Trial without Subscription',
'realusers-server-lost-connection' => 'REAL - Server Lost Connection',
],
@@ -187,7 +183,7 @@ class Emails extends Command
'team_id' => 0,
]);
}
$this->mail = (new BackupSuccess($backup, $db))->toMail();
//$this->mail = (new BackupSuccess($backup->frequency, $db->name))->toMail();
$this->sendEmail();
break;
// case 'invitation-link':
@@ -204,23 +200,6 @@ class Emails extends Command
// $this->mail = (new InvitationLink($user))->toMail();
// $this->sendEmail();
// break;
case 'waitlist-invitation-link':
$this->mail = new MailMessage;
$this->mail->view('emails.waitlist-invitation', [
'loginLink' => 'https://coolify.io',
]);
$this->mail->subject('Congratulations! You are invited to join Coolify Cloud.');
$this->sendEmail();
break;
case 'waitlist-confirmation':
$found = Waitlist::where('email', $this->email)->first();
if ($found) {
SendConfirmationForWaitlistJob::dispatch($this->email, $found->uuid);
} else {
throw new Exception('Waitlist not found');
}
break;
case 'realusers-before-trial':
$this->mail = new MailMessage;
$this->mail->view('emails.before-trial-conversion');

View File

@@ -13,7 +13,7 @@ class Horizon extends Command
public function handle()
{
if (config('constants.horizon.is_horizon_enabled')) {
$this->info('[x]: Horizon is enabled. Starting.');
$this->info('Horizon is enabled on this server.');
$this->call('horizon');
exit(0);
} else {

View File

@@ -55,10 +55,8 @@ 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 {
@@ -114,7 +112,6 @@ class Init extends Command
private function optimize()
{
echo "[1]: Optimizing Laravel (caching config, routes, views).\n";
Artisan::call('optimize:clear');
Artisan::call('optimize');
}
@@ -189,7 +186,6 @@ class Init extends Command
}
}
if ($commands->isNotEmpty()) {
echo "Cleaning up unused networks from coolify proxy\n";
remote_process(command: $commands, type: ActivityTypes::INLINE->value, server: $server, ignore_errors: false);
}
} catch (\Throwable $e) {
@@ -200,7 +196,7 @@ class Init extends Command
private function restore_coolify_db_backup()
{
if (version_compare('4.0.0-beta.179', config('version'), '<=')) {
if (version_compare('4.0.0-beta.179', config('constants.coolify.version'), '<=')) {
try {
$database = StandalonePostgresql::withTrashed()->find(0);
if ($database && $database->trashed()) {
@@ -228,19 +224,18 @@ class Init extends Command
private function send_alive_signal()
{
$id = config('app.id');
$version = config('version');
$version = config('constants.coolify.version');
$settings = instanceSettings();
$do_not_track = data_get($settings, 'do_not_track');
if ($do_not_track == true) {
echo "[2]: Skipping sending live signal as do_not_track is enabled\n";
echo "Do_not_track is enabled\n";
return;
}
try {
Http::get("https://undead.coolify.io/v4/alive?appId=$id&version=$version");
echo "[2]: Sending live signal!\n";
} catch (\Throwable $e) {
echo "[2]: Error in sending live signal: {$e->getMessage()}\n";
echo "Error in sending live signal: {$e->getMessage()}\n";
}
}
@@ -253,7 +248,6 @@ class Init extends Command
}
$queued_inprogress_deployments = ApplicationDeploymentQueue::whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value])->get();
foreach ($queued_inprogress_deployments as $deployment) {
echo "Cleaning up deployment: {$deployment->id}\n";
$deployment->status = ApplicationDeploymentStatus::FAILED->value;
$deployment->save();
}
@@ -264,7 +258,7 @@ class Init extends Command
private function replace_slash_in_environment_name()
{
if (version_compare('4.0.0-beta.298', config('version'), '<=')) {
if (version_compare('4.0.0-beta.298', config('constants.coolify.version'), '<=')) {
$environments = Environment::all();
foreach ($environments as $environment) {
if (str_contains($environment->name, '/')) {

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class Migration extends Command
{
protected $signature = 'start:migration';
protected $description = 'Start Migration';
public function handle()
{
if (config('constants.migration.is_migration_enabled')) {
$this->info('Migration is enabled on this server.');
$this->call('migrate', ['--force' => true, '--isolated' => true]);
exit(0);
} else {
$this->info('Migration is disabled on this server.');
exit(0);
}
}
}

View File

@@ -59,9 +59,10 @@ class NotifyDemo extends Command
<div class="text-yellow-500"> Channels: </div>
<ul class="text-coolify">
<li>email</li>
<li>slack</li>
<li>discord</li>
<li>telegram</li>
<li>slack</li>
<li>pushover</li>
</ul>
</div>
</div>
@@ -72,6 +73,6 @@ class NotifyDemo extends Command
<div class="mr-1">
In which manner you wish a <strong class="text-coolify">coolified</strong> notification?
</div>
HTML, ['email', 'slack', 'discord', 'telegram']);
HTML, ['email', 'discord', 'telegram', 'slack', 'pushover']);
}
}

View File

@@ -13,7 +13,7 @@ class Scheduler extends Command
public function handle()
{
if (config('constants.horizon.is_scheduler_enabled')) {
$this->info('[x]: Scheduler is enabled. Starting.');
$this->info('Scheduler is enabled on this server.');
$this->call('schedule:work');
exit(0);
} else {

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class Seeder extends Command
{
protected $signature = 'start:seeder';
protected $description = 'Start Seeder';
public function handle()
{
if (config('constants.seeder.is_seeder_enabled')) {
$this->info('Seeder is enabled on this server.');
$this->call('db:seed', ['--class' => 'ProductionSeeder', '--force' => true]);
exit(0);
} else {
$this->info('Seeder is disabled on this server.');
exit(0);
}
}
}

View File

@@ -20,7 +20,10 @@ class ServicesGenerate extends Command
public function handle(): int
{
$serviceTemplatesJson = collect(glob(base_path('templates/compose/*.yaml')))
$serviceTemplatesJson = collect(array_merge(
glob(base_path('templates/compose/*.yaml')),
glob(base_path('templates/compose/*.yml'))
))
->mapWithKeys(function ($file): array {
$file = basename($file);
$parsed = $this->processFile($file);
@@ -68,7 +71,7 @@ class ServicesGenerate extends Command
'slogan' => $data->get('slogan', str($file)->headline()),
'compose' => $compose,
'tags' => $tags,
'logo' => $data->get('logo', 'svgs/coolify.png'),
'logo' => $data->get('logo', 'svgs/default.webp'),
'minversion' => $data->get('minversion', '0.0.0'),
];

View File

@@ -1,114 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use App\Models\Waitlist;
use Illuminate\Console\Command;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class WaitlistInvite extends Command
{
public Waitlist|User|null $next_patient = null;
public ?string $password = null;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'waitlist:invite {--people=1} {--only-email} {email?}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send invitation to the next user (or by email) in the waitlist';
/**
* Execute the console command.
*/
public function handle()
{
$people = $this->option('people');
for ($i = 0; $i < $people; $i++) {
$this->main();
}
}
private function main()
{
if ($this->argument('email')) {
if ($this->option('only-email')) {
$this->next_patient = User::whereEmail($this->argument('email'))->first();
$this->password = Str::password();
$this->next_patient->update([
'password' => Hash::make($this->password),
'force_password_reset' => true,
]);
} else {
$this->next_patient = Waitlist::where('email', $this->argument('email'))->first();
}
if (! $this->next_patient) {
$this->error("{$this->argument('email')} not found in the waitlist.");
return;
}
} else {
$this->next_patient = Waitlist::orderBy('created_at', 'asc')->where('verified', true)->first();
}
if ($this->next_patient) {
if ($this->option('only-email')) {
$this->send_email();
return;
}
$this->register_user();
$this->remove_from_waitlist();
$this->send_email();
} else {
$this->info('No verified user found in the waitlist. 👀');
}
}
private function register_user()
{
$already_registered = User::whereEmail($this->next_patient->email)->first();
if (! $already_registered) {
$this->password = Str::password();
User::create([
'name' => str($this->next_patient->email)->before('@'),
'email' => $this->next_patient->email,
'password' => Hash::make($this->password),
'force_password_reset' => true,
]);
$this->info("User registered ({$this->next_patient->email}) successfully. 🎉");
} else {
throw new \Exception('User already registered');
}
}
private function remove_from_waitlist()
{
$this->next_patient->delete();
$this->info('User removed from waitlist successfully.');
}
private function send_email()
{
$token = Crypt::encryptString("{$this->next_patient->email}@@@$this->password");
$loginLink = route('auth.link', ['token' => $token]);
$mail = new MailMessage;
$mail->view('emails.waitlist-invitation', [
'loginLink' => $loginLink,
]);
$mail->subject('Congratulations! You are invited to join Coolify Cloud.');
send_user_an_email($mail, $this->next_patient->email);
$this->info('Email sent successfully. 📧');
}
}

View File

@@ -1,58 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Actions\Server\ServerCheck;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Models\Server;
use Illuminate\Console\Command;
use Str;
class Weird extends Command
{
protected $signature = 'weird {--number=1} {--run}';
protected $description = 'Weird stuff';
public function handle()
{
try {
if (! isDev()) {
$this->error('This command can only be run in development mode');
return;
}
$run = $this->option('run');
if ($run) {
$servers = Server::all();
foreach ($servers as $server) {
ServerCheck::dispatch($server);
}
return;
}
$number = $this->option('number');
for ($i = 0; $i < $number; $i++) {
$uuid = Str::uuid();
$server = Server::create([
'name' => 'localhost-'.$uuid,
'description' => 'This is a test docker container in development mode',
'ip' => 'coolify-testing-host',
'team_id' => 0,
'private_key_id' => 1,
'proxy' => [
'type' => ProxyTypes::NONE->value,
'status' => ProxyStatus::EXITED->value,
],
]);
$server->settings->update([
'is_usable' => true,
'is_reachable' => true,
]);
}
} catch (\Exception $e) {
$this->error($e->getMessage());
}
}
}

View File

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

View File

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

View File

@@ -25,13 +25,13 @@ class ApplicationsController extends Controller
{
private function removeSensitiveData($application)
{
$token = auth()->user()->currentAccessToken();
$application->makeHidden([
'id',
'resourceable',
'resourceable_id',
'resourceable_type',
]);
if ($token->can('view:sensitive')) {
return serializeApiResponse($application);
}
if (request()->attributes->get('can_read_sensitive', false) === false) {
$application->makeHidden([
'custom_labels',
'dockerfile',
@@ -45,6 +45,7 @@ class ApplicationsController extends Controller
'value',
'real_value',
]);
}
return serializeApiResponse($application);
}
@@ -70,7 +71,8 @@ class ApplicationsController extends Controller
items: new OA\Items(ref: '#/components/schemas/Application')
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -180,8 +182,10 @@ class ApplicationsController extends Controller
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
)
),
]
),
responses: [
new OA\Response(
response: 200,
@@ -284,8 +288,10 @@ class ApplicationsController extends Controller
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
)
),
]
),
responses: [
new OA\Response(
response: 200,
@@ -388,8 +394,10 @@ class ApplicationsController extends Controller
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
)
),
]
),
responses: [
new OA\Response(
response: 200,
@@ -476,8 +484,10 @@ class ApplicationsController extends Controller
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
)
),
]
),
responses: [
new OA\Response(
response: 200,
@@ -561,8 +571,10 @@ class ApplicationsController extends Controller
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
)
),
]
),
responses: [
new OA\Response(
response: 200,
@@ -612,8 +624,10 @@ class ApplicationsController extends Controller
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
)
),
]
),
responses: [
new OA\Response(
response: 200,
@@ -1268,7 +1282,8 @@ class ApplicationsController extends Controller
ref: '#/components/schemas/Application'
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1340,7 +1355,8 @@ class ApplicationsController extends Controller
]
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1466,8 +1482,10 @@ class ApplicationsController extends Controller
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
)
),
]
),
responses: [
new OA\Response(
response: 200,
@@ -1482,7 +1500,8 @@ class ApplicationsController extends Controller
]
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1591,16 +1610,33 @@ class ApplicationsController extends Controller
}
$domains = $request->domains;
if ($request->has('domains') && $server->isProxyShouldRun()) {
$errors = [];
$uuid = $request->uuid;
$fqdn = $request->domains;
$fqdn = str($fqdn)->replaceEnd(',', '')->trim();
$fqdn = str($fqdn)->replaceStart(',', '')->trim();
$application->fqdn = $fqdn;
if (! $application->settings->is_container_label_readonly_enabled) {
$customLabels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
$application->custom_labels = base64_encode($customLabels);
$errors = [];
$fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
$domain = trim($domain);
if (filter_var($domain, FILTER_VALIDATE_URL) === false || ! preg_match('/^https?:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}/', $domain)) {
$errors[] = 'Invalid domain: '.$domain;
}
return $domain;
});
if (count($errors) > 0) {
return response()->json([
'message' => 'Validation failed.',
'errors' => $errors,
], 422);
}
if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'domains' => 'One of the domain is already used.',
],
], 422);
}
$request->offsetUnset('domains');
}
$dockerComposeDomainsJson = collect();
@@ -1690,7 +1726,8 @@ class ApplicationsController extends Controller
items: new OA\Items(ref: '#/components/schemas/EnvironmentVariable')
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1796,7 +1833,8 @@ class ApplicationsController extends Controller
]
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1858,8 +1896,9 @@ class ApplicationsController extends Controller
$is_preview = $request->is_preview ?? false;
$is_build_time = $request->is_build_time ?? false;
$is_literal = $request->is_literal ?? false;
$key = str($request->key)->trim()->replace(' ', '_')->value;
if ($is_preview) {
$env = $application->environment_variables_preview->where('key', $request->key)->first();
$env = $application->environment_variables_preview->where('key', $key)->first();
if ($env) {
$env->value = $request->value;
if ($env->is_build_time != $is_build_time) {
@@ -1886,7 +1925,7 @@ class ApplicationsController extends Controller
], 404);
}
} else {
$env = $application->environment_variables->where('key', $request->key)->first();
$env = $application->environment_variables->where('key', $key)->first();
if ($env) {
$env->value = $request->value;
if ($env->is_build_time != $is_build_time) {
@@ -1984,7 +2023,8 @@ class ApplicationsController extends Controller
]
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2028,6 +2068,7 @@ class ApplicationsController extends Controller
$bulk_data = collect($bulk_data)->map(function ($item) {
return collect($item)->only(['key', 'value', 'is_preview', 'is_build_time', 'is_literal']);
});
$returnedEnvs = collect();
foreach ($bulk_data as $item) {
$validator = customApiValidator($item, [
'key' => 'string|required',
@@ -2049,8 +2090,9 @@ class ApplicationsController extends Controller
$is_literal = $item->get('is_literal') ?? false;
$is_multi_line = $item->get('is_multiline') ?? false;
$is_shown_once = $item->get('is_shown_once') ?? false;
$key = str($item->get('key'))->trim()->replace(' ', '_')->value;
if ($is_preview) {
$env = $application->environment_variables_preview->where('key', $item->get('key'))->first();
$env = $application->environment_variables_preview->where('key', $key)->first();
if ($env) {
$env->value = $item->get('value');
if ($env->is_build_time != $is_build_time) {
@@ -2075,10 +2117,12 @@ class ApplicationsController extends Controller
'is_literal' => $is_literal,
'is_multiline' => $is_multi_line,
'is_shown_once' => $is_shown_once,
'resourceable_type' => get_class($application),
'resourceable_id' => $application->id,
]);
}
} else {
$env = $application->environment_variables->where('key', $item->get('key'))->first();
$env = $application->environment_variables->where('key', $key)->first();
if ($env) {
$env->value = $item->get('value');
if ($env->is_build_time != $is_build_time) {
@@ -2103,12 +2147,15 @@ class ApplicationsController extends Controller
'is_literal' => $is_literal,
'is_multiline' => $is_multi_line,
'is_shown_once' => $is_shown_once,
'resourceable_type' => get_class($application),
'resourceable_id' => $application->id,
]);
}
}
$returnedEnvs->push($this->removeSensitiveData($env));
}
return response()->json($this->removeSensitiveData($env))->setStatusCode(201);
return response()->json($returnedEnvs)->setStatusCode(201);
}
#[OA\Post(
@@ -2165,7 +2212,8 @@ class ApplicationsController extends Controller
]
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2220,8 +2268,10 @@ class ApplicationsController extends Controller
], 422);
}
$is_preview = $request->is_preview ?? false;
$key = str($request->key)->trim()->replace(' ', '_')->value;
if ($is_preview) {
$env = $application->environment_variables_preview->where('key', $request->key)->first();
$env = $application->environment_variables_preview->where('key', $key)->first();
if ($env) {
return response()->json([
'message' => 'Environment variable already exists. Use PATCH request to update it.',
@@ -2235,6 +2285,8 @@ class ApplicationsController extends Controller
'is_literal' => $request->is_literal ?? false,
'is_multiline' => $request->is_multiline ?? false,
'is_shown_once' => $request->is_shown_once ?? false,
'resourceable_type' => get_class($application),
'resourceable_id' => $application->id,
]);
return response()->json([
@@ -2242,7 +2294,7 @@ class ApplicationsController extends Controller
])->setStatusCode(201);
}
} else {
$env = $application->environment_variables->where('key', $request->key)->first();
$env = $application->environment_variables->where('key', $key)->first();
if ($env) {
return response()->json([
'message' => 'Environment variable already exists. Use PATCH request to update it.',
@@ -2256,6 +2308,8 @@ class ApplicationsController extends Controller
'is_literal' => $request->is_literal ?? false,
'is_multiline' => $request->is_multiline ?? false,
'is_shown_once' => $request->is_shown_once ?? false,
'resourceable_type' => get_class($application),
'resourceable_id' => $application->id,
]);
return response()->json([
@@ -2314,7 +2368,8 @@ class ApplicationsController extends Controller
]
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2342,7 +2397,10 @@ class ApplicationsController extends Controller
'message' => 'Application not found.',
], 404);
}
$found_env = EnvironmentVariable::where('uuid', $request->env_uuid)->where('application_id', $application->id)->first();
$found_env = EnvironmentVariable::where('uuid', $request->env_uuid)
->where('resourceable_type', Application::class)
->where('resourceable_id', $application->id)
->first();
if (! $found_env) {
return response()->json([
'message' => 'Environment variable not found.',
@@ -2406,9 +2464,11 @@ class ApplicationsController extends Controller
properties: [
'message' => ['type' => 'string', 'example' => 'Deployment request queued.', 'description' => 'Message.'],
'deployment_uuid' => ['type' => 'string', 'example' => 'doogksw', 'description' => 'UUID of the deployment.'],
])
]
)
),
]
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2494,7 +2554,8 @@ class ApplicationsController extends Controller
]
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2568,7 +2629,8 @@ class ApplicationsController extends Controller
]
)
),
]),
]
),
new OA\Response(
response: 401,

View File

@@ -19,15 +19,11 @@ class DatabasesController extends Controller
{
private function removeSensitiveData($database)
{
$token = auth()->user()->currentAccessToken();
$database->makeHidden([
'id',
'laravel_through_key',
]);
if ($token->can('view:sensitive')) {
return serializeApiResponse($database);
}
if (request()->attributes->get('can_read_sensitive', false) === false) {
$database->makeHidden([
'internal_db_url',
'external_db_url',
@@ -38,6 +34,7 @@ class DatabasesController extends Controller
'keydb_password',
'clickhouse_admin_password',
]);
}
return serializeApiResponse($database);
}
@@ -211,8 +208,9 @@ class DatabasesController extends Controller
'mongo_conf' => ['type' => 'string', 'description' => 'Mongo conf'],
'mongo_initdb_root_username' => ['type' => 'string', 'description' => 'Mongo initdb root username'],
'mongo_initdb_root_password' => ['type' => 'string', 'description' => 'Mongo initdb root password'],
'mongo_initdb_init_database' => ['type' => 'string', 'description' => 'Mongo initdb init database'],
'mongo_initdb_database' => ['type' => 'string', 'description' => 'Mongo initdb init database'],
'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'],
'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'],
'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'],
'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'],
'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'],
@@ -241,7 +239,7 @@ class DatabasesController extends Controller
)]
public function update_by_uuid(Request $request)
{
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
@@ -413,12 +411,12 @@ class DatabasesController extends Controller
}
break;
case 'standalone-mongodb':
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database'];
$validator = customApiValidator($request->all(), [
'mongo_conf' => 'string',
'mongo_initdb_root_username' => 'string',
'mongo_initdb_root_password' => 'string',
'mongo_initdb_init_database' => 'string',
'mongo_initdb_database' => 'string',
]);
if ($request->has('mongo_conf')) {
if (! isBase64Encoded($request->mongo_conf)) {
@@ -443,9 +441,10 @@ class DatabasesController extends Controller
break;
case 'standalone-mysql':
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$validator = customApiValidator($request->all(), [
'mysql_root_password' => 'string',
'mysql_password' => 'string',
'mysql_user' => 'string',
'mysql_database' => 'string',
'mysql_conf' => 'string',
@@ -909,6 +908,7 @@ class DatabasesController extends Controller
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'],
'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'],
'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'],
'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'],
'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'],
@@ -1013,7 +1013,7 @@ class DatabasesController extends Controller
public function create_database(Request $request, NewDatabaseTypes $type)
{
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -1220,9 +1220,10 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::MYSQL) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_user', 'mysql_database', 'mysql_conf'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$validator = customApiValidator($request->all(), [
'mysql_root_password' => 'string',
'mysql_password' => 'string',
'mysql_user' => 'string',
'mysql_database' => 'string',
'mysql_conf' => 'string',
@@ -1456,12 +1457,12 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::MONGODB) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database'];
$validator = customApiValidator($request->all(), [
'mongo_conf' => 'string',
'mongo_initdb_root_username' => 'string',
'mongo_initdb_root_password' => 'string',
'mongo_initdb_init_database' => 'string',
'mongo_initdb_database' => 'string',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
@@ -1557,7 +1558,8 @@ class DatabasesController extends Controller
]
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1632,9 +1634,11 @@ class DatabasesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Database starting request queued.'],
])
]
)
),
]
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1708,9 +1712,11 @@ class DatabasesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Database stopping request queued.'],
])
]
)
),
]
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1784,9 +1790,11 @@ class DatabasesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Database restaring request queued.'],
])
]
)
),
]
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',

View File

@@ -16,14 +16,11 @@ class DeployController extends Controller
{
private function removeSensitiveData($deployment)
{
$token = auth()->user()->currentAccessToken();
if ($token->can('view:sensitive')) {
return serializeApiResponse($deployment);
}
if (request()->attributes->get('can_read_sensitive', false) === false) {
$deployment->makeHidden([
'logs',
]);
}
return serializeApiResponse($deployment);
}

View File

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

View File

@@ -53,11 +53,7 @@ class ResourcesController extends Controller
$resources = $resources->flatten();
$resources = $resources->map(function ($resource) {
$payload = $resource->toArray();
if ($resource->getMorphClass() === \App\Models\Service::class) {
$payload['status'] = $resource->status();
} else {
$payload['status'] = $resource->status;
}
$payload['type'] = $resource->type();
return $payload;

View File

@@ -11,13 +11,11 @@ class SecurityController extends Controller
{
private function removeSensitiveData($team)
{
$token = auth()->user()->currentAccessToken();
if ($token->can('view:sensitive')) {
return serializeApiResponse($team);
}
if (request()->attributes->get('can_read_sensitive', false) === false) {
$team->makeHidden([
'private_key',
]);
}
return serializeApiResponse($team);
}

View File

@@ -19,25 +19,22 @@ class ServersController extends Controller
{
private function removeSensitiveDataFromSettings($settings)
{
$token = auth()->user()->currentAccessToken();
if ($token->can('view:sensitive')) {
return serializeApiResponse($settings);
}
if (request()->attributes->get('can_read_sensitive', false) === false) {
$settings = $settings->makeHidden([
'sentinel_token',
]);
}
return serializeApiResponse($settings);
}
private function removeSensitiveData($server)
{
$token = auth()->user()->currentAccessToken();
$server->makeHidden([
'id',
]);
if ($token->can('view:sensitive')) {
return serializeApiResponse($server);
if (request()->attributes->get('can_read_sensitive', false) === false) {
// Do nothing
}
return serializeApiResponse($server);
@@ -157,11 +154,7 @@ class ServersController extends Controller
'created_at' => $resource->created_at,
'updated_at' => $resource->updated_at,
];
if ($resource->type() === 'service') {
$payload['status'] = $resource->status();
} else {
$payload['status'] = $resource->status;
}
return $payload;
});
@@ -240,11 +233,7 @@ class ServersController extends Controller
'created_at' => $resource->created_at,
'updated_at' => $resource->updated_at,
];
if ($resource->type() === 'service') {
$payload['status'] = $resource->status();
} else {
$payload['status'] = $resource->status;
}
return $payload;
});
@@ -567,6 +556,9 @@ class ServersController extends Controller
['bearerAuth' => []],
],
tags: ['Servers'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server UUID', schema: new OA\Schema(type: 'string')),
],
requestBody: new OA\RequestBody(
required: true,
description: 'Server updated.',
@@ -596,8 +588,7 @@ class ServersController extends Controller
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/Server')
ref: '#/components/schemas/Server'
)
),
]),
@@ -679,7 +670,7 @@ class ServersController extends Controller
}
return response()->json([
'uuid' => $server->uuid,
])->setStatusCode(201);
}

View File

@@ -18,18 +18,20 @@ class ServicesController extends Controller
{
private function removeSensitiveData($service)
{
$token = auth()->user()->currentAccessToken();
$service->makeHidden([
'id',
'resourceable',
'resourceable_id',
'resourceable_type',
]);
if ($token->can('view:sensitive')) {
return serializeApiResponse($service);
}
if (request()->attributes->get('can_read_sensitive', false) === false) {
$service->makeHidden([
'docker_compose_raw',
'docker_compose',
'value',
'real_value',
]);
}
return serializeApiResponse($service);
}
@@ -334,7 +336,8 @@ class ServicesController extends Controller
EnvironmentVariable::create([
'key' => $key,
'value' => $generatedValue,
'service_id' => $service->id,
'resourceable_id' => $service->id,
'resourceable_type' => $service->getMorphClass(),
'is_build_time' => false,
'is_preview' => false,
]);
@@ -674,7 +677,8 @@ class ServicesController extends Controller
], 422);
}
$env = $service->environment_variables()->where('key', $request->key)->first();
$key = str($request->key)->trim()->replace(' ', '_')->value;
$env = $service->environment_variables()->where('key', $key)->first();
if (! $env) {
return response()->json(['message' => 'Environment variable not found.'], 404);
}
@@ -800,9 +804,9 @@ class ServicesController extends Controller
'errors' => $validator->errors(),
], 422);
}
$key = str($item['key'])->trim()->replace(' ', '_')->value;
$env = $service->environment_variables()->updateOrCreate(
['key' => $item['key']],
['key' => $key],
$item
);
@@ -910,7 +914,8 @@ class ServicesController extends Controller
], 422);
}
$existingEnv = $service->environment_variables()->where('key', $request->key)->first();
$key = str($request->key)->trim()->replace(' ', '_')->value;
$existingEnv = $service->environment_variables()->where('key', $key)->first();
if ($existingEnv) {
return response()->json([
'message' => 'Environment variable already exists. Use PATCH request to update it.',
@@ -996,7 +1001,8 @@ class ServicesController extends Controller
}
$env = EnvironmentVariable::where('uuid', $request->env_uuid)
->where('service_id', $service->id)
->where('resourceable_type', Service::class)
->where('resourceable_id', $service->id)
->first();
if (! $env) {
@@ -1073,7 +1079,7 @@ class ServicesController extends Controller
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
if (str($service->status())->contains('running')) {
if (str($service->status)->contains('running')) {
return response()->json(['message' => 'Service is already running.'], 400);
}
StartService::dispatch($service);
@@ -1151,7 +1157,7 @@ class ServicesController extends Controller
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
if (str($service->status())->contains('stopped') || str($service->status())->contains('exited')) {
if (str($service->status)->contains('stopped') || str($service->status)->contains('exited')) {
return response()->json(['message' => 'Service is already stopped.'], 400);
}
StopService::dispatch($service);

View File

@@ -10,20 +10,18 @@ class TeamController extends Controller
{
private function removeSensitiveData($team)
{
$token = auth()->user()->currentAccessToken();
$team->makeHidden([
'custom_server_limit',
'pivot',
]);
if ($token->can('view:sensitive')) {
return serializeApiResponse($team);
}
if (request()->attributes->get('can_read_sensitive', false) === false) {
$team->makeHidden([
'smtp_username',
'smtp_password',
'resend_api_key',
'telegram_token',
]);
}
return serializeApiResponse($team);
}

View File

@@ -42,15 +42,13 @@ class Controller extends BaseController
public function email_verify(EmailVerificationRequest $request)
{
$request->fulfill();
$name = request()->user()?->name;
// send_internal_notification("User {$name} verified their email address.");
return redirect(RouteServiceProvider::HOME);
}
public function forgot_password(Request $request)
{
if (is_transactional_emails_active()) {
if (is_transactional_emails_enabled()) {
$arrayOfRequest = $request->only(Fortify::email());
$request->merge([
'email' => Str::lower($arrayOfRequest['email']),

View File

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

View File

@@ -1,63 +0,0 @@
<?php
namespace App\Http\Controllers\Webhook;
use App\Http\Controllers\Controller;
use App\Models\Waitlist as ModelsWaitlist;
use Exception;
use Illuminate\Http\Request;
class Waitlist extends Controller
{
public function confirm(Request $request)
{
$email = request()->get('email');
$confirmation_code = request()->get('confirmation_code');
try {
$found = ModelsWaitlist::where('uuid', $confirmation_code)->where('email', $email)->first();
if ($found) {
if (! $found->verified) {
if ($found->created_at > now()->subMinutes(config('constants.waitlist.expiration'))) {
$found->verified = true;
$found->save();
send_internal_notification('Waitlist confirmed: '.$email);
return 'Thank you for confirming your email address. We will notify you when you are next in line.';
} else {
$found->delete();
send_internal_notification('Waitlist expired: '.$email);
return 'Your confirmation code has expired. Please sign up again.';
}
}
}
return redirect()->route('dashboard');
} catch (Exception $e) {
send_internal_notification('Waitlist confirmation failed: '.$e->getMessage());
return redirect()->route('dashboard');
}
}
public function cancel(Request $request)
{
$email = request()->get('email');
$confirmation_code = request()->get('confirmation_code');
try {
$found = ModelsWaitlist::where('uuid', $confirmation_code)->where('email', $email)->first();
if ($found && ! $found->verified) {
$found->delete();
send_internal_notification('Waitlist cancelled: '.$email);
return 'Your email address has been removed from the waitlist.';
}
return redirect()->route('dashboard');
} catch (Exception $e) {
send_internal_notification('Waitlist cancellation failed: '.$e->getMessage());
return redirect()->route('dashboard');
}
}
}

View File

@@ -69,5 +69,7 @@ class Kernel extends HttpKernel
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
'api.ability' => \App\Http\Middleware\ApiAbility::class,
'api.sensitive' => \App\Http\Middleware\ApiSensitiveData::class,
];
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Middleware;
use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility;
class ApiAbility extends CheckForAnyAbility
{
public function handle($request, $next, ...$abilities)
{
try {
if ($request->user()->tokenCan('root')) {
return $next($request);
}
return parent::handle($request, $next, ...$abilities);
} catch (\Illuminate\Auth\AuthenticationException $e) {
return response()->json([
'message' => 'Unauthenticated.',
], 401);
} catch (\Exception $e) {
return response()->json([
'message' => 'Missing required permissions: '.implode(', ', $abilities),
], 403);
}
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class ApiSensitiveData
{
public function handle(Request $request, Closure $next)
{
$token = $request->user()->currentAccessToken();
// Allow access to sensitive data if token has root or read:sensitive permission
$request->attributes->add([
'can_read_sensitive' => $token->can('root') || $token->can('read:sensitive'),
]);
return $next($request);
}
}

View File

@@ -1,28 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class IgnoreReadOnlyApiToken
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$token = auth()->user()->currentAccessToken();
if ($token->can('*')) {
return $next($request);
}
if ($token->can('read-only')) {
return response()->json(['message' => 'You are not allowed to perform this action.'], 403);
}
return $next($request);
}
}

View File

@@ -1,25 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class OnlyRootApiToken
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$token = auth()->user()->currentAccessToken();
if ($token->can('*')) {
return $next($request);
}
return response()->json(['message' => 'You are not allowed to perform this action.'], 403);
}
}

View File

@@ -140,6 +140,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private ?string $buildTarget = null;
private bool $disableBuildCache = false;
private Collection $saved_outputs;
private ?string $full_healthcheck_url = null;
@@ -178,7 +180,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->pull_request_id = $this->application_deployment_queue->pull_request_id;
$this->commit = $this->application_deployment_queue->commit;
$this->rollback = $this->application_deployment_queue->rollback;
$this->disableBuildCache = $this->application->settings->disable_build_cache;
$this->force_rebuild = $this->application_deployment_queue->force_rebuild;
if ($this->disableBuildCache) {
$this->force_rebuild = true;
}
$this->restart_only = $this->application_deployment_queue->restart_only;
$this->restart_only = $this->restart_only && $this->application->build_pack !== 'dockerimage' && $this->application->build_pack !== 'dockerfile';
$this->only_this_server = $this->application_deployment_queue->only_this_server;
@@ -463,7 +469,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id'));
$this->save_environment_variables();
if (! is_null($this->env_filename)) {
$services = collect($composeFile['services']);
$services = collect(data_get($composeFile, 'services', []));
$services = $services->map(function ($service, $name) {
$service['env_file'] = [$this->env_filename];
@@ -1976,6 +1982,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->build_args = $this->build_args->implode(' ');
$this->application_deployment_queue->addLogEntry('----------------------------------------');
if ($this->disableBuildCache) {
$this->application_deployment_queue->addLogEntry('Docker build cache is disabled. It will not be used during the build process.');
}
if ($this->application->build_pack === 'static') {
$this->application_deployment_queue->addLogEntry('Static deployment. Copying static assets to the image.');
} else {

View File

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

View File

@@ -1,28 +0,0 @@
<?php
namespace App\Jobs;
use App\Actions\License\CheckResaleLicense;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class CheckResaleLicenseJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct() {}
public function handle(): void
{
try {
CheckResaleLicense::run();
} catch (\Throwable $e) {
send_internal_notification('CheckResaleLicenseJob failed with: '.$e->getMessage());
throw $e;
}
}
}

View File

@@ -32,8 +32,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public Server $server;
public ScheduledDatabaseBackup $backup;
public StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database;
public ?string $container_name = null;
@@ -58,15 +56,16 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public ?S3Storage $s3 = null;
public function __construct($backup)
public function __construct(public ScheduledDatabaseBackup $backup)
{
$this->onQueue('high');
$this->backup = $backup;
}
public function handle(): void
{
try {
$databasesToBackup = null;
$this->team = Team::find($this->backup->team_id);
if (! $this->team) {
$this->backup->delete();
@@ -198,8 +197,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$databaseType = $this->database->type();
$databasesToBackup = data_get($this->backup, 'databases_to_backup');
}
if (filled($databasesToBackup)) {
if (blank($databasesToBackup)) {
if (str($databaseType)->contains('postgres')) {
$databasesToBackup = [$this->database->postgres_db];
} elseif (str($databaseType)->contains('mongodb')) {
@@ -305,7 +303,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
if ($this->backup->save_s3) {
$this->upload_to_s3();
}
$this->team?->notify(new BackupSuccess($this->backup, $this->database, $database));
$this->team->notify(new BackupSuccess($this->backup, $this->database, $database));
$this->backup_log->update([
'status' => 'success',
'message' => $this->backup_output,

View File

@@ -4,7 +4,8 @@ namespace App\Jobs;
use App\Actions\Server\CleanupDocker;
use App\Models\Server;
use App\Notifications\Server\DockerCleanup;
use App\Notifications\Server\DockerCleanupFailed;
use App\Notifications\Server\DockerCleanupSuccess;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -12,7 +13,6 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -38,35 +38,36 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
return;
}
if ($this->manualCleanup || $this->server->settings->force_docker_cleanup) {
Log::info('DockerCleanupJob '.($this->manualCleanup ? 'manual' : 'force').' cleanup on '.$this->server->name);
CleanupDocker::run(server: $this->server);
return;
}
$this->usageBefore = $this->server->getDiskUsage();
if (str($this->usageBefore)->isEmpty() || $this->usageBefore === null || $this->usageBefore === 0) {
Log::info('DockerCleanupJob force cleanup on '.$this->server->name);
if ($this->manualCleanup || $this->server->settings->force_docker_cleanup) {
CleanupDocker::run(server: $this->server);
$usageAfter = $this->server->getDiskUsage();
$this->server->team?->notify(new DockerCleanupSuccess($this->server, ($this->manualCleanup ? 'Manual' : 'Forced').' Docker cleanup job executed successfully. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.'));
return;
}
if (str($this->usageBefore)->isEmpty() || $this->usageBefore === null || $this->usageBefore === 0) {
CleanupDocker::run(server: $this->server);
$this->server->team?->notify(new DockerCleanupSuccess($this->server, 'Docker cleanup job executed successfully, but no disk usage could be determined.'));
}
if ($this->usageBefore >= $this->server->settings->docker_cleanup_threshold) {
CleanupDocker::run(server: $this->server);
$usageAfter = $this->server->getDiskUsage();
if ($usageAfter < $this->usageBefore) {
$this->server->team?->notify(new DockerCleanup($this->server, 'Saved '.($this->usageBefore - $usageAfter).'% disk space.'));
Log::info('DockerCleanupJob done: Saved '.($this->usageBefore - $usageAfter).'% disk space on '.$this->server->name);
$diskSaved = $this->usageBefore - $usageAfter;
if ($diskSaved > 0) {
$this->server->team?->notify(new DockerCleanupSuccess($this->server, 'Saved '.$diskSaved.'% disk space. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.'));
} else {
Log::info('DockerCleanupJob failed to save disk space on '.$this->server->name);
$this->server->team?->notify(new DockerCleanupSuccess($this->server, 'Docker cleanup job executed successfully, but no disk space was saved. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.'));
}
} else {
Log::info('No need to clean up '.$this->server->name);
$this->server->team?->notify(new DockerCleanupSuccess($this->server, 'No cleanup needed for '.$this->server->name));
}
} catch (\Throwable $e) {
CleanupDocker::run(server: $this->server);
Log::error('DockerCleanupJob failed: '.$e->getMessage());
$this->server->team?->notify(new DockerCleanupFailed($this->server, 'Docker cleanup job failed with the following error: '.$e->getMessage()));
throw $e;
}
}

View File

@@ -10,6 +10,7 @@ use App\Models\Server;
use App\Models\Service;
use App\Models\Team;
use App\Notifications\ScheduledTask\TaskFailed;
use App\Notifications\ScheduledTask\TaskSuccess;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@@ -111,6 +112,8 @@ class ScheduledTaskJob implements ShouldQueue
'message' => $this->task_output,
]);
$this->team?->notify(new TaskSuccess($this->task, $this->task_output));
return;
}
}
@@ -125,7 +128,6 @@ class ScheduledTaskJob implements ShouldQueue
]);
}
$this->team?->notify(new TaskFailed($this->task, $e->getMessage()));
// send_internal_notification('ScheduledTaskJob failed with: ' . $e->getMessage());
throw $e;
} finally {
ScheduledTaskDone::dispatch($this->team->id);

View File

@@ -1,37 +0,0 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SendConfirmationForWaitlistJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public string $email, public string $uuid) {}
public function handle()
{
try {
$mail = new MailMessage;
$confirmation_url = base_url().'/webhooks/waitlist/confirm?email='.$this->email.'&confirmation_code='.$this->uuid;
$cancel_url = base_url().'/webhooks/waitlist/cancel?email='.$this->email.'&confirmation_code='.$this->uuid;
$mail->view('emails.waitlist-confirmation',
[
'confirmation_url' => $confirmation_url,
'cancel_url' => $cancel_url,
]);
$mail->subject('You are on the waitlist!');
send_user_an_email($mail, $this->email);
} catch (\Throwable $e) {
send_internal_notification("SendConfirmationForWaitlistJob failed for {$this->email} with error: ".$e->getMessage());
throw $e;
}
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Jobs;
use App\Notifications\Dto\PushoverMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
class SendMessageToPushoverJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 5;
public $backoff = 10;
/**
* The maximum number of unhandled exceptions to allow before failing.
*/
public int $maxExceptions = 5;
public function __construct(
public PushoverMessage $message,
public string $token,
public string $user,
) {
$this->onQueue('high');
}
/**
* Execute the job.
*/
public function handle(): void
{
$response = Http::post('https://api.pushover.net/1/messages.json', $this->message->toPayload($this->token, $this->user));
if ($response->failed()) {
throw new \RuntimeException('Pushover notification failed with ' . $response->status() . ' status code.' . $response->body());
}
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Jobs;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
class SendMessageToSlackJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
private SlackMessage $message,
private string $webhookUrl
) {
$this->onQueue('high');
}
public function handle(): void
{
Http::post($this->webhookUrl, [
'blocks' => [
[
'type' => 'section',
'text' => [
'type' => 'plain_text',
'text' => 'Coolify Notification',
],
],
],
'attachments' => [
[
'color' => $this->message->color,
'blocks' => [
[
'type' => 'header',
'text' => [
'type' => 'plain_text',
'text' => $this->message->title,
],
],
[
'type' => 'section',
'text' => [
'type' => 'mrkdwn',
'text' => $this->message->description,
],
],
],
],
],
]);
}
}

View File

@@ -32,7 +32,7 @@ class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue
public array $buttons,
public string $token,
public string $chatId,
public ?string $topicId = null,
public ?string $threadId = null,
) {
$this->onQueue('high');
}
@@ -67,12 +67,12 @@ class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue
'chat_id' => $this->chatId,
'text' => $this->text,
];
if ($this->topicId) {
$payload['message_thread_id'] = $this->topicId;
if ($this->threadId) {
$payload['message_thread_id'] = $this->threadId;
}
$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());
}
}
}

View File

@@ -31,7 +31,12 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
}
public function __construct(public Server $server) {}
public function __construct(public Server $server)
{
if (isDev()) {
$this->handle();
}
}
public function handle()
{

View File

@@ -25,6 +25,7 @@ class StripeProcessJob implements ShouldQueue
public function handle(): void
{
try {
$excludedPlans = config('subscription.stripe_excluded_plans');
$type = data_get($this->event, 'type');
@@ -51,7 +52,7 @@ class StripeProcessJob implements ShouldQueue
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}");
throw new \RuntimeException("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
}
break;
case 'checkout.session.completed':
@@ -68,7 +69,7 @@ class StripeProcessJob implements ShouldQueue
$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}.");
throw new \RuntimeException("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
}
$subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) {
@@ -101,7 +102,7 @@ class StripeProcessJob implements ShouldQueue
'stripe_invoice_paid' => true,
]);
} else {
throw new \Exception("No subscription found for customer: {$customerId}");
throw new \RuntimeException("No subscription found for customer: {$customerId}");
}
break;
case 'invoice.payment_failed':
@@ -109,12 +110,12 @@ class StripeProcessJob implements ShouldQueue
$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}");
throw new \RuntimeException("No subscription found for customer: {$customerId}");
}
$team = data_get($subscription, 'team');
if (! $team) {
send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId);
throw new \Exception("No team found in Coolify for customer: {$customerId}");
throw new \RuntimeException("No team found in Coolify for customer: {$customerId}");
}
if (! $subscription->stripe_invoice_paid) {
SubscriptionInvoiceFailedJob::dispatch($team);
@@ -128,7 +129,7 @@ class StripeProcessJob implements ShouldQueue
$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}");
throw new \RuntimeException("No subscription found in Coolify for customer: {$customerId}");
}
if ($subscription->stripe_invoice_paid) {
send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId);
@@ -145,20 +146,20 @@ class StripeProcessJob implements ShouldQueue
if (! $teamId || ! $userId) {
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if ($subscription) {
throw new \Exception("Subscription already exists for customer: {$customerId}");
throw new \RuntimeException("Subscription already exists for customer: {$customerId}");
}
throw new \Exception('No team id or user id found');
throw new \RuntimeException('No team id or user id found');
}
$team = Team::find($teamId);
$found = $team->members->where('id', $userId)->first();
if (! $found->isAdmin()) {
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
throw new \Exception("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
throw new \RuntimeException("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
}
$subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) {
send_internal_notification("Subscription already exists for team: {$teamId}");
throw new \Exception("Subscription already exists for team: {$teamId}");
throw new \RuntimeException("Subscription already exists for team: {$teamId}");
} else {
Subscription::create([
'team_id' => $teamId,
@@ -172,8 +173,8 @@ class StripeProcessJob implements ShouldQueue
$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');
$subscriptionId = data_get($data, 'items.data.0.subscription') ?? data_get($data, 'id');
$planId = data_get($data, 'items.data.0.plan.id') ?? data_get($data, 'plan.id');
if (Str::contains($excludedPlans, $planId)) {
send_internal_notification('Subscription excluded.');
break;
@@ -182,7 +183,7 @@ class StripeProcessJob implements ShouldQueue
if (! $subscription) {
if ($status === 'incomplete_expired') {
send_internal_notification('Subscription incomplete expired');
throw new \Exception('Subscription incomplete expired');
throw new \RuntimeException('Subscription incomplete expired');
}
if ($teamId) {
$subscription = Subscription::create([
@@ -193,7 +194,7 @@ class StripeProcessJob implements ShouldQueue
]);
} else {
send_internal_notification('No subscription and team id found');
throw new \Exception('No subscription and team id found');
throw new \RuntimeException('No subscription and team id found');
}
}
$cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
@@ -217,26 +218,49 @@ class StripeProcessJob implements ShouldQueue
'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
]);
if ($status === 'paused' || $status === 'incomplete_expired') {
if ($subscription->stripe_subscription_id === $subscriptionId) {
$subscription->update([
'stripe_invoice_paid' => false,
]);
}
}
if ($status === 'active') {
if ($subscription->stripe_subscription_id === $subscriptionId) {
$subscription->update([
'stripe_invoice_paid' => true,
]);
}
}
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();
$subscriptionId = data_get($data, 'id');
$subscription = Subscription::where('stripe_customer_id', $customerId)->where('stripe_subscription_id', $subscriptionId)->first();
if ($subscription) {
$team = data_get($subscription, 'team');
$team?->subscriptionEnded();
if ($team) {
$team->subscriptionEnded();
} else {
send_internal_notification('Subscription deleted but no team found in Coolify for customer: '.$customerId);
throw new \RuntimeException("No team found in Coolify for customer: {$customerId}");
}
} else {
send_internal_notification('Subscription deleted but no subscription found in Coolify for customer: '.$customerId);
throw new \RuntimeException("No subscription found in Coolify for customer: {$customerId}");
}
break;
default:
// Unhandled event type
throw new \RuntimeException("Unhandled event type: {$type}");
}
} catch (\Exception $e) {
send_internal_notification('StripeProcessJob error: '.$e->getMessage());
}
}
}

View File

@@ -14,7 +14,7 @@ class ProxyStartedNotification
public function handle(ProxyStarted $event): void
{
$this->server = data_get($event, 'data');
$this->server->setupDefault404Redirect();
$this->server->setupDefaultRedirect();
$this->server->setupDynamicProxyConfiguration();
$this->server->proxy->force_stop = false;
$this->server->save();

View File

@@ -21,16 +21,28 @@ class Index extends Component
public function mount()
{
if (! isCloud()) {
if (! isCloud() && ! isDev()) {
return redirect()->route('dashboard');
}
if (Auth::id() !== 0) {
if (Auth::id() !== 0 && ! session('impersonating')) {
return redirect()->route('dashboard');
}
$this->getSubscribers();
}
public function back()
{
if (session('impersonating')) {
session()->forget('impersonating');
$user = User::find(0);
$team_to_switch_to = $user->teams->first();
Auth::login($user);
refreshSession($team_to_switch_to);
return redirect(request()->header('Referer'));
}
}
public function submitSearch()
{
if ($this->search !== '') {
@@ -52,9 +64,10 @@ class Index extends Component
if (Auth::id() !== 0) {
return redirect()->route('dashboard');
}
session(['impersonating' => true]);
$user = User::find($user_id);
$team_to_switch_to = $user->teams->first();
Cache::forget("team:{$user->id}");
// Cache::forget("team:{$user->id}");
Auth::login($user);
refreshSession($team_to_switch_to);

View File

@@ -173,13 +173,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function getProxyType()
{
// Set Default Proxy Type
$this->selectProxy(ProxyTypes::TRAEFIK->value);
// $proxyTypeSet = $this->createdServer->proxy->type;
// if (!$proxyTypeSet) {
// $this->currentState = 'select-proxy';
// return;
// }
$this->getProjects();
}
@@ -190,7 +184,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
return;
}
$this->createdPrivateKey = PrivateKey::find($this->selectedExistingPrivateKey);
$this->createdPrivateKey = PrivateKey::where('team_id', currentTeam()->id)->where('id', $this->selectedExistingPrivateKey)->first();
$this->privateKey = $this->createdPrivateKey->private_key;
$this->currentState = 'create-server';
}

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Notifications;
use App\Models\DiscordNotificationSettings;
use App\Models\Team;
use App\Notifications\Test;
use Livewire\Attributes\Validate;
@@ -11,6 +12,8 @@ class Discord extends Component
{
public Team $team;
public DiscordNotificationSettings $settings;
#[Validate(['boolean'])]
public bool $discordEnabled = false;
@@ -18,27 +21,46 @@ class Discord extends Component
public ?string $discordWebhookUrl = null;
#[Validate(['boolean'])]
public bool $discordNotificationsTest = false;
public bool $deploymentSuccessDiscordNotifications = false;
#[Validate(['boolean'])]
public bool $discordNotificationsDeployments = false;
public bool $deploymentFailureDiscordNotifications = true;
#[Validate(['boolean'])]
public bool $discordNotificationsStatusChanges = false;
public bool $statusChangeDiscordNotifications = false;
#[Validate(['boolean'])]
public bool $discordNotificationsDatabaseBackups = false;
public bool $backupSuccessDiscordNotifications = false;
#[Validate(['boolean'])]
public bool $discordNotificationsScheduledTasks = false;
public bool $backupFailureDiscordNotifications = true;
#[Validate(['boolean'])]
public bool $discordNotificationsServerDiskUsage = false;
public bool $scheduledTaskSuccessDiscordNotifications = false;
#[Validate(['boolean'])]
public bool $scheduledTaskFailureDiscordNotifications = true;
#[Validate(['boolean'])]
public bool $dockerCleanupSuccessDiscordNotifications = false;
#[Validate(['boolean'])]
public bool $dockerCleanupFailureDiscordNotifications = true;
#[Validate(['boolean'])]
public bool $serverDiskUsageDiscordNotifications = true;
#[Validate(['boolean'])]
public bool $serverReachableDiscordNotifications = false;
#[Validate(['boolean'])]
public bool $serverUnreachableDiscordNotifications = true;
public function mount()
{
try {
$this->team = auth()->user()->currentTeam();
$this->settings = $this->team->discordNotificationSettings;
$this->syncData();
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -49,25 +71,40 @@ class Discord extends Component
{
if ($toModel) {
$this->validate();
$this->team->discord_enabled = $this->discordEnabled;
$this->team->discord_webhook_url = $this->discordWebhookUrl;
$this->team->discord_notifications_test = $this->discordNotificationsTest;
$this->team->discord_notifications_deployments = $this->discordNotificationsDeployments;
$this->team->discord_notifications_status_changes = $this->discordNotificationsStatusChanges;
$this->team->discord_notifications_database_backups = $this->discordNotificationsDatabaseBackups;
$this->team->discord_notifications_scheduled_tasks = $this->discordNotificationsScheduledTasks;
$this->team->discord_notifications_server_disk_usage = $this->discordNotificationsServerDiskUsage;
$this->team->save();
$this->settings->discord_enabled = $this->discordEnabled;
$this->settings->discord_webhook_url = $this->discordWebhookUrl;
$this->settings->deployment_success_discord_notifications = $this->deploymentSuccessDiscordNotifications;
$this->settings->deployment_failure_discord_notifications = $this->deploymentFailureDiscordNotifications;
$this->settings->status_change_discord_notifications = $this->statusChangeDiscordNotifications;
$this->settings->backup_success_discord_notifications = $this->backupSuccessDiscordNotifications;
$this->settings->backup_failure_discord_notifications = $this->backupFailureDiscordNotifications;
$this->settings->scheduled_task_success_discord_notifications = $this->scheduledTaskSuccessDiscordNotifications;
$this->settings->scheduled_task_failure_discord_notifications = $this->scheduledTaskFailureDiscordNotifications;
$this->settings->docker_cleanup_success_discord_notifications = $this->dockerCleanupSuccessDiscordNotifications;
$this->settings->docker_cleanup_failure_discord_notifications = $this->dockerCleanupFailureDiscordNotifications;
$this->settings->server_disk_usage_discord_notifications = $this->serverDiskUsageDiscordNotifications;
$this->settings->server_reachable_discord_notifications = $this->serverReachableDiscordNotifications;
$this->settings->server_unreachable_discord_notifications = $this->serverUnreachableDiscordNotifications;
$this->settings->save();
refreshSession();
} else {
$this->discordEnabled = $this->team->discord_enabled;
$this->discordWebhookUrl = $this->team->discord_webhook_url;
$this->discordNotificationsTest = $this->team->discord_notifications_test;
$this->discordNotificationsDeployments = $this->team->discord_notifications_deployments;
$this->discordNotificationsStatusChanges = $this->team->discord_notifications_status_changes;
$this->discordNotificationsDatabaseBackups = $this->team->discord_notifications_database_backups;
$this->discordNotificationsScheduledTasks = $this->team->discord_notifications_scheduled_tasks;
$this->discordNotificationsServerDiskUsage = $this->team->discord_notifications_server_disk_usage;
$this->discordEnabled = $this->settings->discord_enabled;
$this->discordWebhookUrl = $this->settings->discord_webhook_url;
$this->deploymentSuccessDiscordNotifications = $this->settings->deployment_success_discord_notifications;
$this->deploymentFailureDiscordNotifications = $this->settings->deployment_failure_discord_notifications;
$this->statusChangeDiscordNotifications = $this->settings->status_change_discord_notifications;
$this->backupSuccessDiscordNotifications = $this->settings->backup_success_discord_notifications;
$this->backupFailureDiscordNotifications = $this->settings->backup_failure_discord_notifications;
$this->scheduledTaskSuccessDiscordNotifications = $this->settings->scheduled_task_success_discord_notifications;
$this->scheduledTaskFailureDiscordNotifications = $this->settings->scheduled_task_failure_discord_notifications;
$this->dockerCleanupSuccessDiscordNotifications = $this->settings->docker_cleanup_success_discord_notifications;
$this->dockerCleanupFailureDiscordNotifications = $this->settings->docker_cleanup_failure_discord_notifications;
$this->serverDiskUsageDiscordNotifications = $this->settings->server_disk_usage_discord_notifications;
$this->serverReachableDiscordNotifications = $this->settings->server_reachable_discord_notifications;
$this->serverUnreachableDiscordNotifications = $this->settings->server_unreachable_discord_notifications;
}
}
@@ -117,7 +154,7 @@ class Discord extends Component
public function sendTestNotification()
{
try {
$this->team->notify(new Test);
$this->team->notify(new Test(channel: 'discord'));
$this->dispatch('success', 'Test notification sent.');
} catch (\Throwable $e) {
return handleError($e, $this);

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Notifications;
use App\Models\EmailNotificationSettings;
use App\Models\Team;
use App\Notifications\Test;
use Illuminate\Support\Facades\RateLimiter;
@@ -11,17 +12,20 @@ use Livewire\Component;
class Email extends Component
{
protected $listeners = ['refresh' => '$refresh'];
#[Locked]
public Team $team;
#[Locked]
public EmailNotificationSettings $settings;
#[Locked]
public string $emails;
#[Validate(['boolean'])]
public bool $smtpEnabled = false;
#[Validate(['boolean'])]
public bool $useInstanceEmailSettings = false;
#[Validate(['nullable', 'email'])]
public ?string $smtpFromAddress = null;
@@ -34,11 +38,11 @@ class Email extends Component
#[Validate(['nullable', 'string'])]
public ?string $smtpHost = null;
#[Validate(['nullable', 'numeric'])]
#[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])]
public ?int $smtpPort = null;
#[Validate(['nullable', 'string'])]
public ?string $smtpEncryption = null;
#[Validate(['nullable', 'string', 'in:tls,ssl,none'])]
public ?string $smtpEncryption = 'tls';
#[Validate(['nullable', 'string'])]
public ?string $smtpUsername = null;
@@ -50,38 +54,61 @@ class Email extends Component
public ?int $smtpTimeout = null;
#[Validate(['boolean'])]
public bool $smtpNotificationsTest = false;
#[Validate(['boolean'])]
public bool $smtpNotificationsDeployments = false;
#[Validate(['boolean'])]
public bool $smtpNotificationsStatusChanges = false;
#[Validate(['boolean'])]
public bool $smtpNotificationsDatabaseBackups = false;
#[Validate(['boolean'])]
public bool $smtpNotificationsScheduledTasks = false;
#[Validate(['boolean'])]
public bool $smtpNotificationsServerDiskUsage = false;
#[Validate(['boolean'])]
public bool $resendEnabled;
public bool $resendEnabled = false;
#[Validate(['nullable', 'string'])]
public ?string $resendApiKey = null;
#[Validate(['required', 'email'])]
public string $testEmailAddress = '';
#[Validate(['boolean'])]
public bool $useInstanceEmailSettings = false;
#[Validate(['boolean'])]
public bool $deploymentSuccessEmailNotifications = false;
#[Validate(['boolean'])]
public bool $deploymentFailureEmailNotifications = true;
#[Validate(['boolean'])]
public bool $statusChangeEmailNotifications = false;
#[Validate(['boolean'])]
public bool $backupSuccessEmailNotifications = false;
#[Validate(['boolean'])]
public bool $backupFailureEmailNotifications = true;
#[Validate(['boolean'])]
public bool $scheduledTaskSuccessEmailNotifications = false;
#[Validate(['boolean'])]
public bool $scheduledTaskFailureEmailNotifications = true;
#[Validate(['boolean'])]
public bool $dockerCleanupSuccessEmailNotifications = false;
#[Validate(['boolean'])]
public bool $dockerCleanupFailureEmailNotifications = true;
#[Validate(['boolean'])]
public bool $serverDiskUsageEmailNotifications = true;
#[Validate(['boolean'])]
public bool $serverReachableEmailNotifications = false;
#[Validate(['boolean'])]
public bool $serverUnreachableEmailNotifications = true;
#[Validate(['nullable', 'email'])]
public ?string $testEmailAddress = null;
public function mount()
{
try {
$this->team = auth()->user()->currentTeam();
$this->emails = auth()->user()->email;
$this->settings = $this->team->emailNotificationSettings;
$this->syncData();
$this->testEmailAddress = auth()->user()->email;
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -91,47 +118,191 @@ class Email extends Component
{
if ($toModel) {
$this->validate();
$this->team->smtp_enabled = $this->smtpEnabled;
$this->team->smtp_from_address = $this->smtpFromAddress;
$this->team->smtp_from_name = $this->smtpFromName;
$this->team->smtp_host = $this->smtpHost;
$this->team->smtp_port = $this->smtpPort;
$this->team->smtp_encryption = $this->smtpEncryption;
$this->team->smtp_username = $this->smtpUsername;
$this->team->smtp_password = $this->smtpPassword;
$this->team->smtp_timeout = $this->smtpTimeout;
$this->team->smtp_recipients = $this->smtpRecipients;
$this->team->smtp_notifications_test = $this->smtpNotificationsTest;
$this->team->smtp_notifications_deployments = $this->smtpNotificationsDeployments;
$this->team->smtp_notifications_status_changes = $this->smtpNotificationsStatusChanges;
$this->team->smtp_notifications_database_backups = $this->smtpNotificationsDatabaseBackups;
$this->team->smtp_notifications_scheduled_tasks = $this->smtpNotificationsScheduledTasks;
$this->team->smtp_notifications_server_disk_usage = $this->smtpNotificationsServerDiskUsage;
$this->team->use_instance_email_settings = $this->useInstanceEmailSettings;
$this->team->resend_enabled = $this->resendEnabled;
$this->team->resend_api_key = $this->resendApiKey;
$this->team->save();
refreshSession();
$this->settings->smtp_enabled = $this->smtpEnabled;
$this->settings->smtp_from_address = $this->smtpFromAddress;
$this->settings->smtp_from_name = $this->smtpFromName;
$this->settings->smtp_recipients = $this->smtpRecipients;
$this->settings->smtp_host = $this->smtpHost;
$this->settings->smtp_port = $this->smtpPort;
$this->settings->smtp_encryption = $this->smtpEncryption;
$this->settings->smtp_username = $this->smtpUsername;
$this->settings->smtp_password = $this->smtpPassword;
$this->settings->smtp_timeout = $this->smtpTimeout;
$this->settings->resend_enabled = $this->resendEnabled;
$this->settings->resend_api_key = $this->resendApiKey;
$this->settings->use_instance_email_settings = $this->useInstanceEmailSettings;
$this->settings->deployment_success_email_notifications = $this->deploymentSuccessEmailNotifications;
$this->settings->deployment_failure_email_notifications = $this->deploymentFailureEmailNotifications;
$this->settings->status_change_email_notifications = $this->statusChangeEmailNotifications;
$this->settings->backup_success_email_notifications = $this->backupSuccessEmailNotifications;
$this->settings->backup_failure_email_notifications = $this->backupFailureEmailNotifications;
$this->settings->scheduled_task_success_email_notifications = $this->scheduledTaskSuccessEmailNotifications;
$this->settings->scheduled_task_failure_email_notifications = $this->scheduledTaskFailureEmailNotifications;
$this->settings->docker_cleanup_success_email_notifications = $this->dockerCleanupSuccessEmailNotifications;
$this->settings->docker_cleanup_failure_email_notifications = $this->dockerCleanupFailureEmailNotifications;
$this->settings->server_disk_usage_email_notifications = $this->serverDiskUsageEmailNotifications;
$this->settings->server_reachable_email_notifications = $this->serverReachableEmailNotifications;
$this->settings->server_unreachable_email_notifications = $this->serverUnreachableEmailNotifications;
$this->settings->save();
} else {
$this->smtpEnabled = $this->team->smtp_enabled;
$this->smtpFromAddress = $this->team->smtp_from_address;
$this->smtpFromName = $this->team->smtp_from_name;
$this->smtpHost = $this->team->smtp_host;
$this->smtpPort = $this->team->smtp_port;
$this->smtpEncryption = $this->team->smtp_encryption;
$this->smtpUsername = $this->team->smtp_username;
$this->smtpPassword = $this->team->smtp_password;
$this->smtpTimeout = $this->team->smtp_timeout;
$this->smtpRecipients = $this->team->smtp_recipients;
$this->smtpNotificationsTest = $this->team->smtp_notifications_test;
$this->smtpNotificationsDeployments = $this->team->smtp_notifications_deployments;
$this->smtpNotificationsStatusChanges = $this->team->smtp_notifications_status_changes;
$this->smtpNotificationsDatabaseBackups = $this->team->smtp_notifications_database_backups;
$this->smtpNotificationsScheduledTasks = $this->team->smtp_notifications_scheduled_tasks;
$this->smtpNotificationsServerDiskUsage = $this->team->smtp_notifications_server_disk_usage;
$this->useInstanceEmailSettings = $this->team->use_instance_email_settings;
$this->resendEnabled = $this->team->resend_enabled;
$this->resendApiKey = $this->team->resend_api_key;
$this->smtpEnabled = $this->settings->smtp_enabled;
$this->smtpFromAddress = $this->settings->smtp_from_address;
$this->smtpFromName = $this->settings->smtp_from_name;
$this->smtpRecipients = $this->settings->smtp_recipients;
$this->smtpHost = $this->settings->smtp_host;
$this->smtpPort = $this->settings->smtp_port;
$this->smtpEncryption = $this->settings->smtp_encryption;
$this->smtpUsername = $this->settings->smtp_username;
$this->smtpPassword = $this->settings->smtp_password;
$this->smtpTimeout = $this->settings->smtp_timeout;
$this->resendEnabled = $this->settings->resend_enabled;
$this->resendApiKey = $this->settings->resend_api_key;
$this->useInstanceEmailSettings = $this->settings->use_instance_email_settings;
$this->deploymentSuccessEmailNotifications = $this->settings->deployment_success_email_notifications;
$this->deploymentFailureEmailNotifications = $this->settings->deployment_failure_email_notifications;
$this->statusChangeEmailNotifications = $this->settings->status_change_email_notifications;
$this->backupSuccessEmailNotifications = $this->settings->backup_success_email_notifications;
$this->backupFailureEmailNotifications = $this->settings->backup_failure_email_notifications;
$this->scheduledTaskSuccessEmailNotifications = $this->settings->scheduled_task_success_email_notifications;
$this->scheduledTaskFailureEmailNotifications = $this->settings->scheduled_task_failure_email_notifications;
$this->dockerCleanupSuccessEmailNotifications = $this->settings->docker_cleanup_success_email_notifications;
$this->dockerCleanupFailureEmailNotifications = $this->settings->docker_cleanup_failure_email_notifications;
$this->serverDiskUsageEmailNotifications = $this->settings->server_disk_usage_email_notifications;
$this->serverReachableEmailNotifications = $this->settings->server_reachable_email_notifications;
$this->serverUnreachableEmailNotifications = $this->settings->server_unreachable_email_notifications;
}
}
public function submit()
{
try {
$this->resetErrorBag();
$this->saveModel();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function saveModel()
{
$this->syncData(true);
$this->dispatch('success', 'Email notifications settings updated.');
}
public function instantSave(?string $type = null)
{
try {
$this->resetErrorBag();
if ($type === 'SMTP') {
$this->submitSmtp();
} elseif ($type === 'Resend') {
$this->submitResend();
} else {
$this->smtpEnabled = false;
$this->resendEnabled = false;
$this->saveModel();
return;
}
} catch (\Throwable $e) {
if ($type === 'SMTP') {
$this->smtpEnabled = false;
} elseif ($type === 'Resend') {
$this->resendEnabled = false;
}
return handleError($e, $this);
} finally {
$this->dispatch('refresh');
}
}
public function submitSmtp()
{
try {
$this->resetErrorBag();
$this->validate([
'smtpEnabled' => 'boolean',
'smtpFromAddress' => 'required|email',
'smtpFromName' => 'required|string',
'smtpHost' => 'required|string',
'smtpPort' => 'required|numeric',
'smtpEncryption' => 'required|string|in:tls,ssl,none',
'smtpUsername' => 'nullable|string',
'smtpPassword' => 'nullable|string',
'smtpTimeout' => 'nullable|numeric',
], [
'smtpFromAddress.required' => 'From Address is required.',
'smtpFromAddress.email' => 'Please enter a valid email address.',
'smtpFromName.required' => 'From Name is required.',
'smtpHost.required' => 'SMTP Host is required.',
'smtpPort.required' => 'SMTP Port is required.',
'smtpPort.numeric' => 'SMTP Port must be a number.',
'smtpEncryption.required' => 'Encryption type is required.',
]);
$this->settings->resend_enabled = false;
$this->settings->use_instance_email_settings = false;
$this->resendEnabled = false;
$this->useInstanceEmailSettings = false;
$this->settings->smtp_enabled = $this->smtpEnabled;
$this->settings->smtp_from_address = $this->smtpFromAddress;
$this->settings->smtp_from_name = $this->smtpFromName;
$this->settings->smtp_host = $this->smtpHost;
$this->settings->smtp_port = $this->smtpPort;
$this->settings->smtp_encryption = $this->smtpEncryption;
$this->settings->smtp_username = $this->smtpUsername;
$this->settings->smtp_password = $this->smtpPassword;
$this->settings->smtp_timeout = $this->smtpTimeout;
$this->settings->save();
$this->dispatch('success', 'SMTP settings updated.');
} catch (\Throwable $e) {
$this->smtpEnabled = false;
return handleError($e);
}
}
public function submitResend()
{
try {
$this->resetErrorBag();
$this->validate([
'resendEnabled' => 'boolean',
'resendApiKey' => 'required|string',
'smtpFromAddress' => 'required|email',
'smtpFromName' => 'required|string',
], [
'resendApiKey.required' => 'Resend API Key is required.',
'smtpFromAddress.required' => 'From Address is required.',
'smtpFromAddress.email' => 'Please enter a valid email address.',
'smtpFromName.required' => 'From Name is required.',
]);
$this->settings->smtp_enabled = false;
$this->settings->use_instance_email_settings = false;
$this->smtpEnabled = false;
$this->useInstanceEmailSettings = false;
$this->settings->resend_enabled = $this->resendEnabled;
$this->settings->resend_api_key = $this->resendApiKey;
$this->settings->smtp_from_address = $this->smtpFromAddress;
$this->settings->smtp_from_name = $this->smtpFromName;
$this->settings->save();
$this->dispatch('success', 'Resend settings updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
@@ -149,7 +320,7 @@ class Email extends Component
'test-email:'.$this->team->id,
$perMinute = 0,
function () {
$this->team?->notify(new Test($this->testEmailAddress));
$this->team?->notify(new Test($this->testEmailAddress, 'email'));
$this->dispatch('success', 'Test Email sent.');
},
$decaySeconds = 10,
@@ -163,70 +334,6 @@ class Email extends Component
}
}
public function instantSaveInstance()
{
try {
$this->smtpEnabled = false;
$this->resendEnabled = false;
$this->saveModel();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSaveSmtpEnabled()
{
try {
$this->validate([
'smtpHost' => 'required',
'smtpPort' => 'required|numeric',
], [
'smtpHost.required' => 'SMTP Host is required.',
'smtpPort.required' => 'SMTP Port is required.',
]);
$this->resendEnabled = false;
$this->saveModel();
} catch (\Throwable $e) {
$this->smtpEnabled = false;
return handleError($e, $this);
}
}
public function instantSaveResend()
{
try {
$this->validate([
'resendApiKey' => 'required',
], [
'resendApiKey.required' => 'Resend API Key is required.',
]);
$this->smtpEnabled = false;
$this->saveModel();
} catch (\Throwable $e) {
$this->resendEnabled = false;
return handleError($e, $this);
}
}
public function saveModel()
{
$this->syncData(true);
refreshSession();
$this->dispatch('success', 'Settings saved.');
}
public function submit()
{
try {
$this->resetErrorBag();
$this->saveModel();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function copyFromInstanceSettings()
{
$settings = instanceSettings();

View File

@@ -0,0 +1,184 @@
<?php
namespace App\Livewire\Notifications;
use App\Models\PushoverNotificationSettings;
use App\Models\Team;
use App\Notifications\Test;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;
use Livewire\Component;
class Pushover extends Component
{
protected $listeners = ['refresh' => '$refresh'];
#[Locked]
public Team $team;
#[Locked]
public PushoverNotificationSettings $settings;
#[Validate(['boolean'])]
public bool $pushoverEnabled = false;
#[Validate(['nullable', 'string'])]
public ?string $pushoverUserKey = null;
#[Validate(['nullable', 'string'])]
public ?string $pushoverApiToken = null;
#[Validate(['boolean'])]
public bool $deploymentSuccessPushoverNotifications = false;
#[Validate(['boolean'])]
public bool $deploymentFailurePushoverNotifications = true;
#[Validate(['boolean'])]
public bool $statusChangePushoverNotifications = false;
#[Validate(['boolean'])]
public bool $backupSuccessPushoverNotifications = false;
#[Validate(['boolean'])]
public bool $backupFailurePushoverNotifications = true;
#[Validate(['boolean'])]
public bool $scheduledTaskSuccessPushoverNotifications = false;
#[Validate(['boolean'])]
public bool $scheduledTaskFailurePushoverNotifications = true;
#[Validate(['boolean'])]
public bool $dockerCleanupSuccessPushoverNotifications = false;
#[Validate(['boolean'])]
public bool $dockerCleanupFailurePushoverNotifications = true;
#[Validate(['boolean'])]
public bool $serverDiskUsagePushoverNotifications = true;
#[Validate(['boolean'])]
public bool $serverReachablePushoverNotifications = false;
#[Validate(['boolean'])]
public bool $serverUnreachablePushoverNotifications = true;
public function mount()
{
try {
$this->team = auth()->user()->currentTeam();
$this->settings = $this->team->pushoverNotificationSettings;
$this->syncData();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function syncData(bool $toModel = false)
{
if ($toModel) {
$this->validate();
$this->settings->pushover_enabled = $this->pushoverEnabled;
$this->settings->pushover_user_key = $this->pushoverUserKey;
$this->settings->pushover_api_token = $this->pushoverApiToken;
$this->settings->deployment_success_pushover_notifications = $this->deploymentSuccessPushoverNotifications;
$this->settings->deployment_failure_pushover_notifications = $this->deploymentFailurePushoverNotifications;
$this->settings->status_change_pushover_notifications = $this->statusChangePushoverNotifications;
$this->settings->backup_success_pushover_notifications = $this->backupSuccessPushoverNotifications;
$this->settings->backup_failure_pushover_notifications = $this->backupFailurePushoverNotifications;
$this->settings->scheduled_task_success_pushover_notifications = $this->scheduledTaskSuccessPushoverNotifications;
$this->settings->scheduled_task_failure_pushover_notifications = $this->scheduledTaskFailurePushoverNotifications;
$this->settings->docker_cleanup_success_pushover_notifications = $this->dockerCleanupSuccessPushoverNotifications;
$this->settings->docker_cleanup_failure_pushover_notifications = $this->dockerCleanupFailurePushoverNotifications;
$this->settings->server_disk_usage_pushover_notifications = $this->serverDiskUsagePushoverNotifications;
$this->settings->server_reachable_pushover_notifications = $this->serverReachablePushoverNotifications;
$this->settings->server_unreachable_pushover_notifications = $this->serverUnreachablePushoverNotifications;
$this->settings->save();
refreshSession();
} else {
$this->pushoverEnabled = $this->settings->pushover_enabled;
$this->pushoverUserKey = $this->settings->pushover_user_key;
$this->pushoverApiToken = $this->settings->pushover_api_token;
$this->deploymentSuccessPushoverNotifications = $this->settings->deployment_success_pushover_notifications;
$this->deploymentFailurePushoverNotifications = $this->settings->deployment_failure_pushover_notifications;
$this->statusChangePushoverNotifications = $this->settings->status_change_pushover_notifications;
$this->backupSuccessPushoverNotifications = $this->settings->backup_success_pushover_notifications;
$this->backupFailurePushoverNotifications = $this->settings->backup_failure_pushover_notifications;
$this->scheduledTaskSuccessPushoverNotifications = $this->settings->scheduled_task_success_pushover_notifications;
$this->scheduledTaskFailurePushoverNotifications = $this->settings->scheduled_task_failure_pushover_notifications;
$this->dockerCleanupSuccessPushoverNotifications = $this->settings->docker_cleanup_success_pushover_notifications;
$this->dockerCleanupFailurePushoverNotifications = $this->settings->docker_cleanup_failure_pushover_notifications;
$this->serverDiskUsagePushoverNotifications = $this->settings->server_disk_usage_pushover_notifications;
$this->serverReachablePushoverNotifications = $this->settings->server_reachable_pushover_notifications;
$this->serverUnreachablePushoverNotifications = $this->settings->server_unreachable_pushover_notifications;
}
}
public function instantSavePushoverEnabled()
{
try {
$this->validate([
'pushoverUserKey' => 'required',
'pushoverApiToken' => 'required',
], [
'pushoverUserKey.required' => 'Pushover User Key is required.',
'pushoverApiToken.required' => 'Pushover API Token is required.',
]);
$this->saveModel();
} catch (\Throwable $e) {
$this->pushoverEnabled = false;
return handleError($e, $this);
} finally {
$this->dispatch('refresh');
}
}
public function instantSave()
{
try {
$this->syncData(true);
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->dispatch('refresh');
}
}
public function submit()
{
try {
$this->resetErrorBag();
$this->syncData(true);
$this->saveModel();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function saveModel()
{
$this->syncData(true);
refreshSession();
$this->dispatch('success', 'Settings saved.');
}
public function sendTestNotification()
{
try {
$this->team->notify(new Test(channel: 'pushover'));
$this->dispatch('success', 'Test notification sent.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.notifications.pushover');
}
}

View File

@@ -0,0 +1,177 @@
<?php
namespace App\Livewire\Notifications;
use App\Models\SlackNotificationSettings;
use App\Models\Team;
use App\Notifications\Test;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;
use Livewire\Component;
class Slack extends Component
{
protected $listeners = ['refresh' => '$refresh'];
#[Locked]
public Team $team;
#[Locked]
public SlackNotificationSettings $settings;
#[Validate(['boolean'])]
public bool $slackEnabled = false;
#[Validate(['url', 'nullable'])]
public ?string $slackWebhookUrl = null;
#[Validate(['boolean'])]
public bool $deploymentSuccessSlackNotifications = false;
#[Validate(['boolean'])]
public bool $deploymentFailureSlackNotifications = true;
#[Validate(['boolean'])]
public bool $statusChangeSlackNotifications = false;
#[Validate(['boolean'])]
public bool $backupSuccessSlackNotifications = false;
#[Validate(['boolean'])]
public bool $backupFailureSlackNotifications = true;
#[Validate(['boolean'])]
public bool $scheduledTaskSuccessSlackNotifications = false;
#[Validate(['boolean'])]
public bool $scheduledTaskFailureSlackNotifications = true;
#[Validate(['boolean'])]
public bool $dockerCleanupSuccessSlackNotifications = false;
#[Validate(['boolean'])]
public bool $dockerCleanupFailureSlackNotifications = true;
#[Validate(['boolean'])]
public bool $serverDiskUsageSlackNotifications = true;
#[Validate(['boolean'])]
public bool $serverReachableSlackNotifications = false;
#[Validate(['boolean'])]
public bool $serverUnreachableSlackNotifications = true;
public function mount()
{
try {
$this->team = auth()->user()->currentTeam();
$this->settings = $this->team->slackNotificationSettings;
$this->syncData();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function syncData(bool $toModel = false)
{
if ($toModel) {
$this->validate();
$this->settings->slack_enabled = $this->slackEnabled;
$this->settings->slack_webhook_url = $this->slackWebhookUrl;
$this->settings->deployment_success_slack_notifications = $this->deploymentSuccessSlackNotifications;
$this->settings->deployment_failure_slack_notifications = $this->deploymentFailureSlackNotifications;
$this->settings->status_change_slack_notifications = $this->statusChangeSlackNotifications;
$this->settings->backup_success_slack_notifications = $this->backupSuccessSlackNotifications;
$this->settings->backup_failure_slack_notifications = $this->backupFailureSlackNotifications;
$this->settings->scheduled_task_success_slack_notifications = $this->scheduledTaskSuccessSlackNotifications;
$this->settings->scheduled_task_failure_slack_notifications = $this->scheduledTaskFailureSlackNotifications;
$this->settings->docker_cleanup_success_slack_notifications = $this->dockerCleanupSuccessSlackNotifications;
$this->settings->docker_cleanup_failure_slack_notifications = $this->dockerCleanupFailureSlackNotifications;
$this->settings->server_disk_usage_slack_notifications = $this->serverDiskUsageSlackNotifications;
$this->settings->server_reachable_slack_notifications = $this->serverReachableSlackNotifications;
$this->settings->server_unreachable_slack_notifications = $this->serverUnreachableSlackNotifications;
$this->settings->save();
refreshSession();
} else {
$this->slackEnabled = $this->settings->slack_enabled;
$this->slackWebhookUrl = $this->settings->slack_webhook_url;
$this->deploymentSuccessSlackNotifications = $this->settings->deployment_success_slack_notifications;
$this->deploymentFailureSlackNotifications = $this->settings->deployment_failure_slack_notifications;
$this->statusChangeSlackNotifications = $this->settings->status_change_slack_notifications;
$this->backupSuccessSlackNotifications = $this->settings->backup_success_slack_notifications;
$this->backupFailureSlackNotifications = $this->settings->backup_failure_slack_notifications;
$this->scheduledTaskSuccessSlackNotifications = $this->settings->scheduled_task_success_slack_notifications;
$this->scheduledTaskFailureSlackNotifications = $this->settings->scheduled_task_failure_slack_notifications;
$this->dockerCleanupSuccessSlackNotifications = $this->settings->docker_cleanup_success_slack_notifications;
$this->dockerCleanupFailureSlackNotifications = $this->settings->docker_cleanup_failure_slack_notifications;
$this->serverDiskUsageSlackNotifications = $this->settings->server_disk_usage_slack_notifications;
$this->serverReachableSlackNotifications = $this->settings->server_reachable_slack_notifications;
$this->serverUnreachableSlackNotifications = $this->settings->server_unreachable_slack_notifications;
}
}
public function instantSaveSlackEnabled()
{
try {
$this->validate([
'slackWebhookUrl' => 'required',
], [
'slackWebhookUrl.required' => 'Slack Webhook URL is required.',
]);
$this->saveModel();
} catch (\Throwable $e) {
$this->slackEnabled = false;
return handleError($e, $this);
} finally {
$this->dispatch('refresh');
}
}
public function instantSave()
{
try {
$this->syncData(true);
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->dispatch('refresh');
}
}
public function submit()
{
try {
$this->resetErrorBag();
$this->syncData(true);
$this->saveModel();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function saveModel()
{
$this->syncData(true);
refreshSession();
$this->dispatch('success', 'Settings saved.');
}
public function sendTestNotification()
{
try {
$this->team->notify(new Test(channel: 'slack'));
$this->dispatch('success', 'Test notification sent.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.notifications.slack');
}
}

View File

@@ -3,14 +3,22 @@
namespace App\Livewire\Notifications;
use App\Models\Team;
use App\Models\TelegramNotificationSettings;
use App\Notifications\Test;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;
use Livewire\Component;
class Telegram extends Component
{
protected $listeners = ['refresh' => '$refresh'];
#[Locked]
public Team $team;
#[Locked]
public TelegramNotificationSettings $settings;
#[Validate(['boolean'])]
public bool $telegramEnabled = false;
@@ -21,42 +29,82 @@ class Telegram extends Component
public ?string $telegramChatId = null;
#[Validate(['boolean'])]
public bool $telegramNotificationsTest = false;
public bool $deploymentSuccessTelegramNotifications = false;
#[Validate(['boolean'])]
public bool $telegramNotificationsDeployments = false;
public bool $deploymentFailureTelegramNotifications = true;
#[Validate(['boolean'])]
public bool $telegramNotificationsStatusChanges = false;
public bool $statusChangeTelegramNotifications = false;
#[Validate(['boolean'])]
public bool $telegramNotificationsDatabaseBackups = false;
public bool $backupSuccessTelegramNotifications = false;
#[Validate(['boolean'])]
public bool $telegramNotificationsScheduledTasks = false;
#[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsTestMessageThreadId = null;
#[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsDeploymentsMessageThreadId = null;
#[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsStatusChangesMessageThreadId = null;
#[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsDatabaseBackupsMessageThreadId = null;
#[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsScheduledTasksThreadId = null;
public bool $backupFailureTelegramNotifications = true;
#[Validate(['boolean'])]
public bool $telegramNotificationsServerDiskUsage = false;
public bool $scheduledTaskSuccessTelegramNotifications = false;
#[Validate(['boolean'])]
public bool $scheduledTaskFailureTelegramNotifications = true;
#[Validate(['boolean'])]
public bool $dockerCleanupSuccessTelegramNotifications = false;
#[Validate(['boolean'])]
public bool $dockerCleanupFailureTelegramNotifications = true;
#[Validate(['boolean'])]
public bool $serverDiskUsageTelegramNotifications = true;
#[Validate(['boolean'])]
public bool $serverReachableTelegramNotifications = false;
#[Validate(['boolean'])]
public bool $serverUnreachableTelegramNotifications = true;
#[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsDeploymentSuccessThreadId = null;
#[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsDeploymentFailureThreadId = null;
#[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsStatusChangeThreadId = null;
#[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsBackupSuccessThreadId = null;
#[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsBackupFailureThreadId = null;
#[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsScheduledTaskSuccessThreadId = null;
#[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsScheduledTaskFailureThreadId = null;
#[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsDockerCleanupSuccessThreadId = null;
#[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsDockerCleanupFailureThreadId = null;
#[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsServerDiskUsageThreadId = null;
#[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsServerReachableThreadId = null;
#[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsServerUnreachableThreadId = null;
public function mount()
{
try {
$this->team = auth()->user()->currentTeam();
$this->settings = $this->team->telegramNotificationSettings;
$this->syncData();
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -67,39 +115,68 @@ class Telegram extends Component
{
if ($toModel) {
$this->validate();
$this->team->telegram_enabled = $this->telegramEnabled;
$this->team->telegram_token = $this->telegramToken;
$this->team->telegram_chat_id = $this->telegramChatId;
$this->team->telegram_notifications_test = $this->telegramNotificationsTest;
$this->team->telegram_notifications_deployments = $this->telegramNotificationsDeployments;
$this->team->telegram_notifications_status_changes = $this->telegramNotificationsStatusChanges;
$this->team->telegram_notifications_database_backups = $this->telegramNotificationsDatabaseBackups;
$this->team->telegram_notifications_scheduled_tasks = $this->telegramNotificationsScheduledTasks;
$this->team->telegram_notifications_test_message_thread_id = $this->telegramNotificationsTestMessageThreadId;
$this->team->telegram_notifications_deployments_message_thread_id = $this->telegramNotificationsDeploymentsMessageThreadId;
$this->team->telegram_notifications_status_changes_message_thread_id = $this->telegramNotificationsStatusChangesMessageThreadId;
$this->team->telegram_notifications_database_backups_message_thread_id = $this->telegramNotificationsDatabaseBackupsMessageThreadId;
$this->team->telegram_notifications_scheduled_tasks_thread_id = $this->telegramNotificationsScheduledTasksThreadId;
$this->team->telegram_notifications_server_disk_usage = $this->telegramNotificationsServerDiskUsage;
$this->team->save();
refreshSession();
} else {
$this->telegramEnabled = $this->team->telegram_enabled;
$this->telegramToken = $this->team->telegram_token;
$this->telegramChatId = $this->team->telegram_chat_id;
$this->telegramNotificationsTest = $this->team->telegram_notifications_test;
$this->telegramNotificationsDeployments = $this->team->telegram_notifications_deployments;
$this->telegramNotificationsStatusChanges = $this->team->telegram_notifications_status_changes;
$this->telegramNotificationsDatabaseBackups = $this->team->telegram_notifications_database_backups;
$this->telegramNotificationsScheduledTasks = $this->team->telegram_notifications_scheduled_tasks;
$this->telegramNotificationsTestMessageThreadId = $this->team->telegram_notifications_test_message_thread_id;
$this->telegramNotificationsDeploymentsMessageThreadId = $this->team->telegram_notifications_deployments_message_thread_id;
$this->telegramNotificationsStatusChangesMessageThreadId = $this->team->telegram_notifications_status_changes_message_thread_id;
$this->telegramNotificationsDatabaseBackupsMessageThreadId = $this->team->telegram_notifications_database_backups_message_thread_id;
$this->telegramNotificationsScheduledTasksThreadId = $this->team->telegram_notifications_scheduled_tasks_thread_id;
$this->telegramNotificationsServerDiskUsage = $this->team->telegram_notifications_server_disk_usage;
}
$this->settings->telegram_enabled = $this->telegramEnabled;
$this->settings->telegram_token = $this->telegramToken;
$this->settings->telegram_chat_id = $this->telegramChatId;
$this->settings->deployment_success_telegram_notifications = $this->deploymentSuccessTelegramNotifications;
$this->settings->deployment_failure_telegram_notifications = $this->deploymentFailureTelegramNotifications;
$this->settings->status_change_telegram_notifications = $this->statusChangeTelegramNotifications;
$this->settings->backup_success_telegram_notifications = $this->backupSuccessTelegramNotifications;
$this->settings->backup_failure_telegram_notifications = $this->backupFailureTelegramNotifications;
$this->settings->scheduled_task_success_telegram_notifications = $this->scheduledTaskSuccessTelegramNotifications;
$this->settings->scheduled_task_failure_telegram_notifications = $this->scheduledTaskFailureTelegramNotifications;
$this->settings->docker_cleanup_success_telegram_notifications = $this->dockerCleanupSuccessTelegramNotifications;
$this->settings->docker_cleanup_failure_telegram_notifications = $this->dockerCleanupFailureTelegramNotifications;
$this->settings->server_disk_usage_telegram_notifications = $this->serverDiskUsageTelegramNotifications;
$this->settings->server_reachable_telegram_notifications = $this->serverReachableTelegramNotifications;
$this->settings->server_unreachable_telegram_notifications = $this->serverUnreachableTelegramNotifications;
$this->settings->telegram_notifications_deployment_success_thread_id = $this->telegramNotificationsDeploymentSuccessThreadId;
$this->settings->telegram_notifications_deployment_failure_thread_id = $this->telegramNotificationsDeploymentFailureThreadId;
$this->settings->telegram_notifications_status_change_thread_id = $this->telegramNotificationsStatusChangeThreadId;
$this->settings->telegram_notifications_backup_success_thread_id = $this->telegramNotificationsBackupSuccessThreadId;
$this->settings->telegram_notifications_backup_failure_thread_id = $this->telegramNotificationsBackupFailureThreadId;
$this->settings->telegram_notifications_scheduled_task_success_thread_id = $this->telegramNotificationsScheduledTaskSuccessThreadId;
$this->settings->telegram_notifications_scheduled_task_failure_thread_id = $this->telegramNotificationsScheduledTaskFailureThreadId;
$this->settings->telegram_notifications_docker_cleanup_success_thread_id = $this->telegramNotificationsDockerCleanupSuccessThreadId;
$this->settings->telegram_notifications_docker_cleanup_failure_thread_id = $this->telegramNotificationsDockerCleanupFailureThreadId;
$this->settings->telegram_notifications_server_disk_usage_thread_id = $this->telegramNotificationsServerDiskUsageThreadId;
$this->settings->telegram_notifications_server_reachable_thread_id = $this->telegramNotificationsServerReachableThreadId;
$this->settings->telegram_notifications_server_unreachable_thread_id = $this->telegramNotificationsServerUnreachableThreadId;
$this->settings->save();
} else {
$this->telegramEnabled = $this->settings->telegram_enabled;
$this->telegramToken = $this->settings->telegram_token;
$this->telegramChatId = $this->settings->telegram_chat_id;
$this->deploymentSuccessTelegramNotifications = $this->settings->deployment_success_telegram_notifications;
$this->deploymentFailureTelegramNotifications = $this->settings->deployment_failure_telegram_notifications;
$this->statusChangeTelegramNotifications = $this->settings->status_change_telegram_notifications;
$this->backupSuccessTelegramNotifications = $this->settings->backup_success_telegram_notifications;
$this->backupFailureTelegramNotifications = $this->settings->backup_failure_telegram_notifications;
$this->scheduledTaskSuccessTelegramNotifications = $this->settings->scheduled_task_success_telegram_notifications;
$this->scheduledTaskFailureTelegramNotifications = $this->settings->scheduled_task_failure_telegram_notifications;
$this->dockerCleanupSuccessTelegramNotifications = $this->settings->docker_cleanup_success_telegram_notifications;
$this->dockerCleanupFailureTelegramNotifications = $this->settings->docker_cleanup_failure_telegram_notifications;
$this->serverDiskUsageTelegramNotifications = $this->settings->server_disk_usage_telegram_notifications;
$this->serverReachableTelegramNotifications = $this->settings->server_reachable_telegram_notifications;
$this->serverUnreachableTelegramNotifications = $this->settings->server_unreachable_telegram_notifications;
$this->telegramNotificationsDeploymentSuccessThreadId = $this->settings->telegram_notifications_deployment_success_thread_id;
$this->telegramNotificationsDeploymentFailureThreadId = $this->settings->telegram_notifications_deployment_failure_thread_id;
$this->telegramNotificationsStatusChangeThreadId = $this->settings->telegram_notifications_status_change_thread_id;
$this->telegramNotificationsBackupSuccessThreadId = $this->settings->telegram_notifications_backup_success_thread_id;
$this->telegramNotificationsBackupFailureThreadId = $this->settings->telegram_notifications_backup_failure_thread_id;
$this->telegramNotificationsScheduledTaskSuccessThreadId = $this->settings->telegram_notifications_scheduled_task_success_thread_id;
$this->telegramNotificationsScheduledTaskFailureThreadId = $this->settings->telegram_notifications_scheduled_task_failure_thread_id;
$this->telegramNotificationsDockerCleanupSuccessThreadId = $this->settings->telegram_notifications_docker_cleanup_success_thread_id;
$this->telegramNotificationsDockerCleanupFailureThreadId = $this->settings->telegram_notifications_docker_cleanup_failure_thread_id;
$this->telegramNotificationsServerDiskUsageThreadId = $this->settings->telegram_notifications_server_disk_usage_thread_id;
$this->telegramNotificationsServerReachableThreadId = $this->settings->telegram_notifications_server_reachable_thread_id;
$this->telegramNotificationsServerUnreachableThreadId = $this->settings->telegram_notifications_server_unreachable_thread_id;
}
}
public function instantSave()
@@ -108,6 +185,8 @@ class Telegram extends Component
$this->syncData(true);
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->dispatch('refresh');
}
}
@@ -137,6 +216,8 @@ class Telegram extends Component
$this->telegramEnabled = false;
return handleError($e, $this);
} finally {
$this->dispatch('refresh');
}
}
@@ -150,7 +231,7 @@ class Telegram extends Component
public function sendTestNotification()
{
try {
$this->team->notify(new Test);
$this->team->notify(new Test(channel: 'telegram'));
$this->dispatch('success', 'Test notification sent.');
} catch (\Throwable $e) {
return handleError($e, $this);

View File

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

View File

@@ -3,37 +3,44 @@
namespace App\Livewire\Project\Application;
use App\Models\Application;
use App\Models\Server;
use Livewire\Component;
class Configuration extends Component
{
public $currentRoute;
public Application $application;
public $project;
public $environment;
public $servers;
protected $listeners = ['buildPackUpdated' => '$refresh'];
public function mount()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (! $project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('uuid', request()->route('environment_uuid'))->first()->load(['applications']);
if (! $environment) {
return redirect()->route('dashboard');
}
$application = $environment->applications->where('uuid', request()->route('application_uuid'))->first();
if (! $application) {
return redirect()->route('dashboard');
}
$this->currentRoute = request()->route()->getName();
$project = currentTeam()
->projects()
->select('id', 'uuid', 'team_id')
->where('uuid', request()->route('project_uuid'))
->firstOrFail();
$environment = $project->environments()
->select('id', 'name', 'project_id')
->where('uuid', request()->route('environment_uuid'))
->firstOrFail();
$application = $environment->applications()
->with(['destination'])
->where('uuid', request()->route('application_uuid'))
->firstOrFail();
$this->project = $project;
$this->environment = $environment;
$this->application = $application;
$mainServer = $this->application->destination->server;
$servers = Server::ownedByCurrentTeam()->get();
$this->servers = $servers->filter(function ($server) use ($mainServer) {
return $server->id != $mainServer->id;
});
}
public function render()

View File

@@ -23,7 +23,7 @@ class DeploymentNavbar extends Component
public function mount()
{
$this->application = Application::find($this->application_deployment_queue->application_id);
$this->application = Application::ownedByCurrentTeam()->find($this->application_deployment_queue->application_id);
$this->server = $this->application->destination->server;
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
}

View File

@@ -327,7 +327,7 @@ class General extends Component
}
}
public function set_redirect()
public function setRedirect()
{
try {
$has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count();
@@ -360,10 +360,10 @@ class General extends Component
if ($warning) {
$this->dispatch('warning', __('warning.sslipdomain'));
}
$this->resetDefaultLabels();
// $this->resetDefaultLabels();
if ($this->application->isDirty('redirect')) {
$this->set_redirect();
$this->setRedirect();
}
$this->checkFqdns();

View File

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

View File

@@ -121,7 +121,7 @@ class CloneMe extends Component
$environmentVaribles = $application->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) {
$newEnvironmentVariable = $environmentVarible->replicate()->fill([
'application_id' => $newApplication->id,
'resourceable_id' => $newApplication->id,
]);
$newEnvironmentVariable->save();
}
@@ -147,17 +147,8 @@ class CloneMe extends Component
$environmentVaribles = $database->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) {
$payload = [];
if ($database->type() === 'standalone-postgresql') {
$payload['standalone_postgresql_id'] = $newDatabase->id;
} elseif ($database->type() === 'standalone-redis') {
$payload['standalone_redis_id'] = $newDatabase->id;
} elseif ($database->type() === 'standalone-mongodb') {
$payload['standalone_mongodb_id'] = $newDatabase->id;
} elseif ($database->type() === 'standalone-mysql') {
$payload['standalone_mysql_id'] = $newDatabase->id;
} elseif ($database->type() === 'standalone-mariadb') {
$payload['standalone_mariadb_id'] = $newDatabase->id;
}
$payload['resourceable_id'] = $newDatabase->id;
$payload['resourceable_type'] = $newDatabase->getMorphClass();
$newEnvironmentVariable = $environmentVarible->replicate()->fill($payload);
$newEnvironmentVariable->save();
}

View File

@@ -9,11 +9,9 @@ class BackupNow extends Component
{
public $backup;
public function backup_now()
public function backupNow()
{
dispatch(new DatabaseBackupJob(
backup: $this->backup
));
DatabaseBackupJob::dispatch($this->backup);
$this->dispatch('success', 'Backup queued. It will be available in a few minutes.');
}
}

View File

@@ -6,23 +6,34 @@ use Livewire\Component;
class Configuration extends Component
{
public $currentRoute;
public $database;
public $project;
public $environment;
public function mount()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (! $project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('uuid', request()->route('environment_uuid'))->first()->load(['applications']);
if (! $environment) {
return redirect()->route('dashboard');
}
$database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first();
if (! $database) {
return redirect()->route('dashboard');
}
$this->currentRoute = request()->route()->getName();
$project = currentTeam()
->projects()
->select('id', 'uuid', 'team_id')
->where('uuid', request()->route('project_uuid'))
->firstOrFail();
$environment = $project->environments()
->select('id', 'name', 'project_id')
->where('uuid', request()->route('environment_uuid'))
->firstOrFail();
$database = $environment->databases()
->where('uuid', request()->route('database_uuid'))
->firstOrFail();
$this->database = $database;
$this->project = $project;
$this->environment = $environment;
if (str($this->database->status)->startsWith('running') && is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true);
$this->dispatch('configurationChanged');

View File

@@ -87,7 +87,8 @@ class DockerCompose extends Component
'value' => $variable,
'is_build_time' => false,
'is_preview' => false,
'service_id' => $service->id,
'resourceable_id' => $service->id,
'resourceable_type' => $service->getMorphClass(),
]);
}
$service->name = "service-$service->uuid";

File diff suppressed because one or more lines are too long

View File

@@ -95,7 +95,8 @@ class Create extends Component
EnvironmentVariable::create([
'key' => $key,
'value' => $value,
'service_id' => $service->id,
'resourceable_id' => $service->id,
'resourceable_type' => $service->getMorphClass(),
'is_build_time' => false,
'is_preview' => false,
]);

View File

@@ -46,126 +46,83 @@ class Index extends Component
return redirect()->route('dashboard');
}
$this->project = $project;
$this->environment = $environment;
$this->applications = $this->environment->applications->load(['tags']);
$this->environment = $environment->loadCount([
'applications',
'redis',
'postgresqls',
'mysqls',
'keydbs',
'dragonflies',
'clickhouses',
'mariadbs',
'mongodbs',
'services',
]);
// Eager load all relationships for applications including nested ones
$this->applications = $this->environment->applications()->with([
'tags',
'additional_servers.settings',
'additional_networks',
'destination.server.settings',
'settings',
])->get()->sortBy('name');
$this->applications = $this->applications->map(function ($application) {
if (data_get($application, 'environment.project.uuid')) {
$application->hrefLink = route('project.application.configuration', [
'project_uuid' => data_get($application, 'environment.project.uuid'),
'environment_uuid' => data_get($application, 'environment.uuid'),
'application_uuid' => data_get($application, 'uuid'),
]);
}
return $application;
});
$this->postgresqls = $this->environment->postgresqls->load(['tags'])->sortBy('name');
$this->postgresqls = $this->postgresqls->map(function ($postgresql) {
if (data_get($postgresql, 'environment.project.uuid')) {
$postgresql->hrefLink = route('project.database.configuration', [
'project_uuid' => data_get($postgresql, 'environment.project.uuid'),
'environment_uuid' => data_get($postgresql, 'environment.uuid'),
'database_uuid' => data_get($postgresql, 'uuid'),
// Load all database resources in a single query per type
$databaseTypes = [
'postgresqls' => 'postgresqls',
'redis' => 'redis',
'mongodbs' => 'mongodbs',
'mysqls' => 'mysqls',
'mariadbs' => 'mariadbs',
'keydbs' => 'keydbs',
'dragonflies' => 'dragonflies',
'clickhouses' => 'clickhouses',
];
// Load all server-related data first to prevent duplicate queries
$serverData = $this->environment->applications()
->with(['destination.server.settings'])
->get()
->pluck('destination.server')
->filter()
->unique('id');
foreach ($databaseTypes as $property => $relation) {
$this->{$property} = $this->environment->{$relation}()->with([
'tags',
'destination.server.settings',
])->get()->sortBy('name');
$this->{$property} = $this->{$property}->map(function ($db) {
$db->hrefLink = route('project.database.configuration', [
'project_uuid' => $this->project->uuid,
'database_uuid' => $db->uuid,
'environment_uuid' => data_get($this->environment, 'uuid'),
]);
return $db;
});
}
return $postgresql;
});
$this->redis = $this->environment->redis->load(['tags'])->sortBy('name');
$this->redis = $this->redis->map(function ($redis) {
if (data_get($redis, 'environment.project.uuid')) {
$redis->hrefLink = route('project.database.configuration', [
'project_uuid' => data_get($redis, 'environment.project.uuid'),
'environment_uuid' => data_get($redis, 'environment.uuid'),
'database_uuid' => data_get($redis, 'uuid'),
]);
}
return $redis;
});
$this->mongodbs = $this->environment->mongodbs->load(['tags'])->sortBy('name');
$this->mongodbs = $this->mongodbs->map(function ($mongodb) {
if (data_get($mongodb, 'environment.project.uuid')) {
$mongodb->hrefLink = route('project.database.configuration', [
'project_uuid' => data_get($mongodb, 'environment.project.uuid'),
'environment_uuid' => data_get($mongodb, 'environment.uuid'),
'database_uuid' => data_get($mongodb, 'uuid'),
]);
}
return $mongodb;
});
$this->mysqls = $this->environment->mysqls->load(['tags'])->sortBy('name');
$this->mysqls = $this->mysqls->map(function ($mysql) {
if (data_get($mysql, 'environment.project.uuid')) {
$mysql->hrefLink = route('project.database.configuration', [
'project_uuid' => data_get($mysql, 'environment.project.uuid'),
'environment_uuid' => data_get($mysql, 'environment.uuid'),
'database_uuid' => data_get($mysql, 'uuid'),
]);
}
return $mysql;
});
$this->mariadbs = $this->environment->mariadbs->load(['tags'])->sortBy('name');
$this->mariadbs = $this->mariadbs->map(function ($mariadb) {
if (data_get($mariadb, 'environment.project.uuid')) {
$mariadb->hrefLink = route('project.database.configuration', [
'project_uuid' => data_get($mariadb, 'environment.project.uuid'),
'environment_uuid' => data_get($mariadb, 'environment.uuid'),
'database_uuid' => data_get($mariadb, 'uuid'),
]);
}
return $mariadb;
});
$this->keydbs = $this->environment->keydbs->load(['tags'])->sortBy('name');
$this->keydbs = $this->keydbs->map(function ($keydb) {
if (data_get($keydb, 'environment.project.uuid')) {
$keydb->hrefLink = route('project.database.configuration', [
'project_uuid' => data_get($keydb, 'environment.project.uuid'),
'environment_uuid' => data_get($keydb, 'environment.uuid'),
'database_uuid' => data_get($keydb, 'uuid'),
]);
}
return $keydb;
});
$this->dragonflies = $this->environment->dragonflies->load(['tags'])->sortBy('name');
$this->dragonflies = $this->dragonflies->map(function ($dragonfly) {
if (data_get($dragonfly, 'environment.project.uuid')) {
$dragonfly->hrefLink = route('project.database.configuration', [
'project_uuid' => data_get($dragonfly, 'environment.project.uuid'),
'environment_uuid' => data_get($dragonfly, 'environment.uuid'),
'database_uuid' => data_get($dragonfly, 'uuid'),
]);
}
return $dragonfly;
});
$this->clickhouses = $this->environment->clickhouses->load(['tags'])->sortBy('name');
$this->clickhouses = $this->clickhouses->map(function ($clickhouse) {
if (data_get($clickhouse, 'environment.project.uuid')) {
$clickhouse->hrefLink = route('project.database.configuration', [
'project_uuid' => data_get($clickhouse, 'environment.project.uuid'),
'environment_uuid' => data_get($clickhouse, 'environment.uuid'),
'database_uuid' => data_get($clickhouse, 'uuid'),
]);
}
return $clickhouse;
});
$this->services = $this->environment->services->load(['tags'])->sortBy('name');
// Load services with their tags and server
$this->services = $this->environment->services()->with([
'tags',
'destination.server.settings',
])->get()->sortBy('name');
$this->services = $this->services->map(function ($service) {
if (data_get($service, 'environment.project.uuid')) {
$service->hrefLink = route('project.service.configuration', [
'project_uuid' => data_get($service, 'environment.project.uuid'),
'environment_uuid' => data_get($service, 'environment.uuid'),
'service_uuid' => data_get($service, 'uuid'),
]);
$service->status = $service->status();
}
return $service;
});
}

View File

@@ -9,16 +9,22 @@ use Livewire\Component;
class Configuration extends Component
{
public $currentRoute;
public $project;
public $environment;
public ?Service $service = null;
public $applications;
public $databases;
public array $parameters;
public array $query;
public array $parameters;
public function getListeners()
{
$userId = Auth::id();
@@ -38,11 +44,21 @@ class Configuration extends Component
public function mount()
{
$this->parameters = get_route_parameters();
$this->currentRoute = request()->route()->getName();
$this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->first();
if (! $this->service) {
return redirect()->route('dashboard');
}
$project = currentTeam()
->projects()
->select('id', 'uuid', 'team_id')
->where('uuid', request()->route('project_uuid'))
->firstOrFail();
$environment = $project->environments()
->select('id', 'name', 'project_id')
->where('name', request()->route('environment_name'))
->firstOrFail();
$this->service = $environment->services()->whereUuid(request()->route('service_uuid'))->firstOrFail();
$this->project = $project;
$this->environment = $environment;
$this->applications = $this->service->applications->sort();
$this->databases = $this->service->databases->sort();
}

View File

@@ -27,7 +27,7 @@ class Navbar extends Component
public function mount()
{
if (str($this->service->status())->contains('running') && is_null($this->service->config_hash)) {
if (str($this->service->status)->contains('running') && is_null($this->service->config_hash)) {
$this->service->isConfigurationChanged(true);
$this->dispatch('configurationChanged');
}

View File

@@ -8,6 +8,7 @@ use App\Events\ApplicationStatusChanged;
use App\Models\InstanceSettings;
use App\Models\Server;
use App\Models\StandaloneDocker;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
@@ -17,7 +18,7 @@ class Destination extends Component
{
public $resource;
public $networks = [];
public Collection $networks;
public function getListeners()
{
@@ -30,6 +31,7 @@ class Destination extends Component
public function mount()
{
$this->networks = collect([]);
$this->loadData();
}
@@ -55,23 +57,28 @@ class Destination extends Component
}
}
public function stop(int $server_id)
public function stop($serverId)
{
$server = Server::find($server_id);
try {
$server = Server::ownedByCurrentTeam()->findOrFail($serverId);
StopApplicationOneServer::run($this->resource, $server);
$this->refreshServers();
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function redeploy(int $network_id, int $server_id)
{
try {
if ($this->resource->additional_servers->count() > 0 && str($this->resource->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'Failed to deploy.', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/multiple-servers">documentation</a>');
return;
}
$deployment_uuid = new Cuid2;
$server = Server::find($server_id);
$destination = StandaloneDocker::find($network_id);
$server = Server::ownedByCurrentTeam()->findOrFail($server_id);
$destination = $server->standaloneDockers->where('id', $network_id)->firstOrFail();
queue_application_deployment(
deployment_uuid: $deployment_uuid,
application: $this->resource,
@@ -87,6 +94,9 @@ class Destination extends Component
'deployment_uuid' => $deployment_uuid,
'environment_uuid' => data_get($this->resource, 'environment.uuid'),
]);
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function promote(int $network_id, int $server_id)
@@ -119,6 +129,7 @@ class Destination extends Component
public function removeServer(int $network_id, int $server_id, $password)
{
try {
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
@@ -132,10 +143,13 @@ class Destination extends Component
return;
}
$server = Server::find($server_id);
$server = Server::ownedByCurrentTeam()->findOrFail($server_id);
StopApplicationOneServer::run($this->resource, $server);
$this->resource->additional_networks()->detach($network_id, ['server_id' => $server_id]);
$this->loadData();
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
} catch (\Exception $e) {
return handleError($e, $this);
}
}
}

View File

@@ -4,7 +4,6 @@ namespace App\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class All extends Component
{
@@ -14,38 +13,35 @@ class All extends Component
public bool $showPreview = false;
public ?string $modalId = null;
public ?string $variables = null;
public ?string $variablesPreview = null;
public string $view = 'normal';
public bool $is_env_sorting_enabled = false;
protected $listeners = [
'saveKey' => 'submit',
'refreshEnvs',
'environmentVariableDeleted' => 'refreshEnvs',
];
protected $rules = [
'resource.settings.is_env_sorting_enabled' => 'required|boolean',
];
public function mount()
{
$this->is_env_sorting_enabled = data_get($this->resource, 'settings.is_env_sorting_enabled', false);
$this->resourceClass = get_class($this->resource);
$resourceWithPreviews = [\App\Models\Application::class];
$simpleDockerfile = ! is_null(data_get($this->resource, 'dockerfile'));
$simpleDockerfile = filled(data_get($this->resource, 'dockerfile'));
if (str($this->resourceClass)->contains($resourceWithPreviews) && ! $simpleDockerfile) {
$this->showPreview = true;
}
$this->modalId = new Cuid2;
$this->sortEnvironmentVariables();
}
public function instantSave()
{
$this->resource->settings->is_env_sorting_enabled = $this->is_env_sorting_enabled;
$this->resource->settings->save();
$this->sortEnvironmentVariables();
$this->dispatch('success', 'Environment variable settings updated.');
@@ -53,7 +49,7 @@ class All extends Component
public function sortEnvironmentVariables()
{
if (! data_get($this->resource, 'settings.is_env_sorting_enabled')) {
if ($this->is_env_sorting_enabled === false) {
if ($this->resource->environment_variables) {
$this->resource->environment_variables = $this->resource->environment_variables->sortBy('order')->values();
}
@@ -178,35 +174,12 @@ class All extends Component
$environment->is_multiline = $data['is_multiline'] ?? false;
$environment->is_literal = $data['is_literal'] ?? false;
$environment->is_preview = $data['is_preview'] ?? false;
$resourceType = $this->resource->type();
$resourceIdField = $this->getResourceIdField($resourceType);
if ($resourceIdField) {
$environment->$resourceIdField = $this->resource->id;
}
$environment->resourceable_id = $this->resource->id;
$environment->resourceable_type = $this->resource->getMorphClass();
return $environment;
}
private function getResourceIdField($resourceType)
{
$resourceTypes = [
'application' => 'application_id',
'standalone-postgresql' => 'standalone_postgresql_id',
'standalone-redis' => 'standalone_redis_id',
'standalone-mongodb' => 'standalone_mongodb_id',
'standalone-mysql' => 'standalone_mysql_id',
'standalone-mariadb' => 'standalone_mariadb_id',
'standalone-keydb' => 'standalone_keydb_id',
'standalone-dragonfly' => 'standalone_dragonfly_id',
'standalone-clickhouse' => 'standalone_clickhouse_id',
'service' => 'service_id',
];
return $resourceTypes[$resourceType] ?? null;
}
private function deleteRemovedVariables($isPreview, $variables)
{
$method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
@@ -231,34 +204,14 @@ class All extends Component
$environment->is_build_time = false;
$environment->is_multiline = false;
$environment->is_preview = $isPreview;
$environment->resourceable_id = $this->resource->id;
$environment->resourceable_type = $this->resource->getMorphClass();
$this->setEnvironmentResourceId($environment);
$environment->save();
}
}
}
private function setEnvironmentResourceId($environment)
{
$resourceTypes = [
'application' => 'application_id',
'standalone-postgresql' => 'standalone_postgresql_id',
'standalone-redis' => 'standalone_redis_id',
'standalone-mongodb' => 'standalone_mongodb_id',
'standalone-mysql' => 'standalone_mysql_id',
'standalone-mariadb' => 'standalone_mariadb_id',
'standalone-keydb' => 'standalone_keydb_id',
'standalone-dragonfly' => 'standalone_dragonfly_id',
'standalone-clickhouse' => 'standalone_clickhouse_id',
'service' => 'service_id',
];
$resourceType = $this->resource->type();
if (isset($resourceTypes[$resourceType])) {
$environment->{$resourceTypes[$resourceType]} = $this->resource->id;
}
}
public function refreshEnvs()
{
$this->resource->refresh();

View File

@@ -5,7 +5,6 @@ namespace App\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
use App\Models\SharedEnvironmentVariable;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Show extends Component
{
@@ -13,8 +12,6 @@ class Show extends Component
public ModelsEnvironmentVariable|SharedEnvironmentVariable $env;
public ?string $modalId = null;
public bool $isDisabled = false;
public bool $isLocked = false;
@@ -23,6 +20,26 @@ class Show extends Component
public string $type;
public string $key;
public ?string $value = null;
public ?string $real_value = null;
public bool $is_shared = false;
public bool $is_build_time = false;
public bool $is_multiline = false;
public bool $is_literal = false;
public bool $is_shown_once = false;
public bool $is_required = false;
public bool $is_really_required = false;
protected $listeners = [
'refreshEnvs' => 'refresh',
'refresh',
@@ -30,40 +47,59 @@ class Show extends Component
];
protected $rules = [
'env.key' => 'required|string',
'env.value' => 'nullable',
'env.is_build_time' => 'required|boolean',
'env.is_multiline' => 'required|boolean',
'env.is_literal' => 'required|boolean',
'env.is_shown_once' => 'required|boolean',
'env.real_value' => 'nullable',
'env.is_required' => 'required|boolean',
'key' => 'required|string',
'value' => 'nullable',
'is_build_time' => 'required|boolean',
'is_multiline' => 'required|boolean',
'is_literal' => 'required|boolean',
'is_shown_once' => 'required|boolean',
'real_value' => 'nullable',
'is_required' => 'required|boolean',
];
protected $validationAttributes = [
'env.key' => 'Key',
'env.value' => 'Value',
'env.is_build_time' => 'Build Time',
'env.is_multiline' => 'Multiline',
'env.is_literal' => 'Literal',
'env.is_shown_once' => 'Shown Once',
'env.is_required' => 'Required',
];
public function refresh()
{
$this->env->refresh();
$this->checkEnvs();
}
public function mount()
{
$this->syncData();
if ($this->env->getMorphClass() === \App\Models\SharedEnvironmentVariable::class) {
$this->isSharedVariable = true;
}
$this->modalId = new Cuid2;
$this->parameters = get_route_parameters();
$this->checkEnvs();
}
public function refresh()
{
$this->syncData();
$this->checkEnvs();
}
public function syncData(bool $toModel = false)
{
if ($toModel) {
$this->validate();
$this->env->key = $this->key;
$this->env->value = $this->value;
$this->env->is_build_time = $this->is_build_time;
$this->env->is_multiline = $this->is_multiline;
$this->env->is_literal = $this->is_literal;
$this->env->is_shown_once = $this->is_shown_once;
$this->env->is_required = $this->is_required;
$this->env->is_shared = $this->is_shared;
$this->env->save();
} else {
$this->key = $this->env->key;
$this->value = $this->env->value;
$this->is_build_time = $this->env->is_build_time ?? false;
$this->is_multiline = $this->env->is_multiline;
$this->is_literal = $this->env->is_literal;
$this->is_shown_once = $this->env->is_shown_once;
$this->is_required = $this->env->is_required ?? false;
$this->is_really_required = $this->env->is_really_required ?? false;
$this->is_shared = $this->env->is_shared ?? false;
$this->real_value = $this->env->real_value;
}
}
public function checkEnvs()
@@ -88,6 +124,9 @@ class Show extends Component
public function lock()
{
$this->env->is_shown_once = true;
if ($this->isSharedVariable) {
unset($this->env->is_required);
}
$this->serialize();
$this->env->save();
$this->checkEnvs();
@@ -104,17 +143,17 @@ class Show extends Component
try {
if ($this->isSharedVariable) {
$this->validate([
'env.key' => 'required|string',
'env.value' => 'nullable',
'env.is_shown_once' => 'required|boolean',
'key' => 'required|string',
'value' => 'nullable',
'is_shown_once' => 'required|boolean',
]);
} else {
$this->validate();
}
if (! $this->isSharedVariable && $this->env->is_required && str($this->env->real_value)->isEmpty()) {
if (! $this->isSharedVariable && $this->is_required && str($this->value)->isEmpty()) {
$oldValue = $this->env->getOriginal('value');
$this->env->value = $oldValue;
$this->value = $oldValue;
$this->dispatch('error', 'Required environment variable cannot be empty.');
return;
@@ -123,10 +162,10 @@ class Show extends Component
$this->serialize();
if ($this->isSharedVariable) {
unset($this->env->is_required);
unset($this->is_required);
}
$this->env->save();
$this->syncData(true);
$this->dispatch('success', 'Environment variable updated.');
$this->dispatch('envsUpdated');
} catch (\Exception $e) {

View File

@@ -168,18 +168,42 @@ class ExecuteContainerCommand extends Component
return;
}
try {
// Validate container name format
if (! preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/', $this->selected_container)) {
throw new \InvalidArgumentException('Invalid container name format');
}
// Verify container exists in our allowed list
$container = collect($this->containers)->firstWhere('container.Names', $this->selected_container);
if (is_null($container)) {
throw new \RuntimeException('Container not found.');
}
$server = data_get($this->container, 'server');
// Verify server ownership and status
$server = data_get($container, 'server');
if (! $server || ! $server instanceof Server) {
throw new \RuntimeException('Invalid server configuration.');
}
if ($server->isForceDisabled()) {
throw new \RuntimeException('Server is disabled.');
}
// Additional ownership verification based on resource type
$resourceServer = match ($this->type) {
'application' => $this->resource->destination->server,
'database' => $this->resource->destination->server,
'service' => $this->resource->server,
default => throw new \RuntimeException('Invalid resource type.')
};
if ($server->id !== $resourceServer->id && ! $this->resource->additional_servers->contains('id', $server->id)) {
throw new \RuntimeException('Server ownership verification failed.');
}
$this->dispatch(
'send-terminal-command',
isset($container),
true,
data_get($container, 'container.Names'),
data_get($container, 'server.uuid')
);

View File

@@ -42,9 +42,11 @@ class ResourceOperations extends Component
$uuid = (string) new Cuid2;
$server = $new_destination->server;
if ($this->resource->getMorphClass() === \App\Models\Application::class) {
$name = 'clone-of-'.str($this->resource->name)->limit(20).'-'.$uuid;
$new_resource = $this->resource->replicate()->fill([
'uuid' => $uuid,
'name' => $this->resource->name.'-clone-'.$uuid,
'name' => $name,
'fqdn' => generateFqdn($server, $uuid),
'status' => 'exited',
'destination_id' => $new_destination->id,
@@ -58,14 +60,19 @@ class ResourceOperations extends Component
$environmentVaribles = $this->resource->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) {
$newEnvironmentVariable = $environmentVarible->replicate()->fill([
'application_id' => $new_resource->id,
'resourceable_id' => $new_resource->id,
'resourceable_type' => $new_resource->getMorphClass(),
]);
$newEnvironmentVariable->save();
}
$persistentVolumes = $this->resource->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
$volumeName = str($volume->name)->replace($this->resource->uuid, $new_resource->uuid)->value();
if ($volumeName === $volume->name) {
$volumeName = $new_resource->uuid.'-'.str($volume->name)->afterLast('-');
}
$newPersistentVolume = $volume->replicate()->fill([
'name' => $new_resource->uuid.'-'.str($volume->name)->afterLast('-'),
'name' => $volumeName,
'resource_id' => $new_resource->id,
]);
$newPersistentVolume->save();

View File

@@ -24,6 +24,14 @@ class Executions extends Component
#[Locked]
public ?string $serverTimezone = null;
public $currentPage = 1;
public $logsPerPage = 100;
public $selectedExecution = null;
public $isPollingActive = false;
public function getListeners()
{
$teamId = Auth::user()->currentTeam()->id;
@@ -54,16 +62,84 @@ class Executions extends Component
public function refreshExecutions(): void
{
$this->executions = $this->task->executions()->take(20)->get();
if ($this->selectedKey) {
$this->selectedExecution = $this->task->executions()->find($this->selectedKey);
if ($this->selectedExecution && $this->selectedExecution->status !== 'running') {
$this->isPollingActive = false;
}
}
}
public function selectTask($key): void
{
if ($key == $this->selectedKey) {
$this->selectedKey = null;
$this->selectedExecution = null;
$this->currentPage = 1;
$this->isPollingActive = false;
return;
}
$this->selectedKey = $key;
$this->selectedExecution = $this->task->executions()->find($key);
$this->currentPage = 1;
// Start polling if task is running
if ($this->selectedExecution && $this->selectedExecution->status === 'running') {
$this->isPollingActive = true;
}
}
public function polling()
{
if ($this->selectedExecution && $this->isPollingActive) {
$this->selectedExecution->refresh();
if ($this->selectedExecution->status !== 'running') {
$this->isPollingActive = false;
}
}
}
public function loadMoreLogs()
{
$this->currentPage++;
}
public function getLogLinesProperty()
{
if (! $this->selectedExecution) {
return collect();
}
if (! $this->selectedExecution->message) {
return collect(['Waiting for task output...']);
}
$lines = collect(explode("\n", $this->selectedExecution->message));
return $lines->take($this->currentPage * $this->logsPerPage);
}
public function downloadLogs(int $executionId)
{
$execution = $this->executions->firstWhere('id', $executionId);
if (! $execution) {
return;
}
return response()->streamDownload(function () use ($execution) {
echo $execution->message;
}, 'task-execution-'.$execution->id.'.log');
}
public function hasMoreLogs()
{
if (! $this->selectedExecution || ! $this->selectedExecution->message) {
return false;
}
$lines = collect(explode("\n", $this->selectedExecution->message));
return $lines->count() > ($this->currentPage * $this->logsPerPage);
}
public function formatDateInServerTimezone($date)

View File

@@ -29,11 +29,20 @@ class Terminal extends Component
$server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail();
if ($isContainer) {
// Validate container identifier format (alphanumeric, dashes, and underscores only)
if (! preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/', $identifier)) {
throw new \InvalidArgumentException('Invalid container identifier format');
}
// Verify container exists and belongs to the user's team
$status = getContainerStatus($server, $identifier);
if ($status !== 'running') {
return;
}
$command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
// Escape the identifier for shell usage
$escapedIdentifier = escapeshellarg($identifier);
$command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$escapedIdentifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
} else {
$command = SshMultiplexingHelper::generateSshCommand($server, 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n "$SHELL" ]; then exec $SHELL; else sh; fi');
}

View File

@@ -11,13 +11,7 @@ class ApiTokens extends Component
public $tokens = [];
public bool $viewSensitiveData = false;
public bool $readOnly = true;
public bool $rootAccess = false;
public array $permissions = ['read-only'];
public array $permissions = ['read'];
public $isApiEnabled;
@@ -29,52 +23,29 @@ class ApiTokens extends Component
public function mount()
{
$this->isApiEnabled = InstanceSettings::get()->is_api_enabled;
$this->getTokens();
}
private function getTokens()
{
$this->tokens = auth()->user()->tokens->sortByDesc('created_at');
}
public function updatedViewSensitiveData()
public function updatedPermissions($permissionToUpdate)
{
if ($this->viewSensitiveData) {
$this->permissions[] = 'view:sensitive';
$this->permissions = array_diff($this->permissions, ['*']);
$this->rootAccess = false;
if ($permissionToUpdate == 'root') {
$this->permissions = ['root'];
} elseif ($permissionToUpdate == 'read:sensitive' && ! in_array('read', $this->permissions)) {
$this->permissions[] = 'read';
} elseif ($permissionToUpdate == 'deploy') {
$this->permissions = ['deploy'];
} else {
$this->permissions = array_diff($this->permissions, ['view:sensitive']);
}
$this->makeSureOneIsSelected();
}
public function updatedReadOnly()
{
if ($this->readOnly) {
$this->permissions[] = 'read-only';
$this->permissions = array_diff($this->permissions, ['*']);
$this->rootAccess = false;
} else {
$this->permissions = array_diff($this->permissions, ['read-only']);
}
$this->makeSureOneIsSelected();
}
public function updatedRootAccess()
{
if ($this->rootAccess) {
$this->permissions = ['*'];
$this->readOnly = false;
$this->viewSensitiveData = false;
} else {
$this->readOnly = true;
$this->permissions = ['read-only'];
}
}
public function makeSureOneIsSelected()
{
if (count($this->permissions) == 0) {
$this->permissions = ['read-only'];
$this->readOnly = true;
$this->permissions = ['read'];
}
}
sort($this->permissions);
}
public function addNewToken()
{
@@ -82,8 +53,8 @@ class ApiTokens extends Component
$this->validate([
'description' => 'required|min:3|max:255',
]);
$token = auth()->user()->createToken($this->description, $this->permissions);
$this->tokens = auth()->user()->tokens;
$token = auth()->user()->createToken($this->description, array_values($this->permissions));
$this->getTokens();
session()->flash('token', $token->plainTextToken);
} catch (\Exception $e) {
return handleError($e, $this);
@@ -92,8 +63,12 @@ class ApiTokens extends Component
public function revoke(int $id)
{
$token = auth()->user()->tokens()->where('id', $id)->first();
try {
$token = auth()->user()->tokens()->where('id', $id)->firstOrFail();
$token->delete();
$this->tokens = auth()->user()->tokens;
$this->getTokens();
} catch (\Exception $e) {
return handleError($e, $this);
}
}
}

View File

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

View File

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

View File

@@ -15,6 +15,8 @@ class Proxy extends Component
public $proxy_settings = null;
public bool $redirect_enabled = true;
public ?string $redirect_url = null;
protected $listeners = ['proxyStatusUpdated', 'saveConfiguration' => 'submit'];
@@ -26,6 +28,7 @@ class Proxy extends Component
public function mount()
{
$this->selectedProxy = $this->server->proxyType();
$this->redirect_enabled = data_get($this->server, 'proxy.redirect_enabled', true);
$this->redirect_url = data_get($this->server, 'proxy.redirect_url');
}
@@ -38,7 +41,7 @@ class Proxy extends Component
{
$this->server->proxy = null;
$this->server->save();
$this->dispatch('proxyChanged');
$this->dispatch('reloadWindow');
}
public function selectProxy($proxy_type)
@@ -46,7 +49,7 @@ class Proxy extends Component
try {
$this->server->changeProxy($proxy_type, async: false);
$this->selectedProxy = $this->server->proxy->type;
$this->dispatch('proxyStatusUpdated');
$this->dispatch('reloadWindow');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -63,13 +66,25 @@ class Proxy extends Component
}
}
public function instantSaveRedirect()
{
try {
$this->server->proxy->redirect_enabled = $this->redirect_enabled;
$this->server->save();
$this->server->setupDefaultRedirect();
$this->dispatch('success', 'Proxy configuration saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {
SaveConfiguration::run($this->server, $this->proxy_settings);
$this->server->proxy->redirect_url = $this->redirect_url;
$this->server->save();
$this->server->setupDefault404Redirect();
$this->server->setupDefaultRedirect();
$this->dispatch('success', 'Proxy configuration saved.');
} catch (\Throwable $e) {
return handleError($e, $this);

View File

@@ -65,7 +65,7 @@ class Deploy extends Component
public function restart()
{
try {
$this->stop(forceStop: false);
$this->stop();
$this->dispatch('checkProxy');
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -105,6 +105,7 @@ class Deploy extends Component
$startTime = Carbon::now()->getTimestamp();
while ($process->running()) {
ray('running');
if (Carbon::now()->getTimestamp() - $startTime >= $timeout) {
$this->forceStopContainer($containerName);
break;

View File

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

View File

@@ -7,7 +7,7 @@ use App\Models\InstanceSettings;
use App\Models\Server;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Validate;
use Livewire\Component;
@@ -17,18 +17,12 @@ class Index extends Component
protected Server $server;
#[Locked]
public $timezones;
#[Validate('boolean')]
public bool $is_auto_update_enabled;
#[Validate('nullable|string|max:255')]
public ?string $fqdn = null;
#[Validate('nullable|string|max:255')]
public ?string $resale_license = null;
#[Validate('required|integer|min:1025|max:65535')]
public int $public_port_min;
@@ -53,7 +47,7 @@ class Index extends Component
#[Validate('string')]
public string $auto_update_frequency;
#[Validate('string')]
#[Validate('string|required')]
public string $update_check_frequency;
#[Validate('required|string|timezone')]
@@ -86,7 +80,6 @@ class Index extends Component
} else {
$this->settings = instanceSettings();
$this->fqdn = $this->settings->fqdn;
$this->resale_license = $this->settings->resale_license;
$this->public_port_min = $this->settings->public_port_min;
$this->public_port_max = $this->settings->public_port_max;
$this->custom_dns_servers = $this->settings->custom_dns_servers;
@@ -101,16 +94,30 @@ class Index extends Component
$this->is_api_enabled = $this->settings->is_api_enabled;
$this->auto_update_frequency = $this->settings->auto_update_frequency;
$this->update_check_frequency = $this->settings->update_check_frequency;
$this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
$this->instance_timezone = $this->settings->instance_timezone;
$this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation;
}
}
#[Computed]
public function timezones(): array
{
return collect(timezone_identifiers_list())
->sort()
->values()
->toArray();
}
public function instantSave($isSave = true)
{
$this->validate();
if ($this->settings->is_auto_update_enabled === true) {
$this->validate([
'auto_update_frequency' => ['required', 'string'],
]);
}
$this->settings->fqdn = $this->fqdn;
$this->settings->resale_license = $this->resale_license;
$this->settings->public_port_min = $this->public_port_min;
$this->settings->public_port_max = $this->public_port_max;
$this->settings->custom_dns_servers = $this->custom_dns_servers;

View File

@@ -3,6 +3,10 @@
namespace App\Livewire;
use App\Models\InstanceSettings;
use App\Models\Team;
use App\Notifications\Test;
use Illuminate\Support\Facades\RateLimiter;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;
use Livewire\Component;
@@ -10,39 +14,48 @@ class SettingsEmail extends Component
{
public InstanceSettings $settings;
#[Locked]
public Team $team;
#[Validate(['boolean'])]
public bool $smtpEnabled = false;
#[Validate(['nullable', 'string'])]
public ?string $smtpHost = null;
#[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])]
public ?int $smtpPort = null;
#[Validate(['nullable', 'string'])]
public ?string $smtpEncryption = null;
#[Validate(['nullable', 'string'])]
public ?string $smtpUsername = null;
#[Validate(['nullable'])]
public ?string $smtpPassword = null;
#[Validate(['nullable', 'numeric'])]
public ?int $smtpTimeout = null;
#[Validate(['nullable', 'email'])]
public ?string $smtpFromAddress = null;
#[Validate(['nullable', 'string'])]
public ?string $smtpFromName = null;
#[Validate(['nullable', 'string'])]
public ?string $smtpRecipients = null;
#[Validate(['nullable', 'string'])]
public ?string $smtpHost = null;
#[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])]
public ?int $smtpPort = null;
#[Validate(['nullable', 'string', 'in:tls,ssl,none'])]
public ?string $smtpEncryption = 'tls';
#[Validate(['nullable', 'string'])]
public ?string $smtpUsername = null;
#[Validate(['nullable', 'string'])]
public ?string $smtpPassword = null;
#[Validate(['nullable', 'numeric'])]
public ?int $smtpTimeout = null;
#[Validate(['boolean'])]
public bool $resendEnabled = false;
#[Validate(['nullable', 'string'])]
public ?string $resendApiKey = null;
#[Validate(['nullable', 'email'])]
public ?string $testEmailAddress = null;
public function mount()
{
if (isInstanceAdmin() === false) {
@@ -50,6 +63,8 @@ class SettingsEmail extends Component
}
$this->settings = instanceSettings();
$this->syncData();
$this->team = auth()->user()->currentTeam();
$this->testEmailAddress = auth()->user()->email;
}
public function syncData(bool $toModel = false)
@@ -90,7 +105,7 @@ class SettingsEmail extends Component
try {
$this->resetErrorBag();
$this->syncData(true);
$this->dispatch('success', 'Settings saved.');
$this->dispatch('success', 'Transactional email settings updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -99,19 +114,129 @@ class SettingsEmail extends Component
public function instantSave(string $type)
{
try {
$this->resetErrorBag();
if ($type === 'SMTP') {
$this->resendEnabled = false;
} else {
$this->smtpEnabled = false;
}
$this->syncData(true);
if ($this->smtpEnabled || $this->resendEnabled) {
$this->dispatch('success', "{$type} enabled.");
} else {
$this->dispatch('success', "{$type} disabled.");
$this->submitSmtp();
} elseif ($type === 'Resend') {
$this->submitResend();
}
} catch (\Throwable $e) {
if ($type === 'SMTP') {
$this->smtpEnabled = false;
} elseif ($type === 'Resend') {
$this->resendEnabled = false;
}
return handleError($e, $this);
}
}
public function submitSmtp()
{
try {
$this->validate([
'smtpEnabled' => 'boolean',
'smtpFromAddress' => 'required|email',
'smtpFromName' => 'required|string',
'smtpHost' => 'required|string',
'smtpPort' => 'required|numeric',
'smtpEncryption' => 'required|string|in:tls,ssl,none',
'smtpUsername' => 'nullable|string',
'smtpPassword' => 'nullable|string',
'smtpTimeout' => 'nullable|numeric',
], [
'smtpFromAddress.required' => 'From Address is required.',
'smtpFromAddress.email' => 'Please enter a valid email address.',
'smtpFromName.required' => 'From Name is required.',
'smtpHost.required' => 'SMTP Host is required.',
'smtpPort.required' => 'SMTP Port is required.',
'smtpPort.numeric' => 'SMTP Port must be a number.',
'smtpEncryption.required' => 'Encryption type is required.',
]);
$this->resendEnabled = false;
$this->settings->resend_enabled = false;
$this->settings->smtp_enabled = $this->smtpEnabled;
$this->settings->smtp_host = $this->smtpHost;
$this->settings->smtp_port = $this->smtpPort;
$this->settings->smtp_encryption = $this->smtpEncryption;
$this->settings->smtp_username = $this->smtpUsername;
$this->settings->smtp_password = $this->smtpPassword;
$this->settings->smtp_timeout = $this->smtpTimeout;
$this->settings->smtp_from_address = $this->smtpFromAddress;
$this->settings->smtp_from_name = $this->smtpFromName;
$this->settings->save();
$this->dispatch('success', 'SMTP settings updated.');
} catch (\Throwable $e) {
$this->smtpEnabled = false;
return handleError($e);
}
}
public function submitResend()
{
try {
$this->validate([
'resendEnabled' => 'boolean',
'resendApiKey' => 'required|string',
'smtpFromAddress' => 'required|email',
'smtpFromName' => 'required|string',
], [
'resendApiKey.required' => 'Resend API Key is required.',
'smtpFromAddress.required' => 'From Address is required.',
'smtpFromAddress.email' => 'Please enter a valid email address.',
'smtpFromName.required' => 'From Name is required.',
]);
$this->smtpEnabled = false;
$this->settings->smtp_enabled = false;
$this->settings->resend_enabled = $this->resendEnabled;
$this->settings->resend_api_key = $this->resendApiKey;
$this->settings->smtp_from_address = $this->smtpFromAddress;
$this->settings->smtp_from_name = $this->smtpFromName;
$this->settings->save();
$this->dispatch('success', 'Resend settings updated.');
} catch (\Throwable $e) {
$this->resendEnabled = false;
return handleError($e);
}
}
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->testEmailAddress, 'email'));
$this->dispatch('success', 'Test Email sent.');
},
$decaySeconds = 10,
);
if (! $executed) {
throw new \Exception('Too many messages sent!');
}
} catch (\Throwable $e) {
return handleError($e);
}
}
}

View File

@@ -17,6 +17,7 @@ class SettingsOauth extends Component
$carry["oauth_settings_map.$setting->provider.client_secret"] = 'nullable';
$carry["oauth_settings_map.$setting->provider.redirect_uri"] = 'nullable';
$carry["oauth_settings_map.$setting->provider.tenant"] = 'nullable';
$carry["oauth_settings_map.$setting->provider.base_url"] = 'nullable';
return $carry;
}, []);
@@ -34,16 +35,30 @@ class SettingsOauth extends Component
}, []);
}
private function updateOauthSettings()
private function updateOauthSettings(?string $provider = null)
{
if ($provider) {
$oauth = $this->oauth_settings_map[$provider];
if (! $oauth->couldBeEnabled()) {
$oauth->update(['enabled' => false]);
throw new \Exception('OAuth settings are not complete for '.$oauth->provider.'.<br/>Please fill in all required fields.');
}
$oauth->save();
$this->dispatch('success', 'OAuth settings for '.$oauth->provider.' updated successfully!');
} else {
foreach (array_values($this->oauth_settings_map) as &$setting) {
$setting->save();
}
}
}
public function instantSave()
public function instantSave(string $provider)
{
$this->updateOauthSettings();
try {
$this->updateOauthSettings($provider);
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function submit()

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