From b997b7393b40f300ea1064cc84d304aeb8ae9911 Mon Sep 17 00:00:00 2001 From: Kael Date: Fri, 11 Oct 2024 02:44:52 +1100 Subject: [PATCH 01/54] feat: allow disabling default redirect, set status to 503 --- app/Listeners/ProxyStartedNotification.php | 2 +- app/Livewire/Server/Proxy.php | 16 +- app/Models/Server.php | 139 +++++++++--------- .../views/livewire/server/proxy.blade.php | 9 +- .../proxy/dynamic-configurations.blade.php | 2 +- 5 files changed, 91 insertions(+), 77 deletions(-) diff --git a/app/Listeners/ProxyStartedNotification.php b/app/Listeners/ProxyStartedNotification.php index d0541b162..9045b1e5c 100644 --- a/app/Listeners/ProxyStartedNotification.php +++ b/app/Listeners/ProxyStartedNotification.php @@ -14,7 +14,7 @@ class ProxyStartedNotification public function handle(ProxyStarted $event): void { $this->server = data_get($event, 'data'); - $this->server->setupDefault404Redirect(); + $this->server->setupDefaultRedirect(); $this->server->setupDynamicProxyConfiguration(); $this->server->proxy->force_stop = false; $this->server->save(); diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index 55d0c4966..fbdba53c1 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -16,6 +16,7 @@ class Proxy extends Component public $proxy_settings = null; + public bool $redirect_enabled = true; public ?string $redirect_url = null; protected $listeners = ['proxyStatusUpdated', 'saveConfiguration' => 'submit']; @@ -27,6 +28,7 @@ class Proxy extends Component public function mount() { $this->selectedProxy = $this->server->proxyType(); + $this->redirect_enabled = data_get($this->server, 'proxy.redirect_enabled', true); $this->redirect_url = data_get($this->server, 'proxy.redirect_url'); } @@ -65,13 +67,25 @@ class Proxy extends Component } } + public function instantSaveRedirect() + { + try { + $this->server->proxy->redirect_enabled = $this->redirect_enabled; + $this->server->save(); + $this->server->setupDefaultRedirect(); + $this->dispatch('success', 'Proxy configuration saved.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function submit() { try { SaveConfiguration::run($this->server, $this->proxy_settings); $this->server->proxy->redirect_url = $this->redirect_url; $this->server->save(); - $this->server->setupDefault404Redirect(); + $this->server->setupDefaultRedirect(); $this->dispatch('success', 'Proxy configuration saved.'); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Models/Server.php b/app/Models/Server.php index 8864deef1..0a92caa60 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -94,6 +94,14 @@ class Server extends BaseModel ]); } } + if (!isset($server->proxy->redirect_enabled)) { + $server->proxy->redirect_enabled = true; + } + }); + static::retrieved(function ($server) { + if (!isset($server->proxy->redirect_enabled)) { + $server->proxy->redirect_enabled = true; + } }); static::deleting(function ($server) { $server->destinations()->each(function ($destination) { @@ -164,70 +172,72 @@ class Server extends BaseModel return $this->proxyType() && $this->proxyType() !== 'NONE' && $this->isFunctional() && ! $this->isSwarmWorker() && ! $this->settings->is_build_server; } - public function setupDefault404Redirect() + public function setupDefaultRedirect() { + $banner = + "# This file is generated by Coolify, do not edit it manually.\n" . + "# Disable the default redirect to customize (only if you know what are you doing).\n\n"; $dynamic_conf_path = $this->proxyPath().'/dynamic'; $proxy_type = $this->proxyType(); + $redirect_enabled = $this->proxy->redirect_enabled ?? true; $redirect_url = $this->proxy->redirect_url; + if ($proxy_type === ProxyTypes::TRAEFIK->value) { - $default_redirect_file = "$dynamic_conf_path/default_redirect_404.yaml"; + $default_redirect_file = "$dynamic_conf_path/default_redirect_503.yaml"; } elseif ($proxy_type === ProxyTypes::CADDY->value) { - $default_redirect_file = "$dynamic_conf_path/default_redirect_404.caddy"; + $default_redirect_file = "$dynamic_conf_path/default_redirect_503.caddy"; } - if (empty($redirect_url)) { + + instant_remote_process([ + "mkdir -p $dynamic_conf_path", + "rm -f $dynamic_conf_path/default_redirect_404.yaml", + "rm -f $dynamic_conf_path/default_redirect_404.caddy", + ], $this); + + if (!$redirect_enabled) { + instant_remote_process(["rm -f $default_redirect_file"], $this); + } else { if ($proxy_type === ProxyTypes::CADDY->value) { - $conf = ':80, :443 { -respond 404 + if (empty($redirect_url)) { + $conf = ':80, :443 { + respond 503 }'; - $conf = - "# This file is automatically generated by Coolify.\n". - "# Do not edit it manually (only if you know what are you doing).\n\n". - $conf; - $base64 = base64_encode($conf); - instant_remote_process([ - "mkdir -p $dynamic_conf_path", - "echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null", - ], $this); - $this->reloadCaddy(); - - return; - } - instant_remote_process([ - "mkdir -p $dynamic_conf_path", - "rm -f $default_redirect_file", - ], $this); - - return; - } - if ($proxy_type === ProxyTypes::TRAEFIK->value) { - $dynamic_conf = [ - 'http' => [ - 'routers' => [ - 'catchall' => [ - 'entryPoints' => [ - 0 => 'http', - 1 => 'https', - ], - 'service' => 'noop', - 'rule' => 'HostRegexp(`{catchall:.*}`)', - 'priority' => 1, - 'middlewares' => [ - 0 => 'redirect-regexp@file', + } else { + $conf = ":80, :443 { + redir $redirect_url +}"; + } + } elseif ($proxy_type === ProxyTypes::TRAEFIK->value) { + $dynamic_conf = [ + 'http' => [ + 'routers' => [ + 'catchall' => [ + 'entryPoints' => [ + 0 => 'http', + 1 => 'https', + ], + 'service' => 'noop', + 'rule' => 'HostRegexp(`{catchall:.*}`)', + 'priority' => 1, ], ], - ], - 'services' => [ - 'noop' => [ - 'loadBalancer' => [ - 'servers' => [ - 0 => [ - 'url' => '', - ], + 'services' => [ + 'noop' => [ + 'loadBalancer' => [ + 'servers' => [], ], ], ], ], - 'middlewares' => [ + ]; + if (!empty($redirect_url)) { + $dynamic_conf['http']['routers']['catchall']['middlewares'] = [ + 0 => 'redirect-regexp@file', + ]; + $dynamic_conf['http']['services']['noop']['loadBalancer']['servers'][0] = [ + 'url' => '', + ]; + $dynamic_conf['http']['middlewares'] = [ 'redirect-regexp' => [ 'redirectRegex' => [ 'regex' => '(.*)', @@ -235,32 +245,17 @@ respond 404 'permanent' => false, ], ], - ], - ], - ]; - $conf = Yaml::dump($dynamic_conf, 12, 2); - $conf = - "# This file is automatically generated by Coolify.\n". - "# Do not edit it manually (only if you know what are you doing).\n\n". - $conf; - - $base64 = base64_encode($conf); - } elseif ($proxy_type === ProxyTypes::CADDY->value) { - $conf = ":80, :443 { - redir $redirect_url -}"; - $conf = - "# This file is automatically generated by Coolify.\n". - "# Do not edit it manually (only if you know what are you doing).\n\n". - $conf; + ]; + } + $conf = Yaml::dump($dynamic_conf, 12, 2); + } + $conf = $banner . $conf; $base64 = base64_encode($conf); + instant_remote_process([ + "echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null", + ], $this); } - instant_remote_process([ - "mkdir -p $dynamic_conf_path", - "echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null", - ], $this); - if ($proxy_type === 'CADDY') { $this->reloadCaddy(); } diff --git a/resources/views/livewire/server/proxy.blade.php b/resources/views/livewire/server/proxy.blade.php index 25c3f7acd..5748a5876 100644 --- a/resources/views/livewire/server/proxy.blade.php +++ b/resources/views/livewire/server/proxy.blade.php @@ -28,6 +28,13 @@ id="server.settings.generate_exact_labels" label="Generate labels only for {{ str($server->proxyType())->title() }}" instantSave /> +
Default request handler
+
+ + @if ($redirect_enabled) + + @endif +
@if ($server->proxyType() === ProxyTypes::TRAEFIK->value)

Traefik

@elseif ($server->proxyType() === 'CADDY') @@ -40,8 +47,6 @@ configurations. @endif -
diff --git a/resources/views/livewire/server/proxy/dynamic-configurations.blade.php b/resources/views/livewire/server/proxy/dynamic-configurations.blade.php index a8192cdb1..dd1fa59f0 100644 --- a/resources/views/livewire/server/proxy/dynamic-configurations.blade.php +++ b/resources/views/livewire/server/proxy/dynamic-configurations.blade.php @@ -29,7 +29,7 @@ @if (str_replace('|', '.', $fileName) === 'coolify.yaml' || str_replace('|', '.', $fileName) === 'Caddyfile' || str_replace('|', '.', $fileName) === 'coolify.caddy' || - str_replace('|', '.', $fileName) === 'default_redirect_404.caddy') + str_replace('|', '.', $fileName) === 'default_redirect_503.caddy')

File: {{ str_replace('|', '.', $fileName) }}

From 2f3503d8b8509ddc9904a2fc3ffc12c3cd98e7b4 Mon Sep 17 00:00:00 2001 From: Kael Date: Fri, 11 Oct 2024 03:24:46 +1100 Subject: [PATCH 02/54] fix: don't allow editing traefik config --- .../views/livewire/server/proxy/dynamic-configurations.blade.php | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/views/livewire/server/proxy/dynamic-configurations.blade.php b/resources/views/livewire/server/proxy/dynamic-configurations.blade.php index dd1fa59f0..5499ea95b 100644 --- a/resources/views/livewire/server/proxy/dynamic-configurations.blade.php +++ b/resources/views/livewire/server/proxy/dynamic-configurations.blade.php @@ -29,6 +29,7 @@ @if (str_replace('|', '.', $fileName) === 'coolify.yaml' || str_replace('|', '.', $fileName) === 'Caddyfile' || str_replace('|', '.', $fileName) === 'coolify.caddy' || + str_replace('|', '.', $fileName) === 'default_redirect_503.yaml' || str_replace('|', '.', $fileName) === 'default_redirect_503.caddy')

File: {{ str_replace('|', '.', $fileName) }}

From d4d63ff273ce776644bd8cd47afa2b9c643aae8e Mon Sep 17 00:00:00 2001 From: Kael Date: Wed, 30 Oct 2024 17:00:55 +1100 Subject: [PATCH 03/54] feat: add deploy-only token permission --- app/Livewire/Security/ApiTokens.php | 16 ++++++++++++++-- .../views/livewire/security/api-tokens.blade.php | 1 + routes/api.php | 3 ++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/Livewire/Security/ApiTokens.php b/app/Livewire/Security/ApiTokens.php index fe68a8ba5..9add0b0ca 100644 --- a/app/Livewire/Security/ApiTokens.php +++ b/app/Livewire/Security/ApiTokens.php @@ -12,10 +12,9 @@ class ApiTokens extends Component public $tokens = []; public bool $viewSensitiveData = false; - public bool $readOnly = true; - public bool $rootAccess = false; + public bool $triggerDeploy = false; public array $permissions = ['read-only']; @@ -62,12 +61,25 @@ class ApiTokens extends Component $this->permissions = ['*']; $this->readOnly = false; $this->viewSensitiveData = false; + $this->triggerDeploy = false; } else { $this->readOnly = true; $this->permissions = ['read-only']; } } + public function updatedTriggerDeploy() + { + if ($this->triggerDeploy) { + $this->permissions[] = 'trigger-deploy'; + $this->permissions = array_diff($this->permissions, ['*']); + $this->rootAccess = false; + } else { + $this->permissions = array_diff($this->permissions, ['trigger-deploy']); + } + $this->makeSureOneIsSelected(); + } + public function makeSureOneIsSelected() { if (count($this->permissions) == 0) { diff --git a/resources/views/livewire/security/api-tokens.blade.php b/resources/views/livewire/security/api-tokens.blade.php index 1bcd64710..a360d4a3b 100644 --- a/resources/views/livewire/security/api-tokens.blade.php +++ b/resources/views/livewire/security/api-tokens.blade.php @@ -39,6 +39,7 @@ +
@if (session()->has('token')) diff --git a/routes/api.php b/routes/api.php index b63fde871..05fe4f5e8 100644 --- a/routes/api.php +++ b/routes/api.php @@ -54,7 +54,8 @@ Route::group([ Route::patch('/security/keys/{uuid}', [SecurityController::class, 'update_key'])->middleware([IgnoreReadOnlyApiToken::class]); Route::delete('/security/keys/{uuid}', [SecurityController::class, 'delete_key'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::match(['get', 'post'], '/deploy', [DeployController::class, 'deploy'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::match(['get', 'post'], '/deploy', [DeployController::class, 'deploy']) + ->middleware([IgnoreReadOnlyApiToken::class, 'auth:sanctum', 'ability:trigger-deploy']); Route::get('/deployments', [DeployController::class, 'deployments']); Route::get('/deployments/{uuid}', [DeployController::class, 'deployment_by_uuid']); From 652023566707ac65942c5b8f3181f2ac354e7584 Mon Sep 17 00:00:00 2001 From: Kael Date: Wed, 30 Oct 2024 19:06:50 +1100 Subject: [PATCH 04/54] middleware should allow, not deny --- .../Api/ApplicationsController.php | 2 +- .../Controllers/Api/DatabasesController.php | 2 +- app/Http/Controllers/Api/DeployController.php | 2 +- .../Controllers/Api/SecurityController.php | 2 +- .../Controllers/Api/ServersController.php | 4 +- .../Controllers/Api/ServicesController.php | 2 +- app/Http/Controllers/Api/TeamController.php | 2 +- .../Middleware/IgnoreReadOnlyApiToken.php | 28 ---- app/Http/Middleware/OnlyRootApiToken.php | 25 --- app/Livewire/Security/ApiTokens.php | 66 +------- app/View/Components/Forms/Checkbox.php | 1 + ..._10_30_074601_rename_token_permissions.php | 44 +++++ .../views/components/forms/checkbox.blade.php | 5 +- .../livewire/security/api-tokens.blade.php | 17 +- routes/api.php | 158 +++++++++--------- 15 files changed, 149 insertions(+), 211 deletions(-) delete mode 100644 app/Http/Middleware/IgnoreReadOnlyApiToken.php delete mode 100644 app/Http/Middleware/OnlyRootApiToken.php create mode 100644 database/migrations/2024_10_30_074601_rename_token_permissions.php diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 46dd8120e..0a088d1c3 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -29,7 +29,7 @@ class ApplicationsController extends Controller $application->makeHidden([ 'id', ]); - if ($token->can('view:sensitive')) { + if ($token->can('read:sensitive')) { return serializeApiResponse($application); } $application->makeHidden([ diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 65873f818..e30388ec8 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -24,7 +24,7 @@ class DatabasesController extends Controller 'id', 'laravel_through_key', ]); - if ($token->can('view:sensitive')) { + if ($token->can('read:sensitive')) { return serializeApiResponse($database); } diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php index 666dc55a5..1d162c7ee 100644 --- a/app/Http/Controllers/Api/DeployController.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -17,7 +17,7 @@ class DeployController extends Controller private function removeSensitiveData($deployment) { $token = auth()->user()->currentAccessToken(); - if ($token->can('view:sensitive')) { + if ($token->can('read:sensitive')) { return serializeApiResponse($deployment); } diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php index bb474aed3..aa636983f 100644 --- a/app/Http/Controllers/Api/SecurityController.php +++ b/app/Http/Controllers/Api/SecurityController.php @@ -12,7 +12,7 @@ class SecurityController extends Controller private function removeSensitiveData($team) { $token = auth()->user()->currentAccessToken(); - if ($token->can('view:sensitive')) { + if ($token->can('read:sensitive')) { return serializeApiResponse($team); } $team->makeHidden([ diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index af4e008ef..34498bbb6 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -20,7 +20,7 @@ class ServersController extends Controller private function removeSensitiveDataFromSettings($settings) { $token = auth()->user()->currentAccessToken(); - if ($token->can('view:sensitive')) { + if ($token->can('read:sensitive')) { return serializeApiResponse($settings); } $settings = $settings->makeHidden([ @@ -36,7 +36,7 @@ class ServersController extends Controller $server->makeHidden([ 'id', ]); - if ($token->can('view:sensitive')) { + if ($token->can('read:sensitive')) { return serializeApiResponse($server); } diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 89418517b..8ba2a938c 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -22,7 +22,7 @@ class ServicesController extends Controller $service->makeHidden([ 'id', ]); - if ($token->can('view:sensitive')) { + if ($token->can('read:sensitive')) { return serializeApiResponse($service); } diff --git a/app/Http/Controllers/Api/TeamController.php b/app/Http/Controllers/Api/TeamController.php index 3f951c6f7..239c950c0 100644 --- a/app/Http/Controllers/Api/TeamController.php +++ b/app/Http/Controllers/Api/TeamController.php @@ -15,7 +15,7 @@ class TeamController extends Controller 'custom_server_limit', 'pivot', ]); - if ($token->can('view:sensitive')) { + if ($token->can('read:sensitive')) { return serializeApiResponse($team); } $team->makeHidden([ diff --git a/app/Http/Middleware/IgnoreReadOnlyApiToken.php b/app/Http/Middleware/IgnoreReadOnlyApiToken.php deleted file mode 100644 index bd6cd1f8a..000000000 --- a/app/Http/Middleware/IgnoreReadOnlyApiToken.php +++ /dev/null @@ -1,28 +0,0 @@ -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); - } -} diff --git a/app/Http/Middleware/OnlyRootApiToken.php b/app/Http/Middleware/OnlyRootApiToken.php deleted file mode 100644 index 8ff1fa0e5..000000000 --- a/app/Http/Middleware/OnlyRootApiToken.php +++ /dev/null @@ -1,25 +0,0 @@ -user()->currentAccessToken(); - if ($token->can('*')) { - return $next($request); - } - - return response()->json(['message' => 'You are not allowed to perform this action.'], 403); - } -} diff --git a/app/Livewire/Security/ApiTokens.php b/app/Livewire/Security/ApiTokens.php index 9add0b0ca..6e58df0f0 100644 --- a/app/Livewire/Security/ApiTokens.php +++ b/app/Livewire/Security/ApiTokens.php @@ -11,12 +11,7 @@ class ApiTokens extends Component public $tokens = []; - public bool $viewSensitiveData = false; - public bool $readOnly = true; - public bool $rootAccess = false; - public bool $triggerDeploy = false; - - public array $permissions = ['read-only']; + public array $permissions = ['read']; public $isApiEnabled; @@ -31,60 +26,13 @@ class ApiTokens extends Component $this->tokens = auth()->user()->tokens->sortByDesc('created_at'); } - public function updatedViewSensitiveData() - { - if ($this->viewSensitiveData) { - $this->permissions[] = 'view:sensitive'; - $this->permissions = array_diff($this->permissions, ['*']); - $this->rootAccess = false; - } else { - $this->permissions = array_diff($this->permissions, ['view:sensitive']); - } - $this->makeSureOneIsSelected(); - } - - public function updatedReadOnly() - { - if ($this->readOnly) { - $this->permissions[] = 'read-only'; - $this->permissions = array_diff($this->permissions, ['*']); - $this->rootAccess = false; - } else { - $this->permissions = array_diff($this->permissions, ['read-only']); - } - $this->makeSureOneIsSelected(); - } - - public function updatedRootAccess() - { - if ($this->rootAccess) { - $this->permissions = ['*']; - $this->readOnly = false; - $this->viewSensitiveData = false; - $this->triggerDeploy = false; - } else { - $this->readOnly = true; - $this->permissions = ['read-only']; - } - } - - public function updatedTriggerDeploy() - { - if ($this->triggerDeploy) { - $this->permissions[] = 'trigger-deploy'; - $this->permissions = array_diff($this->permissions, ['*']); - $this->rootAccess = false; - } else { - $this->permissions = array_diff($this->permissions, ['trigger-deploy']); - } - $this->makeSureOneIsSelected(); - } - - public function makeSureOneIsSelected() + public function updated() { if (count($this->permissions) == 0) { - $this->permissions = ['read-only']; - $this->readOnly = true; + $this->permissions = ['read']; + } + if (in_array('read:sensitive', $this->permissions) && !in_array('read', $this->permissions)) { + $this->permissions[] = 'read'; } } @@ -94,7 +42,7 @@ class ApiTokens extends Component $this->validate([ '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; session()->flash('token', $token->plainTextToken); } catch (\Exception $e) { diff --git a/app/View/Components/Forms/Checkbox.php b/app/View/Components/Forms/Checkbox.php index 414dbf2ae..8abe96c1b 100644 --- a/app/View/Components/Forms/Checkbox.php +++ b/app/View/Components/Forms/Checkbox.php @@ -15,6 +15,7 @@ class Checkbox extends Component public ?string $id = null, public ?string $name = null, public ?string $value = null, + public ?string $domValue = null, public ?string $label = null, public ?string $helper = null, public string|bool $instantSave = false, diff --git a/database/migrations/2024_10_30_074601_rename_token_permissions.php b/database/migrations/2024_10_30_074601_rename_token_permissions.php new file mode 100644 index 000000000..d35d75481 --- /dev/null +++ b/database/migrations/2024_10_30_074601_rename_token_permissions.php @@ -0,0 +1,44 @@ +abilities)) $abilities->push('write', 'read', 'read:sensitive'); + if (in_array('read-only', $token->abilities)) $abilities->push('read'); + if (in_array('view:sensitive', $token->abilities)) $abilities->push('read', 'read:sensitive'); + $token->abilities = $abilities->unique()->values()->all(); + $token->save(); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $tokens = PersonalAccessToken::all(); + foreach ($tokens as $token) { + $abilities = collect(); + if (in_array('write', $token->abilities)) { + $abilities->push('*'); + } else { + if (in_array('read', $token->abilities)) $abilities->push('read-only'); + if (in_array('read:sensitive', $token->abilities)) $abilities->push('view:sensitive'); + } + $token->abilities = $abilities->unique()->values()->all(); + $token->save(); + } + } +}; diff --git a/resources/views/components/forms/checkbox.blade.php b/resources/views/components/forms/checkbox.blade.php index fed6ad77f..3f01a70c5 100644 --- a/resources/views/components/forms/checkbox.blade.php +++ b/resources/views/components/forms/checkbox.blade.php @@ -5,6 +5,7 @@ 'disabled' => false, 'instantSave' => false, 'value' => null, + 'domValue' => null, 'hideLabel' => false, 'fullWidth' => false, ]) @@ -33,5 +34,7 @@ merge(['class' => $defaultClass]) }} @if ($instantSave) wire:loading.attr="disabled" wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}' - wire:model={{ $id }} @else wire:model={{ $value ?? $id }} @endif /> + wire:model="{{ $id }}" @else wire:model="{{ $value ?? $id }}" @endif + @if ($domValue) value="{{ $domValue }}" @endif + /> diff --git a/resources/views/livewire/security/api-tokens.blade.php b/resources/views/livewire/security/api-tokens.blade.php index a360d4a3b..3ff52417f 100644 --- a/resources/views/livewire/security/api-tokens.blade.php +++ b/resources/views/livewire/security/api-tokens.blade.php @@ -25,21 +25,20 @@
@if ($permissions) @foreach ($permissions as $permission) - @if ($permission === '*') -
Root access, be careful!
- @else -
{{ $permission }}
- @endif +
{{ $permission }}
@endforeach @endif
+ @if (in_array('write', $permissions)) +
Root access, be careful!
+ @endif

Token Permissions

- - - - + + + +
@if (session()->has('token')) diff --git a/routes/api.php b/routes/api.php index 05fe4f5e8..230290d57 100644 --- a/routes/api.php +++ b/routes/api.php @@ -11,8 +11,6 @@ use App\Http\Controllers\Api\ServersController; use App\Http\Controllers\Api\ServicesController; use App\Http\Controllers\Api\TeamController; use App\Http\Middleware\ApiAllowed; -use App\Http\Middleware\IgnoreReadOnlyApiToken; -use App\Http\Middleware\OnlyRootApiToken; use App\Jobs\PushServerUpdateJob; use App\Models\Server; use Illuminate\Support\Facades\Route; @@ -21,7 +19,7 @@ Route::get('/health', [OtherController::class, 'healthcheck']); Route::post('/feedback', [OtherController::class, 'feedback']); Route::group([ - 'middleware' => ['auth:sanctum', OnlyRootApiToken::class], + 'middleware' => ['auth:sanctum', 'ability:write'], 'prefix' => 'v1', ], function () { Route::get('/enable', [OtherController::class, 'enable_api']); @@ -31,105 +29,103 @@ Route::group([ 'middleware' => ['auth:sanctum', ApiAllowed::class], 'prefix' => 'v1', ], function () { - Route::get('/version', [OtherController::class, 'version']); + Route::get('/version', [OtherController::class, 'version'])->middleware(['ability:read']); - Route::get('/teams', [TeamController::class, 'teams']); - Route::get('/teams/current', [TeamController::class, 'current_team']); - Route::get('/teams/current/members', [TeamController::class, 'current_team_members']); - Route::get('/teams/{id}', [TeamController::class, 'team_by_id']); - Route::get('/teams/{id}/members', [TeamController::class, 'members_by_id']); + Route::get('/teams', [TeamController::class, 'teams'])->middleware(['ability:read']); + Route::get('/teams/current', [TeamController::class, 'current_team'])->middleware(['ability:read']); + Route::get('/teams/current/members', [TeamController::class, 'current_team_members'])->middleware(['ability:read']); + Route::get('/teams/{id}', [TeamController::class, 'team_by_id'])->middleware(['ability:read']); + Route::get('/teams/{id}/members', [TeamController::class, 'members_by_id'])->middleware(['ability:read']); - Route::get('/projects', [ProjectController::class, 'projects']); - Route::get('/projects/{uuid}', [ProjectController::class, 'project_by_uuid']); - Route::get('/projects/{uuid}/{environment_name}', [ProjectController::class, 'environment_details']); + Route::get('/projects', [ProjectController::class, 'projects'])->middleware(['ability:read']); + Route::get('/projects/{uuid}', [ProjectController::class, 'project_by_uuid'])->middleware(['ability:read']); + Route::get('/projects/{uuid}/{environment_name}', [ProjectController::class, 'environment_details'])->middleware(['ability:read']); - Route::post('/projects', [ProjectController::class, 'create_project']); - Route::patch('/projects/{uuid}', [ProjectController::class, 'update_project']); - Route::delete('/projects/{uuid}', [ProjectController::class, 'delete_project']); + Route::post('/projects', [ProjectController::class, 'create_project'])->middleware(['ability:read']); + Route::patch('/projects/{uuid}', [ProjectController::class, 'update_project'])->middleware(['ability:write']); + Route::delete('/projects/{uuid}', [ProjectController::class, 'delete_project'])->middleware(['ability:write']); - Route::get('/security/keys', [SecurityController::class, 'keys']); - Route::post('/security/keys', [SecurityController::class, 'create_key'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::get('/security/keys', [SecurityController::class, 'keys'])->middleware(['ability:read']); + Route::post('/security/keys', [SecurityController::class, 'create_key'])->middleware(['ability:write']); - Route::get('/security/keys/{uuid}', [SecurityController::class, 'key_by_uuid']); - Route::patch('/security/keys/{uuid}', [SecurityController::class, 'update_key'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::delete('/security/keys/{uuid}', [SecurityController::class, 'delete_key'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::get('/security/keys/{uuid}', [SecurityController::class, 'key_by_uuid'])->middleware(['ability:read']); + Route::patch('/security/keys/{uuid}', [SecurityController::class, 'update_key'])->middleware(['ability:write']); + Route::delete('/security/keys/{uuid}', [SecurityController::class, 'delete_key'])->middleware(['ability:write']); - Route::match(['get', 'post'], '/deploy', [DeployController::class, 'deploy']) - ->middleware([IgnoreReadOnlyApiToken::class, 'auth:sanctum', 'ability:trigger-deploy']); - Route::get('/deployments', [DeployController::class, 'deployments']); - Route::get('/deployments/{uuid}', [DeployController::class, 'deployment_by_uuid']); + Route::match(['get', 'post'], '/deploy', [DeployController::class, 'deploy'])->middleware(['ability:write,deploy']); + Route::get('/deployments', [DeployController::class, 'deployments'])->middleware(['ability:read']); + Route::get('/deployments/{uuid}', [DeployController::class, 'deployment_by_uuid'])->middleware(['ability:read']); - Route::get('/servers', [ServersController::class, 'servers']); - Route::get('/servers/{uuid}', [ServersController::class, 'server_by_uuid']); - Route::get('/servers/{uuid}/domains', [ServersController::class, 'domains_by_server']); - Route::get('/servers/{uuid}/resources', [ServersController::class, 'resources_by_server']); + Route::get('/servers', [ServersController::class, 'servers'])->middleware(['ability:read']); + Route::get('/servers/{uuid}', [ServersController::class, 'server_by_uuid'])->middleware(['ability:read']); + Route::get('/servers/{uuid}/domains', [ServersController::class, 'domains_by_server'])->middleware(['ability:read']); + Route::get('/servers/{uuid}/resources', [ServersController::class, 'resources_by_server'])->middleware(['ability:read']); - Route::get('/servers/{uuid}/validate', [ServersController::class, 'validate_server']); + Route::get('/servers/{uuid}/validate', [ServersController::class, 'validate_server'])->middleware(['ability:read']); - Route::post('/servers', [ServersController::class, 'create_server']); - Route::patch('/servers/{uuid}', [ServersController::class, 'update_server']); - Route::delete('/servers/{uuid}', [ServersController::class, 'delete_server']); + Route::post('/servers', [ServersController::class, 'create_server'])->middleware(['ability:read']); + Route::patch('/servers/{uuid}', [ServersController::class, 'update_server'])->middleware(['ability:write']); + Route::delete('/servers/{uuid}', [ServersController::class, 'delete_server'])->middleware(['ability:write']); - Route::get('/resources', [ResourcesController::class, 'resources']); + Route::get('/resources', [ResourcesController::class, 'resources'])->middleware(['ability:read']); - Route::get('/applications', [ApplicationsController::class, 'applications']); - Route::post('/applications/public', [ApplicationsController::class, 'create_public_application'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/applications/private-github-app', [ApplicationsController::class, 'create_private_gh_app_application'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/applications/private-deploy-key', [ApplicationsController::class, 'create_private_deploy_key_application'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/applications/dockerfile', [ApplicationsController::class, 'create_dockerfile_application'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/applications/dockerimage', [ApplicationsController::class, 'create_dockerimage_application'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/applications/dockercompose', [ApplicationsController::class, 'create_dockercompose_application'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::get('/applications', [ApplicationsController::class, 'applications'])->middleware(['ability:read']); + Route::post('/applications/public', [ApplicationsController::class, 'create_public_application'])->middleware(['ability:write']); + Route::post('/applications/private-github-app', [ApplicationsController::class, 'create_private_gh_app_application'])->middleware(['ability:write']); + Route::post('/applications/private-deploy-key', [ApplicationsController::class, 'create_private_deploy_key_application'])->middleware(['ability:write']); + Route::post('/applications/dockerfile', [ApplicationsController::class, 'create_dockerfile_application'])->middleware(['ability:write']); + Route::post('/applications/dockerimage', [ApplicationsController::class, 'create_dockerimage_application'])->middleware(['ability:write']); + Route::post('/applications/dockercompose', [ApplicationsController::class, 'create_dockercompose_application'])->middleware(['ability:write']); - Route::get('/applications/{uuid}', [ApplicationsController::class, 'application_by_uuid']); - Route::patch('/applications/{uuid}', [ApplicationsController::class, 'update_by_uuid'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::delete('/applications/{uuid}', [ApplicationsController::class, 'delete_by_uuid'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::get('/applications/{uuid}', [ApplicationsController::class, 'application_by_uuid'])->middleware(['ability:read']); + Route::patch('/applications/{uuid}', [ApplicationsController::class, 'update_by_uuid'])->middleware(['ability:write']); + Route::delete('/applications/{uuid}', [ApplicationsController::class, 'delete_by_uuid'])->middleware(['ability:write']); - Route::get('/applications/{uuid}/envs', [ApplicationsController::class, 'envs']); - Route::post('/applications/{uuid}/envs', [ApplicationsController::class, 'create_env'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::patch('/applications/{uuid}/envs/bulk', [ApplicationsController::class, 'create_bulk_envs'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::patch('/applications/{uuid}/envs', [ApplicationsController::class, 'update_env_by_uuid'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::delete('/applications/{uuid}/envs/{env_uuid}', [ApplicationsController::class, 'delete_env_by_uuid'])->middleware([IgnoreReadOnlyApiToken::class]); - // Route::post('/applications/{uuid}/execute', [ApplicationsController::class, 'execute_command_by_uuid'])->middleware([OnlyRootApiToken::class]); + Route::get('/applications/{uuid}/envs', [ApplicationsController::class, 'envs'])->middleware(['ability:read']); + Route::post('/applications/{uuid}/envs', [ApplicationsController::class, 'create_env'])->middleware(['ability:write']); + Route::patch('/applications/{uuid}/envs/bulk', [ApplicationsController::class, 'create_bulk_envs'])->middleware(['ability:write']); + Route::patch('/applications/{uuid}/envs', [ApplicationsController::class, 'update_env_by_uuid'])->middleware(['ability:write']); + Route::delete('/applications/{uuid}/envs/{env_uuid}', [ApplicationsController::class, 'delete_env_by_uuid'])->middleware(['ability:write']); + // Route::post('/applications/{uuid}/execute', [ApplicationsController::class, 'execute_command_by_uuid'])->middleware(['ability:write']); - Route::match(['get', 'post'], '/applications/{uuid}/start', [ApplicationsController::class, 'action_deploy'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::match(['get', 'post'], '/applications/{uuid}/restart', [ApplicationsController::class, 'action_restart'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::match(['get', 'post'], '/applications/{uuid}/stop', [ApplicationsController::class, 'action_stop'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::match(['get', 'post'], '/applications/{uuid}/start', [ApplicationsController::class, 'action_deploy'])->middleware(['ability:write']); + Route::match(['get', 'post'], '/applications/{uuid}/restart', [ApplicationsController::class, 'action_restart'])->middleware(['ability:write']); + Route::match(['get', 'post'], '/applications/{uuid}/stop', [ApplicationsController::class, 'action_stop'])->middleware(['ability:write']); - Route::get('/databases', [DatabasesController::class, 'databases']); - Route::post('/databases/postgresql', [DatabasesController::class, 'create_database_postgresql'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/databases/mysql', [DatabasesController::class, 'create_database_mysql'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/databases/mariadb', [DatabasesController::class, 'create_database_mariadb'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/databases/mongodb', [DatabasesController::class, 'create_database_mongodb'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/databases/redis', [DatabasesController::class, 'create_database_redis'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/databases/clickhouse', [DatabasesController::class, 'create_database_clickhouse'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/databases/dragonfly', [DatabasesController::class, 'create_database_dragonfly'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/databases/keydb', [DatabasesController::class, 'create_database_keydb'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::get('/databases', [DatabasesController::class, 'databases'])->middleware(['ability:read']); + Route::post('/databases/postgresql', [DatabasesController::class, 'create_database_postgresql'])->middleware(['ability:write']); + Route::post('/databases/mysql', [DatabasesController::class, 'create_database_mysql'])->middleware(['ability:write']); + Route::post('/databases/mariadb', [DatabasesController::class, 'create_database_mariadb'])->middleware(['ability:write']); + Route::post('/databases/mongodb', [DatabasesController::class, 'create_database_mongodb'])->middleware(['ability:write']); + Route::post('/databases/redis', [DatabasesController::class, 'create_database_redis'])->middleware(['ability:write']); + Route::post('/databases/clickhouse', [DatabasesController::class, 'create_database_clickhouse'])->middleware(['ability:write']); + Route::post('/databases/dragonfly', [DatabasesController::class, 'create_database_dragonfly'])->middleware(['ability:write']); + Route::post('/databases/keydb', [DatabasesController::class, 'create_database_keydb'])->middleware(['ability:write']); - Route::get('/databases/{uuid}', [DatabasesController::class, 'database_by_uuid']); - Route::patch('/databases/{uuid}', [DatabasesController::class, 'update_by_uuid'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::get('/databases/{uuid}', [DatabasesController::class, 'database_by_uuid'])->middleware(['ability:read']); + Route::patch('/databases/{uuid}', [DatabasesController::class, 'update_by_uuid'])->middleware(['ability:write']); + Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid'])->middleware(['ability:write']); - Route::match(['get', 'post'], '/databases/{uuid}/start', [DatabasesController::class, 'action_deploy'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::match(['get', 'post'], '/databases/{uuid}/restart', [DatabasesController::class, 'action_restart'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::match(['get', 'post'], '/databases/{uuid}/stop', [DatabasesController::class, 'action_stop'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::match(['get', 'post'], '/databases/{uuid}/start', [DatabasesController::class, 'action_deploy'])->middleware(['ability:write']); + Route::match(['get', 'post'], '/databases/{uuid}/restart', [DatabasesController::class, 'action_restart'])->middleware(['ability:write']); + Route::match(['get', 'post'], '/databases/{uuid}/stop', [DatabasesController::class, 'action_stop'])->middleware(['ability:write']); - Route::get('/services', [ServicesController::class, 'services']); - Route::post('/services', [ServicesController::class, 'create_service'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::get('/services', [ServicesController::class, 'services'])->middleware(['ability:read']); + Route::post('/services', [ServicesController::class, 'create_service'])->middleware(['ability:write']); - Route::get('/services/{uuid}', [ServicesController::class, 'service_by_uuid']); - // Route::patch('/services/{uuid}', [ServicesController::class, 'update_by_uuid'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::delete('/services/{uuid}', [ServicesController::class, 'delete_by_uuid'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::get('/services/{uuid}', [ServicesController::class, 'service_by_uuid'])->middleware(['ability:read']); + // Route::patch('/services/{uuid}', [ServicesController::class, 'update_by_uuid'])->middleware(['ability:write']); + Route::delete('/services/{uuid}', [ServicesController::class, 'delete_by_uuid'])->middleware(['ability:write']); - Route::get('/services/{uuid}/envs', [ServicesController::class, 'envs']); - Route::post('/services/{uuid}/envs', [ServicesController::class, 'create_env'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::patch('/services/{uuid}/envs/bulk', [ServicesController::class, 'create_bulk_envs'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::patch('/services/{uuid}/envs', [ServicesController::class, 'update_env_by_uuid'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::delete('/services/{uuid}/envs/{env_uuid}', [ServicesController::class, 'delete_env_by_uuid'])->middleware([IgnoreReadOnlyApiToken::class]); - - Route::match(['get', 'post'], '/services/{uuid}/start', [ServicesController::class, 'action_deploy'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::match(['get', 'post'], '/services/{uuid}/restart', [ServicesController::class, 'action_restart'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::match(['get', 'post'], '/services/{uuid}/stop', [ServicesController::class, 'action_stop'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::get('/services/{uuid}/envs', [ServicesController::class, 'envs'])->middleware(['ability:read']); + Route::post('/services/{uuid}/envs', [ServicesController::class, 'create_env'])->middleware(['ability:write']); + Route::patch('/services/{uuid}/envs/bulk', [ServicesController::class, 'create_bulk_envs'])->middleware(['ability:write']); + Route::patch('/services/{uuid}/envs', [ServicesController::class, 'update_env_by_uuid'])->middleware(['ability:write']); + Route::delete('/services/{uuid}/envs/{env_uuid}', [ServicesController::class, 'delete_env_by_uuid'])->middleware(['ability:write']); + Route::match(['get', 'post'], '/services/{uuid}/start', [ServicesController::class, 'action_deploy'])->middleware(['ability:write']); + Route::match(['get', 'post'], '/services/{uuid}/restart', [ServicesController::class, 'action_restart'])->middleware(['ability:write']); + Route::match(['get', 'post'], '/services/{uuid}/stop', [ServicesController::class, 'action_stop'])->middleware(['ability:write']); }); Route::group([ From eb0686fe2035ce4624e04037c66620f6cb348376 Mon Sep 17 00:00:00 2001 From: Marvin von Rappard Date: Tue, 12 Nov 2024 22:37:55 +0100 Subject: [PATCH 05/54] feat: slack notifications --- app/Jobs/SendMessageToSlackJob.php | 58 ++++ app/Livewire/Notifications/Slack.php | 130 +++++++++ app/Models/Team.php | 10 +- .../Application/DeploymentFailed.php | 46 ++- .../Application/DeploymentSuccess.php | 46 ++- .../Application/StatusChanged.php | 24 +- app/Notifications/Channels/SendsSlack.php | 8 + app/Notifications/Channels/SlackChannel.php | 22 ++ .../Container/ContainerRestarted.php | 24 +- .../Container/ContainerStopped.php | 24 +- app/Notifications/Database/BackupFailed.php | 17 +- app/Notifications/Database/BackupSuccess.php | 16 +- app/Notifications/Dto/SlackMessage.php | 28 ++ .../Internal/GeneralNotification.php | 19 +- .../ScheduledTask/TaskFailed.php | 24 +- app/Notifications/Server/DockerCleanup.php | 19 +- app/Notifications/Server/ForceDisabled.php | 26 +- app/Notifications/Server/ForceEnabled.php | 22 +- app/Notifications/Server/HighDiskUsage.php | 25 +- app/Notifications/Server/Reachable.php | 21 +- app/Notifications/Server/Unreachable.php | 22 +- app/Notifications/Test.php | 15 +- bootstrap/helpers/shared.php | 270 +++++++++--------- ...add_slack_notifications_to_teams_table.php | 37 +++ .../components/notification/navbar.blade.php | 6 +- .../livewire/notifications/slack.blade.php | 43 +++ routes/web.php | 1 + 27 files changed, 822 insertions(+), 181 deletions(-) create mode 100644 app/Jobs/SendMessageToSlackJob.php create mode 100644 app/Livewire/Notifications/Slack.php create mode 100644 app/Notifications/Channels/SendsSlack.php create mode 100644 app/Notifications/Channels/SlackChannel.php create mode 100644 app/Notifications/Dto/SlackMessage.php create mode 100644 database/migrations/2024_11_12_213200_add_slack_notifications_to_teams_table.php create mode 100644 resources/views/livewire/notifications/slack.blade.php diff --git a/app/Jobs/SendMessageToSlackJob.php b/app/Jobs/SendMessageToSlackJob.php new file mode 100644 index 000000000..b78088f50 --- /dev/null +++ b/app/Jobs/SendMessageToSlackJob.php @@ -0,0 +1,58 @@ +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 + ] + ] + ] + ] + ] + ]); + } +} \ No newline at end of file diff --git a/app/Livewire/Notifications/Slack.php b/app/Livewire/Notifications/Slack.php new file mode 100644 index 000000000..edd32a071 --- /dev/null +++ b/app/Livewire/Notifications/Slack.php @@ -0,0 +1,130 @@ +team = auth()->user()->currentTeam(); + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->team->slack_enabled = $this->slackEnabled; + $this->team->slack_webhook_url = $this->slackWebhookUrl; + $this->team->slack_notifications_test = $this->slackNotificationsTest; + $this->team->slack_notifications_deployments = $this->slackNotificationsDeployments; + $this->team->slack_notifications_status_changes = $this->slackNotificationsStatusChanges; + $this->team->slack_notifications_database_backups = $this->slackNotificationsDatabaseBackups; + $this->team->slack_notifications_scheduled_tasks = $this->slackNotificationsScheduledTasks; + $this->team->slack_notifications_server_disk_usage = $this->slackNotificationsServerDiskUsage; + $this->team->save(); + refreshSession(); + } else { + $this->slackEnabled = $this->team->slack_enabled; + $this->slackWebhookUrl = $this->team->slack_webhook_url; + $this->slackNotificationsTest = $this->team->slack_notifications_test; + $this->slackNotificationsDeployments = $this->team->slack_notifications_deployments; + $this->slackNotificationsStatusChanges = $this->team->slack_notifications_status_changes; + $this->slackNotificationsDatabaseBackups = $this->team->slack_notifications_database_backups; + $this->slackNotificationsScheduledTasks = $this->team->slack_notifications_scheduled_tasks; + $this->slackNotificationsServerDiskUsage = $this->team->slack_notifications_server_disk_usage; + } + } + + 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); + } + } + + public function instantSave() + { + try { + $this->syncData(true); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + 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); + $this->dispatch('success', 'Test notification sent.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.notifications.slack'); + } +} \ No newline at end of file diff --git a/app/Models/Team.php b/app/Models/Team.php index db485054b..2035abdee 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -4,6 +4,7 @@ namespace App\Models; use App\Notifications\Channels\SendsDiscord; use App\Notifications\Channels\SendsEmail; +use App\Notifications\Channels\SendsSlack; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; @@ -70,7 +71,7 @@ use OpenApi\Attributes as OA; ), ] )] -class Team extends Model implements SendsDiscord, SendsEmail +class Team extends Model implements SendsDiscord, SendsEmail, SendsSlack { use Notifiable; @@ -127,6 +128,11 @@ class Team extends Model implements SendsDiscord, SendsEmail ]; } + public function routeNotificationForSlack() + { + return data_get($this, 'slack_webhook_url', null); + } + public function getRecepients($notification) { $recipients = data_get($notification, 'emails', null); @@ -161,7 +167,7 @@ class Team extends Model implements SendsDiscord, SendsEmail return 9999999; } $team = Team::find(currentTeam()->id); - if (! $team) { + if (!$team) { return 0; } diff --git a/app/Notifications/Application/DeploymentFailed.php b/app/Notifications/Application/DeploymentFailed.php index 242980e00..dc6b93aad 100644 --- a/app/Notifications/Application/DeploymentFailed.php +++ b/app/Notifications/Application/DeploymentFailed.php @@ -5,6 +5,7 @@ namespace App\Notifications\Application; use App\Models\Application; use App\Models\ApplicationPreview; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\SlackMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -44,7 +45,7 @@ class DeploymentFailed extends Notification implements ShouldQueue if (str($this->fqdn)->explode(',')->count() > 1) { $this->fqdn = str($this->fqdn)->explode(',')->first(); } - $this->deployment_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}"; + $this->deployment_url = base_url() . "/project/{$this->project_uuid}/" . urlencode($this->environment_name) . "/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}"; } public function via(object $notifiable): array @@ -58,10 +59,10 @@ class DeploymentFailed extends Notification implements ShouldQueue $pull_request_id = data_get($this->preview, 'pull_request_id', 0); $fqdn = $this->fqdn; if ($pull_request_id === 0) { - $mail->subject('Coolify: Deployment failed of '.$this->application_name.'.'); + $mail->subject('Coolify: Deployment failed of ' . $this->application_name . '.'); } else { $fqdn = $this->preview->fqdn; - $mail->subject('Coolify: Deployment failed of pull request #'.$this->preview->pull_request_id.' of '.$this->application_name.'.'); + $mail->subject('Coolify: Deployment failed of pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . '.'); } $mail->view('emails.application-deployment-failed', [ 'name' => $this->application_name, @@ -78,7 +79,7 @@ class DeploymentFailed extends Notification implements ShouldQueue if ($this->preview) { $message = new DiscordMessage( title: ':cross_mark: Deployment failed', - description: 'Pull request: '.$this->preview->pull_request_id, + description: 'Pull request: ' . $this->preview->pull_request_id, color: DiscordMessage::errorColor(), isCritical: true, ); @@ -87,13 +88,13 @@ class DeploymentFailed extends Notification implements ShouldQueue $message->addField('Environment', $this->environment_name, true); $message->addField('Name', $this->application_name, true); - $message->addField('Deployment Logs', '[Link]('.$this->deployment_url.')'); + $message->addField('Deployment Logs', '[Link](' . $this->deployment_url . ')'); if ($this->fqdn) { $message->addField('Domain', $this->fqdn, true); } } else { if ($this->fqdn) { - $description = '[Open application]('.$this->fqdn.')'; + $description = '[Open application](' . $this->fqdn . ')'; } else { $description = ''; } @@ -108,7 +109,7 @@ class DeploymentFailed extends Notification implements ShouldQueue $message->addField('Environment', $this->environment_name, true); $message->addField('Name', $this->application_name, true); - $message->addField('Deployment Logs', '[Link]('.$this->deployment_url.')'); + $message->addField('Deployment Logs', '[Link](' . $this->deployment_url . ')'); } return $message; @@ -117,9 +118,9 @@ class DeploymentFailed extends Notification implements ShouldQueue public function toTelegram(): array { if ($this->preview) { - $message = 'Coolify: Pull request #'.$this->preview->pull_request_id.' of '.$this->application_name.' ('.$this->preview->fqdn.') deployment failed: '; + $message = 'Coolify: Pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . ' (' . $this->preview->fqdn . ') deployment failed: '; } else { - $message = 'Coolify: Deployment failed of '.$this->application_name.' ('.$this->fqdn.'): '; + $message = 'Coolify: Deployment failed of ' . $this->application_name . ' (' . $this->fqdn . '): '; } $buttons[] = [ 'text' => 'Deployment logs', @@ -133,4 +134,31 @@ class DeploymentFailed extends Notification implements ShouldQueue ], ]; } + + public function toSlack(): SlackMessage + { + if ($this->preview) { + $title = "Pull request #{$this->preview->pull_request_id} deployment failed"; + $description = "Pull request deployment failed for {$this->application_name}"; + if ($this->preview->fqdn) { + $description .= "\nPreview URL: {$this->preview->fqdn}"; + } + } else { + $title = "Deployment failed"; + $description = "Deployment failed for {$this->application_name}"; + if ($this->fqdn) { + $description .= "\nApplication URL: {$this->fqdn}"; + } + } + + $description .= "\n\n**Project:** " . data_get($this->application, 'environment.project.name'); + $description .= "\n**Environment:** {$this->environment_name}"; + $description .= "\n**Deployment Logs:** {$this->deployment_url}"; + + return new SlackMessage( + title: $title, + description: $description, + color: SlackMessage::errorColor() + ); + } } diff --git a/app/Notifications/Application/DeploymentSuccess.php b/app/Notifications/Application/DeploymentSuccess.php index 946a622ca..e11d6db1c 100644 --- a/app/Notifications/Application/DeploymentSuccess.php +++ b/app/Notifications/Application/DeploymentSuccess.php @@ -9,7 +9,7 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; - +use App\Notifications\Dto\SlackMessage; class DeploymentSuccess extends Notification implements ShouldQueue { use Queueable; @@ -44,7 +44,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue if (str($this->fqdn)->explode(',')->count() > 1) { $this->fqdn = str($this->fqdn)->explode(',')->first(); } - $this->deployment_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}"; + $this->deployment_url = base_url() . "/project/{$this->project_uuid}/" . urlencode($this->environment_name) . "/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}"; } public function via(object $notifiable): array @@ -84,21 +84,21 @@ class DeploymentSuccess extends Notification implements ShouldQueue if ($this->preview) { $message = new DiscordMessage( title: ':white_check_mark: Preview deployment successful', - description: 'Pull request: '.$this->preview->pull_request_id, + description: 'Pull request: ' . $this->preview->pull_request_id, color: DiscordMessage::successColor(), ); if ($this->preview->fqdn) { - $message->addField('Application', '[Link]('.$this->preview->fqdn.')'); + $message->addField('Application', '[Link](' . $this->preview->fqdn . ')'); } $message->addField('Project', data_get($this->application, 'environment.project.name'), true); $message->addField('Environment', $this->environment_name, true); $message->addField('Name', $this->application_name, true); - $message->addField('Deployment logs', '[Link]('.$this->deployment_url.')'); + $message->addField('Deployment logs', '[Link](' . $this->deployment_url . ')'); } else { if ($this->fqdn) { - $description = '[Open application]('.$this->fqdn.')'; + $description = '[Open application](' . $this->fqdn . ')'; } else { $description = ''; } @@ -111,7 +111,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue $message->addField('Environment', $this->environment_name, true); $message->addField('Name', $this->application_name, true); - $message->addField('Deployment logs', '[Link]('.$this->deployment_url.')'); + $message->addField('Deployment logs', '[Link](' . $this->deployment_url . ')'); } return $message; @@ -120,7 +120,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue public function toTelegram(): array { if ($this->preview) { - $message = 'Coolify: New PR'.$this->preview->pull_request_id.' version successfully deployed of '.$this->application_name.''; + $message = 'Coolify: New PR' . $this->preview->pull_request_id . ' version successfully deployed of ' . $this->application_name . ''; if ($this->preview->fqdn) { $buttons[] = [ 'text' => 'Open Application', @@ -128,7 +128,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue ]; } } else { - $message = '✅ New version successfully deployed of '.$this->application_name.''; + $message = '✅ New version successfully deployed of ' . $this->application_name . ''; if ($this->fqdn) { $buttons[] = [ 'text' => 'Open Application', @@ -148,4 +148,32 @@ class DeploymentSuccess extends Notification implements ShouldQueue ], ]; } + + + public function toSlack(): SlackMessage + { + if ($this->preview) { + $title = "Pull request #{$this->preview->pull_request_id} successfully deployed"; + $description = "New version successfully deployed for {$this->application_name}"; + if ($this->preview->fqdn) { + $description .= "\nPreview URL: {$this->preview->fqdn}"; + } + } else { + $title = "New version successfully deployed"; + $description = "New version successfully deployed for {$this->application_name}"; + if ($this->fqdn) { + $description .= "\nApplication URL: {$this->fqdn}"; + } + } + + $description .= "\n\n**Project:** " . data_get($this->application, 'environment.project.name'); + $description .= "\n**Environment:** {$this->environment_name}"; + $description .= "\n**Deployment Logs:** {$this->deployment_url}"; + + return new SlackMessage( + title: $title, + description: $description, + color: SlackMessage::successColor() + ); + } } diff --git a/app/Notifications/Application/StatusChanged.php b/app/Notifications/Application/StatusChanged.php index 852c6b526..c7445cb70 100644 --- a/app/Notifications/Application/StatusChanged.php +++ b/app/Notifications/Application/StatusChanged.php @@ -8,7 +8,7 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; - +use App\Notifications\Dto\SlackMessage; class StatusChanged extends Notification implements ShouldQueue { use Queueable; @@ -34,7 +34,7 @@ class StatusChanged extends Notification implements ShouldQueue if (str($this->fqdn)->explode(',')->count() > 1) { $this->fqdn = str($this->fqdn)->explode(',')->first(); } - $this->resource_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->resource->uuid}"; + $this->resource_url = base_url() . "/project/{$this->project_uuid}/" . urlencode($this->environment_name) . "/application/{$this->resource->uuid}"; } public function via(object $notifiable): array @@ -60,7 +60,7 @@ class StatusChanged extends Notification implements ShouldQueue { return new DiscordMessage( title: ':cross_mark: Application stopped', - description: '[Open Application in Coolify]('.$this->resource_url.')', + description: '[Open Application in Coolify](' . $this->resource_url . ')', color: DiscordMessage::errorColor(), isCritical: true, ); @@ -68,7 +68,7 @@ class StatusChanged extends Notification implements ShouldQueue public function toTelegram(): array { - $message = 'Coolify: '.$this->resource_name.' has been stopped.'; + $message = 'Coolify: ' . $this->resource_name . ' has been stopped.'; return [ 'message' => $message, @@ -80,4 +80,20 @@ class StatusChanged extends Notification implements ShouldQueue ], ]; } + + public function toSlack(): SlackMessage + { + $title = "Application stopped"; + $description = "{$this->resource_name} has been stopped"; + + $description .= "\n\n**Project:** " . data_get($this->resource, 'environment.project.name'); + $description .= "\n**Environment:** {$this->environment_name}"; + $description .= "\n**Application URL:** {$this->resource_url}"; + + return new SlackMessage( + title: $title, + description: $description, + color: SlackMessage::errorColor() + ); + } } diff --git a/app/Notifications/Channels/SendsSlack.php b/app/Notifications/Channels/SendsSlack.php new file mode 100644 index 000000000..417d4adda --- /dev/null +++ b/app/Notifications/Channels/SendsSlack.php @@ -0,0 +1,8 @@ +toSlack(); + $webhookUrl = $notifiable->routeNotificationForSlack(); + if (!$webhookUrl) { + return; + } + dispatch(new SendMessageToSlackJob($message, $webhookUrl))->onQueue('high'); + } +} \ No newline at end of file diff --git a/app/Notifications/Container/ContainerRestarted.php b/app/Notifications/Container/ContainerRestarted.php index 182a1f5fc..40bd9b7ee 100644 --- a/app/Notifications/Container/ContainerRestarted.php +++ b/app/Notifications/Container/ContainerRestarted.php @@ -8,14 +8,16 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; - +use App\Notifications\Dto\SlackMessage; class ContainerRestarted extends Notification implements ShouldQueue { use Queueable; public $tries = 1; - public function __construct(public string $name, public Server $server, public ?string $url = null) {} + public function __construct(public string $name, public Server $server, public ?string $url = null) + { + } public function via(object $notifiable): array { @@ -44,7 +46,7 @@ class ContainerRestarted extends Notification implements ShouldQueue ); if ($this->url) { - $message->addField('Resource', '[Link]('.$this->url.')'); + $message->addField('Resource', '[Link](' . $this->url . ')'); } return $message; @@ -69,4 +71,20 @@ class ContainerRestarted extends Notification implements ShouldQueue return $payload; } + + public function toSlack(): SlackMessage + { + $title = "Resource restarted"; + $description = "A resource ({$this->name}) has been restarted automatically on {$this->server->name}"; + + if ($this->url) { + $description .= "\n**Resource URL:** {$this->url}"; + } + + return new SlackMessage( + title: $title, + description: $description, + color: SlackMessage::warningColor() + ); + } } diff --git a/app/Notifications/Container/ContainerStopped.php b/app/Notifications/Container/ContainerStopped.php index 33a55c65a..9b3824624 100644 --- a/app/Notifications/Container/ContainerStopped.php +++ b/app/Notifications/Container/ContainerStopped.php @@ -8,14 +8,16 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; - +use App\Notifications\Dto\SlackMessage; class ContainerStopped extends Notification implements ShouldQueue { use Queueable; public $tries = 1; - public function __construct(public string $name, public Server $server, public ?string $url = null) {} + public function __construct(public string $name, public Server $server, public ?string $url = null) + { + } public function via(object $notifiable): array { @@ -44,7 +46,7 @@ class ContainerStopped extends Notification implements ShouldQueue ); if ($this->url) { - $message->addField('Resource', '[Link]('.$this->url.')'); + $message->addField('Resource', '[Link](' . $this->url . ')'); } return $message; @@ -69,4 +71,20 @@ class ContainerStopped extends Notification implements ShouldQueue return $payload; } + + public function toSlack(): SlackMessage + { + $title = "Resource stopped"; + $description = "A resource ({$this->name}) has been stopped unexpectedly on {$this->server->name}"; + + if ($this->url) { + $description .= "\n**Resource URL:** {$this->url}"; + } + + return new SlackMessage( + title: $title, + description: $description, + color: SlackMessage::errorColor() + ); + } } diff --git a/app/Notifications/Database/BackupFailed.php b/app/Notifications/Database/BackupFailed.php index 8e2733339..137a6d730 100644 --- a/app/Notifications/Database/BackupFailed.php +++ b/app/Notifications/Database/BackupFailed.php @@ -8,7 +8,7 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; - +use App\Notifications\Dto\SlackMessage; class BackupFailed extends Notification implements ShouldQueue { use Queueable; @@ -69,4 +69,19 @@ class BackupFailed extends Notification implements ShouldQueue 'message' => $message, ]; } + + public function toSlack(): SlackMessage + { + $title = "Database backup failed"; + $description = "Database backup for {$this->name} (db:{$this->database_name}) has FAILED."; + + $description .= "\n\n**Frequency:** {$this->frequency}"; + $description .= "\n\n**Error Output:**\n{$this->output}"; + + return new SlackMessage( + title: $title, + description: $description, + color: SlackMessage::errorColor() + ); + } } diff --git a/app/Notifications/Database/BackupSuccess.php b/app/Notifications/Database/BackupSuccess.php index 5128c8ed6..76ddc9221 100644 --- a/app/Notifications/Database/BackupSuccess.php +++ b/app/Notifications/Database/BackupSuccess.php @@ -8,7 +8,7 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; - +use App\Notifications\Dto\SlackMessage; class BackupSuccess extends Notification implements ShouldQueue { use Queueable; @@ -66,4 +66,18 @@ class BackupSuccess extends Notification implements ShouldQueue 'message' => $message, ]; } + + public function toSlack(): SlackMessage + { + $title = "Database backup successful"; + $description = "Database backup for {$this->name} (db:{$this->database_name}) was successful."; + + $description .= "\n\n**Frequency:** {$this->frequency}"; + + return new SlackMessage( + title: $title, + description: $description, + color: SlackMessage::successColor() + ); + } } diff --git a/app/Notifications/Dto/SlackMessage.php b/app/Notifications/Dto/SlackMessage.php new file mode 100644 index 000000000..efd0cf5e6 --- /dev/null +++ b/app/Notifications/Dto/SlackMessage.php @@ -0,0 +1,28 @@ + $this->message, ]; } + + public function toSlack(): SlackMessage + { + return new SlackMessage( + title: 'Coolify: General Notification', + description: $this->message, + color: SlackMessage::infoColor(), + ); + } } diff --git a/app/Notifications/ScheduledTask/TaskFailed.php b/app/Notifications/ScheduledTask/TaskFailed.php index c3501a8eb..99d83fdf6 100644 --- a/app/Notifications/ScheduledTask/TaskFailed.php +++ b/app/Notifications/ScheduledTask/TaskFailed.php @@ -8,7 +8,7 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; - +use App\Notifications\Dto\SlackMessage; class TaskFailed extends Notification implements ShouldQueue { use Queueable; @@ -55,7 +55,7 @@ class TaskFailed extends Notification implements ShouldQueue ); if ($this->url) { - $message->addField('Scheduled task', '[Link]('.$this->url.')'); + $message->addField('Scheduled task', '[Link](' . $this->url . ')'); } return $message; @@ -75,4 +75,24 @@ class TaskFailed extends Notification implements ShouldQueue 'message' => $message, ]; } + + public function toSlack(): SlackMessage + { + $title = "Scheduled task failed"; + $description = "Scheduled task ({$this->task->name}) failed."; + + if ($this->output) { + $description .= "\n\n**Error Output:**\n{$this->output}"; + } + + if ($this->url) { + $description .= "\n\n**Task URL:** {$this->url}"; + } + + return new SlackMessage( + title: $title, + description: $description, + color: SlackMessage::errorColor() + ); + } } diff --git a/app/Notifications/Server/DockerCleanup.php b/app/Notifications/Server/DockerCleanup.php index 7ea1b84c2..eb4498835 100644 --- a/app/Notifications/Server/DockerCleanup.php +++ b/app/Notifications/Server/DockerCleanup.php @@ -6,6 +6,7 @@ use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\TelegramChannel; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\SlackMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Notification; @@ -16,7 +17,9 @@ class DockerCleanup extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public Server $server, public string $message) {} + public function __construct(public Server $server, public string $message) + { + } public function via(object $notifiable): array { @@ -24,7 +27,7 @@ class DockerCleanup extends Notification implements ShouldQueue // $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); - + $isSlackEnabled = data_get($notifiable, 'slack_enabled'); if ($isDiscordEnabled) { $channels[] = DiscordChannel::class; } @@ -34,6 +37,9 @@ class DockerCleanup extends Notification implements ShouldQueue if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + if ($isSlackEnabled) { + $channels[] = SlackChannel::class; + } return $channels; } @@ -65,4 +71,13 @@ class DockerCleanup extends Notification implements ShouldQueue 'message' => "Coolify: Server '{$this->server->name}' cleanup job done!\n\n{$this->message}", ]; } + + public function toSlack(): SlackMessage + { + return new SlackMessage( + title: 'Server cleanup job done', + description: "Server '{$this->server->name}' cleanup job done!\n\n{$this->message}", + color: SlackMessage::successColor() + ); + } } diff --git a/app/Notifications/Server/ForceDisabled.php b/app/Notifications/Server/ForceDisabled.php index a26c803ee..969f60d79 100644 --- a/app/Notifications/Server/ForceDisabled.php +++ b/app/Notifications/Server/ForceDisabled.php @@ -6,7 +6,9 @@ use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Channels\SlackChannel; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\SlackMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -18,7 +20,9 @@ class ForceDisabled extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public Server $server) {} + public function __construct(public Server $server) + { + } public function via(object $notifiable): array { @@ -26,7 +30,7 @@ class ForceDisabled extends Notification implements ShouldQueue $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); - + $isSlackEnabled = data_get($notifiable, 'slack_enabled'); if ($isDiscordEnabled) { $channels[] = DiscordChannel::class; } @@ -36,6 +40,9 @@ class ForceDisabled extends Notification implements ShouldQueue if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + if ($isSlackEnabled) { + $channels[] = SlackChannel::class; + } return $channels; } @@ -70,4 +77,19 @@ class ForceDisabled extends Notification implements ShouldQueue 'message' => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).", ]; } + + + public function toSlack(): SlackMessage + { + $title = "Server disabled"; + $description = "Server ({$this->server->name}) disabled because it is not paid!\n"; + $description .= "All automations and integrations are stopped.\n\n"; + $description .= "Please update your subscription to enable the server again: https://app.coolify.io/subscriptions"; + + return new SlackMessage( + title: $title, + description: $description, + color: SlackMessage::errorColor() + ); + } } diff --git a/app/Notifications/Server/ForceEnabled.php b/app/Notifications/Server/ForceEnabled.php index 65b65a10c..e24136c81 100644 --- a/app/Notifications/Server/ForceEnabled.php +++ b/app/Notifications/Server/ForceEnabled.php @@ -6,7 +6,9 @@ use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Channels\SlackChannel; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\SlackMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -18,7 +20,9 @@ class ForceEnabled extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public Server $server) {} + public function __construct(public Server $server) + { + } public function via(object $notifiable): array { @@ -26,7 +30,7 @@ class ForceEnabled extends Notification implements ShouldQueue $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); - + $isSlackEnabled = data_get($notifiable, 'slack_enabled'); if ($isDiscordEnabled) { $channels[] = DiscordChannel::class; } @@ -36,6 +40,9 @@ class ForceEnabled extends Notification implements ShouldQueue if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + if ($isSlackEnabled) { + $channels[] = SlackChannel::class; + } return $channels; } @@ -66,4 +73,15 @@ class ForceEnabled extends Notification implements ShouldQueue 'message' => "Coolify: Server ({$this->server->name}) enabled again!", ]; } + + + public function toSlack(): SlackMessage + { + return new SlackMessage( + title: 'Server enabled', + description: "Server '{$this->server->name}' enabled again!", + color: SlackMessage::successColor() + ); + } + } diff --git a/app/Notifications/Server/HighDiskUsage.php b/app/Notifications/Server/HighDiskUsage.php index e373abc03..a780d9d15 100644 --- a/app/Notifications/Server/HighDiskUsage.php +++ b/app/Notifications/Server/HighDiskUsage.php @@ -4,6 +4,7 @@ namespace App\Notifications\Server; use App\Models\Server; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\SlackMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -15,7 +16,9 @@ class HighDiskUsage extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public Server $server, public int $disk_usage, public int $server_disk_usage_notification_threshold) {} + public function __construct(public Server $server, public int $disk_usage, public int $server_disk_usage_notification_threshold) + { + } public function via(object $notifiable): array { @@ -47,7 +50,7 @@ class HighDiskUsage extends Notification implements ShouldQueue $message->addField('Disk usage', "{$this->disk_usage}%", true); $message->addField('Threshold', "{$this->server_disk_usage_notification_threshold}%", true); $message->addField('What to do?', '[Link](https://coolify.io/docs/knowledge-base/server/automated-cleanup)', true); - $message->addField('Change Settings', '[Threshold]('.base_url().'/server/'.$this->server->uuid.'#advanced) | [Notification]('.base_url().'/notifications/discord)'); + $message->addField('Change Settings', '[Threshold](' . base_url() . '/server/' . $this->server->uuid . '#advanced) | [Notification](' . base_url() . '/notifications/discord)'); return $message; } @@ -58,4 +61,22 @@ class HighDiskUsage extends Notification implements ShouldQueue 'message' => "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->server_disk_usage_notification_threshold}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.", ]; } + + public function toSlack(): SlackMessage + { + $description = "Server '{$this->server->name}' high disk usage detected!\n"; + $description .= "Disk usage: {$this->disk_usage}%\n"; + $description .= "Threshold: {$this->server_disk_usage_notification_threshold}%\n\n"; + $description .= "Please cleanup your disk to prevent data-loss.\n"; + $description .= "Tips for cleanup: https://coolify.io/docs/knowledge-base/server/automated-cleanup\n"; + $description .= "Change settings:\n"; + $description .= "- Threshold: " . base_url() . "/server/" . $this->server->uuid . "#advanced\n"; + $description .= "- Notifications: " . base_url() . "/notifications/discord"; + + return new SlackMessage( + title: 'High disk usage detected', + description: $description, + color: SlackMessage::errorColor() + ); + } } diff --git a/app/Notifications/Server/Reachable.php b/app/Notifications/Server/Reachable.php index 9b54501d9..6ca4170f5 100644 --- a/app/Notifications/Server/Reachable.php +++ b/app/Notifications/Server/Reachable.php @@ -6,7 +6,9 @@ use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Channels\SlackChannel; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\SlackMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -23,7 +25,7 @@ class Reachable extends Notification implements ShouldQueue public function __construct(public Server $server) { $this->isRateLimited = isEmailRateLimited( - limiterKey: 'server-reachable:'.$this->server->id, + limiterKey: 'server-reachable:' . $this->server->id, ); } @@ -37,7 +39,7 @@ class Reachable extends Notification implements ShouldQueue $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); - + $isSlackEnabled = data_get($notifiable, 'slack_enabled'); if ($isDiscordEnabled) { $channels[] = DiscordChannel::class; } @@ -47,6 +49,9 @@ class Reachable extends Notification implements ShouldQueue if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + if ($isSlackEnabled) { + $channels[] = SlackChannel::class; + } return $channels; } @@ -77,4 +82,16 @@ class Reachable extends Notification implements ShouldQueue 'message' => "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!", ]; } + + + + + public function toSlack(): SlackMessage + { + return new SlackMessage( + title: "Server revived", + description: "Server '{$this->server->name}' revived.\nAll automations & integrations are turned on again!", + color: SlackMessage::successColor() + ); + } } diff --git a/app/Notifications/Server/Unreachable.php b/app/Notifications/Server/Unreachable.php index 5bc568e82..cdb18b1bd 100644 --- a/app/Notifications/Server/Unreachable.php +++ b/app/Notifications/Server/Unreachable.php @@ -6,7 +6,9 @@ use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Channels\SlackChannel; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\SlackMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -23,7 +25,7 @@ class Unreachable extends Notification implements ShouldQueue public function __construct(public Server $server) { $this->isRateLimited = isEmailRateLimited( - limiterKey: 'server-unreachable:'.$this->server->id, + limiterKey: 'server-unreachable:' . $this->server->id, ); } @@ -37,6 +39,7 @@ class Unreachable extends Notification implements ShouldQueue $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); + $isSlackEnabled = data_get($notifiable, 'slack_enabled'); if ($isDiscordEnabled) { $channels[] = DiscordChannel::class; @@ -47,6 +50,9 @@ class Unreachable extends Notification implements ShouldQueue if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + if ($isSlackEnabled) { + $channels[] = SlackChannel::class; + } return $channels; } @@ -81,4 +87,18 @@ class Unreachable extends Notification implements ShouldQueue 'message' => "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server and turn on all automations & integrations.", ]; } + + + public function toSlack(): SlackMessage + { + $description = "Your server '{$this->server->name}' is unreachable.\n"; + $description .= "All automations & integrations are turned off!\n\n"; + $description .= "*IMPORTANT:* We automatically try to revive your server and turn on all automations & integrations."; + + return new SlackMessage( + title: 'Server unreachable', + description: $description, + color: SlackMessage::errorColor() + ); + } } diff --git a/app/Notifications/Test.php b/app/Notifications/Test.php index a43b1e153..6aae641c4 100644 --- a/app/Notifications/Test.php +++ b/app/Notifications/Test.php @@ -3,6 +3,7 @@ namespace App\Notifications; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\SlackMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -15,7 +16,9 @@ class Test extends Notification implements ShouldQueue public $tries = 5; - public function __construct(public ?string $emails = null) {} + public function __construct(public ?string $emails = null) + { + } public function via(object $notifiable): array { @@ -47,7 +50,7 @@ class Test extends Notification implements ShouldQueue color: DiscordMessage::successColor(), ); - $message->addField(name: 'Dashboard', value: '[Link]('.base_url().')', inline: true); + $message->addField(name: 'Dashboard', value: '[Link](' . base_url() . ')', inline: true); return $message; } @@ -64,4 +67,12 @@ class Test extends Notification implements ShouldQueue ], ]; } + + public function toSlack(): SlackMessage + { + return new SlackMessage( + title: 'Test Slack Notification', + description: 'This is a test Slack notification from Coolify.' + ); + } } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 2f0a3ac2a..e8bfbbd64 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -26,6 +26,7 @@ use App\Models\Team; use App\Models\User; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; +use App\Notifications\Channels\SlackChannel; use App\Notifications\Channels\TelegramChannel; use App\Notifications\Internal\GeneralNotification; use Carbon\CarbonImmutable; @@ -66,27 +67,27 @@ function base_configuration_dir(): string } function application_configuration_dir(): string { - return base_configuration_dir().'/applications'; + return base_configuration_dir() . '/applications'; } function service_configuration_dir(): string { - return base_configuration_dir().'/services'; + return base_configuration_dir() . '/services'; } function database_configuration_dir(): string { - return base_configuration_dir().'/databases'; + return base_configuration_dir() . '/databases'; } function database_proxy_dir($uuid): string { - return base_configuration_dir()."/databases/$uuid/proxy"; + return base_configuration_dir() . "/databases/$uuid/proxy"; } function backup_dir(): string { - return base_configuration_dir().'/backups'; + return base_configuration_dir() . '/backups'; } function metrics_dir(): string { - return base_configuration_dir().'/metrics'; + return base_configuration_dir() . '/metrics'; } function generate_readme_file(string $name, string $updated_at): string @@ -114,15 +115,15 @@ function showBoarding(): bool } function refreshSession(?Team $team = null): void { - if (! $team) { + if (!$team) { if (Auth::user()->currentTeam()) { $team = Team::find(Auth::user()->currentTeam()->id); } else { $team = User::find(Auth::id())->teams->first(); } } - Cache::forget('team:'.Auth::id()); - Cache::remember('team:'.Auth::id(), 3600, function () use ($team) { + Cache::forget('team:' . Auth::id()); + Cache::remember('team:' . Auth::id(), 3600, function () use ($team) { return $team; }); session(['currentTeam' => $team]); @@ -154,7 +155,7 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n $message = null; } if ($customErrorMessage) { - $message = $customErrorMessage.' '.$message; + $message = $customErrorMessage . ' ' . $message; } if (isset($livewire)) { @@ -227,7 +228,7 @@ function generateSSHKey(string $type = 'rsa') function formatPrivateKey(string $privateKey) { $privateKey = trim($privateKey); - if (! str_ends_with($privateKey, "\n")) { + if (!str_ends_with($privateKey, "\n")) { $privateKey .= "\n"; } @@ -249,7 +250,7 @@ function is_transactional_emails_active(): bool function set_transanctional_email_settings(?InstanceSettings $settings = null): ?string { - if (! $settings) { + if (!$settings) { $settings = instanceSettings(); } config()->set('mail.from.address', data_get($settings, 'smtp_from_address')); @@ -349,7 +350,7 @@ function isSubscribed() function isProduction(): bool { - return ! isDev(); + return !isDev(); } function isDev(): bool { @@ -358,7 +359,7 @@ function isDev(): bool function isCloud(): bool { - return ! config('coolify.self_hosted'); + return !config('coolify.self_hosted'); } function translate_cron_expression($expression_to_validate): string @@ -397,14 +398,14 @@ function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null { $settings = instanceSettings(); $type = set_transanctional_email_settings($settings); - if (! $type) { + if (!$type) { throw new Exception('No email settings found.'); } if ($cc) { Mail::send( [], [], - fn (Message $message) => $message + fn(Message $message) => $message ->to($email) ->replyTo($email) ->cc($cc) @@ -415,7 +416,7 @@ function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null Mail::send( [], [], - fn (Message $message) => $message + fn(Message $message) => $message ->to($email) ->subject($mail->subject) ->html((string) $mail->render()) @@ -440,11 +441,13 @@ function setNotificationChannels($notifiable, $event) { $channels = []; $isEmailEnabled = isEmailEnabled($notifiable); + $isSlackEnabled = data_get($notifiable, 'slack_enabled'); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); $isSubscribedToEmailEvent = data_get($notifiable, "smtp_notifications_$event"); $isSubscribedToDiscordEvent = data_get($notifiable, "discord_notifications_$event"); $isSubscribedToTelegramEvent = data_get($notifiable, "telegram_notifications_$event"); + $isSubscribedToSlackEvent = data_get($notifiable, "slack_notifications_$event"); if ($isDiscordEnabled && $isSubscribedToDiscordEvent) { $channels[] = DiscordChannel::class; @@ -455,6 +458,9 @@ function setNotificationChannels($notifiable, $event) if ($isTelegramEnabled && $isSubscribedToTelegramEvent) { $channels[] = TelegramChannel::class; } + if ($isSlackEnabled && $isSubscribedToSlackEvent) { + $channels[] = SlackChannel::class; + } return $channels; } @@ -559,7 +565,7 @@ function getResourceByUuid(string $uuid, ?int $teamId = null) return null; } $resource = queryResourcesByUuid($uuid); - if (! is_null($resource) && $resource->environment->project->team_id === $teamId) { + if (!is_null($resource) && $resource->environment->project->team_id === $teamId) { return $resource; } @@ -651,29 +657,29 @@ function queryResourcesByUuid(string $uuid) function generateTagDeployWebhook($tag_name) { $baseUrl = base_url(); - $api = Url::fromString($baseUrl).'/api/v1'; + $api = Url::fromString($baseUrl) . '/api/v1'; $endpoint = "/deploy?tag=$tag_name"; - return $api.$endpoint; + return $api . $endpoint; } function generateDeployWebhook($resource) { $baseUrl = base_url(); - $api = Url::fromString($baseUrl).'/api/v1'; + $api = Url::fromString($baseUrl) . '/api/v1'; $endpoint = '/deploy'; $uuid = data_get($resource, 'uuid'); - return $api.$endpoint."?uuid=$uuid&force=false"; + return $api . $endpoint . "?uuid=$uuid&force=false"; } function generateGitManualWebhook($resource, $type) { - if ($resource->source_id !== 0 && ! is_null($resource->source_id)) { + if ($resource->source_id !== 0 && !is_null($resource->source_id)) { return null; } if ($resource->getMorphClass() === \App\Models\Application::class) { $baseUrl = base_url(); - return Url::fromString($baseUrl)."/webhooks/source/$type/events/manual"; + return Url::fromString($baseUrl) . "/webhooks/source/$type/events/manual"; } return null; @@ -700,7 +706,7 @@ function getTopLevelNetworks(Service|Application $resource) $hasHostNetworkMode = data_get($service, 'network_mode') === 'host' ? true : false; // Only add 'networks' key if 'network_mode' is not 'host' - if (! $hasHostNetworkMode) { + if (!$hasHostNetworkMode) { // Collect/create/update networks if ($serviceNetworks->count() > 0) { foreach ($serviceNetworks as $networkName => $networkDetails) { @@ -714,7 +720,7 @@ function getTopLevelNetworks(Service|Application $resource) $networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) { return $value == $networkName || $key == $networkName; }); - if (! $networkExists) { + if (!$networkExists) { if (is_string($networkDetails) || is_int($networkDetails)) { $topLevelNetworks->put($networkDetails, null); } @@ -725,7 +731,7 @@ function getTopLevelNetworks(Service|Application $resource) $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { return $value == $definedNetwork; }); - if (! $definedNetworkExists) { + if (!$definedNetworkExists) { foreach ($definedNetwork as $network) { $topLevelNetworks->put($network, [ 'name' => $network, @@ -766,7 +772,7 @@ function getTopLevelNetworks(Service|Application $resource) $networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) { return $value == $networkName || $key == $networkName; }); - if (! $networkExists) { + if (!$networkExists) { if (is_string($networkDetails) || is_int($networkDetails)) { $topLevelNetworks->put($networkDetails, null); } @@ -776,7 +782,7 @@ function getTopLevelNetworks(Service|Application $resource) $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { return $value == $definedNetwork; }); - if (! $definedNetworkExists) { + if (!$definedNetworkExists) { foreach ($definedNetwork as $network) { $topLevelNetworks->put($network, [ 'name' => $network, @@ -912,7 +918,7 @@ function generateEnvValue(string $command, Service|Application|null $service = n case 'PASSWORD_64': $generatedValue = Str::password(length: 64, symbols: false); break; - // This is not base64, it's just a random string + // This is not base64, it's just a random string case 'BASE64_64': $generatedValue = Str::random(64); break; @@ -923,7 +929,7 @@ function generateEnvValue(string $command, Service|Application|null $service = n case 'BASE64_32': $generatedValue = Str::random(32); break; - // This is base64, + // This is base64, case 'REALBASE64_64': $generatedValue = base64_encode(Str::random(64)); break; @@ -1014,7 +1020,7 @@ function validate_dns_entry(string $fqdn, Server $server) } $settings = instanceSettings(); $is_dns_validation_enabled = data_get($settings, 'is_dns_validation_enabled'); - if (! $is_dns_validation_enabled) { + if (!$is_dns_validation_enabled) { return true; } $dns_servers = data_get($settings, 'custom_dns_servers'); @@ -1032,7 +1038,7 @@ function validate_dns_entry(string $fqdn, Server $server) $query = new DNSQuery($dns_server); $results = $query->query($host, $type); if ($results === false || $query->hasError()) { - ray('Error: '.$query->getLasterror()); + ray('Error: ' . $query->getLasterror()); } else { foreach ($results as $result) { if ($result->getType() == $type) { @@ -1042,7 +1048,7 @@ function validate_dns_entry(string $fqdn, Server $server) break; } if ($result->getData() === $ip) { - ray($host.' has IP address '.$result->getData()); + ray($host . ' has IP address ' . $result->getData()); ray($result->getString()); $found_matching_ip = true; break; @@ -1090,15 +1096,15 @@ function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId = $applications = Application::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid']); $serviceApplications = ServiceApplication::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid']); if ($uuid) { - $applications = $applications->filter(fn ($app) => $app->uuid !== $uuid); - $serviceApplications = $serviceApplications->filter(fn ($app) => $app->uuid !== $uuid); + $applications = $applications->filter(fn($app) => $app->uuid !== $uuid); + $serviceApplications = $serviceApplications->filter(fn($app) => $app->uuid !== $uuid); } $domainFound = false; foreach ($applications as $app) { if (is_null($app->fqdn)) { continue; } - $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); + $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn($fqdn) => $fqdn !== ''); foreach ($list_of_domains as $domain) { if (str($domain)->endsWith('/')) { $domain = str($domain)->beforeLast('/'); @@ -1117,7 +1123,7 @@ function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId = if (str($app->fqdn)->isEmpty()) { continue; } - $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); + $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn($fqdn) => $fqdn !== ''); foreach ($list_of_domains as $domain) { if (str($domain)->endsWith('/')) { $domain = str($domain)->beforeLast('/'); @@ -1167,7 +1173,7 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null }); $apps = Application::all(); foreach ($apps as $app) { - $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); + $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn($fqdn) => $fqdn !== ''); foreach ($list_of_domains as $domain) { if (str($domain)->endsWith('/')) { $domain = str($domain)->beforeLast('/'); @@ -1186,7 +1192,7 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null } $apps = ServiceApplication::all(); foreach ($apps as $app) { - $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); + $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn($fqdn) => $fqdn !== ''); foreach ($list_of_domains as $domain) { if (str($domain)->endsWith('/')) { $domain = str($domain)->beforeLast('/'); @@ -1222,7 +1228,7 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array { $commands = $commands->map(function ($line) { if ( - ! str(trim($line))->startsWith([ + !str(trim($line))->startsWith([ 'cd', 'command', 'echo', @@ -1243,7 +1249,7 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array $commands = $commands->map(function ($line) use ($server) { if (Str::startsWith($line, 'sudo mkdir -p')) { - return "$line && sudo chown -R $server->user:$server->user ".Str::after($line, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($line, 'sudo mkdir -p'); + return "$line && sudo chown -R $server->user:$server->user " . Str::after($line, 'sudo mkdir -p') . ' && sudo chmod -R o-rwx ' . Str::after($line, 'sudo mkdir -p'); } return $line; @@ -1271,11 +1277,11 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array } function parseLineForSudo(string $command, Server $server): string { - if (! str($command)->startSwith('cd') && ! str($command)->startSwith('command')) { + if (!str($command)->startSwith('cd') && !str($command)->startSwith('command')) { $command = "sudo $command"; } if (Str::startsWith($command, 'sudo mkdir -p')) { - $command = "$command && sudo chown -R $server->user:$server->user ".Str::after($command, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($command, 'sudo mkdir -p'); + $command = "$command && sudo chown -R $server->user:$server->user " . Str::after($command, 'sudo mkdir -p') . ' && sudo chmod -R o-rwx ' . Str::after($command, 'sudo mkdir -p'); } if (str($command)->contains('$(') || str($command)->contains('`')) { $command = str($command)->replace('$(', '$(sudo ')->replace('`', '`sudo ')->value(); @@ -1397,7 +1403,7 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull $isDirectory = data_get($foundConfig, 'is_directory'); } else { $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null); - if ((is_null($isDirectory) || ! $isDirectory) && is_null($content)) { + if ((is_null($isDirectory) || !$isDirectory) && is_null($content)) { // if isDirectory is not set (or false) & content is also not set, we assume it is a directory ray('setting isDirectory to true'); $isDirectory = true; @@ -1412,9 +1418,9 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull return $volume; } if (get_class($resource) === \App\Models\Application::class) { - $dir = base_configuration_dir().'/applications/'.$resource->uuid; + $dir = base_configuration_dir() . '/applications/' . $resource->uuid; } else { - $dir = base_configuration_dir().'/services/'.$resource->service->uuid; + $dir = base_configuration_dir() . '/services/' . $resource->service->uuid; } if ($source->startsWith('.')) { @@ -1424,9 +1430,9 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull $source = $source->replaceFirst('~', $dir); } if ($pull_request_id !== 0) { - $source = $source."-pr-$pull_request_id"; + $source = $source . "-pr-$pull_request_id"; } - if (! $resource?->settings?->is_preserve_repository_enabled || $foundConfig?->is_based_on_git) { + if (!$resource?->settings?->is_preserve_repository_enabled || $foundConfig?->is_based_on_git) { LocalFileVolume::updateOrCreate( [ 'mount_path' => $target, @@ -1555,7 +1561,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($serviceLabels->count() > 0) { $removedLabels = collect([]); $serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) { - if (! str($serviceLabel)->contains('=')) { + if (!str($serviceLabel)->contains('=')) { $removedLabels->put($serviceLabelName, $serviceLabel); return false; @@ -1637,7 +1643,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) { return $value == $networkName || $key == $networkName; }); - if (! $networkExists) { + if (!$networkExists) { if (is_string($networkDetails) || is_int($networkDetails)) { $topLevelNetworks->put($networkDetails, null); } @@ -1663,12 +1669,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $savedService->ports = $collectedPorts->implode(','); $savedService->save(); - if (! $hasHostNetworkMode) { + if (!$hasHostNetworkMode) { // Add Coolify specific networks $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { return $value == $definedNetwork; }); - if (! $definedNetworkExists) { + if (!$definedNetworkExists) { foreach ($definedNetwork as $network) { $topLevelNetworks->put($network, [ 'name' => $network, @@ -1880,9 +1886,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $fqdn = "$fqdn$path"; } - if (! $isDatabase) { + if (!$isDatabase) { if ($savedService->fqdn) { - data_set($savedService, 'fqdn', $savedService->fqdn.','.$fqdn); + data_set($savedService, 'fqdn', $savedService->fqdn . ',' . $fqdn); } else { data_set($savedService, 'fqdn', $fqdn); } @@ -1897,7 +1903,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal ]); } // Caddy needs exact port in some cases. - if ($predefinedPort && ! $key->endsWith("_{$predefinedPort}")) { + if ($predefinedPort && !$key->endsWith("_{$predefinedPort}")) { $fqdns_exploded = str($savedService->fqdn)->explode(','); if ($fqdns_exploded->count() > 1) { continue; @@ -1937,12 +1943,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'service_id' => $resource->id, ])->first(); ['command' => $command, 'forService' => $forService, 'generatedValue' => $generatedValue, 'port' => $port] = parseEnvVariable($value); - if (! is_null($command)) { + if (!is_null($command)) { if ($command?->value() === 'FQDN' || $command?->value() === 'URL') { if (Str::lower($forService) === $serviceName) { $fqdn = generateFqdn($resource->server, $containerName); } else { - $fqdn = generateFqdn($resource->server, Str::lower($forService).'-'.$resource->uuid); + $fqdn = generateFqdn($resource->server, Str::lower($forService) . '-' . $resource->uuid); } if ($port) { $fqdn = "$fqdn:$port"; @@ -1972,13 +1978,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'is_preview' => false, ]); } - if (! $isDatabase) { - if ($command->value() === 'FQDN' && is_null($savedService->fqdn) && ! $foundEnv) { + if (!$isDatabase) { + if ($command->value() === 'FQDN' && is_null($savedService->fqdn) && !$foundEnv) { $savedService->fqdn = $fqdn; $savedService->save(); } // Caddy needs exact port in some cases. - if ($predefinedPort && ! $key->endsWith("_{$predefinedPort}") && $command?->value() === 'FQDN' && $resource->server->proxyType() === 'CADDY') { + if ($predefinedPort && !$key->endsWith("_{$predefinedPort}") && $command?->value() === 'FQDN' && $resource->server->proxyType() === 'CADDY') { $fqdns_exploded = str($savedService->fqdn)->explode(','); if ($fqdns_exploded->count() > 1) { continue; @@ -2000,7 +2006,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } } else { $generatedValue = generateEnvValue($command, $resource); - if (! $foundEnv) { + if (!$foundEnv) { EnvironmentVariable::create([ 'key' => $key, 'value' => $generatedValue, @@ -2055,7 +2061,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } $defaultLabels = defaultLabels($resource->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id); $serviceLabels = $serviceLabels->merge($defaultLabels); - if (! $isDatabase && $fqdns->count() > 0) { + if (!$isDatabase && $fqdns->count() > 0) { if ($fqdns) { $shouldGenerateLabelsExactly = $resource->server->settings->generate_exact_labels; if ($shouldGenerateLabelsExactly) { @@ -2123,7 +2129,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } data_set($service, 'labels', $serviceLabels->toArray()); data_forget($service, 'is_database'); - if (! data_get($service, 'restart')) { + if (!data_get($service, 'restart')) { data_set($service, 'restart', RESTART_MODE); } if (data_get($service, 'restart') === 'no' || data_get($service, 'exclude_from_hc')) { @@ -2162,21 +2168,21 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $parsedServiceVariables->put('COOLIFY_CONTAINER_NAME', "$serviceName-{$resource->uuid}"); // TODO: move this in a shared function - if (! $parsedServiceVariables->has('COOLIFY_APP_NAME')) { + if (!$parsedServiceVariables->has('COOLIFY_APP_NAME')) { $parsedServiceVariables->put('COOLIFY_APP_NAME', "\"{$resource->name}\""); } - if (! $parsedServiceVariables->has('COOLIFY_SERVER_IP')) { + if (!$parsedServiceVariables->has('COOLIFY_SERVER_IP')) { $parsedServiceVariables->put('COOLIFY_SERVER_IP', "\"{$resource->destination->server->ip}\""); } - if (! $parsedServiceVariables->has('COOLIFY_ENVIRONMENT_NAME')) { + if (!$parsedServiceVariables->has('COOLIFY_ENVIRONMENT_NAME')) { $parsedServiceVariables->put('COOLIFY_ENVIRONMENT_NAME', "\"{$resource->environment->name}\""); } - if (! $parsedServiceVariables->has('COOLIFY_PROJECT_NAME')) { + if (!$parsedServiceVariables->has('COOLIFY_PROJECT_NAME')) { $parsedServiceVariables->put('COOLIFY_PROJECT_NAME', "\"{$resource->project()->name}\""); } $parsedServiceVariables = $parsedServiceVariables->map(function ($value, $key) use ($envs_from_coolify) { - if (! str($value)->startsWith('$')) { + if (!str($value)->startsWith('$')) { $found_env = $envs_from_coolify->where('key', $key)->first(); if ($found_env) { return $found_env->value; @@ -2260,7 +2266,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($serviceLabels->count() > 0) { $removedLabels = collect([]); $serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) { - if (! str($serviceLabel)->contains('=')) { + if (!str($serviceLabel)->contains('=')) { $removedLabels->put($serviceLabelName, $serviceLabel); return false; @@ -2280,11 +2286,11 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $pull_request_id) { if (is_string($volume)) { $volume = str($volume); - if ($volume->contains(':') && ! $volume->startsWith('/')) { + if ($volume->contains(':') && !$volume->startsWith('/')) { $name = $volume->before(':'); $mount = $volume->after(':'); if ($name->startsWith('.') || $name->startsWith('~')) { - $dir = base_configuration_dir().'/applications/'.$resource->uuid; + $dir = base_configuration_dir() . '/applications/' . $resource->uuid; if ($name->startsWith('.')) { $name = $name->replaceFirst('.', $dir); } @@ -2292,12 +2298,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $name = $name->replaceFirst('~', $dir); } if ($pull_request_id !== 0) { - $name = $name."-pr-$pull_request_id"; + $name = $name . "-pr-$pull_request_id"; } $volume = str("$name:$mount"); } else { if ($pull_request_id !== 0) { - $name = $name."-pr-$pull_request_id"; + $name = $name . "-pr-$pull_request_id"; $volume = str("$name:$mount"); if ($topLevelVolumes->has($name)) { $v = $topLevelVolumes->get($name); @@ -2336,7 +2342,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $name = $volume->before(':'); $mount = $volume->after(':'); if ($pull_request_id !== 0) { - $name = $name."-pr-$pull_request_id"; + $name = $name . "-pr-$pull_request_id"; } $volume = str("$name:$mount"); } @@ -2347,7 +2353,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $read_only = data_get($volume, 'read_only'); if ($source && $target) { if ((str($source)->startsWith('.') || str($source)->startsWith('~'))) { - $dir = base_configuration_dir().'/applications/'.$resource->uuid; + $dir = base_configuration_dir() . '/applications/' . $resource->uuid; if (str($source, '.')) { $source = str($source)->replaceFirst('.', $dir); } @@ -2355,23 +2361,23 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $source = str($source)->replaceFirst('~', $dir); } if ($pull_request_id !== 0) { - $source = $source."-pr-$pull_request_id"; + $source = $source . "-pr-$pull_request_id"; } if ($read_only) { - data_set($volume, 'source', $source.':'.$target.':ro'); + data_set($volume, 'source', $source . ':' . $target . ':ro'); } else { - data_set($volume, 'source', $source.':'.$target); + data_set($volume, 'source', $source . ':' . $target); } } else { if ($pull_request_id !== 0) { - $source = $source."-pr-$pull_request_id"; + $source = $source . "-pr-$pull_request_id"; } if ($read_only) { - data_set($volume, 'source', $source.':'.$target.':ro'); + data_set($volume, 'source', $source . ':' . $target . ':ro'); } else { - data_set($volume, 'source', $source.':'.$target); + data_set($volume, 'source', $source . ':' . $target); } - if (! str($source)->startsWith('/')) { + if (!str($source)->startsWith('/')) { if ($topLevelVolumes->has($source)) { $v = $topLevelVolumes->get($source); if (data_get($v, 'driver_opts.type') === 'cifs') { @@ -2404,11 +2410,11 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $pull_request_id) { if (is_string($volume)) { $volume = str($volume); - if ($volume->contains(':') && ! $volume->startsWith('/')) { + if ($volume->contains(':') && !$volume->startsWith('/')) { $name = $volume->before(':'); $mount = $volume->after(':'); if ($name->startsWith('.') || $name->startsWith('~')) { - $dir = base_configuration_dir().'/applications/'.$resource->uuid; + $dir = base_configuration_dir() . '/applications/' . $resource->uuid; if ($name->startsWith('.')) { $name = $name->replaceFirst('.', $dir); } @@ -2416,13 +2422,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $name = $name->replaceFirst('~', $dir); } if ($pull_request_id !== 0) { - $name = $name."-pr-$pull_request_id"; + $name = $name . "-pr-$pull_request_id"; } $volume = str("$name:$mount"); } else { if ($pull_request_id !== 0) { $uuid = $resource->uuid; - $name = $uuid."-$name-pr-$pull_request_id"; + $name = $uuid . "-$name-pr-$pull_request_id"; $volume = str("$name:$mount"); if ($topLevelVolumes->has($name)) { $v = $topLevelVolumes->get($name); @@ -2441,7 +2447,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } } else { $uuid = $resource->uuid; - $name = str($uuid."-$name"); + $name = str($uuid . "-$name"); $volume = str("$name:$mount"); if ($topLevelVolumes->has($name->value())) { $v = $topLevelVolumes->get($name->value()); @@ -2464,7 +2470,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $name = $volume->before(':'); $mount = $volume->after(':'); if ($pull_request_id !== 0) { - $name = $name."-pr-$pull_request_id"; + $name = $name . "-pr-$pull_request_id"; } $volume = str("$name:$mount"); } @@ -2476,7 +2482,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($source && $target) { $uuid = $resource->uuid; if ((str($source)->startsWith('.') || str($source)->startsWith('~') || str($source)->startsWith('/'))) { - $dir = base_configuration_dir().'/applications/'.$resource->uuid; + $dir = base_configuration_dir() . '/applications/' . $resource->uuid; if (str($source, '.')) { $source = str($source)->replaceFirst('.', $dir); } @@ -2484,22 +2490,22 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $source = str($source)->replaceFirst('~', $dir); } if ($read_only) { - data_set($volume, 'source', $source.':'.$target.':ro'); + data_set($volume, 'source', $source . ':' . $target . ':ro'); } else { - data_set($volume, 'source', $source.':'.$target); + data_set($volume, 'source', $source . ':' . $target); } } else { if ($pull_request_id === 0) { - $source = $uuid."-$source"; + $source = $uuid . "-$source"; } else { - $source = $uuid."-$source-pr-$pull_request_id"; + $source = $uuid . "-$source-pr-$pull_request_id"; } if ($read_only) { - data_set($volume, 'source', $source.':'.$target.':ro'); + data_set($volume, 'source', $source . ':' . $target . ':ro'); } else { - data_set($volume, 'source', $source.':'.$target); + data_set($volume, 'source', $source . ':' . $target); } - if (! str($source)->startsWith('/')) { + if (!str($source)->startsWith('/')) { if ($topLevelVolumes->has($source)) { $v = $topLevelVolumes->get($source); if (data_get($v, 'driver_opts.type') === 'cifs') { @@ -2532,7 +2538,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($pull_request_id !== 0 && count($serviceDependencies) > 0) { $serviceDependencies = $serviceDependencies->map(function ($dependency) use ($pull_request_id) { - return $dependency."-pr-$pull_request_id"; + return $dependency . "-pr-$pull_request_id"; }); data_set($service, 'depends_on', $serviceDependencies->toArray()); } @@ -2554,7 +2560,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) { return $value == $networkName || $key == $networkName; }); - if (! $networkExists) { + if (!$networkExists) { if (is_string($networkDetails) || is_int($networkDetails)) { $topLevelNetworks->put($networkDetails, null); } @@ -2582,7 +2588,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { return $value == $definedNetwork; }); - if (! $definedNetworkExists) { + if (!$definedNetworkExists) { foreach ($definedNetwork as $network) { if ($pull_request_id !== 0) { $topLevelNetworks->put($network, [ @@ -2700,12 +2706,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'application_id' => $resource->id, ])->first(); ['command' => $command, 'forService' => $forService, 'generatedValue' => $generatedValue, 'port' => $port] = parseEnvVariable($value); - if (! is_null($command)) { + if (!is_null($command)) { if ($command?->value() === 'FQDN' || $command?->value() === 'URL') { if (Str::lower($forService) === $serviceName) { $fqdn = generateFqdn($server, $containerName); } else { - $fqdn = generateFqdn($server, Str::lower($forService).'-'.$resource->uuid); + $fqdn = generateFqdn($server, Str::lower($forService) . '-' . $resource->uuid); } if ($port) { $fqdn = "$fqdn:$port"; @@ -2726,7 +2732,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } } else { $generatedValue = generateEnvValue($command); - if (! $foundEnv) { + if (!$foundEnv) { EnvironmentVariable::create([ 'key' => $key, 'value' => $generatedValue, @@ -2898,7 +2904,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } data_set($service, 'labels', $serviceLabels->toArray()); data_forget($service, 'is_database'); - if (! data_get($service, 'restart')) { + if (!data_get($service, 'restart')) { data_set($service, 'restart', RESTART_MODE); } data_set($service, 'container_name', $containerName); @@ -2909,7 +2915,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal }); if ($pull_request_id !== 0) { $services->each(function ($service, $serviceName) use ($pull_request_id, $services) { - $services[$serviceName."-pr-$pull_request_id"] = $service; + $services[$serviceName . "-pr-$pull_request_id"] = $service; data_forget($services, $serviceName); }); } @@ -2937,7 +2943,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $uuid = data_get($resource, 'uuid'); $compose = data_get($resource, 'docker_compose_raw'); - if (! $compose) { + if (!$compose) { return collect([]); } @@ -3330,29 +3336,29 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $isDirectory = data_get($foundConfig, 'is_directory'); } else { // if isDirectory is not set (or false) & content is also not set, we assume it is a directory - if ((is_null($isDirectory) || ! $isDirectory) && is_null($content)) { + if ((is_null($isDirectory) || !$isDirectory) && is_null($content)) { $isDirectory = true; } } } if ($type->value() === 'bind') { if ($source->value() === '/var/run/docker.sock') { - $volume = $source->value().':'.$target->value(); + $volume = $source->value() . ':' . $target->value(); } elseif ($source->value() === '/tmp' || $source->value() === '/tmp/') { - $volume = $source->value().':'.$target->value(); + $volume = $source->value() . ':' . $target->value(); } else { if ((int) $resource->compose_parsing_version >= 4) { if ($isApplication) { - $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); + $mainDirectory = str(base_configuration_dir() . '/applications/' . $uuid); } elseif ($isService) { - $mainDirectory = str(base_configuration_dir().'/services/'.$uuid); + $mainDirectory = str(base_configuration_dir() . '/services/' . $uuid); } } else { - $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); + $mainDirectory = str(base_configuration_dir() . '/applications/' . $uuid); } $source = replaceLocalSource($source, $mainDirectory); if ($isApplication && $isPullRequest) { - $source = $source."-pr-$pullRequestId"; + $source = $source . "-pr-$pullRequestId"; } LocalFileVolume::updateOrCreate( [ @@ -3372,12 +3378,12 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int if (isDev()) { if ((int) $resource->compose_parsing_version >= 4) { if ($isApplication) { - $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid); + $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/' . $uuid); } elseif ($isService) { - $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/services/'.$uuid); + $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/services/' . $uuid); } } else { - $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid); + $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/' . $uuid); } } $volume = "$source:$target"; @@ -3444,7 +3450,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $depends_on = $newDependsOn; } } - if (! $use_network_mode) { + if (!$use_network_mode) { if ($topLevel->get('networks')?->count() > 0) { foreach ($topLevel->get('networks') as $networkName => $network) { if ($networkName === 'default') { @@ -3457,7 +3463,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $networkExists = $networks->contains(function ($value, $key) use ($networkName) { return $value == $networkName || $key == $networkName; }); - if (! $networkExists) { + if (!$networkExists) { $networks->put($networkName, null); } } @@ -3465,7 +3471,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $baseNetworkExists = $networks->contains(function ($value, $_) use ($baseNetwork) { return $value == $baseNetwork; }); - if (! $baseNetworkExists) { + if (!$baseNetworkExists) { foreach ($baseNetwork as $network) { $topLevel->get('networks')->put($network, [ 'name' => $network, @@ -3497,7 +3503,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $networks_temp = collect(); - if (! $use_network_mode) { + if (!$use_network_mode) { foreach ($networks as $key => $network) { if (gettype($network) === 'string') { // networks: @@ -3528,7 +3534,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $normalEnvironments = $environment->diffKeys($allMagicEnvironments); $normalEnvironments = $normalEnvironments->filter(function ($value, $key) { - return ! str($value)->startsWith('SERVICE_'); + return !str($value)->startsWith('SERVICE_'); }); foreach ($normalEnvironments as $key => $value) { @@ -3548,7 +3554,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int continue; } - if (! $value->startsWith('$')) { + if (!$value->startsWith('$')) { continue; } if ($key->value() === $parsedValue->value()) { @@ -3679,7 +3685,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $defaultLabels = defaultLabels($resource->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id); } // Add COOLIFY_FQDN & COOLIFY_URL to environment - if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { + if (!$isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { $coolifyEnvironments->put('COOLIFY_URL', $fqdns->implode(',')); $urls = $fqdns->map(function ($fqdn) { @@ -3691,7 +3697,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int if ($environment->count() > 0) { $environment = $environment->filter(function ($value, $key) { - return ! str($key)->startsWith('SERVICE_FQDN_'); + return !str($key)->startsWith('SERVICE_FQDN_'); })->map(function ($value, $key) use ($resource) { // if value is empty, set it to null so if you set the environment variable in the .env file (Coolify's UI), it will used if (str($value)->isEmpty()) { @@ -3718,7 +3724,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int }); } } - if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { + if (!$isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { if ($isApplication) { $shouldGenerateLabelsExactly = $resource->destination->server->settings->generate_exact_labels; $uuid = $resource->uuid; @@ -3811,7 +3817,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int 'restart' => $restart->value(), 'labels' => $serviceLabels, ]); - if (! $use_network_mode) { + if (!$use_network_mode) { $payload['networks'] = $networks_temp; } if ($ports->count() > 0) { @@ -3871,7 +3877,7 @@ function isAssociativeArray($array) $array = $array->toArray(); } - if (! is_array($array)) { + if (!is_array($array)) { throw new \InvalidArgumentException('Input must be an array or a Collection.'); } @@ -3984,7 +3990,7 @@ function instanceSettings() function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id) { $server = Server::find($server_id)->where('team_id', $team_id)->first(); - if (! $server) { + if (!$server) { return; } $uuid = new Cuid2; @@ -4011,7 +4017,7 @@ function loadConfigFromGit(string $repository, string $branch, string $base_dire function loggy($message = null, array $context = []) { - if (! isDev()) { + if (!isDev()) { return; } if (function_exists('ray') && config('app.debug')) { @@ -4046,7 +4052,7 @@ function isEmailRateLimited(string $limiterKey, int $decaySeconds = 3600, ?calla $limiterKey, $maxAttempts = 0, function () use (&$rateLimited, &$limiterKey, $callbackOnSuccess) { - isDev() && loggy('Rate limit not reached for '.$limiterKey); + isDev() && loggy('Rate limit not reached for ' . $limiterKey); $rateLimited = false; if ($callbackOnSuccess) { @@ -4055,8 +4061,8 @@ function isEmailRateLimited(string $limiterKey, int $decaySeconds = 3600, ?calla }, $decaySeconds, ); - if (! $executed) { - isDev() && loggy('Rate limit reached for '.$limiterKey.'. Rate limiter will be disabled for '.$decaySeconds.' seconds.'); + if (!$executed) { + isDev() && loggy('Rate limit reached for ' . $limiterKey . '. Rate limiter will be disabled for ' . $decaySeconds . ' seconds.'); $rateLimited = true; } diff --git a/database/migrations/2024_11_12_213200_add_slack_notifications_to_teams_table.php b/database/migrations/2024_11_12_213200_add_slack_notifications_to_teams_table.php new file mode 100644 index 000000000..c3896a053 --- /dev/null +++ b/database/migrations/2024_11_12_213200_add_slack_notifications_to_teams_table.php @@ -0,0 +1,37 @@ +boolean('slack_enabled')->default(false); + $table->string('slack_webhook_url')->nullable(); + $table->boolean('slack_notifications_test')->default(false); + $table->boolean('slack_notifications_deployments')->default(false); + $table->boolean('slack_notifications_status_changes')->default(false); + $table->boolean('slack_notifications_database_backups')->default(false); + $table->boolean('slack_notifications_scheduled_tasks')->default(false); + $table->boolean('slack_notifications_server_disk_usage')->default(false); + }); + } + + public function down(): void + { + Schema::table('teams', function (Blueprint $table) { + $table->dropColumn([ + 'slack_enabled', + 'slack_webhook_url', + 'slack_notifications_test', + 'slack_notifications_deployments', + 'slack_notifications_status_changes', + 'slack_notifications_database_backups', + 'slack_notifications_scheduled_tasks', + 'slack_notifications_server_disk_usage', + ]); + }); + } +}; \ No newline at end of file diff --git a/resources/views/components/notification/navbar.blade.php b/resources/views/components/notification/navbar.blade.php index 0fbbc69a2..c4dbd25af 100644 --- a/resources/views/components/notification/navbar.blade.php +++ b/resources/views/components/notification/navbar.blade.php @@ -15,6 +15,10 @@ href="{{ route('notifications.discord') }}"> + + + - + \ No newline at end of file diff --git a/resources/views/livewire/notifications/slack.blade.php b/resources/views/livewire/notifications/slack.blade.php new file mode 100644 index 000000000..b3685173c --- /dev/null +++ b/resources/views/livewire/notifications/slack.blade.php @@ -0,0 +1,43 @@ +
+ + Notifications | Coolify + + +
+
+

Slack

+ + Save + + @if ($slackEnabled) + + Send Test Notifications + + @endif +
+
+ +
+ + + @if ($slackEnabled) +

Subscribe to events

+
+ @if (isDev()) + + @endif + + + + + +
+ @endif +
\ No newline at end of file diff --git a/routes/web.php b/routes/web.php index afe392052..7aa23ac95 100644 --- a/routes/web.php +++ b/routes/web.php @@ -133,6 +133,7 @@ Route::middleware(['auth', 'verified'])->group(function () { Route::get('/email', NotificationEmail::class)->name('notifications.email'); Route::get('/telegram', NotificationTelegram::class)->name('notifications.telegram'); Route::get('/discord', NotificationDiscord::class)->name('notifications.discord'); + Route::get('/slack', App\Livewire\Notifications\Slack::class)->name('notifications.slack'); }); Route::prefix('storages')->group(function () { From 80ed561374438076f50684453d65531bd63d09b2 Mon Sep 17 00:00:00 2001 From: Marvin von Rappard Date: Tue, 12 Nov 2024 22:45:07 +0100 Subject: [PATCH 06/54] fix: add warning color --- app/Notifications/Dto/SlackMessage.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/Notifications/Dto/SlackMessage.php b/app/Notifications/Dto/SlackMessage.php index efd0cf5e6..86532c65b 100644 --- a/app/Notifications/Dto/SlackMessage.php +++ b/app/Notifications/Dto/SlackMessage.php @@ -25,4 +25,9 @@ class SlackMessage { return '#00ff00'; } + + public static function warningColor(): string + { + return '#ffa500'; + } } \ No newline at end of file From 40fb73ee8e06300b304dc7736111d8fd8fc64fc5 Mon Sep 17 00:00:00 2001 From: Marvin von Rappard Date: Wed, 20 Nov 2024 13:01:56 +0100 Subject: [PATCH 07/54] fix: import NotificationSlack correctly --- routes/web.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/routes/web.php b/routes/web.php index 2fc5046e5..d8ba925f2 100644 --- a/routes/web.php +++ b/routes/web.php @@ -13,6 +13,7 @@ use App\Livewire\ForcePasswordReset; use App\Livewire\Notifications\Discord as NotificationDiscord; use App\Livewire\Notifications\Email as NotificationEmail; use App\Livewire\Notifications\Telegram as NotificationTelegram; +use App\Livewire\Notifications\Slack as NotificationSlack; use App\Livewire\Profile\Index as ProfileIndex; use App\Livewire\Project\Application\Configuration as ApplicationConfiguration; use App\Livewire\Project\Application\Deployment\Index as DeploymentIndex; @@ -132,7 +133,7 @@ Route::middleware(['auth', 'verified'])->group(function () { Route::get('/email', NotificationEmail::class)->name('notifications.email'); Route::get('/telegram', NotificationTelegram::class)->name('notifications.telegram'); Route::get('/discord', NotificationDiscord::class)->name('notifications.discord'); - Route::get('/slack', App\Livewire\Notifications\Slack::class)->name('notifications.slack'); + Route::get('/slack', NotificationSlack::class)->name('notifications.slack'); }); Route::prefix('storages')->group(function () { @@ -286,7 +287,7 @@ Route::middleware(['auth'])->group(function () { 'privateKey' => $privateKeyLocation, 'root' => '/', ]); - if (! $disk->exists($filename)) { + if (!$disk->exists($filename)) { return response()->json(['message' => 'Backup not found.'], 404); } @@ -298,7 +299,7 @@ Route::middleware(['auth'])->group(function () { if ($stream === false || is_null($stream)) { abort(500, 'Failed to open stream for the requested file.'); } - while (! feof($stream)) { + while (!feof($stream)) { echo fread($stream, 2048); flush(); } @@ -306,7 +307,7 @@ Route::middleware(['auth'])->group(function () { fclose($stream); }, 200, [ 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="'.basename($filename).'"', + 'Content-Disposition' => 'attachment; filename="' . basename($filename) . '"', ]); } catch (\Throwable $e) { return response()->json(['message' => $e->getMessage()], 500); From 1f499c148867295b1db3f329330c01d7e4d3fe8a Mon Sep 17 00:00:00 2001 From: Vishwanath Martur <64204611+vishwamartur@users.noreply.github.com> Date: Sat, 23 Nov 2024 13:04:54 +0530 Subject: [PATCH 08/54] Add no encryption option for SMTP settings Related to #4311 Add option to configure SMTP settings without encryption. * Update `app/Livewire/Notifications/Email.php` and `app/Livewire/SettingsEmail.php` to include "No Encryption" option in the `smtpEncryption` field and update validation rules. * Modify `app/Notifications/Channels/EmailChannel.php` to handle the "No Encryption" option in the `bootConfigs` method. * Add `set_transanctional_email_settings` function in `app/Livewire/Help.php` to support the "No Encryption" option. * Update `config/mail.php` to handle the "No Encryption" option in the mail configuration. --- app/Livewire/Help.php | 32 +++++++++++++++++++++ app/Livewire/Notifications/Email.php | 2 +- app/Livewire/SettingsEmail.php | 2 +- app/Notifications/Channels/EmailChannel.php | 2 +- config/mail.php | 2 +- 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/app/Livewire/Help.php b/app/Livewire/Help.php index f51527fbe..aa354e94e 100644 --- a/app/Livewire/Help.php +++ b/app/Livewire/Help.php @@ -56,3 +56,35 @@ class Help extends Component return view('livewire.help')->layout('layouts.app'); } } + +function set_transanctional_email_settings($settings = null) +{ + if (is_null($settings)) { + $settings = instanceSettings(); + } + + if ($settings->resend_enabled) { + config()->set('mail.default', 'resend'); + config()->set('resend.api_key', $settings->resend_api_key); + + return 'resend'; + } + + if ($settings->smtp_enabled) { + config()->set('mail.default', 'smtp'); + config()->set('mail.mailers.smtp', [ + 'transport' => 'smtp', + 'host' => $settings->smtp_host, + 'port' => $settings->smtp_port, + 'encryption' => $settings->smtp_encryption === 'none' ? null : $settings->smtp_encryption, + 'username' => $settings->smtp_username, + 'password' => $settings->smtp_password, + 'timeout' => $settings->smtp_timeout, + 'local_domain' => null, + ]); + + return 'smtp'; + } + + return false; +} diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php index fcedf1305..682180aa8 100644 --- a/app/Livewire/Notifications/Email.php +++ b/app/Livewire/Notifications/Email.php @@ -37,7 +37,7 @@ class Email extends Component #[Validate(['nullable', 'numeric'])] public ?int $smtpPort = null; - #[Validate(['nullable', 'string'])] + #[Validate(['nullable', 'string', 'in:tls,ssl,none'])] public ?string $smtpEncryption = null; #[Validate(['nullable', 'string'])] diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php index 61f720b3a..abf3a12f9 100644 --- a/app/Livewire/SettingsEmail.php +++ b/app/Livewire/SettingsEmail.php @@ -19,7 +19,7 @@ class SettingsEmail extends Component #[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])] public ?int $smtpPort = null; - #[Validate(['nullable', 'string'])] + #[Validate(['nullable', 'string', 'in:tls,ssl,none'])] public ?string $smtpEncryption = null; #[Validate(['nullable', 'string'])] diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index af9af978d..e745990e4 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -66,7 +66,7 @@ class EmailChannel 'transport' => 'smtp', 'host' => data_get($notifiable, 'smtp_host'), 'port' => data_get($notifiable, 'smtp_port'), - 'encryption' => data_get($notifiable, 'smtp_encryption'), + 'encryption' => data_get($notifiable, 'smtp_encryption') === 'none' ? null : data_get($notifiable, 'smtp_encryption'), 'username' => data_get($notifiable, 'smtp_username'), 'password' => data_get($notifiable, 'smtp_password'), 'timeout' => data_get($notifiable, 'smtp_timeout'), diff --git a/config/mail.php b/config/mail.php index 26af507d9..b36bd363c 100644 --- a/config/mail.php +++ b/config/mail.php @@ -38,7 +38,7 @@ return [ 'transport' => 'smtp', 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), 'port' => env('MAIL_PORT', 587), - 'encryption' => env('MAIL_ENCRYPTION', 'tls'), + 'encryption' => env('MAIL_ENCRYPTION', 'tls') === 'none' ? null : env('MAIL_ENCRYPTION', 'tls'), 'username' => env('MAIL_USERNAME'), 'password' => env('MAIL_PASSWORD'), 'timeout' => null, From f45411f3d6f869ea52ccc0022a28856e31830cc5 Mon Sep 17 00:00:00 2001 From: Vishwanath Martur <64204611+vishwamartur@users.noreply.github.com> Date: Sat, 23 Nov 2024 13:08:53 +0530 Subject: [PATCH 09/54] Fix metrics error when data is less than selected date Related to #4309 Fix the 'sql: Scan error on column index 5, name "usedPercent": converting NULL to float64 is unsupported' error on the metrics page. * Update the `getMemoryMetrics` method in `app/Models/Server.php` to handle NULL values for the "usedPercent" field by setting them to 0.0. * Add a check for NULL values in the `getMemoryMetrics` method before converting to float64. --- app/Models/Server.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Models/Server.php b/app/Models/Server.php index 5bbd13ac7..1e872227a 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -611,7 +611,8 @@ $schema://$host { } $memory = json_decode($memory, true); $parsedCollection = collect($memory)->map(function ($metric) { - return [(int) $metric['time'], (float) $metric['usedPercent']]; + $usedPercent = $metric['usedPercent'] ?? 0.0; + return [(int) $metric['time'], (float) $usedPercent]; }); return $parsedCollection->toArray(); From a836d78f0bb7716134f2424d917636046b41e91a Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 25 Nov 2024 14:07:50 +0100 Subject: [PATCH 10/54] remove unnecessary function --- app/Livewire/Help.php | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/app/Livewire/Help.php b/app/Livewire/Help.php index aa354e94e..f51527fbe 100644 --- a/app/Livewire/Help.php +++ b/app/Livewire/Help.php @@ -56,35 +56,3 @@ class Help extends Component return view('livewire.help')->layout('layouts.app'); } } - -function set_transanctional_email_settings($settings = null) -{ - if (is_null($settings)) { - $settings = instanceSettings(); - } - - if ($settings->resend_enabled) { - config()->set('mail.default', 'resend'); - config()->set('resend.api_key', $settings->resend_api_key); - - return 'resend'; - } - - if ($settings->smtp_enabled) { - config()->set('mail.default', 'smtp'); - config()->set('mail.mailers.smtp', [ - 'transport' => 'smtp', - 'host' => $settings->smtp_host, - 'port' => $settings->smtp_port, - 'encryption' => $settings->smtp_encryption === 'none' ? null : $settings->smtp_encryption, - 'username' => $settings->smtp_username, - 'password' => $settings->smtp_password, - 'timeout' => $settings->smtp_timeout, - 'local_domain' => null, - ]); - - return 'smtp'; - } - - return false; -} From b3f968db76193f62dc51f4bb2bf996358441cc17 Mon Sep 17 00:00:00 2001 From: SierraJC <7351311+SierraJC@users.noreply.github.com> Date: Sun, 1 Dec 2024 10:15:45 +1100 Subject: [PATCH 11/54] fix: missing `mysql_password` API property --- app/Http/Controllers/Api/DatabasesController.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index ce658d2a2..21816557f 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -213,6 +213,7 @@ class DatabasesController extends Controller 'mongo_initdb_root_password' => ['type' => 'string', 'description' => 'Mongo initdb root password'], 'mongo_initdb_init_database' => ['type' => 'string', 'description' => 'Mongo initdb init database'], 'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'], + 'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'], 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'], 'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'], 'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'], @@ -443,9 +444,10 @@ class DatabasesController extends Controller break; case 'standalone-mysql': - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $validator = customApiValidator($request->all(), [ 'mysql_root_password' => 'string', + 'mysql_password' => 'string', 'mysql_user' => 'string', 'mysql_database' => 'string', 'mysql_conf' => 'string', @@ -909,6 +911,7 @@ class DatabasesController extends Controller 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], 'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'], + 'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'], 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'], 'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'], 'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'], @@ -1013,7 +1016,7 @@ class DatabasesController extends Controller public function create_database(Request $request, NewDatabaseTypes $type) { - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -1220,9 +1223,10 @@ class DatabasesController extends Controller return response()->json(serializeApiResponse($payload))->setStatusCode(201); } elseif ($type === NewDatabaseTypes::MYSQL) { - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_user', 'mysql_database', 'mysql_conf']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $validator = customApiValidator($request->all(), [ 'mysql_root_password' => 'string', + 'mysql_password' => 'string', 'mysql_user' => 'string', 'mysql_database' => 'string', 'mysql_conf' => 'string', From f279729f088629ecdf4de3b058540c40c34f19bb Mon Sep 17 00:00:00 2001 From: SierraJC <7351311+SierraJC@users.noreply.github.com> Date: Sun, 1 Dec 2024 10:16:41 +1100 Subject: [PATCH 12/54] fix: incorrect MongoDB init API property --- app/Http/Controllers/Api/DatabasesController.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 21816557f..58c4467e9 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -211,7 +211,7 @@ class DatabasesController extends Controller 'mongo_conf' => ['type' => 'string', 'description' => 'Mongo conf'], 'mongo_initdb_root_username' => ['type' => 'string', 'description' => 'Mongo initdb root username'], 'mongo_initdb_root_password' => ['type' => 'string', 'description' => 'Mongo initdb root password'], - 'mongo_initdb_init_database' => ['type' => 'string', 'description' => 'Mongo initdb init database'], + 'mongo_initdb_database' => ['type' => 'string', 'description' => 'Mongo initdb init database'], 'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'], 'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'], 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'], @@ -242,7 +242,7 @@ class DatabasesController extends Controller )] public function update_by_uuid(Request $request) { - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { return invalidTokenResponse(); @@ -414,12 +414,12 @@ class DatabasesController extends Controller } break; case 'standalone-mongodb': - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database']; $validator = customApiValidator($request->all(), [ 'mongo_conf' => 'string', 'mongo_initdb_root_username' => 'string', 'mongo_initdb_root_password' => 'string', - 'mongo_initdb_init_database' => 'string', + 'mongo_initdb_database' => 'string', ]); if ($request->has('mongo_conf')) { if (! isBase64Encoded($request->mongo_conf)) { @@ -1016,7 +1016,7 @@ class DatabasesController extends Controller public function create_database(Request $request, NewDatabaseTypes $type) { - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -1460,12 +1460,12 @@ class DatabasesController extends Controller return response()->json(serializeApiResponse($payload))->setStatusCode(201); } elseif ($type === NewDatabaseTypes::MONGODB) { - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database']; $validator = customApiValidator($request->all(), [ 'mongo_conf' => 'string', 'mongo_initdb_root_username' => 'string', 'mongo_initdb_root_password' => 'string', - 'mongo_initdb_init_database' => 'string', + 'mongo_initdb_database' => 'string', ]); $extraFields = array_diff(array_keys($request->all()), $allowedFields); if ($validator->fails() || ! empty($extraFields)) { From ce6602eb6398a2f99edb4180b300d89d19e186d5 Mon Sep 17 00:00:00 2001 From: SierraJC <7351311+SierraJC@users.noreply.github.com> Date: Sun, 1 Dec 2024 10:17:49 +1100 Subject: [PATCH 13/54] chore: regenerate openapi spec --- openapi.json | 10 +++++++++- openapi.yaml | 8 +++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/openapi.json b/openapi.json index 2ec218438..5d35331ec 100644 --- a/openapi.json +++ b/openapi.json @@ -3011,7 +3011,7 @@ "type": "string", "description": "Mongo initdb root password" }, - "mongo_initdb_init_database": { + "mongo_initdb_database": { "type": "string", "description": "Mongo initdb init database" }, @@ -3019,6 +3019,10 @@ "type": "string", "description": "MySQL root password" }, + "mysql_password": { + "type": "string", + "description": "MySQL password" + }, "mysql_user": { "type": "string", "description": "MySQL user" @@ -3842,6 +3846,10 @@ "type": "string", "description": "MySQL root password" }, + "mysql_password": { + "type": "string", + "description": "MySQL password" + }, "mysql_user": { "type": "string", "description": "MySQL user" diff --git a/openapi.yaml b/openapi.yaml index 2a22c730c..20bf34873 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2089,12 +2089,15 @@ paths: mongo_initdb_root_password: type: string description: 'Mongo initdb root password' - mongo_initdb_init_database: + mongo_initdb_database: type: string description: 'Mongo initdb init database' mysql_root_password: type: string description: 'MySQL root password' + mysql_password: + type: string + description: 'MySQL password' mysql_user: type: string description: 'MySQL user' @@ -2684,6 +2687,9 @@ paths: mysql_root_password: type: string description: 'MySQL root password' + mysql_password: + type: string + description: 'MySQL password' mysql_user: type: string description: 'MySQL user' From c74728162ede3aa137affd457bdc606d0d69be77 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:41:56 +0100 Subject: [PATCH 14/54] wip: test rename GitHub app --- app/Livewire/Source/Github/Change.php | 5 +++++ .../views/livewire/source/github/change.blade.php | 10 +++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index 07cef54f9..79cc02219 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -142,6 +142,11 @@ class Change extends Component } } + public function getUpdatePath() + { + return "{$this->github_app->html_url}/settings/apps/{$this->github_app->app_id}"; + } + public function submit() { try { diff --git a/resources/views/livewire/source/github/change.blade.php b/resources/views/livewire/source/github/change.blade.php index 5e576fa85..215c54411 100644 --- a/resources/views/livewire/source/github/change.blade.php +++ b/resources/views/livewire/source/github/change.blade.php @@ -58,7 +58,15 @@ @else
From f38196c4213e1358d89c35c66f4c3b4f8ba6e258 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:54:20 +0100 Subject: [PATCH 15/54] fix: URL and sync new app name --- app/Livewire/Source/Github/Change.php | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index 79cc02219..5d57ecf1e 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -4,6 +4,7 @@ namespace App\Livewire\Source\Github; use App\Jobs\GithubAppPermissionJob; use App\Models\GithubApp; +use Illuminate\Support\Facades\Http; use Livewire\Component; class Change extends Component @@ -144,7 +145,29 @@ class Change extends Component public function getUpdatePath() { - return "{$this->github_app->html_url}/settings/apps/{$this->github_app->app_id}"; + return "{$this->github_app->html_url}/settings/apps/{$this->github_app->name}"; + } + + public function syncGithubAppName() + { + try { + $github_access_token = generate_github_installation_token($this->github_app); + + $response = Http::withToken($github_access_token) + ->get("{$this->github_app->api_url}/app"); + + if ($response->successful()) { + $app_data = $response->json(); + if ($app_data['name'] !== $this->github_app->name) { + $this->github_app->name = $app_data['name']; + $this->github_app->save(); + $this->name = str($this->github_app->name)->kebab(); + $this->dispatch('success', 'Github App name synchronized.'); + } + } + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function submit() From 737e81aa38f854b860047e3782912e33cab244a3 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:12:58 +0100 Subject: [PATCH 16/54] wip button to sync new app name --- app/Livewire/Source/Github/Change.php | 20 ++++++++++++++----- .../livewire/source/github/change.blade.php | 1 + 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index 5d57ecf1e..55ced1e3e 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -154,16 +154,26 @@ class Change extends Component $github_access_token = generate_github_installation_token($this->github_app); $response = Http::withToken($github_access_token) - ->get("{$this->github_app->api_url}/app"); + ->withHeaders([ + 'Accept' => 'application/vnd.github+json', + 'X-GitHub-Api-Version' => '2022-11-28', + ]) + ->get("{$this->github_app->api_url}/installation/repositories"); if ($response->successful()) { $app_data = $response->json(); - if ($app_data['name'] !== $this->github_app->name) { - $this->github_app->name = $app_data['name']; + $app_name = data_get($app_data, 'installation.app_slug'); + + if ($app_name && $app_name !== $this->github_app->name) { + $this->github_app->name = $app_name; + $this->name = str($app_name)->kebab(); $this->github_app->save(); - $this->name = str($this->github_app->name)->kebab(); - $this->dispatch('success', 'Github App name synchronized.'); + $this->dispatch('success', 'Github App name synchronized successfully.'); + } else { + $this->dispatch('info', 'Github App name is already up to date.'); } + } else { + $this->dispatch('error', 'Failed to fetch Github App information. Status: '.$response->status()); } } catch (\Throwable $e) { return handleError($e, $this); diff --git a/resources/views/livewire/source/github/change.blade.php b/resources/views/livewire/source/github/change.blade.php index 215c54411..2d873b465 100644 --- a/resources/views/livewire/source/github/change.blade.php +++ b/resources/views/livewire/source/github/change.blade.php @@ -60,6 +60,7 @@
+ Sync Name Update From a2860971a6dc8b17072b18962552ff604c28d552 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Tue, 3 Dec 2024 14:36:30 +0100 Subject: [PATCH 17/54] try jwt --- app/Livewire/Source/Github/Change.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index 55ced1e3e..4c41cb9e4 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -151,18 +151,18 @@ class Change extends Component public function syncGithubAppName() { try { - $github_access_token = generate_github_installation_token($this->github_app); + $jwt = $this->github_app->generateJWT(); - $response = Http::withToken($github_access_token) + $response = Http::withToken($jwt) ->withHeaders([ 'Accept' => 'application/vnd.github+json', 'X-GitHub-Api-Version' => '2022-11-28', ]) - ->get("{$this->github_app->api_url}/installation/repositories"); + ->get('https://api.github.com/app'); if ($response->successful()) { $app_data = $response->json(); - $app_name = data_get($app_data, 'installation.app_slug'); + $app_name = $app_data['name'] ?? null; if ($app_name && $app_name !== $this->github_app->name) { $this->github_app->name = $app_name; @@ -170,7 +170,7 @@ class Change extends Component $this->github_app->save(); $this->dispatch('success', 'Github App name synchronized successfully.'); } else { - $this->dispatch('info', 'Github App name is already up to date.'); + $this->dispatch('info', 'If you changed the name in GitHub, please wait a few moments and try syncing again.'); } } else { $this->dispatch('error', 'Failed to fetch Github App information. Status: '.$response->status()); From 6d43bbc6b95c552bf3398517a4b79c9b06a48642 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Tue, 3 Dec 2024 14:43:11 +0100 Subject: [PATCH 18/54] fix naming --- app/Livewire/Source/Github/Change.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index 4c41cb9e4..4f76d7299 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -151,14 +151,14 @@ class Change extends Component public function syncGithubAppName() { try { - $jwt = $this->github_app->generateJWT(); + $github_access_token = generate_github_installation_token($this->github_app); - $response = Http::withToken($jwt) + $response = Http::withToken($github_access_token) ->withHeaders([ 'Accept' => 'application/vnd.github+json', 'X-GitHub-Api-Version' => '2022-11-28', ]) - ->get('https://api.github.com/app'); + ->get("{$this->github_app->api_url}/app"); if ($response->successful()) { $app_data = $response->json(); From 56f6bdf7a776b951cdad2920c4e3b07a196ada00 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:11:35 +0100 Subject: [PATCH 19/54] use private key to make a jwt --- app/Livewire/Source/Github/Change.php | 58 +++++++++++++++++++++------ 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index 4f76d7299..4968a549d 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -4,7 +4,11 @@ namespace App\Livewire\Source\Github; use App\Jobs\GithubAppPermissionJob; use App\Models\GithubApp; +use App\Models\PrivateKey; use Illuminate\Support\Facades\Http; +use Lcobucci\JWT\Configuration; +use Lcobucci\JWT\Signer\Key\InMemory; +use Lcobucci\JWT\Signer\Rsa\Sha256; use Livewire\Component; class Change extends Component @@ -148,32 +152,60 @@ class Change extends Component return "{$this->github_app->html_url}/settings/apps/{$this->github_app->name}"; } + private function generateGithubJwt($private_key, $app_id): string + { + $configuration = Configuration::forAsymmetricSigner( + new Sha256, + InMemory::plainText($private_key), + InMemory::plainText($private_key) + ); + + $now = time(); + + return $configuration->builder() + ->issuedBy((string) $app_id) + ->permittedFor('https://api.github.com') + ->identifiedBy((string) $now) + ->issuedAt(new \DateTimeImmutable("@{$now}")) + ->expiresAt(new \DateTimeImmutable('@'.($now + 600))) + ->getToken($configuration->signer(), $configuration->signingKey()) + ->toString(); + } + public function syncGithubAppName() { try { - $github_access_token = generate_github_installation_token($this->github_app); + $privateKey = PrivateKey::find($this->github_app->private_key_id); - $response = Http::withToken($github_access_token) - ->withHeaders([ - 'Accept' => 'application/vnd.github+json', - 'X-GitHub-Api-Version' => '2022-11-28', - ]) - ->get("{$this->github_app->api_url}/app"); + if (! $privateKey) { + $this->dispatch('error', 'Private key not found for this GitHub App.'); + + return; + } + + $jwt = $this->generateGithubJwt($privateKey->private_key, $this->github_app->app_id); + + $response = Http::withHeaders([ + 'Accept' => 'application/vnd.github+json', + 'X-GitHub-Api-Version' => '2022-11-28', + 'Authorization' => "Bearer {$jwt}", + ])->get("{$this->github_app->api_url}/app"); if ($response->successful()) { $app_data = $response->json(); - $app_name = $app_data['name'] ?? null; + $app_slug = $app_data['slug'] ?? null; - if ($app_name && $app_name !== $this->github_app->name) { - $this->github_app->name = $app_name; - $this->name = str($app_name)->kebab(); + if ($app_slug) { + $this->github_app->name = $app_slug; + $this->name = str($app_slug)->kebab(); $this->github_app->save(); $this->dispatch('success', 'Github App name synchronized successfully.'); } else { - $this->dispatch('info', 'If you changed the name in GitHub, please wait a few moments and try syncing again.'); + $this->dispatch('info', 'Could not find app slug in GitHub response.'); } } else { - $this->dispatch('error', 'Failed to fetch Github App information. Status: '.$response->status()); + $error_message = $response->json()['message'] ?? 'Unknown error'; + $this->dispatch('error', "Failed to fetch Github App information: {$error_message}"); } } catch (\Throwable $e) { return handleError($e, $this); From 5f985426abc7d11f8d16fb675f6c3430fc4e6ae1 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:27:20 +0100 Subject: [PATCH 20/54] feat: update private key nam with new slug as well --- app/Http/Controllers/Webhook/Github.php | 2 +- app/Livewire/Source/Github/Change.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index 3683adaa8..ac1d4ded2 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -463,7 +463,7 @@ class Github extends Controller $private_key = data_get($data, 'pem'); $webhook_secret = data_get($data, 'webhook_secret'); $private_key = PrivateKey::create([ - 'name' => $slug, + 'name' => "github-app-{$slug}", 'private_key' => $private_key, 'team_id' => $github_app->team_id, 'is_git_related' => true, diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index 4968a549d..7f9200891 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -198,8 +198,10 @@ class Change extends Component if ($app_slug) { $this->github_app->name = $app_slug; $this->name = str($app_slug)->kebab(); + $privateKey->name = "github-app-{$app_slug}"; + $privateKey->save(); $this->github_app->save(); - $this->dispatch('success', 'Github App name synchronized successfully.'); + $this->dispatch('success', 'Github App name and SSH key name synchronized successfully.'); } else { $this->dispatch('info', 'Could not find app slug in GitHub response.'); } From fef8d0c62c103df93d840d3e86eab488940915fb Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:50:45 +0100 Subject: [PATCH 21/54] fix: typos and naming --- app/Livewire/Source/Github/Change.php | 14 ++++++++------ .../views/livewire/source/github/change.blade.php | 8 +++++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index 7f9200891..b1f0605bb 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -62,6 +62,7 @@ class Change extends Component $this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret'); $this->dispatch('success', 'Github App permissions updated.'); } + // public function check() // { @@ -95,6 +96,7 @@ class Change extends Component // ray($runners_by_repository); // } + public function mount() { try { @@ -147,7 +149,7 @@ class Change extends Component } } - public function getUpdatePath() + public function getGithubAppNameUpdatePath() { return "{$this->github_app->html_url}/settings/apps/{$this->github_app->name}"; } @@ -172,13 +174,13 @@ class Change extends Component ->toString(); } - public function syncGithubAppName() + public function updateGithubAppName() { try { $privateKey = PrivateKey::find($this->github_app->private_key_id); if (! $privateKey) { - $this->dispatch('error', 'Private key not found for this GitHub App.'); + $this->dispatch('error', 'No private key found for this GitHub App.'); return; } @@ -201,13 +203,13 @@ class Change extends Component $privateKey->name = "github-app-{$app_slug}"; $privateKey->save(); $this->github_app->save(); - $this->dispatch('success', 'Github App name and SSH key name synchronized successfully.'); + $this->dispatch('success', 'GitHub App name and SSH key name synchronized successfully.'); } else { - $this->dispatch('info', 'Could not find app slug in GitHub response.'); + $this->dispatch('info', 'Could not find App Name (slug) in GitHub response.'); } } else { $error_message = $response->json()['message'] ?? 'Unknown error'; - $this->dispatch('error', "Failed to fetch Github App information: {$error_message}"); + $this->dispatch('error', "Failed to fetch GitHub App information: {$error_message}"); } } catch (\Throwable $e) { return handleError($e, $this); diff --git a/resources/views/livewire/source/github/change.blade.php b/resources/views/livewire/source/github/change.blade.php index 2d873b465..3c11646c2 100644 --- a/resources/views/livewire/source/github/change.blade.php +++ b/resources/views/livewire/source/github/change.blade.php @@ -60,10 +60,12 @@
- Sync Name - + + Sync Name + + - Update + Rename From f51300a19213df0d29fbf0161fc91495bd296c52 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Tue, 3 Dec 2024 16:17:35 +0100 Subject: [PATCH 22/54] fix: client and webhook secret disappear after sync --- app/Livewire/Source/Github/Change.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index b1f0605bb..51c5ae3e9 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -56,6 +56,13 @@ class Change extends Component 'github_app.administration' => 'nullable|string', ]; + public function boot() + { + if ($this->github_app) { + $this->github_app->makeVisible(['client_secret', 'webhook_secret']); + } + } + public function checkPermissions() { GithubAppPermissionJob::dispatchSync($this->github_app); @@ -102,10 +109,10 @@ class Change extends Component try { $github_app_uuid = request()->github_app_uuid; $this->github_app = GithubApp::ownedByCurrentTeam()->whereUuid($github_app_uuid)->firstOrFail(); + $this->github_app->makeVisible(['client_secret', 'webhook_secret']); $this->applications = $this->github_app->applications; $settings = instanceSettings(); - $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); $this->name = str($this->github_app->name)->kebab(); $this->fqdn = $settings->fqdn; From a9e7eff5d06e415300ee4c0d324ba338775a0742 Mon Sep 17 00:00:00 2001 From: Drdiffie <61631493+dr-diffie@users.noreply.github.com> Date: Wed, 4 Dec 2024 22:30:55 +0100 Subject: [PATCH 23/54] Update postiz.yaml with Email Provider Fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Small Bug Fix: “EMAIL_PRROVIDER” was missing. This made the Email Invitations unusable. --- templates/compose/postiz.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/compose/postiz.yaml b/templates/compose/postiz.yaml index 503d0f67e..6060fb8a6 100644 --- a/templates/compose/postiz.yaml +++ b/templates/compose/postiz.yaml @@ -35,6 +35,7 @@ services: - RESEND_API_KEY=${RESEND_API_KEY} - EMAIL_FROM_ADDRESS=${EMAIL_FROM_ADDRESS} - EMAIL_FROM_NAME=${EMAIL_FROM_NAME} + - EMAIL_PROVIDER=${EMAIL_PROVIDER} # Social Media API Settings - X_API_KEY=${SERVICE_X_API} From c4d326da3ef00026d3041ac80d5614e77ba0d812 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Thu, 5 Dec 2024 10:14:00 +0100 Subject: [PATCH 24/54] version++ --- config/constants.php | 2 +- versions.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/constants.php b/config/constants.php index 4923cfc15..f7d5a7831 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.375', + 'version' => '4.0.0-beta.376', 'self_hosted' => env('SELF_HOSTED', true), 'autoupdate' => env('AUTOUPDATE'), 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), diff --git a/versions.json b/versions.json index 1c4e813ea..2d1633ed6 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.375" + "version": "4.0.0-beta.376" }, "nightly": { - "version": "4.0.0-beta.376" + "version": "4.0.0-beta.377" }, "helper": { "version": "1.0.4" @@ -16,4 +16,4 @@ "version": "0.0.15" } } -} +} \ No newline at end of file From 7519dff04d27e66bbcdf000b0f2ca388b39dfc57 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Thu, 5 Dec 2024 10:30:04 +0100 Subject: [PATCH 25/54] ui update on logs --- .../application/deployment/show.blade.php | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/resources/views/livewire/project/application/deployment/show.blade.php b/resources/views/livewire/project/application/deployment/show.blade.php index 9d9301d5c..92ed72981 100644 --- a/resources/views/livewire/project/application/deployment/show.blade.php +++ b/resources/views/livewire/project/application/deployment/show.blade.php @@ -19,7 +19,7 @@ }, toggleScroll() { this.alwaysScroll = !this.alwaysScroll; - + if (this.alwaysScroll) { this.intervalId = setInterval(() => { const screen = document.getElementById('screen'); @@ -58,30 +58,34 @@
-
+