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/build
/public/hot /public/hot
/public/storage /public/storage
/storage/*.key
/vendor /vendor
.env .env
.env.backup .env.backup
@@ -25,3 +24,15 @@ yarn-error.log
.ignition.json .ignition.json
.env.dusk.local .env.dusk.local
docker/coolify-realtime/node_modules 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 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: docker/prod/Dockerfile file: docker/production/Dockerfile
platforms: linux/amd64 platforms: linux/amd64
push: true push: true
tags: | tags: |
@@ -82,7 +82,7 @@ jobs:
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: docker/prod/Dockerfile file: docker/production/Dockerfile
platforms: linux/aarch64 platforms: linux/aarch64
push: true push: true
tags: | tags: |

View File

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

View File

@@ -40,7 +40,8 @@ Special thanks to our biggest sponsors!
### Special 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. * [CCCareers](https://cccareers.org/) - A career development platform connecting coding bootcamp graduates with job opportunities in the tech industry.
* [Hetzner](http://htznr.li/CoolifyXHetzner) - A German web hosting company offering affordable dedicated servers, cloud services, and web hosting solutions. * [Hetzner](http://htznr.li/CoolifyXHetzner) - A German web hosting company offering affordable dedicated servers, cloud services, and web hosting solutions.
@@ -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. * [QuantCDN](https://www.quantcdn.io/?ref=coolify.io) - A content delivery network (CDN) optimizing website performance through global content distribution.
* [Arcjet](https://arcjet.com/?ref=coolify.io) - A cloud-based platform providing real-time protection against API abuse and bot attacks. * [Arcjet](https://arcjet.com/?ref=coolify.io) - A cloud-based platform providing real-time protection against API abuse and bot attacks.
* [SupaGuide](https://supa.guide/?ref=coolify.io) - A comprehensive resource hub offering guides and tutorials for web development using Supabase. * [SupaGuide](https://supa.guide/?ref=coolify.io) - A comprehensive resource hub offering guides and tutorials for web development using Supabase.
* [GoldenVM](https://billing.goldenvm.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes.
* [Tigris](https://tigrisdata.com/?ref=coolify.io) - A fully managed serverless object storage service compatible with Amazon S3 API. Offers high performance, scalability, and built-in search capabilities for efficient data management. * [Tigris](https://tigrisdata.com/?ref=coolify.io) - A fully managed serverless object storage service compatible with Amazon S3 API. Offers high performance, scalability, and built-in search capabilities for efficient data management.
* [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. * [Treive](https://trieve.ai/?ref=coolify.io) - An AI-powered search and discovery platform for enhancing information retrieval in large datasets.
* [Blacksmith](https://blacksmith.sh/?ref=coolify.io) - A cloud-native platform for automating infrastructure provisioning and management across multiple cloud providers. * [Blacksmith](https://blacksmith.sh/?ref=coolify.io) - A cloud-native platform for automating infrastructure provisioning and management across multiple cloud providers.
* [Brand Dev](https://brand.dev/?ref=coolify.io) - A web development agency specializing in creating custom digital experiences and brand identities. * [Brand Dev](https://brand.dev/?ref=coolify.io) - A web development agency specializing in creating custom digital experiences and brand identities.
@@ -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/urtho"><img src="https://github.com/urtho.png" width="60px" alt="Paweł Pierścionek" /></a>
<a href="https://github.com/monocursive"><img src="https://github.com/monocursive.png" width="60px" alt="Michael Mazurczak" /></a> <a href="https://github.com/monocursive"><img src="https://github.com/monocursive.png" width="60px" alt="Michael Mazurczak" /></a>
<a href="https://formbricks.com/?utm_source=coolify.io"><img src="https://github.com/formbricks.png" width="60px" alt="Formbricks" /></a> <a href="https://formbricks.com/?utm_source=coolify.io"><img src="https://github.com/formbricks.png" width="60px" alt="Formbricks" /></a>
<a href="https://x.com/adithsuhas17?utm_source=coolify.io"><img src="https://github.com/adith-suhas-sv.png" width="60px" alt="Adith Suhas" /></a>
<a href="https://startupfa.me?utm_source=coolify.io"><img src="https://github.com/startupfame.png" width="60px" alt="StartupFame" /></a> <a href="https://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://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://github.com/therealjp?utm_source=coolify.io"><img src="https://github.com/therealjp.png" width="60px" alt="JP" /></a>
<a href="https://evercam.io/?utm_source=coolify.io"><img src="https://github.com/evercam.png" width="60px" alt="Evercam" /></a> <a href="https://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 # Core Maintainers
| Andras Bacsai | Peak | | Andras Bacsai | 🏔️ Peak |
|------------|------------| |------------|------------|
| <img src="https://github.com/andrasbacsai.png" width="200px" alt="Andras Bacsai" /> | <img src="https://github.com/peaklabs-dev.png" width="200px" alt="Peak Labs" /> | | <img src="https://github.com/andrasbacsai.png" width="200px" alt="Andras Bacsai" /> | <img src="https://github.com/peaklabs-dev.png" width="200px" alt="peaklabs-dev" /> |
| <a href="https://x.com/heyandras"><img src="https://raw.githubusercontent.com/gauravghongde/social-icons/master/SVG/Color/Twitter.svg" width="25px"></a> <a href="https://github.com/andrasbacsai"><img src="https://raw.githubusercontent.com/gauravghongde/social-icons/master/SVG/Color/Github.svg" width="25px"></a> | <a href="https://x.com/peaklabs_dev"><img src="https://raw.githubusercontent.com/gauravghongde/social-icons/master/SVG/Color/Twitter.svg" width="25px"></a> <a href="https://github.com/peaklabs-dev"><img src="https://raw.githubusercontent.com/gauravghongde/social-icons/master/SVG/Color/Github.svg" width="25px"></a> | | <a href="https://github.com/andrasbacsai"><img src="https://api.iconify.design/devicon:github.svg" width="25px"></a> <a href="https://x.com/heyandras"><img src="https://api.iconify.design/devicon:twitter.svg" width="25px"></a> <a href="https://bsky.app/profile/heyandras.dev"><img src="https://api.iconify.design/simple-icons:bluesky.svg" width="25px"></a> | <a href="https://github.com/peaklabs-dev"><img src="https://api.iconify.design/devicon:github.svg" width="25px"></a> <a href="https://x.com/peaklabs_dev"><img src="https://api.iconify.design/devicon:twitter.svg" width="25px"></a> <a href="https://bsky.app/profile/peaklabs.dev"><img src="https://api.iconify.design/simple-icons:bluesky.svg" width="25px"></a> |
# Repo Activity # Repo Activity

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,6 @@ class CleanupUnreachableServers extends Command
if ($servers->count() > 0) { if ($servers->count() > 0) {
foreach ($servers as $server) { foreach ($servers as $server) {
echo "Cleanup unreachable server ($server->id) with name $server->name"; 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([ $server->update([
'ip' => '1.2.3.4', '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 the team has no subscription id and the invoice is paid, we need to reset the invoice paid status
if (! (data_get($team, 'subscription.stripe_subscription_id'))) { if (! (data_get($team, 'subscription.stripe_subscription_id'))) {
$this->info("Resetting invoice paid status for team {$team->id} {$team->name}"); $this->info("Resetting invoice paid status for team {$team->id}");
$team->subscription->update([ $team->subscription->update([
'stripe_invoice_paid' => false, 'stripe_invoice_paid' => false,
@@ -61,9 +61,9 @@ class CloudCleanupSubscriptions extends Command
$this->info('Subscription id: '.data_get($team, 'subscription.stripe_subscription_id')); $this->info('Subscription id: '.data_get($team, 'subscription.stripe_subscription_id'));
$confirm = $this->confirm('Do you want to cancel the subscription?', true); $confirm = $this->confirm('Do you want to cancel the subscription?', true);
if (! $confirm) { if (! $confirm) {
$this->info("Skipping team {$team->id} {$team->name}"); $this->info("Skipping team {$team->id}");
} else { } else {
$this->info("Cancelling subscription for team {$team->id} {$team->name}"); $this->info("Cancelling subscription for team {$team->id}");
$team->subscription->update([ $team->subscription->update([
'stripe_invoice_paid' => false, 'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => false, 'stripe_trial_already_ended' => false,

View File

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

View File

@@ -2,14 +2,12 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Jobs\SendConfirmationForWaitlistJob;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\Server; use App\Models\Server;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\Team; use App\Models\Team;
use App\Models\Waitlist;
use App\Notifications\Application\DeploymentFailed; use App\Notifications\Application\DeploymentFailed;
use App\Notifications\Application\DeploymentSuccess; use App\Notifications\Application\DeploymentSuccess;
use App\Notifications\Application\StatusChanged; use App\Notifications\Application\StatusChanged;
@@ -64,8 +62,6 @@ class Emails extends Command
'backup-success' => 'Database - Backup Success', 'backup-success' => 'Database - Backup Success',
'backup-failed' => 'Database - Backup Failed', 'backup-failed' => 'Database - Backup Failed',
// 'invitation-link' => 'Invitation Link', // '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-before-trial' => 'REAL - Registered Users Before Trial without Subscription',
'realusers-server-lost-connection' => 'REAL - Server Lost Connection', 'realusers-server-lost-connection' => 'REAL - Server Lost Connection',
], ],
@@ -187,7 +183,7 @@ class Emails extends Command
'team_id' => 0, 'team_id' => 0,
]); ]);
} }
$this->mail = (new BackupSuccess($backup, $db))->toMail(); //$this->mail = (new BackupSuccess($backup->frequency, $db->name))->toMail();
$this->sendEmail(); $this->sendEmail();
break; break;
// case 'invitation-link': // case 'invitation-link':
@@ -204,23 +200,6 @@ class Emails extends Command
// $this->mail = (new InvitationLink($user))->toMail(); // $this->mail = (new InvitationLink($user))->toMail();
// $this->sendEmail(); // $this->sendEmail();
// break; // 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': case 'realusers-before-trial':
$this->mail = new MailMessage; $this->mail = new MailMessage;
$this->mail->view('emails.before-trial-conversion'); $this->mail->view('emails.before-trial-conversion');

View File

@@ -13,7 +13,7 @@ class Horizon extends Command
public function handle() public function handle()
{ {
if (config('constants.horizon.is_horizon_enabled')) { 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'); $this->call('horizon');
exit(0); exit(0);
} else { } else {

View File

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

View File

@@ -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> <div class="text-yellow-500"> Channels: </div>
<ul class="text-coolify"> <ul class="text-coolify">
<li>email</li> <li>email</li>
<li>slack</li>
<li>discord</li> <li>discord</li>
<li>telegram</li> <li>telegram</li>
<li>slack</li>
<li>pushover</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -72,6 +73,6 @@ class NotifyDemo extends Command
<div class="mr-1"> <div class="mr-1">
In which manner you wish a <strong class="text-coolify">coolified</strong> notification? In which manner you wish a <strong class="text-coolify">coolified</strong> notification?
</div> </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() public function handle()
{ {
if (config('constants.horizon.is_scheduler_enabled')) { 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'); $this->call('schedule:work');
exit(0); exit(0);
} else { } 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 public function handle(): int
{ {
$serviceTemplatesJson = collect(glob(base_path('templates/compose/*.yaml'))) $serviceTemplatesJson = collect(array_merge(
glob(base_path('templates/compose/*.yaml')),
glob(base_path('templates/compose/*.yml'))
))
->mapWithKeys(function ($file): array { ->mapWithKeys(function ($file): array {
$file = basename($file); $file = basename($file);
$parsed = $this->processFile($file); $parsed = $this->processFile($file);
@@ -68,7 +71,7 @@ class ServicesGenerate extends Command
'slogan' => $data->get('slogan', str($file)->headline()), 'slogan' => $data->get('slogan', str($file)->headline()),
'compose' => $compose, 'compose' => $compose,
'tags' => $tags, 'tags' => $tags,
'logo' => $data->get('logo', 'svgs/coolify.png'), 'logo' => $data->get('logo', 'svgs/default.webp'),
'minversion' => $data->get('minversion', '0.0.0'), 'minversion' => $data->get('minversion', '0.0.0'),
]; ];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,8 @@ namespace App\Jobs;
use App\Actions\Server\CleanupDocker; use App\Actions\Server\CleanupDocker;
use App\Models\Server; 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\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
@@ -12,7 +13,6 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
{ {
@@ -38,35 +38,36 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
return; 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(); $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); 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; 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) { if ($this->usageBefore >= $this->server->settings->docker_cleanup_threshold) {
CleanupDocker::run(server: $this->server); CleanupDocker::run(server: $this->server);
$usageAfter = $this->server->getDiskUsage(); $usageAfter = $this->server->getDiskUsage();
if ($usageAfter < $this->usageBefore) { $diskSaved = $this->usageBefore - $usageAfter;
$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); 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 { } 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 { } 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) { } catch (\Throwable $e) {
CleanupDocker::run(server: $this->server); $this->server->team?->notify(new DockerCleanupFailed($this->server, 'Docker cleanup job failed with the following error: '.$e->getMessage()));
Log::error('DockerCleanupJob failed: '.$e->getMessage());
throw $e; throw $e;
} }
} }

View File

@@ -10,6 +10,7 @@ use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use App\Models\Team; use App\Models\Team;
use App\Notifications\ScheduledTask\TaskFailed; use App\Notifications\ScheduledTask\TaskFailed;
use App\Notifications\ScheduledTask\TaskSuccess;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
@@ -111,6 +112,8 @@ class ScheduledTaskJob implements ShouldQueue
'message' => $this->task_output, 'message' => $this->task_output,
]); ]);
$this->team?->notify(new TaskSuccess($this->task, $this->task_output));
return; return;
} }
} }
@@ -125,7 +128,6 @@ class ScheduledTaskJob implements ShouldQueue
]); ]);
} }
$this->team?->notify(new TaskFailed($this->task, $e->getMessage())); $this->team?->notify(new TaskFailed($this->task, $e->getMessage()));
// send_internal_notification('ScheduledTaskJob failed with: ' . $e->getMessage());
throw $e; throw $e;
} finally { } finally {
ScheduledTaskDone::dispatch($this->team->id); 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 array $buttons,
public string $token, public string $token,
public string $chatId, public string $chatId,
public ?string $topicId = null, public ?string $threadId = null,
) { ) {
$this->onQueue('high'); $this->onQueue('high');
} }
@@ -67,12 +67,12 @@ class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue
'chat_id' => $this->chatId, 'chat_id' => $this->chatId,
'text' => $this->text, 'text' => $this->text,
]; ];
if ($this->topicId) { if ($this->threadId) {
$payload['message_thread_id'] = $this->topicId; $payload['message_thread_id'] = $this->threadId;
} }
$response = Http::post($url, $payload); $response = Http::post($url, $payload);
if ($response->failed()) { if ($response->failed()) {
throw new \Exception('Telegram notification failed with '.$response->status().' status code.'.$response->body()); throw new \RuntimeException('Telegram notification failed with '.$response->status().' status code.'.$response->body());
} }
} }
} }

View File

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

View File

@@ -25,218 +25,242 @@ class StripeProcessJob implements ShouldQueue
public function handle(): void public function handle(): void
{ {
$excludedPlans = config('subscription.stripe_excluded_plans'); try {
$excludedPlans = config('subscription.stripe_excluded_plans');
$type = data_get($this->event, 'type'); $type = data_get($this->event, 'type');
$this->type = $type; $this->type = $type;
$data = data_get($this->event, 'data.object'); $data = data_get($this->event, 'data.object');
switch ($type) { switch ($type) {
case 'radar.early_fraud_warning.created': case 'radar.early_fraud_warning.created':
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
$id = data_get($data, 'id'); $id = data_get($data, 'id');
$charge = data_get($data, 'charge'); $charge = data_get($data, 'charge');
if ($charge) { if ($charge) {
$stripe->refunds->create(['charge' => $charge]); $stripe->refunds->create(['charge' => $charge]);
} }
$pi = data_get($data, 'payment_intent'); $pi = data_get($data, 'payment_intent');
$piData = $stripe->paymentIntents->retrieve($pi, []); $piData = $stripe->paymentIntents->retrieve($pi, []);
$customerId = data_get($piData, 'customer'); $customerId = data_get($piData, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if ($subscription) {
$subscriptionId = data_get($subscription, 'stripe_subscription_id');
$stripe->subscriptions->cancel($subscriptionId, []);
$subscription->update([
'stripe_invoice_paid' => false,
]);
send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}");
} else {
send_internal_notification("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
throw new \Exception("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
}
break;
case 'checkout.session.completed':
$clientReferenceId = data_get($data, 'client_reference_id');
if (is_null($clientReferenceId)) {
send_internal_notification('Checkout session completed without client reference id.');
break;
}
$userId = Str::before($clientReferenceId, ':');
$teamId = Str::after($clientReferenceId, ':');
$subscriptionId = data_get($data, 'subscription');
$customerId = data_get($data, 'customer');
$team = Team::find($teamId);
$found = $team->members->where('id', $userId)->first();
if (! $found->isAdmin()) {
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
throw new \Exception("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
}
$subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) {
send_internal_notification('Old subscription activated for team: '.$teamId);
$subscription->update([
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true,
]);
} else {
send_internal_notification('New subscription for team: '.$teamId);
Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true,
]);
}
break;
case 'invoice.paid':
$customerId = data_get($data, 'customer');
$planId = data_get($data, 'lines.data.0.plan.id');
if (Str::contains($excludedPlans, $planId)) {
send_internal_notification('Subscription excluded.');
break;
}
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if ($subscription) {
$subscription->update([
'stripe_invoice_paid' => true,
]);
} else {
throw new \Exception("No subscription found for customer: {$customerId}");
}
break;
case 'invoice.payment_failed':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId);
throw new \Exception("No subscription found for customer: {$customerId}");
}
$team = data_get($subscription, 'team');
if (! $team) {
send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId);
throw new \Exception("No team found in Coolify for customer: {$customerId}");
}
if (! $subscription->stripe_invoice_paid) {
SubscriptionInvoiceFailedJob::dispatch($team);
send_internal_notification('Invoice payment failed: '.$customerId);
} else {
send_internal_notification('Invoice payment failed but already paid: '.$customerId);
}
break;
case 'payment_intent.payment_failed':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId);
throw new \Exception("No subscription found in Coolify for customer: {$customerId}");
}
if ($subscription->stripe_invoice_paid) {
send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId);
return;
}
send_internal_notification('Subscription payment failed for customer: '.$customerId);
break;
case 'customer.subscription.created':
$customerId = data_get($data, 'customer');
$subscriptionId = data_get($data, 'id');
$teamId = data_get($data, 'metadata.team_id');
$userId = data_get($data, 'metadata.user_id');
if (! $teamId || ! $userId) {
$subscription = Subscription::where('stripe_customer_id', $customerId)->first(); $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if ($subscription) { if ($subscription) {
throw new \Exception("Subscription already exists for customer: {$customerId}"); $subscriptionId = data_get($subscription, 'stripe_subscription_id');
$stripe->subscriptions->cancel($subscriptionId, []);
$subscription->update([
'stripe_invoice_paid' => false,
]);
send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}");
} else {
send_internal_notification("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
throw new \RuntimeException("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
} }
throw new \Exception('No team id or user id found');
}
$team = Team::find($teamId);
$found = $team->members->where('id', $userId)->first();
if (! $found->isAdmin()) {
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
throw new \Exception("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
}
$subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) {
send_internal_notification("Subscription already exists for team: {$teamId}");
throw new \Exception("Subscription already exists for team: {$teamId}");
} else {
Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => false,
]);
}
case 'customer.subscription.updated':
$teamId = data_get($data, 'metadata.team_id');
$userId = data_get($data, 'metadata.user_id');
$customerId = data_get($data, 'customer');
$status = data_get($data, 'status');
$subscriptionId = data_get($data, 'items.data.0.subscription');
$planId = data_get($data, 'items.data.0.plan.id');
if (Str::contains($excludedPlans, $planId)) {
send_internal_notification('Subscription excluded.');
break; break;
} case 'checkout.session.completed':
$subscription = Subscription::where('stripe_customer_id', $customerId)->first(); $clientReferenceId = data_get($data, 'client_reference_id');
if (! $subscription) { if (is_null($clientReferenceId)) {
if ($status === 'incomplete_expired') { send_internal_notification('Checkout session completed without client reference id.');
send_internal_notification('Subscription incomplete expired'); break;
throw new \Exception('Subscription incomplete expired');
} }
if ($teamId) { $userId = Str::before($clientReferenceId, ':');
$subscription = Subscription::create([ $teamId = Str::after($clientReferenceId, ':');
$subscriptionId = data_get($data, 'subscription');
$customerId = data_get($data, 'customer');
$team = Team::find($teamId);
$found = $team->members->where('id', $userId)->first();
if (! $found->isAdmin()) {
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
throw new \RuntimeException("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
}
$subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) {
send_internal_notification('Old subscription activated for team: '.$teamId);
$subscription->update([
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true,
]);
} else {
send_internal_notification('New subscription for team: '.$teamId);
Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true,
]);
}
break;
case 'invoice.paid':
$customerId = data_get($data, 'customer');
$planId = data_get($data, 'lines.data.0.plan.id');
if (Str::contains($excludedPlans, $planId)) {
send_internal_notification('Subscription excluded.');
break;
}
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if ($subscription) {
$subscription->update([
'stripe_invoice_paid' => true,
]);
} else {
throw new \RuntimeException("No subscription found for customer: {$customerId}");
}
break;
case 'invoice.payment_failed':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId);
throw new \RuntimeException("No subscription found for customer: {$customerId}");
}
$team = data_get($subscription, 'team');
if (! $team) {
send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId);
throw new \RuntimeException("No team found in Coolify for customer: {$customerId}");
}
if (! $subscription->stripe_invoice_paid) {
SubscriptionInvoiceFailedJob::dispatch($team);
send_internal_notification('Invoice payment failed: '.$customerId);
} else {
send_internal_notification('Invoice payment failed but already paid: '.$customerId);
}
break;
case 'payment_intent.payment_failed':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId);
throw new \RuntimeException("No subscription found in Coolify for customer: {$customerId}");
}
if ($subscription->stripe_invoice_paid) {
send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId);
return;
}
send_internal_notification('Subscription payment failed for customer: '.$customerId);
break;
case 'customer.subscription.created':
$customerId = data_get($data, 'customer');
$subscriptionId = data_get($data, 'id');
$teamId = data_get($data, 'metadata.team_id');
$userId = data_get($data, 'metadata.user_id');
if (! $teamId || ! $userId) {
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if ($subscription) {
throw new \RuntimeException("Subscription already exists for customer: {$customerId}");
}
throw new \RuntimeException('No team id or user id found');
}
$team = Team::find($teamId);
$found = $team->members->where('id', $userId)->first();
if (! $found->isAdmin()) {
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
throw new \RuntimeException("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
}
$subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) {
send_internal_notification("Subscription already exists for team: {$teamId}");
throw new \RuntimeException("Subscription already exists for team: {$teamId}");
} else {
Subscription::create([
'team_id' => $teamId, 'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId, 'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId, 'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => false, 'stripe_invoice_paid' => false,
]); ]);
} else {
send_internal_notification('No subscription and team id found');
throw new \Exception('No subscription and team id found');
} }
} case 'customer.subscription.updated':
$cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end'); $teamId = data_get($data, 'metadata.team_id');
$feedback = data_get($data, 'cancellation_details.feedback'); $userId = data_get($data, 'metadata.user_id');
$comment = data_get($data, 'cancellation_details.comment'); $customerId = data_get($data, 'customer');
$lookup_key = data_get($data, 'items.data.0.price.lookup_key'); $status = data_get($data, 'status');
if (str($lookup_key)->contains('dynamic')) { $subscriptionId = data_get($data, 'items.data.0.subscription') ?? data_get($data, 'id');
$quantity = data_get($data, 'items.data.0.quantity', 2); $planId = data_get($data, 'items.data.0.plan.id') ?? data_get($data, 'plan.id');
$team = data_get($subscription, 'team'); if (Str::contains($excludedPlans, $planId)) {
if ($team) { send_internal_notification('Subscription excluded.');
$team->update([ break;
'custom_server_limit' => $quantity, }
]); $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
if ($status === 'incomplete_expired') {
send_internal_notification('Subscription incomplete expired');
throw new \RuntimeException('Subscription incomplete expired');
}
if ($teamId) {
$subscription = Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => false,
]);
} else {
send_internal_notification('No subscription and team id found');
throw new \RuntimeException('No subscription and team id found');
}
}
$cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
$feedback = data_get($data, 'cancellation_details.feedback');
$comment = data_get($data, 'cancellation_details.comment');
$lookup_key = data_get($data, 'items.data.0.price.lookup_key');
if (str($lookup_key)->contains('dynamic')) {
$quantity = data_get($data, 'items.data.0.quantity', 2);
$team = data_get($subscription, 'team');
if ($team) {
$team->update([
'custom_server_limit' => $quantity,
]);
}
ServerLimitCheckJob::dispatch($team);
} }
ServerLimitCheckJob::dispatch($team);
}
$subscription->update([
'stripe_feedback' => $feedback,
'stripe_comment' => $comment,
'stripe_plan_id' => $planId,
'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
]);
if ($status === 'paused' || $status === 'incomplete_expired') {
$subscription->update([ $subscription->update([
'stripe_invoice_paid' => false, 'stripe_feedback' => $feedback,
'stripe_comment' => $comment,
'stripe_plan_id' => $planId,
'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
]); ]);
} if ($status === 'paused' || $status === 'incomplete_expired') {
if ($feedback) { if ($subscription->stripe_subscription_id === $subscriptionId) {
$reason = "Cancellation feedback for {$customerId}: '".$feedback."'"; $subscription->update([
if ($comment) { 'stripe_invoice_paid' => false,
$reason .= ' with comment: \''.$comment."'"; ]);
}
} }
} if ($status === 'active') {
break; if ($subscription->stripe_subscription_id === $subscriptionId) {
case 'customer.subscription.deleted': $subscription->update([
// End subscription 'stripe_invoice_paid' => true,
$customerId = data_get($data, 'customer'); ]);
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); }
$team = data_get($subscription, 'team'); }
$team?->subscriptionEnded(); if ($feedback) {
break; $reason = "Cancellation feedback for {$customerId}: '".$feedback."'";
default: if ($comment) {
// Unhandled event type $reason .= ' with comment: \''.$comment."'";
}
}
break;
case 'customer.subscription.deleted':
$customerId = data_get($data, 'customer');
$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');
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:
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 public function handle(ProxyStarted $event): void
{ {
$this->server = data_get($event, 'data'); $this->server = data_get($event, 'data');
$this->server->setupDefault404Redirect(); $this->server->setupDefaultRedirect();
$this->server->setupDynamicProxyConfiguration(); $this->server->setupDynamicProxyConfiguration();
$this->server->proxy->force_stop = false; $this->server->proxy->force_stop = false;
$this->server->save(); $this->server->save();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Notifications; namespace App\Livewire\Notifications;
use App\Models\EmailNotificationSettings;
use App\Models\Team; use App\Models\Team;
use App\Notifications\Test; use App\Notifications\Test;
use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\RateLimiter;
@@ -11,17 +12,20 @@ use Livewire\Component;
class Email extends Component class Email extends Component
{ {
protected $listeners = ['refresh' => '$refresh'];
#[Locked]
public Team $team; public Team $team;
#[Locked]
public EmailNotificationSettings $settings;
#[Locked] #[Locked]
public string $emails; public string $emails;
#[Validate(['boolean'])] #[Validate(['boolean'])]
public bool $smtpEnabled = false; public bool $smtpEnabled = false;
#[Validate(['boolean'])]
public bool $useInstanceEmailSettings = false;
#[Validate(['nullable', 'email'])] #[Validate(['nullable', 'email'])]
public ?string $smtpFromAddress = null; public ?string $smtpFromAddress = null;
@@ -34,11 +38,11 @@ class Email extends Component
#[Validate(['nullable', 'string'])] #[Validate(['nullable', 'string'])]
public ?string $smtpHost = null; public ?string $smtpHost = null;
#[Validate(['nullable', 'numeric'])] #[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])]
public ?int $smtpPort = null; public ?int $smtpPort = null;
#[Validate(['nullable', 'string'])] #[Validate(['nullable', 'string', 'in:tls,ssl,none'])]
public ?string $smtpEncryption = null; public ?string $smtpEncryption = 'tls';
#[Validate(['nullable', 'string'])] #[Validate(['nullable', 'string'])]
public ?string $smtpUsername = null; public ?string $smtpUsername = null;
@@ -50,38 +54,61 @@ class Email extends Component
public ?int $smtpTimeout = null; public ?int $smtpTimeout = null;
#[Validate(['boolean'])] #[Validate(['boolean'])]
public bool $smtpNotificationsTest = false; public bool $resendEnabled = 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;
#[Validate(['nullable', 'string'])] #[Validate(['nullable', 'string'])]
public ?string $resendApiKey = null; public ?string $resendApiKey = null;
#[Validate(['required', 'email'])] #[Validate(['boolean'])]
public string $testEmailAddress = ''; 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() public function mount()
{ {
try { try {
$this->team = auth()->user()->currentTeam(); $this->team = auth()->user()->currentTeam();
$this->emails = auth()->user()->email; $this->emails = auth()->user()->email;
$this->settings = $this->team->emailNotificationSettings;
$this->syncData(); $this->syncData();
$this->testEmailAddress = auth()->user()->email;
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
@@ -91,47 +118,191 @@ class Email extends Component
{ {
if ($toModel) { if ($toModel) {
$this->validate(); $this->validate();
$this->team->smtp_enabled = $this->smtpEnabled; $this->settings->smtp_enabled = $this->smtpEnabled;
$this->team->smtp_from_address = $this->smtpFromAddress; $this->settings->smtp_from_address = $this->smtpFromAddress;
$this->team->smtp_from_name = $this->smtpFromName; $this->settings->smtp_from_name = $this->smtpFromName;
$this->team->smtp_host = $this->smtpHost; $this->settings->smtp_recipients = $this->smtpRecipients;
$this->team->smtp_port = $this->smtpPort; $this->settings->smtp_host = $this->smtpHost;
$this->team->smtp_encryption = $this->smtpEncryption; $this->settings->smtp_port = $this->smtpPort;
$this->team->smtp_username = $this->smtpUsername; $this->settings->smtp_encryption = $this->smtpEncryption;
$this->team->smtp_password = $this->smtpPassword; $this->settings->smtp_username = $this->smtpUsername;
$this->team->smtp_timeout = $this->smtpTimeout; $this->settings->smtp_password = $this->smtpPassword;
$this->team->smtp_recipients = $this->smtpRecipients; $this->settings->smtp_timeout = $this->smtpTimeout;
$this->team->smtp_notifications_test = $this->smtpNotificationsTest;
$this->team->smtp_notifications_deployments = $this->smtpNotificationsDeployments; $this->settings->resend_enabled = $this->resendEnabled;
$this->team->smtp_notifications_status_changes = $this->smtpNotificationsStatusChanges; $this->settings->resend_api_key = $this->resendApiKey;
$this->team->smtp_notifications_database_backups = $this->smtpNotificationsDatabaseBackups;
$this->team->smtp_notifications_scheduled_tasks = $this->smtpNotificationsScheduledTasks; $this->settings->use_instance_email_settings = $this->useInstanceEmailSettings;
$this->team->smtp_notifications_server_disk_usage = $this->smtpNotificationsServerDiskUsage;
$this->team->use_instance_email_settings = $this->useInstanceEmailSettings; $this->settings->deployment_success_email_notifications = $this->deploymentSuccessEmailNotifications;
$this->team->resend_enabled = $this->resendEnabled; $this->settings->deployment_failure_email_notifications = $this->deploymentFailureEmailNotifications;
$this->team->resend_api_key = $this->resendApiKey; $this->settings->status_change_email_notifications = $this->statusChangeEmailNotifications;
$this->team->save(); $this->settings->backup_success_email_notifications = $this->backupSuccessEmailNotifications;
refreshSession(); $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 { } else {
$this->smtpEnabled = $this->team->smtp_enabled; $this->smtpEnabled = $this->settings->smtp_enabled;
$this->smtpFromAddress = $this->team->smtp_from_address; $this->smtpFromAddress = $this->settings->smtp_from_address;
$this->smtpFromName = $this->team->smtp_from_name; $this->smtpFromName = $this->settings->smtp_from_name;
$this->smtpHost = $this->team->smtp_host; $this->smtpRecipients = $this->settings->smtp_recipients;
$this->smtpPort = $this->team->smtp_port; $this->smtpHost = $this->settings->smtp_host;
$this->smtpEncryption = $this->team->smtp_encryption; $this->smtpPort = $this->settings->smtp_port;
$this->smtpUsername = $this->team->smtp_username; $this->smtpEncryption = $this->settings->smtp_encryption;
$this->smtpPassword = $this->team->smtp_password; $this->smtpUsername = $this->settings->smtp_username;
$this->smtpTimeout = $this->team->smtp_timeout; $this->smtpPassword = $this->settings->smtp_password;
$this->smtpRecipients = $this->team->smtp_recipients; $this->smtpTimeout = $this->settings->smtp_timeout;
$this->smtpNotificationsTest = $this->team->smtp_notifications_test;
$this->smtpNotificationsDeployments = $this->team->smtp_notifications_deployments; $this->resendEnabled = $this->settings->resend_enabled;
$this->smtpNotificationsStatusChanges = $this->team->smtp_notifications_status_changes; $this->resendApiKey = $this->settings->resend_api_key;
$this->smtpNotificationsDatabaseBackups = $this->team->smtp_notifications_database_backups;
$this->smtpNotificationsScheduledTasks = $this->team->smtp_notifications_scheduled_tasks; $this->useInstanceEmailSettings = $this->settings->use_instance_email_settings;
$this->smtpNotificationsServerDiskUsage = $this->team->smtp_notifications_server_disk_usage;
$this->useInstanceEmailSettings = $this->team->use_instance_email_settings; $this->deploymentSuccessEmailNotifications = $this->settings->deployment_success_email_notifications;
$this->resendEnabled = $this->team->resend_enabled; $this->deploymentFailureEmailNotifications = $this->settings->deployment_failure_email_notifications;
$this->resendApiKey = $this->team->resend_api_key; $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, 'test-email:'.$this->team->id,
$perMinute = 0, $perMinute = 0,
function () { function () {
$this->team?->notify(new Test($this->testEmailAddress)); $this->team?->notify(new Test($this->testEmailAddress, 'email'));
$this->dispatch('success', 'Test Email sent.'); $this->dispatch('success', 'Test Email sent.');
}, },
$decaySeconds = 10, $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() public function copyFromInstanceSettings()
{ {
$settings = instanceSettings(); $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; namespace App\Livewire\Notifications;
use App\Models\Team; use App\Models\Team;
use App\Models\TelegramNotificationSettings;
use App\Notifications\Test; use App\Notifications\Test;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
class Telegram extends Component class Telegram extends Component
{ {
protected $listeners = ['refresh' => '$refresh'];
#[Locked]
public Team $team; public Team $team;
#[Locked]
public TelegramNotificationSettings $settings;
#[Validate(['boolean'])] #[Validate(['boolean'])]
public bool $telegramEnabled = false; public bool $telegramEnabled = false;
@@ -21,42 +29,82 @@ class Telegram extends Component
public ?string $telegramChatId = null; public ?string $telegramChatId = null;
#[Validate(['boolean'])] #[Validate(['boolean'])]
public bool $telegramNotificationsTest = false; public bool $deploymentSuccessTelegramNotifications = false;
#[Validate(['boolean'])] #[Validate(['boolean'])]
public bool $telegramNotificationsDeployments = false; public bool $deploymentFailureTelegramNotifications = true;
#[Validate(['boolean'])] #[Validate(['boolean'])]
public bool $telegramNotificationsStatusChanges = false; public bool $statusChangeTelegramNotifications = false;
#[Validate(['boolean'])] #[Validate(['boolean'])]
public bool $telegramNotificationsDatabaseBackups = false; public bool $backupSuccessTelegramNotifications = false;
#[Validate(['boolean'])] #[Validate(['boolean'])]
public bool $telegramNotificationsScheduledTasks = false; public bool $backupFailureTelegramNotifications = true;
#[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;
#[Validate(['boolean'])] #[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() public function mount()
{ {
try { try {
$this->team = auth()->user()->currentTeam(); $this->team = auth()->user()->currentTeam();
$this->settings = $this->team->telegramNotificationSettings;
$this->syncData(); $this->syncData();
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
@@ -67,39 +115,68 @@ class Telegram extends Component
{ {
if ($toModel) { if ($toModel) {
$this->validate(); $this->validate();
$this->team->telegram_enabled = $this->telegramEnabled; $this->settings->telegram_enabled = $this->telegramEnabled;
$this->team->telegram_token = $this->telegramToken; $this->settings->telegram_token = $this->telegramToken;
$this->team->telegram_chat_id = $this->telegramChatId; $this->settings->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->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() public function instantSave()
@@ -108,6 +185,8 @@ class Telegram extends Component
$this->syncData(true); $this->syncData(true);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} finally {
$this->dispatch('refresh');
} }
} }
@@ -137,6 +216,8 @@ class Telegram extends Component
$this->telegramEnabled = false; $this->telegramEnabled = false;
return handleError($e, $this); return handleError($e, $this);
} finally {
$this->dispatch('refresh');
} }
} }
@@ -150,7 +231,7 @@ class Telegram extends Component
public function sendTestNotification() public function sendTestNotification()
{ {
try { try {
$this->team->notify(new Test); $this->team->notify(new Test(channel: 'telegram'));
$this->dispatch('success', 'Test notification sent.'); $this->dispatch('success', 'Test notification sent.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);

View File

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

View File

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

View File

@@ -23,7 +23,7 @@ class DeploymentNavbar extends Component
public function mount() 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->server = $this->application->destination->server;
$this->is_debug_enabled = $this->application->settings->is_debug_enabled; $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 { try {
$has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count(); $has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count();
@@ -360,10 +360,10 @@ class General extends Component
if ($warning) { if ($warning) {
$this->dispatch('warning', __('warning.sslipdomain')); $this->dispatch('warning', __('warning.sslipdomain'));
} }
$this->resetDefaultLabels(); // $this->resetDefaultLabels();
if ($this->application->isDirty('redirect')) { if ($this->application->isDirty('redirect')) {
$this->set_redirect(); $this->setRedirect();
} }
$this->checkFqdns(); $this->checkFqdns();

View File

@@ -36,7 +36,11 @@ class Heading extends Component
public function mount() public function mount()
{ {
$this->parameters = get_route_parameters(); $this->parameters = [
'project_uuid' => $this->application->project()->uuid,
'environment_name' => $this->application->environment->name,
'application_uuid' => $this->application->uuid,
];
$lastDeployment = $this->application->get_last_successful_deployment(); $lastDeployment = $this->application->get_last_successful_deployment();
$this->lastDeploymentInfo = data_get_str($lastDeployment, 'commit')->limit(7).' '.data_get($lastDeployment, 'commit_message'); $this->lastDeploymentInfo = data_get_str($lastDeployment, 'commit')->limit(7).' '.data_get($lastDeployment, 'commit_message');
$this->lastDeploymentLink = $this->application->gitCommitLink(data_get($lastDeployment, 'commit')); $this->lastDeploymentLink = $this->application->gitCommitLink(data_get($lastDeployment, 'commit'));

View File

@@ -121,7 +121,7 @@ class CloneMe extends Component
$environmentVaribles = $application->environment_variables()->get(); $environmentVaribles = $application->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) { foreach ($environmentVaribles as $environmentVarible) {
$newEnvironmentVariable = $environmentVarible->replicate()->fill([ $newEnvironmentVariable = $environmentVarible->replicate()->fill([
'application_id' => $newApplication->id, 'resourceable_id' => $newApplication->id,
]); ]);
$newEnvironmentVariable->save(); $newEnvironmentVariable->save();
} }
@@ -147,17 +147,8 @@ class CloneMe extends Component
$environmentVaribles = $database->environment_variables()->get(); $environmentVaribles = $database->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) { foreach ($environmentVaribles as $environmentVarible) {
$payload = []; $payload = [];
if ($database->type() === 'standalone-postgresql') { $payload['resourceable_id'] = $newDatabase->id;
$payload['standalone_postgresql_id'] = $newDatabase->id; $payload['resourceable_type'] = $newDatabase->getMorphClass();
} 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;
}
$newEnvironmentVariable = $environmentVarible->replicate()->fill($payload); $newEnvironmentVariable = $environmentVarible->replicate()->fill($payload);
$newEnvironmentVariable->save(); $newEnvironmentVariable->save();
} }

View File

@@ -9,11 +9,9 @@ class BackupNow extends Component
{ {
public $backup; public $backup;
public function backup_now() public function backupNow()
{ {
dispatch(new DatabaseBackupJob( DatabaseBackupJob::dispatch($this->backup);
backup: $this->backup
));
$this->dispatch('success', 'Backup queued. It will be available in a few minutes.'); $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 class Configuration extends Component
{ {
public $currentRoute;
public $database; public $database;
public $project;
public $environment;
public function mount() public function mount()
{ {
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); $this->currentRoute = request()->route()->getName();
if (! $project) {
return redirect()->route('dashboard'); $project = currentTeam()
} ->projects()
$environment = $project->load(['environments'])->environments->where('uuid', request()->route('environment_uuid'))->first()->load(['applications']); ->select('id', 'uuid', 'team_id')
if (! $environment) { ->where('uuid', request()->route('project_uuid'))
return redirect()->route('dashboard'); ->firstOrFail();
} $environment = $project->environments()
$database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first(); ->select('id', 'name', 'project_id')
if (! $database) { ->where('uuid', request()->route('environment_uuid'))
return redirect()->route('dashboard'); ->firstOrFail();
} $database = $environment->databases()
->where('uuid', request()->route('database_uuid'))
->firstOrFail();
$this->database = $database; $this->database = $database;
$this->project = $project;
$this->environment = $environment;
if (str($this->database->status)->startsWith('running') && is_null($this->database->config_hash)) { if (str($this->database->status)->startsWith('running') && is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true); $this->database->isConfigurationChanged(true);
$this->dispatch('configurationChanged'); $this->dispatch('configurationChanged');

View File

@@ -87,7 +87,8 @@ class DockerCompose extends Component
'value' => $variable, 'value' => $variable,
'is_build_time' => false, 'is_build_time' => false,
'is_preview' => false, 'is_preview' => false,
'service_id' => $service->id, 'resourceable_id' => $service->id,
'resourceable_type' => $service->getMorphClass(),
]); ]);
} }
$service->name = "service-$service->uuid"; $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([ EnvironmentVariable::create([
'key' => $key, 'key' => $key,
'value' => $value, 'value' => $value,
'service_id' => $service->id, 'resourceable_id' => $service->id,
'resourceable_type' => $service->getMorphClass(),
'is_build_time' => false, 'is_build_time' => false,
'is_preview' => false, 'is_preview' => false,
]); ]);

View File

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

View File

@@ -9,16 +9,22 @@ use Livewire\Component;
class Configuration extends Component class Configuration extends Component
{ {
public $currentRoute;
public $project;
public $environment;
public ?Service $service = null; public ?Service $service = null;
public $applications; public $applications;
public $databases; public $databases;
public array $parameters;
public array $query; public array $query;
public array $parameters;
public function getListeners() public function getListeners()
{ {
$userId = Auth::id(); $userId = Auth::id();
@@ -38,11 +44,21 @@ class Configuration extends Component
public function mount() public function mount()
{ {
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->currentRoute = request()->route()->getName();
$this->query = request()->query(); $this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->first(); $project = currentTeam()
if (! $this->service) { ->projects()
return redirect()->route('dashboard'); ->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->applications = $this->service->applications->sort();
$this->databases = $this->service->databases->sort(); $this->databases = $this->service->databases->sort();
} }

View File

@@ -27,7 +27,7 @@ class Navbar extends Component
public function mount() 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->service->isConfigurationChanged(true);
$this->dispatch('configurationChanged'); $this->dispatch('configurationChanged');
} }

View File

@@ -8,6 +8,7 @@ use App\Events\ApplicationStatusChanged;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Livewire\Component; use Livewire\Component;
@@ -17,7 +18,7 @@ class Destination extends Component
{ {
public $resource; public $resource;
public $networks = []; public Collection $networks;
public function getListeners() public function getListeners()
{ {
@@ -30,6 +31,7 @@ class Destination extends Component
public function mount() public function mount()
{ {
$this->networks = collect([]);
$this->loadData(); $this->loadData();
} }
@@ -55,38 +57,46 @@ class Destination extends Component
} }
} }
public function stop(int $server_id) public function stop($serverId)
{ {
$server = Server::find($server_id); try {
StopApplicationOneServer::run($this->resource, $server); $server = Server::ownedByCurrentTeam()->findOrFail($serverId);
$this->refreshServers(); StopApplicationOneServer::run($this->resource, $server);
$this->refreshServers();
} catch (\Exception $e) {
return handleError($e, $this);
}
} }
public function redeploy(int $network_id, int $server_id) public function redeploy(int $network_id, int $server_id)
{ {
if ($this->resource->additional_servers->count() > 0 && str($this->resource->docker_registry_image_name)->isEmpty()) { try {
$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>'); 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; return;
}
$deployment_uuid = new Cuid2;
$server = Server::ownedByCurrentTeam()->findOrFail($server_id);
$destination = $server->standaloneDockers->where('id', $network_id)->firstOrFail();
queue_application_deployment(
deployment_uuid: $deployment_uuid,
application: $this->resource,
server: $server,
destination: $destination,
only_this_server: true,
no_questions_asked: true,
);
return redirect()->route('project.application.deployment.show', [
'project_uuid' => data_get($this->resource, 'environment.project.uuid'),
'application_uuid' => data_get($this->resource, 'uuid'),
'deployment_uuid' => $deployment_uuid,
'environment_uuid' => data_get($this->resource, 'environment.uuid'),
]);
} catch (\Exception $e) {
return handleError($e, $this);
} }
$deployment_uuid = new Cuid2;
$server = Server::find($server_id);
$destination = StandaloneDocker::find($network_id);
queue_application_deployment(
deployment_uuid: $deployment_uuid,
application: $this->resource,
server: $server,
destination: $destination,
only_this_server: true,
no_questions_asked: true,
);
return redirect()->route('project.application.deployment.show', [
'project_uuid' => data_get($this->resource, 'environment.project.uuid'),
'application_uuid' => data_get($this->resource, 'uuid'),
'deployment_uuid' => $deployment_uuid,
'environment_uuid' => data_get($this->resource, 'environment.uuid'),
]);
} }
public function promote(int $network_id, int $server_id) public function promote(int $network_id, int $server_id)
@@ -119,23 +129,27 @@ class Destination extends Component
public function removeServer(int $network_id, int $server_id, $password) public function removeServer(int $network_id, int $server_id, $password)
{ {
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { try {
if (! Hash::check($password, Auth::user()->password)) { if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
$this->addError('password', 'The provided password is incorrect.'); if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
}
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
$this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.');
return; return;
} }
$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);
} }
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
$this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.');
return;
}
$server = Server::find($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'));
} }
} }

View File

@@ -4,7 +4,6 @@ namespace App\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable; use App\Models\EnvironmentVariable;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2;
class All extends Component class All extends Component
{ {
@@ -14,38 +13,35 @@ class All extends Component
public bool $showPreview = false; public bool $showPreview = false;
public ?string $modalId = null;
public ?string $variables = null; public ?string $variables = null;
public ?string $variablesPreview = null; public ?string $variablesPreview = null;
public string $view = 'normal'; public string $view = 'normal';
public bool $is_env_sorting_enabled = false;
protected $listeners = [ protected $listeners = [
'saveKey' => 'submit', 'saveKey' => 'submit',
'refreshEnvs', 'refreshEnvs',
'environmentVariableDeleted' => 'refreshEnvs', 'environmentVariableDeleted' => 'refreshEnvs',
]; ];
protected $rules = [
'resource.settings.is_env_sorting_enabled' => 'required|boolean',
];
public function mount() public function mount()
{ {
$this->is_env_sorting_enabled = data_get($this->resource, 'settings.is_env_sorting_enabled', false);
$this->resourceClass = get_class($this->resource); $this->resourceClass = get_class($this->resource);
$resourceWithPreviews = [\App\Models\Application::class]; $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) { if (str($this->resourceClass)->contains($resourceWithPreviews) && ! $simpleDockerfile) {
$this->showPreview = true; $this->showPreview = true;
} }
$this->modalId = new Cuid2;
$this->sortEnvironmentVariables(); $this->sortEnvironmentVariables();
} }
public function instantSave() public function instantSave()
{ {
$this->resource->settings->is_env_sorting_enabled = $this->is_env_sorting_enabled;
$this->resource->settings->save(); $this->resource->settings->save();
$this->sortEnvironmentVariables(); $this->sortEnvironmentVariables();
$this->dispatch('success', 'Environment variable settings updated.'); $this->dispatch('success', 'Environment variable settings updated.');
@@ -53,7 +49,7 @@ class All extends Component
public function sortEnvironmentVariables() 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) { if ($this->resource->environment_variables) {
$this->resource->environment_variables = $this->resource->environment_variables->sortBy('order')->values(); $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_multiline = $data['is_multiline'] ?? false;
$environment->is_literal = $data['is_literal'] ?? false; $environment->is_literal = $data['is_literal'] ?? false;
$environment->is_preview = $data['is_preview'] ?? false; $environment->is_preview = $data['is_preview'] ?? false;
$environment->resourceable_id = $this->resource->id;
$resourceType = $this->resource->type(); $environment->resourceable_type = $this->resource->getMorphClass();
$resourceIdField = $this->getResourceIdField($resourceType);
if ($resourceIdField) {
$environment->$resourceIdField = $this->resource->id;
}
return $environment; 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) private function deleteRemovedVariables($isPreview, $variables)
{ {
$method = $isPreview ? 'environment_variables_preview' : 'environment_variables'; $method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
@@ -231,34 +204,14 @@ class All extends Component
$environment->is_build_time = false; $environment->is_build_time = false;
$environment->is_multiline = false; $environment->is_multiline = false;
$environment->is_preview = $isPreview; $environment->is_preview = $isPreview;
$environment->resourceable_id = $this->resource->id;
$environment->resourceable_type = $this->resource->getMorphClass();
$this->setEnvironmentResourceId($environment);
$environment->save(); $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() public function refreshEnvs()
{ {
$this->resource->refresh(); $this->resource->refresh();

View File

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

View File

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

View File

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

View File

@@ -24,6 +24,14 @@ class Executions extends Component
#[Locked] #[Locked]
public ?string $serverTimezone = null; public ?string $serverTimezone = null;
public $currentPage = 1;
public $logsPerPage = 100;
public $selectedExecution = null;
public $isPollingActive = false;
public function getListeners() public function getListeners()
{ {
$teamId = Auth::user()->currentTeam()->id; $teamId = Auth::user()->currentTeam()->id;
@@ -54,16 +62,84 @@ class Executions extends Component
public function refreshExecutions(): void public function refreshExecutions(): void
{ {
$this->executions = $this->task->executions()->take(20)->get(); $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 public function selectTask($key): void
{ {
if ($key == $this->selectedKey) { if ($key == $this->selectedKey) {
$this->selectedKey = null; $this->selectedKey = null;
$this->selectedExecution = null;
$this->currentPage = 1;
$this->isPollingActive = false;
return; return;
} }
$this->selectedKey = $key; $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) public function formatDateInServerTimezone($date)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,10 @@
namespace App\Livewire; namespace App\Livewire;
use App\Models\InstanceSettings; 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\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
@@ -10,39 +14,48 @@ class SettingsEmail extends Component
{ {
public InstanceSettings $settings; public InstanceSettings $settings;
#[Locked]
public Team $team;
#[Validate(['boolean'])] #[Validate(['boolean'])]
public bool $smtpEnabled = false; 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'])] #[Validate(['nullable', 'email'])]
public ?string $smtpFromAddress = null; public ?string $smtpFromAddress = null;
#[Validate(['nullable', 'string'])] #[Validate(['nullable', 'string'])]
public ?string $smtpFromName = null; 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'])] #[Validate(['boolean'])]
public bool $resendEnabled = false; public bool $resendEnabled = false;
#[Validate(['nullable', 'string'])] #[Validate(['nullable', 'string'])]
public ?string $resendApiKey = null; public ?string $resendApiKey = null;
#[Validate(['nullable', 'email'])]
public ?string $testEmailAddress = null;
public function mount() public function mount()
{ {
if (isInstanceAdmin() === false) { if (isInstanceAdmin() === false) {
@@ -50,6 +63,8 @@ class SettingsEmail extends Component
} }
$this->settings = instanceSettings(); $this->settings = instanceSettings();
$this->syncData(); $this->syncData();
$this->team = auth()->user()->currentTeam();
$this->testEmailAddress = auth()->user()->email;
} }
public function syncData(bool $toModel = false) public function syncData(bool $toModel = false)
@@ -90,7 +105,7 @@ class SettingsEmail extends Component
try { try {
$this->resetErrorBag(); $this->resetErrorBag();
$this->syncData(true); $this->syncData(true);
$this->dispatch('success', 'Settings saved.'); $this->dispatch('success', 'Transactional email settings updated.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
@@ -99,19 +114,129 @@ class SettingsEmail extends Component
public function instantSave(string $type) public function instantSave(string $type)
{ {
try { try {
$this->resetErrorBag();
if ($type === 'SMTP') { if ($type === 'SMTP') {
$this->resendEnabled = false; $this->submitSmtp();
} else { } elseif ($type === 'Resend') {
$this->smtpEnabled = false; $this->submitResend();
}
$this->syncData(true);
if ($this->smtpEnabled || $this->resendEnabled) {
$this->dispatch('success', "{$type} enabled.");
} else {
$this->dispatch('success', "{$type} disabled.");
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
if ($type === 'SMTP') {
$this->smtpEnabled = false;
} elseif ($type === 'Resend') {
$this->resendEnabled = false;
}
return handleError($e, $this); 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.client_secret"] = 'nullable';
$carry["oauth_settings_map.$setting->provider.redirect_uri"] = 'nullable'; $carry["oauth_settings_map.$setting->provider.redirect_uri"] = 'nullable';
$carry["oauth_settings_map.$setting->provider.tenant"] = 'nullable'; $carry["oauth_settings_map.$setting->provider.tenant"] = 'nullable';
$carry["oauth_settings_map.$setting->provider.base_url"] = 'nullable';
return $carry; return $carry;
}, []); }, []);
@@ -34,16 +35,30 @@ class SettingsOauth extends Component
}, []); }, []);
} }
private function updateOauthSettings() private function updateOauthSettings(?string $provider = null)
{ {
foreach (array_values($this->oauth_settings_map) as &$setting) { if ($provider) {
$setting->save(); $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() public function submit()

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