From 09636b413e962ad2f775a7855e12c535e22f81b8 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:55:13 +0100 Subject: [PATCH 01/18] refactor(email): validate team membership for email recipients --- app/Notifications/Channels/EmailChannel.php | 41 +++++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index 4cdf8d08f..8287fd24b 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -2,6 +2,7 @@ namespace App\Notifications\Channels; +use App\Models\Team; use App\Services\ConfigurationRepository; use Exception; use Illuminate\Mail\Message; @@ -20,14 +21,36 @@ class EmailChannel public function send(SendsEmail $notifiable, Notification $notification): void { try { - $this->bootConfigs($notifiable); + $team = data_get($notifiable, 'id'); + $members = Team::find($team)->members; + $mailerType = $this->bootConfigs($notifiable); + $recipients = $notifiable->getRecipients(); if (count($recipients) === 0) { throw new Exception('No email recipients found'); } + foreach ($recipients as $recipient) { + // check if the recipient is part of the team + if (! $members->contains('email', $recipient)) { + $emailSettings = $notifiable->emailNotificationSettings; + data_set($emailSettings, 'smtp_password', '********'); + data_set($emailSettings, 'resend_api_key', '********'); + send_internal_notification(sprintf( + "Recipient is not part of the team: %s\nTeam: %s\nNotification: %s\nNotifiable: %s\nMailer Type: %s\nEmail Settings:\n%s", + $recipient, + $team, + get_class($notification), + get_class($notifiable), + $mailerType, + json_encode($emailSettings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) + )); + throw new Exception('Recipient is not part of the team'); + } + } $mailMessage = $notification->toMail($notifiable); - Mail::send( + + Mail::mailer($mailerType)->send( [], [], fn (Message $message) => $message @@ -52,7 +75,7 @@ class EmailChannel } } - private function bootConfigs($notifiable): void + private function bootConfigs($notifiable): string { $emailSettings = $notifiable->emailNotificationSettings; @@ -62,9 +85,19 @@ class EmailChannel throw new Exception('No email settings found.'); } - return; + return $type; } $this->configRepository->updateMailConfig($emailSettings); + + if ($emailSettings->resend_enabled) { + return 'resend'; + } + + if ($emailSettings->smtp_enabled) { + return 'smtp'; + } + + throw new Exception('No email settings found.'); } } From 2a08f4d653cafe0d53c5513c9841146768a1c3af Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:09:38 +0100 Subject: [PATCH 02/18] refactor(shared): simplify deployment status check logic --- bootstrap/helpers/shared.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 60e71cd9c..ff8ac4c8a 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1254,9 +1254,8 @@ function isAnyDeploymentInprogress() foreach ($runningJobs as $runningJob) { $horizonJobStatus = getJobStatus($runningJob->horizon_job_id); if ($horizonJobStatus === 'unknown') { - return true; + $horizonJobIds[] = $runningJob->horizon_job_id; } - $horizonJobIds[] = $runningJob->horizon_job_id; } if (count($horizonJobIds) === 0) { echo "No deployments in progress.\n"; From 735fcaad3333706d621c098bc24ee33c4ced39d0 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:30:21 +0100 Subject: [PATCH 03/18] refactor(shared): add logging for running deployment jobs --- bootstrap/helpers/shared.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index ff8ac4c8a..45982a3f6 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1250,6 +1250,17 @@ function get_public_ips() function isAnyDeploymentInprogress() { $runningJobs = ApplicationDeploymentQueue::where('horizon_job_worker', gethostname())->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value)->get(); + $basicDetails = $runningJobs->map(function ($job) { + return [ + 'id' => $job->id, + 'created_at' => $job->created_at, + 'application_id' => $job->application_id, + 'server_id' => $job->server_id, + 'horizon_job_id' => $job->horizon_job_id, + 'status' => $job->status, + ]; + }); + echo 'Running jobs: '.json_encode($basicDetails)."\n"; $horizonJobIds = []; foreach ($runningJobs as $runningJob) { $horizonJobStatus = getJobStatus($runningJob->horizon_job_id); From f060b7d3d29947cabc5eeadce689d88b095600af Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:42:25 +0100 Subject: [PATCH 04/18] refactor(shared): enhance job status check to include 'reserved' --- bootstrap/helpers/shared.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 45982a3f6..7dc636587 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1264,7 +1264,7 @@ function isAnyDeploymentInprogress() $horizonJobIds = []; foreach ($runningJobs as $runningJob) { $horizonJobStatus = getJobStatus($runningJob->horizon_job_id); - if ($horizonJobStatus === 'unknown') { + if ($horizonJobStatus === 'unknown' || $horizonJobStatus === 'reserved') { $horizonJobIds[] = $runningJob->horizon_job_id; } } From 39cb270eb7244e0ad71c551f81c62221a8e909c7 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 28 Mar 2025 18:44:03 +0100 Subject: [PATCH 05/18] refactor(email): improve error handling by passing context to handleError --- app/Livewire/SettingsEmail.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php index 4205594a5..858f3b187 100644 --- a/app/Livewire/SettingsEmail.php +++ b/app/Livewire/SettingsEmail.php @@ -235,7 +235,7 @@ class SettingsEmail extends Component throw new \Exception('Too many messages sent!'); } } catch (\Throwable $e) { - return handleError($e); + return handleError($e, $this); } } } From 2b046c4bc4d214f24a64ddb6c46920d7f38f2eb4 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 28 Mar 2025 18:44:10 +0100 Subject: [PATCH 06/18] refactor(email): streamline email sending logic and improve configuration handling --- app/Notifications/Channels/EmailChannel.php | 133 +++++++------------- 1 file changed, 47 insertions(+), 86 deletions(-) diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index 8287fd24b..c9bf8d54a 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -2,102 +2,63 @@ namespace App\Notifications\Channels; -use App\Models\Team; -use App\Services\ConfigurationRepository; -use Exception; -use Illuminate\Mail\Message; use Illuminate\Notifications\Notification; -use Illuminate\Support\Facades\Mail; +use Resend; class EmailChannel { - private ConfigurationRepository $configRepository; - - public function __construct(ConfigurationRepository $configRepository) - { - $this->configRepository = $configRepository; - } + public function __construct() {} public function send(SendsEmail $notifiable, Notification $notification): void { - try { - $team = data_get($notifiable, 'id'); - $members = Team::find($team)->members; - $mailerType = $this->bootConfigs($notifiable); + $useInstanceEmailSettings = $notifiable->emailNotificationSettings->use_instance_email_settings; + if ($useInstanceEmailSettings) { + $settings = instanceSettings(); + } else { + $settings = $notifiable->emailNotificationSettings; + } + $isResendEnabled = $settings->resend_enabled; + $isSmtpEnabled = $settings->smtp_enabled; + $recipients = $notifiable->getRecipients(); + $mailMessage = $notification->toMail($notifiable); - $recipients = $notifiable->getRecipients(); - if (count($recipients) === 0) { - throw new Exception('No email recipients found'); - } - foreach ($recipients as $recipient) { - // check if the recipient is part of the team - if (! $members->contains('email', $recipient)) { - $emailSettings = $notifiable->emailNotificationSettings; - data_set($emailSettings, 'smtp_password', '********'); - data_set($emailSettings, 'resend_api_key', '********'); - send_internal_notification(sprintf( - "Recipient is not part of the team: %s\nTeam: %s\nNotification: %s\nNotifiable: %s\nMailer Type: %s\nEmail Settings:\n%s", - $recipient, - $team, - get_class($notification), - get_class($notifiable), - $mailerType, - json_encode($emailSettings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) - )); - throw new Exception('Recipient is not part of the team'); - } - } + if ($isResendEnabled) { + $resend = Resend::client($settings->resend_api_key); + $resend->emails->send([ + 'from' => $settings->smtp_from_address, + 'to' => $recipients, + 'subject' => $mailMessage->subject, + 'html' => (string) $mailMessage->render(), + ]); + } elseif ($isSmtpEnabled) { + $encryption = match (strtolower($settings->smtp_encryption)) { + 'starttls' => null, + 'tls' => 'tls', + 'none' => null, + default => null, + }; - $mailMessage = $notification->toMail($notifiable); - - Mail::mailer($mailerType)->send( - [], - [], - fn (Message $message) => $message - ->to($recipients) - ->subject($mailMessage->subject) - ->html((string) $mailMessage->render()) + $transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport( + $settings->smtp_host, + $settings->smtp_port, + $encryption ); - } catch (Exception $e) { - $error = $e->getMessage(); - if ($error === 'No email settings found.') { - throw $e; - } - $message = "EmailChannel error: {$e->getMessage()}. Failed to send email to:"; - if (isset($recipients)) { - $message .= implode(', ', $recipients); - } - if (isset($mailMessage)) { - $message .= " with subject: {$mailMessage->subject}"; - } - send_internal_notification($message); - throw $e; + $transport->setUsername($settings->smtp_username); + $transport->setPassword($settings->smtp_password); + + $mailer = new \Symfony\Component\Mailer\Mailer($transport); + + $fromEmail = $settings->smtp_from_address ?? 'noreply@localhost'; + $fromName = $settings->smtp_from_name ?? 'System'; + $from = new \Symfony\Component\Mime\Address($fromEmail, $fromName); + + $email = (new \Symfony\Component\Mime\Email) + ->from($from) + ->to(...$recipients) + ->subject($mailMessage->subject) + ->html((string) $mailMessage->render()); + + $mailer->send($email); } } - - private function bootConfigs($notifiable): string - { - $emailSettings = $notifiable->emailNotificationSettings; - - if ($emailSettings->use_instance_email_settings) { - $type = set_transanctional_email_settings(); - if (blank($type)) { - throw new Exception('No email settings found.'); - } - - return $type; - } - - $this->configRepository->updateMailConfig($emailSettings); - - if ($emailSettings->resend_enabled) { - return 'resend'; - } - - if ($emailSettings->smtp_enabled) { - return 'smtp'; - } - - throw new Exception('No email settings found.'); - } } From 6d9887afbad09f5826a90430ca09f05dfd7ca002 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 28 Mar 2025 18:46:46 +0100 Subject: [PATCH 07/18] refactor(email): remove unnecessary whitespace in email sending logic --- app/Notifications/Channels/EmailChannel.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index c9bf8d54a..e29b99f5f 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -51,7 +51,6 @@ class EmailChannel $fromEmail = $settings->smtp_from_address ?? 'noreply@localhost'; $fromName = $settings->smtp_from_name ?? 'System'; $from = new \Symfony\Component\Mime\Address($fromEmail, $fromName); - $email = (new \Symfony\Component\Mime\Email) ->from($from) ->to(...$recipients) From ebb81aff68c7a445901d6af223a448936941a61d Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 28 Mar 2025 19:09:36 +0100 Subject: [PATCH 08/18] refactor(email): allow custom email recipients in email sending logic --- app/Notifications/Channels/EmailChannel.php | 7 ++++++- resources/views/livewire/notifications/email.blade.php | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index e29b99f5f..582d9a1e4 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -12,6 +12,7 @@ class EmailChannel public function send(SendsEmail $notifiable, Notification $notification): void { $useInstanceEmailSettings = $notifiable->emailNotificationSettings->use_instance_email_settings; + $customEmails = data_get($notification, 'emails', null); if ($useInstanceEmailSettings) { $settings = instanceSettings(); } else { @@ -19,7 +20,11 @@ class EmailChannel } $isResendEnabled = $settings->resend_enabled; $isSmtpEnabled = $settings->smtp_enabled; - $recipients = $notifiable->getRecipients(); + if ($customEmails) { + $recipients = [$customEmails]; + } else { + $recipients = $notifiable->getRecipients(); + } $mailMessage = $notification->toMail($notifiable); if ($isResendEnabled) { diff --git a/resources/views/livewire/notifications/email.blade.php b/resources/views/livewire/notifications/email.blade.php index 7b0878b25..194cb3b43 100644 --- a/resources/views/livewire/notifications/email.blade.php +++ b/resources/views/livewire/notifications/email.blade.php @@ -14,7 +14,7 @@
+ id="testEmailAddress" label="Recipient" required /> Send Email From d110c93ba9c5e6ddf805c02917ab2882db2c419a Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 28 Mar 2025 20:44:28 +0100 Subject: [PATCH 09/18] refactor(email): enhance sender information formatting in email logic --- app/Notifications/Channels/EmailChannel.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index 582d9a1e4..a452ef8dd 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -29,8 +29,9 @@ class EmailChannel if ($isResendEnabled) { $resend = Resend::client($settings->resend_api_key); + $from = "{$settings->smtp_from_name} <{$settings->smtp_from_address}>"; $resend->emails->send([ - 'from' => $settings->smtp_from_address, + 'from' => $from, 'to' => $recipients, 'subject' => $mailMessage->subject, 'html' => (string) $mailMessage->render(), From 0b413780148a8a68588aff623e68996a1bcf13b0 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 28 Mar 2025 20:54:27 +0100 Subject: [PATCH 10/18] refactor(proxy): remove redundant stop call in restart method --- app/Livewire/Server/Proxy/Deploy.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Livewire/Server/Proxy/Deploy.php b/app/Livewire/Server/Proxy/Deploy.php index f823ff3d4..4a7e4124e 100644 --- a/app/Livewire/Server/Proxy/Deploy.php +++ b/app/Livewire/Server/Proxy/Deploy.php @@ -65,7 +65,6 @@ class Deploy extends Component public function restart() { try { - $this->stop(); $this->dispatch('checkProxy'); } catch (\Throwable $e) { return handleError($e, $this); From 7686ebfd6ce622740018795a568aba76410e1ba4 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 28 Mar 2025 21:05:34 +0100 Subject: [PATCH 11/18] refactor(file-storage): add loadStorageOnServer method for improved error handling --- app/Livewire/Project/Service/FileStorage.php | 13 ++++++++++++- .../livewire/project/service/file-storage.blade.php | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php index 4d070bc0c..5b88c15eb 100644 --- a/app/Livewire/Project/Service/FileStorage.php +++ b/app/Livewire/Project/Service/FileStorage.php @@ -49,7 +49,6 @@ class FileStorage extends Component $this->workdir = null; $this->fs_path = $this->fileStorage->fs_path; } - $this->fileStorage->loadStorageOnServer(); } public function convertToDirectory() @@ -68,6 +67,18 @@ class FileStorage extends Component } } + public function loadStorageOnServer() + { + try { + $this->fileStorage->loadStorageOnServer(); + $this->dispatch('success', 'File storage loaded from server.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } finally { + $this->dispatch('refreshStorages'); + } + } + public function convertToFile() { try { diff --git a/resources/views/livewire/project/service/file-storage.blade.php b/resources/views/livewire/project/service/file-storage.blade.php index 6010000c4..c1479a9f9 100644 --- a/resources/views/livewire/project/service/file-storage.blade.php +++ b/resources/views/livewire/project/service/file-storage.blade.php @@ -39,6 +39,7 @@ confirmationLabel="Please confirm the execution of the actions by entering the Filepath below" shortConfirmationLabel="Filepath" :confirmWithPassword="false" step2ButtonText="Convert to directory" /> @endif + Load from server Date: Fri, 28 Mar 2025 21:11:28 +0100 Subject: [PATCH 12/18] refactor(docker): parse and sanitize YAML compose file before encoding --- bootstrap/helpers/docker.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 80e19d80f..de80adbef 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -8,6 +8,7 @@ use App\Models\ServiceApplication; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Spatie\Url\Url; +use Symfony\Component\Yaml\Yaml; use Visus\Cuid2\Cuid2; function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null, ?bool $includePullrequests = false): Collection @@ -834,7 +835,15 @@ function validateComposeFile(string $compose, int $server_id): string|Throwable if (! $server) { throw new \Exception('Server not found'); } - $base64_compose = base64_encode($compose); + $yaml_compose = Yaml::parse($compose); + foreach ($yaml_compose['services'] as $service_name => $service) { + foreach ($service['volumes'] as $volume_name => $volume) { + if (data_get($volume, 'type') === 'bind' && data_get($volume, 'content')) { + unset($yaml_compose['services'][$service_name]['volumes'][$volume_name]['content']); + } + } + } + $base64_compose = base64_encode(Yaml::dump($yaml_compose)); instant_remote_process([ "echo {$base64_compose} | base64 -d | tee /tmp/{$uuid}.yml > /dev/null", "chmod 600 /tmp/{$uuid}.yml", From 0bb47dfa566bb4635cb17988eccd5fb74bd525f2 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 28 Mar 2025 22:10:15 +0100 Subject: [PATCH 13/18] refactor(file-storage): improve layout and structure of input fields --- .../project/service/file-storage.blade.php | 17 ++--- .../project/shared/storages/show.blade.php | 76 ++++++++++--------- 2 files changed, 47 insertions(+), 46 deletions(-) diff --git a/resources/views/livewire/project/service/file-storage.blade.php b/resources/views/livewire/project/service/file-storage.blade.php index c1479a9f9..594f9f7cc 100644 --- a/resources/views/livewire/project/service/file-storage.blade.php +++ b/resources/views/livewire/project/service/file-storage.blade.php @@ -1,16 +1,9 @@ -
+
- {{-- @if (data_get($resource, 'build_pack') === 'dockercompose') -

{{ data_get($resource, 'name', 'unknown') }}

- @endif --}} - @if ($fileStorage->is_directory) -

Directory Mount

- @else -

File Mount

- @endif - - - +
+ + +
diff --git a/resources/views/livewire/project/shared/storages/show.blade.php b/resources/views/livewire/project/shared/storages/show.blade.php index f64e5d77b..339942ccb 100644 --- a/resources/views/livewire/project/shared/storages/show.blade.php +++ b/resources/views/livewire/project/shared/storages/show.blade.php @@ -2,45 +2,53 @@ @if ($isReadOnly) @if ($isFirst) - @if ( - $storage->resource_type === 'App\Models\ServiceApplication' || - $storage->resource_type === 'App\Models\ServiceDatabase') - - @else - - @endif - @if ($isService || $startedAt) - - - @else - - - - Update - - @endif +
+ @if ( + $storage->resource_type === 'App\Models\ServiceApplication' || + $storage->resource_type === 'App\Models\ServiceDatabase') + + @else + + @endif + @if ($isService || $startedAt) + + + @else + + + + Update + + @endif +
@else - - - +
+ + + +
@endif @else @if ($isFirst) - - - +
+ + + +
@else - - - +
+ + + +
@endif
From c2941abe572f1881bdcbe456fcabd09318995f2d Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 28 Mar 2025 22:10:33 +0100 Subject: [PATCH 14/18] fix(file-storage): double save on compose volumes --- app/Models/LocalFileVolume.php | 15 +++++++ bootstrap/helpers/shared.php | 73 ++++++++++++++++------------------ 2 files changed, 49 insertions(+), 39 deletions(-) diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php index a3bbbc64a..1a5430c5b 100644 --- a/app/Models/LocalFileVolume.php +++ b/app/Models/LocalFileVolume.php @@ -176,4 +176,19 @@ class LocalFileVolume extends BaseModel return instant_remote_process($commands, $server); } + + // Accessor for convenient access + protected function plainMountPath(): Attribute + { + return Attribute::make( + get: fn () => $this->mount_path, + set: fn ($value) => $this->mount_path = $value + ); + } + + // Scope for searching + public function scopeWherePlainMountPath($query, $path) + { + return $query->get()->where('plain_mount_path', $path); + } } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 7dc636587..218ca1b37 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1363,21 +1363,15 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull $source = $source."-pr-$pull_request_id"; } if (! $resource?->settings?->is_preserve_repository_enabled || $foundConfig?->is_based_on_git) { - LocalFileVolume::updateOrCreate( - [ - 'mount_path' => $target, - 'resource_id' => $resource->id, - 'resource_type' => get_class($resource), - ], - [ - 'fs_path' => $source, - 'mount_path' => $target, - 'content' => $content, - 'is_directory' => $isDirectory, - 'resource_id' => $resource->id, - 'resource_type' => get_class($resource), - ] - ); + $volume = LocalFileVolume::wherePlainMountPath($target)->first() ?? new LocalFileVolume; + $volume->fill([ + 'fs_path' => $source, + 'mount_path' => $target, + 'content' => $content, + 'is_directory' => $isDirectory, + 'resource_id' => $resource->id, + 'resource_type' => get_class($resource), + ])->save(); } } elseif ($type->value() === 'volume') { if ($topLevelVolumes->has($source->value())) { @@ -1675,21 +1669,28 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($source->value() === '/tmp' || $source->value() === '/tmp/') { return $volume; } - LocalFileVolume::updateOrCreate( - [ - 'mount_path' => $target, - 'resource_id' => $savedService->id, - 'resource_type' => get_class($savedService), - ], - [ + + $existingVolume = LocalFileVolume::wherePlainMountPath($target)->first(); + + if ($existingVolume) { + $existingVolume->update([ 'fs_path' => $source, 'mount_path' => $target, 'content' => $content, 'is_directory' => $isDirectory, 'resource_id' => $savedService->id, 'resource_type' => get_class($savedService), - ] - ); + ]); + } else { + LocalFileVolume::create([ + 'fs_path' => $source, + 'mount_path' => $target, + 'content' => $content, + 'is_directory' => $isDirectory, + 'resource_id' => $savedService->id, + 'resource_type' => get_class($savedService), + ]); + } } elseif ($type->value() === 'volume') { if ($topLevelVolumes->has($source->value())) { $v = $topLevelVolumes->get($source->value()); @@ -3327,21 +3328,15 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int if ($isApplication && $isPullRequest) { $source = $source."-pr-$pullRequestId"; } - LocalFileVolume::updateOrCreate( - [ - 'mount_path' => $target, - 'resource_id' => $originalResource->id, - 'resource_type' => get_class($originalResource), - ], - [ - 'fs_path' => $source, - 'mount_path' => $target, - 'content' => $content, - 'is_directory' => $isDirectory, - 'resource_id' => $originalResource->id, - 'resource_type' => get_class($originalResource), - ] - ); + $volume = LocalFileVolume::wherePlainMountPath($target)->first() ?? new LocalFileVolume; + $volume->fill([ + 'fs_path' => $source, + 'mount_path' => $target, + 'content' => $content, + 'is_directory' => $isDirectory, + 'resource_id' => $originalResource->id, + 'resource_type' => get_class($originalResource), + ])->save(); if (isDev()) { if ((int) $resource->compose_parsing_version >= 4) { if ($isApplication) { From d124abef2f46f53235166a74f6e379786b05fecf Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 28 Mar 2025 22:18:07 +0100 Subject: [PATCH 15/18] refactor(email): update label for test email recipient input --- resources/views/livewire/settings-email.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/settings-email.blade.php b/resources/views/livewire/settings-email.blade.php index 02523bbcc..4376cbf92 100644 --- a/resources/views/livewire/settings-email.blade.php +++ b/resources/views/livewire/settings-email.blade.php @@ -13,7 +13,7 @@ + label="Recipient" required /> Send Email From dd345d5ac7325636bb4880cf37fbfc42309a8295 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 28 Mar 2025 22:45:09 +0100 Subject: [PATCH 16/18] chore(versions): update version numbers for coolify and nightly --- config/constants.php | 2 +- other/nightly/versions.json | 4 ++-- versions.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/constants.php b/config/constants.php index c3f177f67..c674fe3c7 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.401', + 'version' => '4.0.0-beta.402', 'helper_version' => '1.0.7', 'realtime_version' => '1.0.6', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/other/nightly/versions.json b/other/nightly/versions.json index 03c56756d..1ad2d31cf 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.401" + "version": "4.0.0-beta.402" }, "nightly": { - "version": "4.0.0-beta.402" + "version": "4.0.0-beta.403" }, "helper": { "version": "1.0.7" diff --git a/versions.json b/versions.json index 03c56756d..1ad2d31cf 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.401" + "version": "4.0.0-beta.402" }, "nightly": { - "version": "4.0.0-beta.402" + "version": "4.0.0-beta.403" }, "helper": { "version": "1.0.7" From 34e1587068001f6cbbbc30e8d319287f9a103782 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 28 Mar 2025 22:45:12 +0100 Subject: [PATCH 17/18] refactor(database-backup): remove existing Docker container before backup upload --- app/Jobs/DatabaseBackupJob.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 6070ad16a..3276711c5 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -484,6 +484,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue $fullImageName = $this->getFullImageName(); + $containerExists = instant_remote_process(["docker ps -a -q -f name=backup-of-{$this->backup->uuid}"], $this->server, false); + if (filled($containerExists)) { + instant_remote_process(["docker rm -f backup-of-{$this->backup->uuid}"], $this->server, false); + } + if (isDev()) { if ($this->database->name === 'coolify-db') { $backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/coolify/coolify-db-'.$this->server->ip.$this->backup_file; From b376d6df2a58b8057cfb78b2e5bf8d3ab4af01ab Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 29 Mar 2025 22:16:12 +0100 Subject: [PATCH 18/18] reverting: encrypting mount and fs_path --- app/Models/LocalFileVolume.php | 4 +- bootstrap/helpers/shared.php | 72 +++++++------- ...00_revert_some_local_volume_encryption.php | 96 +++++++++++++++++++ 3 files changed, 137 insertions(+), 35 deletions(-) create mode 100644 database/migrations/2025_03_29_204400_revert_some_local_volume_encryption.php diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php index 1a5430c5b..c56cd7694 100644 --- a/app/Models/LocalFileVolume.php +++ b/app/Models/LocalFileVolume.php @@ -9,8 +9,8 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; class LocalFileVolume extends BaseModel { protected $casts = [ - 'fs_path' => 'encrypted', - 'mount_path' => 'encrypted', + // 'fs_path' => 'encrypted', + // 'mount_path' => 'encrypted', 'content' => 'encrypted', 'is_directory' => 'boolean', ]; diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 218ca1b37..a020e7558 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1363,15 +1363,21 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull $source = $source."-pr-$pull_request_id"; } if (! $resource?->settings?->is_preserve_repository_enabled || $foundConfig?->is_based_on_git) { - $volume = LocalFileVolume::wherePlainMountPath($target)->first() ?? new LocalFileVolume; - $volume->fill([ - 'fs_path' => $source, - 'mount_path' => $target, - 'content' => $content, - 'is_directory' => $isDirectory, - 'resource_id' => $resource->id, - 'resource_type' => get_class($resource), - ])->save(); + LocalFileVolume::updateOrCreate( + [ + 'mount_path' => $target, + 'resource_id' => $resource->id, + 'resource_type' => get_class($resource), + ], + [ + 'fs_path' => $source, + 'mount_path' => $target, + 'content' => $content, + 'is_directory' => $isDirectory, + 'resource_id' => $resource->id, + 'resource_type' => get_class($resource), + ] + ); } } elseif ($type->value() === 'volume') { if ($topLevelVolumes->has($source->value())) { @@ -1670,27 +1676,21 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal return $volume; } - $existingVolume = LocalFileVolume::wherePlainMountPath($target)->first(); - - if ($existingVolume) { - $existingVolume->update([ + LocalFileVolume::updateOrCreate( + [ + 'mount_path' => $target, + 'resource_id' => $savedService->id, + 'resource_type' => get_class($savedService), + ], + [ 'fs_path' => $source, 'mount_path' => $target, 'content' => $content, 'is_directory' => $isDirectory, 'resource_id' => $savedService->id, 'resource_type' => get_class($savedService), - ]); - } else { - LocalFileVolume::create([ - 'fs_path' => $source, - 'mount_path' => $target, - 'content' => $content, - 'is_directory' => $isDirectory, - 'resource_id' => $savedService->id, - 'resource_type' => get_class($savedService), - ]); - } + ] + ); } elseif ($type->value() === 'volume') { if ($topLevelVolumes->has($source->value())) { $v = $topLevelVolumes->get($source->value()); @@ -3328,15 +3328,21 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int if ($isApplication && $isPullRequest) { $source = $source."-pr-$pullRequestId"; } - $volume = LocalFileVolume::wherePlainMountPath($target)->first() ?? new LocalFileVolume; - $volume->fill([ - 'fs_path' => $source, - 'mount_path' => $target, - 'content' => $content, - 'is_directory' => $isDirectory, - 'resource_id' => $originalResource->id, - 'resource_type' => get_class($originalResource), - ])->save(); + LocalFileVolume::updateOrCreate( + [ + 'mount_path' => $target, + 'resource_id' => $originalResource->id, + 'resource_type' => get_class($originalResource), + ], + [ + 'fs_path' => $source, + 'mount_path' => $target, + 'content' => $content, + 'is_directory' => $isDirectory, + 'resource_id' => $originalResource->id, + 'resource_type' => get_class($originalResource), + ] + ); if (isDev()) { if ((int) $resource->compose_parsing_version >= 4) { if ($isApplication) { diff --git a/database/migrations/2025_03_29_204400_revert_some_local_volume_encryption.php b/database/migrations/2025_03_29_204400_revert_some_local_volume_encryption.php new file mode 100644 index 000000000..683f1be3d --- /dev/null +++ b/database/migrations/2025_03_29_204400_revert_some_local_volume_encryption.php @@ -0,0 +1,96 @@ +exists()) { + DB::table('local_file_volumes') + ->orderBy('id') + ->chunk(100, function ($volumes) { + foreach ($volumes as $volume) { + DB::beginTransaction(); + + try { + $fs_path = $volume->fs_path; + $mount_path = $volume->mount_path; + try { + if ($fs_path) { + $fs_path = Crypt::decryptString($fs_path); + } + } catch (\Exception $e) { + } + + try { + if ($mount_path) { + $mount_path = Crypt::decryptString($mount_path); + } + } catch (\Exception $e) { + } + + DB::table('local_file_volumes')->where('id', $volume->id)->update([ + 'fs_path' => $fs_path, + 'mount_path' => $mount_path, + ]); + echo "Updated volume {$volume->id}\n"; + } catch (\Exception $e) { + echo "Error encrypting local file volume fields: {$e->getMessage()}\n"; + Log::error('Error encrypting local file volume fields: '.$e->getMessage()); + } + DB::commit(); + } + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (DB::table('local_file_volumes')->exists()) { + DB::table('local_file_volumes') + ->orderBy('id') + ->chunk(100, function ($volumes) { + foreach ($volumes as $volume) { + DB::beginTransaction(); + try { + $fs_path = $volume->fs_path; + $mount_path = $volume->mount_path; + try { + if ($fs_path) { + $fs_path = Crypt::encrypt($fs_path); + } + } catch (\Exception $e) { + } + + try { + if ($mount_path) { + $mount_path = Crypt::encrypt($mount_path); + } + } catch (\Exception $e) { + } + + DB::table('local_file_volumes')->where('id', $volume->id)->update([ + 'fs_path' => $fs_path, + 'mount_path' => $mount_path, + ]); + echo "Updated volume {$volume->id}\n"; + } catch (\Exception $e) { + echo "Error decrypting local file volume fields: {$e->getMessage()}\n"; + Log::error('Error decrypting local file volume fields: '.$e->getMessage()); + } + DB::commit(); + } + }); + } + } +};