Error: '.$error);
-
- return;
- }
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
- }
-
- public function mount()
- {
- $this->parameters = get_route_parameters();
- }
-}
diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php
index 754f0929bd..2991b8ae84 100644
--- a/app/Livewire/Settings/Index.php
+++ b/app/Livewire/Settings/Index.php
@@ -5,62 +5,95 @@
use App\Jobs\CheckForUpdatesJob;
use App\Models\InstanceSettings;
use App\Models\Server;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
+use Livewire\Attributes\Locked;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Index extends Component
{
public InstanceSettings $settings;
- public bool $do_not_track;
+ protected Server $server;
+
+ #[Locked]
+ public $timezones;
+ #[Validate('boolean')]
public bool $is_auto_update_enabled;
- public bool $is_registration_enabled;
+ #[Validate('nullable|string|max:255')]
+ public ?string $fqdn = null;
- public bool $is_dns_validation_enabled;
+ #[Validate('nullable|string|max:255')]
+ public ?string $resale_license = null;
- public bool $is_api_enabled;
+ #[Validate('required|integer|min:1025|max:65535')]
+ public int $public_port_min;
+
+ #[Validate('required|integer|min:1025|max:65535')]
+ public int $public_port_max;
+
+ #[Validate('nullable|string')]
+ public ?string $custom_dns_servers = null;
+
+ #[Validate('nullable|string|max:255')]
+ public ?string $instance_name = null;
+
+ #[Validate('nullable|string')]
+ public ?string $allowed_ips = null;
+ #[Validate('nullable|string')]
+ public ?string $public_ipv4 = null;
+
+ #[Validate('nullable|string')]
+ public ?string $public_ipv6 = null;
+
+ #[Validate('string')]
public string $auto_update_frequency;
+ #[Validate('string')]
public string $update_check_frequency;
- protected string $dynamic_config_path = '/data/coolify/proxy/dynamic';
+ #[Validate('required|string|timezone')]
+ public string $instance_timezone;
- protected Server $server;
+ #[Validate('boolean')]
+ public bool $do_not_track;
+
+ #[Validate('boolean')]
+ public bool $is_registration_enabled;
- protected $rules = [
- 'settings.fqdn' => 'nullable',
- 'settings.resale_license' => 'nullable',
- 'settings.public_port_min' => 'required',
- 'settings.public_port_max' => 'required',
- 'settings.custom_dns_servers' => 'nullable',
- 'settings.instance_name' => 'nullable',
- 'settings.allowed_ips' => 'nullable',
- 'settings.is_auto_update_enabled' => 'boolean',
- 'auto_update_frequency' => 'string',
- 'update_check_frequency' => 'string',
- 'settings.instance_timezone' => 'required|string|timezone',
- ];
-
- protected $validationAttributes = [
- 'settings.fqdn' => 'FQDN',
- 'settings.resale_license' => 'Resale License',
- 'settings.public_port_min' => 'Public port min',
- 'settings.public_port_max' => 'Public port max',
- 'settings.custom_dns_servers' => 'Custom DNS servers',
- 'settings.allowed_ips' => 'Allowed IPs',
- 'settings.is_auto_update_enabled' => 'Auto Update Enabled',
- 'auto_update_frequency' => 'Auto Update Frequency',
- 'update_check_frequency' => 'Update Check Frequency',
- ];
+ #[Validate('boolean')]
+ public bool $is_dns_validation_enabled;
- public $timezones;
+ #[Validate('boolean')]
+ public bool $is_api_enabled;
+
+ #[Validate('boolean')]
+ public bool $disable_two_step_confirmation;
+
+ public function render()
+ {
+ return view('livewire.settings.index');
+ }
public function mount()
{
- if (isInstanceAdmin()) {
+ if (! isInstanceAdmin()) {
+ return redirect()->route('dashboard');
+ } else {
$this->settings = instanceSettings();
+ $this->fqdn = $this->settings->fqdn;
+ $this->resale_license = $this->settings->resale_license;
+ $this->public_port_min = $this->settings->public_port_min;
+ $this->public_port_max = $this->settings->public_port_max;
+ $this->custom_dns_servers = $this->settings->custom_dns_servers;
+ $this->instance_name = $this->settings->instance_name;
+ $this->allowed_ips = $this->settings->allowed_ips;
+ $this->public_ipv4 = $this->settings->public_ipv4;
+ $this->public_ipv6 = $this->settings->public_ipv6;
$this->do_not_track = $this->settings->do_not_track;
$this->is_auto_update_enabled = $this->settings->is_auto_update_enabled;
$this->is_registration_enabled = $this->settings->is_registration_enabled;
@@ -69,13 +102,22 @@ public function mount()
$this->auto_update_frequency = $this->settings->auto_update_frequency;
$this->update_check_frequency = $this->settings->update_check_frequency;
$this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
- } else {
- return redirect()->route('dashboard');
+ $this->instance_timezone = $this->settings->instance_timezone;
+ $this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation;
}
}
- public function instantSave()
+ public function instantSave($isSave = true)
{
+ $this->settings->fqdn = $this->fqdn;
+ $this->settings->resale_license = $this->resale_license;
+ $this->settings->public_port_min = $this->public_port_min;
+ $this->settings->public_port_max = $this->public_port_max;
+ $this->settings->custom_dns_servers = $this->custom_dns_servers;
+ $this->settings->instance_name = $this->instance_name;
+ $this->settings->allowed_ips = $this->allowed_ips;
+ $this->settings->public_ipv4 = $this->public_ipv4;
+ $this->settings->public_ipv6 = $this->public_ipv6;
$this->settings->do_not_track = $this->do_not_track;
$this->settings->is_auto_update_enabled = $this->is_auto_update_enabled;
$this->settings->is_registration_enabled = $this->is_registration_enabled;
@@ -83,8 +125,12 @@ public function instantSave()
$this->settings->is_api_enabled = $this->is_api_enabled;
$this->settings->auto_update_frequency = $this->auto_update_frequency;
$this->settings->update_check_frequency = $this->update_check_frequency;
- $this->settings->save();
- $this->dispatch('success', 'Settings updated!');
+ $this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation;
+ $this->settings->instance_timezone = $this->instance_timezone;
+ if ($isSave) {
+ $this->settings->save();
+ $this->dispatch('success', 'Settings updated!');
+ }
}
public function submit()
@@ -141,13 +187,8 @@ public function submit()
$this->settings->allowed_ips = $this->settings->allowed_ips->unique();
$this->settings->allowed_ips = $this->settings->allowed_ips->implode(',');
- $this->settings->do_not_track = $this->do_not_track;
- $this->settings->is_auto_update_enabled = $this->is_auto_update_enabled;
- $this->settings->is_registration_enabled = $this->is_registration_enabled;
- $this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled;
- $this->settings->is_api_enabled = $this->is_api_enabled;
- $this->settings->auto_update_frequency = $this->auto_update_frequency;
- $this->settings->update_check_frequency = $this->update_check_frequency;
+ $this->instantSave(isSave: false);
+
$this->settings->save();
$this->server->setupDynamicProxyConfiguration();
if (! $error_show) {
@@ -170,15 +211,16 @@ public function checkManually()
}
}
- public function updatedSettingsInstanceTimezone($value)
+ public function toggleTwoStepConfirmation($password)
{
- $this->settings->instance_timezone = $value;
- $this->settings->save();
- $this->dispatch('success', 'Instance timezone updated.');
- }
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
- public function render()
- {
- return view('livewire.settings.index');
+ return;
+ }
+
+ $this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation = true;
+ $this->settings->save();
+ $this->dispatch('success', 'Two step confirmation has been disabled.');
}
}
diff --git a/app/Livewire/Settings/License.php b/app/Livewire/Settings/License.php
index ca0c9c1ae3..79f8269f3f 100644
--- a/app/Livewire/Settings/License.php
+++ b/app/Livewire/Settings/License.php
@@ -28,6 +28,9 @@ public function mount()
if (! isCloud()) {
abort(404);
}
+ if (! isInstanceAdmin()) {
+ return redirect()->route('home');
+ }
$this->instance_id = config('app.id');
$this->settings = instanceSettings();
}
@@ -47,7 +50,6 @@ public function submit()
$this->dispatch('reloadWindow');
} catch (\Throwable $e) {
session()->flash('error', 'Something went wrong. Please contact support. Error: '.$e->getMessage());
- ray($e->getMessage());
return redirect()->route('settings.license');
}
diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php
index 9240aa96d2..6dc5d6ab39 100644
--- a/app/Livewire/SettingsBackup.php
+++ b/app/Livewire/SettingsBackup.php
@@ -2,50 +2,59 @@
namespace App\Livewire;
-use App\Jobs\DatabaseBackupJob;
use App\Models\InstanceSettings;
use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\StandalonePostgresql;
+use Livewire\Attributes\Locked;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class SettingsBackup extends Component
{
public InstanceSettings $settings;
- public $s3s;
-
public ?StandalonePostgresql $database = null;
public ScheduledDatabaseBackup|null|array $backup = [];
+ #[Locked]
+ public $s3s;
+
+ #[Locked]
public $executions = [];
- protected $rules = [
- 'database.uuid' => 'required',
- 'database.name' => 'required',
- 'database.description' => 'nullable',
- 'database.postgres_user' => 'required',
- 'database.postgres_password' => 'required',
+ #[Validate(['required'])]
+ public string $uuid;
- ];
+ #[Validate(['required'])]
+ public string $name;
- protected $validationAttributes = [
- 'database.uuid' => 'uuid',
- 'database.name' => 'name',
- 'database.description' => 'description',
- 'database.postgres_user' => 'postgres user',
- 'database.postgres_password' => 'postgres password',
- ];
+ #[Validate(['nullable'])]
+ public ?string $description = null;
+
+ #[Validate(['required'])]
+ public string $postgres_user;
+
+ #[Validate(['required'])]
+ public string $postgres_password;
public function mount()
{
- if (isInstanceAdmin()) {
+ if (! isInstanceAdmin()) {
+ return redirect()->route('dashboard');
+ } else {
$settings = instanceSettings();
$this->database = StandalonePostgresql::whereName('coolify-db')->first();
$s3s = S3Storage::whereTeamId(0)->get() ?? [];
if ($this->database) {
+ $this->uuid = $this->database->uuid;
+ $this->name = $this->database->name;
+ $this->description = $this->database->description;
+ $this->postgres_user = $this->database->postgres_user;
+ $this->postgres_password = $this->database->postgres_password;
+
if ($this->database->status !== 'running') {
$this->database->status = 'running';
$this->database->save();
@@ -55,13 +64,10 @@ public function mount()
}
$this->settings = $settings;
$this->s3s = $s3s;
-
- } else {
- return redirect()->route('dashboard');
}
}
- public function add_coolify_database()
+ public function addCoolifyDatabase()
{
try {
$server = Server::findOrFail(0);
@@ -78,7 +84,7 @@ public function add_coolify_database()
'postgres_password' => $postgres_password,
'postgres_db' => $postgres_db,
'status' => 'running',
- 'destination_type' => 'App\Models\StandaloneDocker',
+ 'destination_type' => \App\Models\StandaloneDocker::class,
'destination_id' => 0,
]);
$this->backup = ScheduledDatabaseBackup::create([
@@ -87,7 +93,7 @@ public function add_coolify_database()
'save_s3' => false,
'frequency' => '0 0 * * *',
'database_id' => $this->database->id,
- 'database_type' => 'App\Models\StandalonePostgresql',
+ 'database_type' => \App\Models\StandalonePostgresql::class,
'team_id' => currentTeam()->id,
]);
$this->database->refresh();
@@ -98,16 +104,14 @@ public function add_coolify_database()
}
}
- public function backup_now()
- {
- dispatch(new DatabaseBackupJob(
- backup: $this->backup
- ));
- $this->dispatch('success', 'Backup queued. It will be available in a few minutes.');
- }
-
public function submit()
{
+ $this->database->update([
+ 'name' => $this->name,
+ 'description' => $this->description,
+ 'postgres_user' => $this->postgres_user,
+ 'postgres_password' => $this->postgres_password,
+ ]);
$this->dispatch('success', 'Backup updated.');
}
}
diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php
index 4515df9a7d..0ab5754f20 100644
--- a/app/Livewire/SettingsEmail.php
+++ b/app/Livewire/SettingsEmail.php
@@ -3,131 +3,113 @@
namespace App\Livewire;
use App\Models\InstanceSettings;
-use App\Notifications\TransactionalEmails\Test;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class SettingsEmail extends Component
{
public InstanceSettings $settings;
- public string $emails;
-
- protected $rules = [
- 'settings.smtp_enabled' => 'nullable|boolean',
- 'settings.smtp_host' => 'required',
- 'settings.smtp_port' => 'required|numeric',
- 'settings.smtp_encryption' => 'nullable',
- 'settings.smtp_username' => 'nullable',
- 'settings.smtp_password' => 'nullable',
- 'settings.smtp_timeout' => 'nullable',
- 'settings.smtp_from_address' => 'required|email',
- 'settings.smtp_from_name' => 'required',
- 'settings.resend_enabled' => 'nullable|boolean',
- 'settings.resend_api_key' => 'nullable',
-
- ];
-
- protected $validationAttributes = [
- 'settings.smtp_from_address' => 'From Address',
- 'settings.smtp_from_name' => 'From Name',
- 'settings.smtp_recipients' => 'Recipients',
- 'settings.smtp_host' => 'Host',
- 'settings.smtp_port' => 'Port',
- 'settings.smtp_encryption' => 'Encryption',
- 'settings.smtp_username' => 'Username',
- 'settings.smtp_password' => 'Password',
- 'settings.smtp_timeout' => 'Timeout',
- 'settings.resend_api_key' => 'Resend API Key',
- ];
+ #[Validate(['boolean'])]
+ public bool $smtpEnabled = false;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $smtpHost = null;
+
+ #[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])]
+ public ?int $smtpPort = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $smtpEncryption = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $smtpUsername = null;
+
+ #[Validate(['nullable'])]
+ public ?string $smtpPassword = null;
+
+ #[Validate(['nullable', 'numeric'])]
+ public ?int $smtpTimeout = null;
+
+ #[Validate(['nullable', 'email'])]
+ public ?string $smtpFromAddress = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $smtpFromName = null;
+
+ #[Validate(['boolean'])]
+ public bool $resendEnabled = false;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $resendApiKey = null;
public function mount()
{
- if (isInstanceAdmin()) {
- $this->settings = instanceSettings();
- $this->emails = auth()->user()->email;
- } else {
+ if (isInstanceAdmin() === false) {
return redirect()->route('dashboard');
}
-
+ $this->settings = instanceSettings();
+ $this->syncData();
}
- public function submitFromFields()
+ public function syncData(bool $toModel = false)
{
- try {
- $this->resetErrorBag();
- $this->validate([
- 'settings.smtp_from_address' => 'required|email',
- 'settings.smtp_from_name' => 'required',
- ]);
+ if ($toModel) {
+ $this->validate();
+ $this->settings->smtp_enabled = $this->smtpEnabled;
+ $this->settings->smtp_host = $this->smtpHost;
+ $this->settings->smtp_port = $this->smtpPort;
+ $this->settings->smtp_encryption = $this->smtpEncryption;
+ $this->settings->smtp_username = $this->smtpUsername;
+ $this->settings->smtp_password = $this->smtpPassword;
+ $this->settings->smtp_timeout = $this->smtpTimeout;
+
+ $this->settings->resend_enabled = $this->resendEnabled;
+ $this->settings->resend_api_key = $this->resendApiKey;
$this->settings->save();
- $this->dispatch('success', 'Settings saved.');
- } catch (\Throwable $e) {
- return handleError($e, $this);
+ } else {
+ $this->smtpEnabled = $this->settings->smtp_enabled;
+ $this->smtpHost = $this->settings->smtp_host;
+ $this->smtpPort = $this->settings->smtp_port;
+ $this->smtpEncryption = $this->settings->smtp_encryption;
+ $this->smtpUsername = $this->settings->smtp_username;
+ $this->smtpPassword = $this->settings->smtp_password;
+ $this->smtpTimeout = $this->settings->smtp_timeout;
+ $this->smtpFromAddress = $this->settings->smtp_from_address;
+ $this->smtpFromName = $this->settings->smtp_from_name;
+
+ $this->resendEnabled = $this->settings->resend_enabled;
+ $this->resendApiKey = $this->settings->resend_api_key;
}
}
- public function submitResend()
+ public function submit()
{
try {
$this->resetErrorBag();
- $this->validate([
- 'settings.smtp_from_address' => 'required|email',
- 'settings.smtp_from_name' => 'required',
- 'settings.resend_api_key' => 'required',
- ]);
- $this->settings->save();
+ $this->syncData(true);
$this->dispatch('success', 'Settings saved.');
- } catch (\Throwable $e) {
- $this->settings->resend_enabled = false;
-
- return handleError($e, $this);
- }
- }
-
- public function instantSaveResend()
- {
- try {
- $this->settings->smtp_enabled = false;
- $this->submitResend();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
- public function instantSave()
+ public function instantSave(string $type)
{
try {
- $this->settings->resend_enabled = false;
- $this->submit();
+ if ($type === 'SMTP') {
+ $this->resendEnabled = false;
+ } else {
+ $this->smtpEnabled = false;
+ }
+ $this->syncData(true);
+ if ($this->smtpEnabled || $this->resendEnabled) {
+ $this->dispatch('success', "{$type} enabled.");
+ } else {
+ $this->dispatch('success', "{$type} disabled.");
+ }
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
-
- public function submit()
- {
- try {
- $this->resetErrorBag();
- $this->validate([
- 'settings.smtp_from_address' => 'required|email',
- 'settings.smtp_from_name' => 'required',
- 'settings.smtp_host' => 'required',
- 'settings.smtp_port' => 'required|numeric',
- 'settings.smtp_encryption' => 'nullable',
- 'settings.smtp_username' => 'nullable',
- 'settings.smtp_password' => 'nullable',
- 'settings.smtp_timeout' => 'nullable',
- ]);
- $this->settings->save();
- $this->dispatch('success', 'Settings saved.');
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
- }
-
- public function sendTestNotification()
- {
- $this->settings?->notify(new Test($this->emails));
- $this->dispatch('success', 'Test email sent.');
- }
}
diff --git a/app/Livewire/SettingsOauth.php b/app/Livewire/SettingsOauth.php
index c3884589f6..17b3b89a3b 100644
--- a/app/Livewire/SettingsOauth.php
+++ b/app/Livewire/SettingsOauth.php
@@ -24,6 +24,9 @@ protected function rules()
public function mount()
{
+ if (! isInstanceAdmin()) {
+ return redirect()->route('home');
+ }
$this->oauth_settings_map = OauthSetting::all()->sortBy('provider')->reduce(function ($carry, $setting) {
$carry[$setting->provider] = $setting;
diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php
index 193b650ffe..07cef54f9f 100644
--- a/app/Livewire/Source/Github/Change.php
+++ b/app/Livewire/Source/Github/Change.php
@@ -4,7 +4,6 @@
use App\Jobs\GithubAppPermissionJob;
use App\Models\GithubApp;
-use Illuminate\Support\Facades\Http;
use Livewire\Component;
class Change extends Component
@@ -93,51 +92,53 @@ public function checkPermissions()
// }
public function mount()
{
- $github_app_uuid = request()->github_app_uuid;
- $this->github_app = GithubApp::where('uuid', $github_app_uuid)->first();
- if (! $this->github_app) {
- return redirect()->route('source.all');
- }
- $this->applications = $this->github_app->applications;
- $settings = instanceSettings();
- $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
+ try {
+ $github_app_uuid = request()->github_app_uuid;
+ $this->github_app = GithubApp::ownedByCurrentTeam()->whereUuid($github_app_uuid)->firstOrFail();
- $this->name = str($this->github_app->name)->kebab();
- $this->fqdn = $settings->fqdn;
+ $this->applications = $this->github_app->applications;
+ $settings = instanceSettings();
+ $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
- if ($settings->public_ipv4) {
- $this->ipv4 = 'http://'.$settings->public_ipv4.':'.config('app.port');
- }
- if ($settings->public_ipv6) {
- $this->ipv6 = 'http://'.$settings->public_ipv6.':'.config('app.port');
- }
- if ($this->github_app->installation_id && session('from')) {
- $source_id = data_get(session('from'), 'source_id');
- if (! $source_id || $this->github_app->id !== $source_id) {
- session()->forget('from');
+ $this->name = str($this->github_app->name)->kebab();
+ $this->fqdn = $settings->fqdn;
+
+ if ($settings->public_ipv4) {
+ $this->ipv4 = 'http://'.$settings->public_ipv4.':'.config('app.port');
+ }
+ if ($settings->public_ipv6) {
+ $this->ipv6 = 'http://'.$settings->public_ipv6.':'.config('app.port');
+ }
+ if ($this->github_app->installation_id && session('from')) {
+ $source_id = data_get(session('from'), 'source_id');
+ if (! $source_id || $this->github_app->id !== $source_id) {
+ session()->forget('from');
+ } else {
+ $parameters = data_get(session('from'), 'parameters');
+ $back = data_get(session('from'), 'back');
+ $environment_name = data_get($parameters, 'environment_name');
+ $project_uuid = data_get($parameters, 'project_uuid');
+ $type = data_get($parameters, 'type');
+ $destination = data_get($parameters, 'destination');
+ session()->forget('from');
+
+ return redirect()->route($back, [
+ 'environment_name' => $environment_name,
+ 'project_uuid' => $project_uuid,
+ 'type' => $type,
+ 'destination' => $destination,
+ ]);
+ }
+ }
+ $this->parameters = get_route_parameters();
+ if (isCloud() && ! isDev()) {
+ $this->webhook_endpoint = config('app.url');
} else {
- $parameters = data_get(session('from'), 'parameters');
- $back = data_get(session('from'), 'back');
- $environment_name = data_get($parameters, 'environment_name');
- $project_uuid = data_get($parameters, 'project_uuid');
- $type = data_get($parameters, 'type');
- $destination = data_get($parameters, 'destination');
- session()->forget('from');
-
- return redirect()->route($back, [
- 'environment_name' => $environment_name,
- 'project_uuid' => $project_uuid,
- 'type' => $type,
- 'destination' => $destination,
- ]);
+ $this->webhook_endpoint = $this->ipv4;
+ $this->is_system_wide = $this->github_app->is_system_wide;
}
- }
- $this->parameters = get_route_parameters();
- if (isCloud() && ! isDev()) {
- $this->webhook_endpoint = config('app.url');
- } else {
- $this->webhook_endpoint = $this->ipv4;
- $this->is_system_wide = $this->github_app->is_system_wide;
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
}
diff --git a/app/Livewire/Source/Github/Create.php b/app/Livewire/Source/Github/Create.php
index f85e8646e1..103c5c9fbb 100644
--- a/app/Livewire/Source/Github/Create.php
+++ b/app/Livewire/Source/Github/Create.php
@@ -23,7 +23,7 @@ class Create extends Component
public function mount()
{
- $this->name = generate_random_name();
+ $this->name = substr(generate_random_name(), 0, 34); // GitHub Apps names can only be 34 characters long
}
public function createGitHubApp()
diff --git a/app/Livewire/Subscription/PricingPlans.php b/app/Livewire/Subscription/PricingPlans.php
index 9bc11d862d..6b2d3fb364 100644
--- a/app/Livewire/Subscription/PricingPlans.php
+++ b/app/Livewire/Subscription/PricingPlans.php
@@ -2,55 +2,23 @@
namespace App\Livewire\Subscription;
+use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use Stripe\Checkout\Session;
use Stripe\Stripe;
class PricingPlans extends Component
{
- public bool $isTrial = false;
-
- public function mount()
- {
- $this->isTrial = ! data_get(currentTeam(), 'subscription.stripe_trial_already_ended');
- if (config('constants.limits.trial_period') == 0) {
- $this->isTrial = false;
- }
- }
-
public function subscribeStripe($type)
{
- $team = currentTeam();
Stripe::setApiKey(config('subscription.stripe_api_key'));
- switch ($type) {
- case 'basic-monthly':
- $priceId = config('subscription.stripe_price_id_basic_monthly');
- break;
- case 'basic-yearly':
- $priceId = config('subscription.stripe_price_id_basic_yearly');
- break;
- case 'pro-monthly':
- $priceId = config('subscription.stripe_price_id_pro_monthly');
- break;
- case 'pro-yearly':
- $priceId = config('subscription.stripe_price_id_pro_yearly');
- break;
- case 'ultimate-monthly':
- $priceId = config('subscription.stripe_price_id_ultimate_monthly');
- break;
- case 'ultimate-yearly':
- $priceId = config('subscription.stripe_price_id_ultimate_yearly');
- break;
- case 'dynamic-monthly':
- $priceId = config('subscription.stripe_price_id_dynamic_monthly');
- break;
- case 'dynamic-yearly':
- $priceId = config('subscription.stripe_price_id_dynamic_yearly');
- break;
- default:
- $priceId = config('subscription.stripe_price_id_basic_monthly');
- break;
- }
+
+ $priceId = match ($type) {
+ 'dynamic-monthly' => config('subscription.stripe_price_id_dynamic_monthly'),
+ 'dynamic-yearly' => config('subscription.stripe_price_id_dynamic_yearly'),
+ default => config('subscription.stripe_price_id_dynamic_monthly'),
+ };
+
if (! $priceId) {
$this->dispatch('error', 'Price ID not found! Please contact the administrator.');
@@ -59,10 +27,14 @@ public function subscribeStripe($type)
$payload = [
'allow_promotion_codes' => true,
'billing_address_collection' => 'required',
- 'client_reference_id' => auth()->user()->id.':'.currentTeam()->id,
+ 'client_reference_id' => Auth::id().':'.currentTeam()->id,
'line_items' => [[
'price' => $priceId,
- 'quantity' => 1,
+ 'adjustable_quantity' => [
+ 'enabled' => true,
+ 'minimum' => 2,
+ ],
+ 'quantity' => 2,
]],
'tax_id_collection' => [
'enabled' => true,
@@ -70,39 +42,18 @@ public function subscribeStripe($type)
'automatic_tax' => [
'enabled' => true,
],
-
+ 'subscription_data' => [
+ 'metadata' => [
+ 'user_id' => Auth::id(),
+ 'team_id' => currentTeam()->id,
+ ],
+ ],
+ 'payment_method_collection' => 'if_required',
'mode' => 'subscription',
'success_url' => route('dashboard', ['success' => true]),
'cancel_url' => route('subscription.index', ['cancelled' => true]),
];
- if (str($type)->contains('ultimate')) {
- $payload['line_items'][0]['adjustable_quantity'] = [
- 'enabled' => true,
- 'minimum' => 10,
- ];
- $payload['line_items'][0]['quantity'] = 10;
- }
- if (str($type)->contains('dynamic')) {
- $payload['line_items'][0]['adjustable_quantity'] = [
- 'enabled' => true,
- 'minimum' => 2,
- ];
- $payload['line_items'][0]['quantity'] = 2;
- }
- if (! data_get($team, 'subscription.stripe_trial_already_ended')) {
- if (config('constants.limits.trial_period') > 0) {
- $payload['subscription_data'] = [
- 'trial_period_days' => config('constants.limits.trial_period'),
- 'trial_settings' => [
- 'end_behavior' => [
- 'missing_payment_method' => 'cancel',
- ],
- ],
- ];
- }
- $payload['payment_method_collection'] = 'if_required';
- }
$customer = currentTeam()->subscription?->stripe_customer_id ?? null;
if ($customer) {
$payload['customer'] = $customer;
@@ -110,7 +61,7 @@ public function subscribeStripe($type)
'name' => 'auto',
];
} else {
- $payload['customer_email'] = auth()->user()->email;
+ $payload['customer_email'] = Auth::user()->email;
}
$session = Session::create($payload);
diff --git a/app/Livewire/Tags/Deployments.php b/app/Livewire/Tags/Deployments.php
index 270aa176a1..e4afa5b606 100644
--- a/app/Livewire/Tags/Deployments.php
+++ b/app/Livewire/Tags/Deployments.php
@@ -7,19 +7,19 @@
class Deployments extends Component
{
- public $deployments_per_tag_per_server = [];
+ public $deploymentsPerTagPerServer = [];
- public $resource_ids = [];
+ public $resourceIds = [];
public function render()
{
return view('livewire.tags.deployments');
}
- public function get_deployments()
+ public function getDeployments()
{
try {
- $this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $this->resource_ids)->get([
+ $this->deploymentsPerTagPerServer = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $this->resourceIds)->get([
'id',
'application_id',
'application_name',
@@ -29,7 +29,7 @@ public function get_deployments()
'server_id',
'status',
])->sortBy('id')->groupBy('server_name')->toArray();
- $this->dispatch('deployments', $this->deployments_per_tag_per_server);
+ $this->dispatch('deployments', $this->deploymentsPerTagPerServer);
} catch (\Exception $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Tags/Index.php b/app/Livewire/Tags/Index.php
deleted file mode 100644
index a01d00a70e..0000000000
--- a/app/Livewire/Tags/Index.php
+++ /dev/null
@@ -1,79 +0,0 @@
- 'update_deployments'];
-
- public function update_deployments($deployments)
- {
- $this->deployments_per_tag_per_server = $deployments;
- }
-
- public function tag_updated()
- {
- if ($this->tag == '') {
- return;
- }
- $tag = $this->tags->where('name', $this->tag)->first();
- if (! $tag) {
- $this->dispatch('error', "Tag ({$this->tag}) not found.");
- $this->tag = '';
-
- return;
- }
- $this->webhook = generatTagDeployWebhook($tag->name);
- $this->applications = $tag->applications()->get();
- $this->services = $tag->services()->get();
- }
-
- public function redeploy_all()
- {
- try {
- $this->applications->each(function ($resource) {
- $deploy = new DeployController;
- $deploy->deploy_resource($resource);
- });
- $this->services->each(function ($resource) {
- $deploy = new DeployController;
- $deploy->deploy_resource($resource);
- });
- $this->dispatch('success', 'Mass deployment started.');
- } catch (\Exception $e) {
- return handleError($e, $this);
- }
- }
-
- public function mount()
- {
- $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name');
- if ($this->tag) {
- $this->tag_updated();
- }
- }
-
- public function render()
- {
- return view('livewire.tags.index');
- }
-}
diff --git a/app/Livewire/Tags/Show.php b/app/Livewire/Tags/Show.php
index 668101edb6..fc5b133749 100644
--- a/app/Livewire/Tags/Show.php
+++ b/app/Livewire/Tags/Show.php
@@ -5,41 +5,57 @@
use App\Http\Controllers\Api\DeployController;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Tag;
+use Illuminate\Support\Collection;
+use Livewire\Attributes\Locked;
+use Livewire\Attributes\Title;
use Livewire\Component;
+#[Title('Tags | Coolify')]
class Show extends Component
{
- public $tags;
+ #[Locked]
+ public ?string $tagName = null;
- public Tag $tag;
+ #[Locked]
+ public ?Collection $tags = null;
- public $applications;
+ #[Locked]
+ public ?Tag $tag = null;
- public $services;
+ #[Locked]
+ public ?Collection $applications = null;
- public $webhook = null;
+ #[Locked]
+ public ?Collection $services = null;
- public $deployments_per_tag_per_server = [];
+ #[Locked]
+ public ?string $webhook = null;
+
+ #[Locked]
+ public ?array $deploymentsPerTagPerServer = null;
public function mount()
{
- $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name');
- $tag = $this->tags->where('name', request()->tag_name)->first();
- if (! $tag) {
- return redirect()->route('tags.index');
+ try {
+ $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name');
+ if (str($this->tagName)->isNotEmpty()) {
+ $tag = $this->tags->where('name', $this->tagName)->first();
+ $this->webhook = generateTagDeployWebhook($tag->name);
+ $this->applications = $tag->applications()->get();
+ $this->services = $tag->services()->get();
+ $this->tag = $tag;
+ $this->getDeployments();
+ }
+ } catch (\Exception $e) {
+ return handleError($e, $this);
}
- $this->webhook = generatTagDeployWebhook($tag->name);
- $this->applications = $tag->applications()->get();
- $this->services = $tag->services()->get();
- $this->tag = $tag;
- $this->get_deployments();
}
- public function get_deployments()
+ public function getDeployments()
{
try {
$resource_ids = $this->applications->pluck('id');
- $this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $resource_ids)->get([
+ $this->deploymentsPerTagPerServer = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $resource_ids)->get([
'id',
'application_id',
'application_name',
@@ -54,7 +70,7 @@ public function get_deployments()
}
}
- public function redeploy_all()
+ public function redeployAll()
{
try {
$message = collect([]);
diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php
index 3026cb2971..cfb47d9d83 100644
--- a/app/Livewire/Team/AdminView.php
+++ b/app/Livewire/Team/AdminView.php
@@ -2,6 +2,7 @@
namespace App\Livewire\Team;
+use App\Models\InstanceSettings;
use App\Models\Team;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
@@ -58,29 +59,30 @@ private function finalizeDeletion(User $user, Team $team)
foreach ($servers as $server) {
$resources = $server->definedResources();
foreach ($resources as $resource) {
- ray('Deleting resource: '.$resource->name);
$resource->forceDelete();
}
- ray('Deleting server: '.$server->name);
$server->forceDelete();
}
$projects = $team->projects;
foreach ($projects as $project) {
- ray('Deleting project: '.$project->name);
$project->forceDelete();
}
$team->members()->detach($user->id);
- ray('Deleting team: '.$team->name);
$team->delete();
}
public function delete($id, $password)
{
- if (! Hash::check($password, Auth::user()->password)) {
- $this->addError('password', 'The provided password is incorrect.');
+ if (! isInstanceAdmin()) {
+ return redirect()->route('dashboard');
+ }
+ if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
- return;
+ return;
+ }
}
if (! auth()->user()->isInstanceAdmin()) {
return $this->dispatch('error', 'You are not authorized to delete users');
@@ -88,29 +90,23 @@ public function delete($id, $password)
$user = User::find($id);
$teams = $user->teams;
foreach ($teams as $team) {
- ray($team->name);
$user_alone_in_team = $team->members->count() === 1;
if ($team->id === 0) {
if ($user_alone_in_team) {
- ray('user is alone in the root team, do nothing');
-
return $this->dispatch('error', 'User is alone in the root team, cannot delete');
}
}
if ($user_alone_in_team) {
- ray('user is alone in the team');
$this->finalizeDeletion($user, $team);
continue;
}
- ray('user is not alone in the team');
if ($user->isOwner()) {
$found_other_owner_or_admin = $team->members->filter(function ($member) {
return $member->pivot->role === 'owner' || $member->pivot->role === 'admin';
})->where('id', '!=', $user->id)->first();
if ($found_other_owner_or_admin) {
- ray('found other owner or admin');
$team->members()->detach($user->id);
continue;
@@ -119,24 +115,19 @@ public function delete($id, $password)
return $member->pivot->role === 'member';
})->first();
if ($found_other_member_who_is_not_owner) {
- ray('found other member who is not owner');
$found_other_member_who_is_not_owner->pivot->role = 'owner';
$found_other_member_who_is_not_owner->pivot->save();
$team->members()->detach($user->id);
} else {
- // This should never happen as if the user is the only member in the team, the team should be deleted already.
- ray('found no other member who is not owner');
$this->finalizeDeletion($user, $team);
}
continue;
}
} else {
- ray('user is not owner');
$team->members()->detach($user->id);
}
}
- ray('Deleting user: '.$user->name);
$user->delete();
$this->getUsers();
}
diff --git a/app/Livewire/Team/Create.php b/app/Livewire/Team/Create.php
index 992833da5e..f805d61222 100644
--- a/app/Livewire/Team/Create.php
+++ b/app/Livewire/Team/Create.php
@@ -3,28 +3,21 @@
namespace App\Livewire\Team;
use App\Models\Team;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Create extends Component
{
+ #[Validate(['required', 'min:3', 'max:255'])]
public string $name = '';
+ #[Validate(['nullable', 'min:3', 'max:255'])]
public ?string $description = null;
- protected $rules = [
- 'name' => 'required|min:3|max:255',
- 'description' => 'nullable|min:3|max:255',
- ];
-
- protected $validationAttributes = [
- 'name' => 'name',
- 'description' => 'description',
- ];
-
public function submit()
{
- $this->validate();
try {
+ $this->validate();
$team = Team::create([
'name' => $this->name,
'description' => $this->description,
diff --git a/app/Livewire/Team/Index.php b/app/Livewire/Team/Index.php
index 45600dbfe6..0972e7364f 100644
--- a/app/Livewire/Team/Index.php
+++ b/app/Livewire/Team/Index.php
@@ -4,6 +4,7 @@
use App\Models\Team;
use App\Models\TeamInvitation;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Livewire\Component;
@@ -55,7 +56,7 @@ public function delete()
$currentTeam->delete();
$currentTeam->members->each(function ($user) use ($currentTeam) {
- if ($user->id === auth()->user()->id) {
+ if ($user->id === Auth::id()) {
return;
}
$user->teams()->detach($currentTeam);
diff --git a/app/Livewire/Team/Invitations.php b/app/Livewire/Team/Invitations.php
index 6a32a1d161..93432efc86 100644
--- a/app/Livewire/Team/Invitations.php
+++ b/app/Livewire/Team/Invitations.php
@@ -13,17 +13,18 @@ class Invitations extends Component
public function deleteInvitation(int $invitation_id)
{
- $initiation_found = TeamInvitation::find($invitation_id);
- if (! $initiation_found) {
+ try {
+ $initiation_found = TeamInvitation::ownedByCurrentTeam()->findOrFail($invitation_id);
+ $initiation_found->delete();
+ $this->refreshInvitations();
+ $this->dispatch('success', 'Invitation revoked.');
+ } catch (\Exception) {
return $this->dispatch('error', 'Invitation not found.');
}
- $initiation_found->delete();
- $this->refreshInvitations();
- $this->dispatch('success', 'Invitation revoked.');
}
public function refreshInvitations()
{
- $this->invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
+ $this->invitations = TeamInvitation::ownedByCurrentTeam()->get();
}
}
diff --git a/app/Livewire/Team/InviteLink.php b/app/Livewire/Team/InviteLink.php
index 6c9e405fc9..25f8a1ff51 100644
--- a/app/Livewire/Team/InviteLink.php
+++ b/app/Livewire/Team/InviteLink.php
@@ -41,6 +41,9 @@ private function generate_invite_link(bool $sendEmail = false)
{
try {
$this->validate();
+ if (auth()->user()->role() === 'admin' && $this->role === 'owner') {
+ throw new \Exception('Admins cannot invite owners.');
+ }
$member_emails = currentTeam()->members()->get()->pluck('email');
if ($member_emails->contains($this->email)) {
return handleError(livewire: $this, customErrorMessage: "$this->email is already a member of ".currentTeam()->name.'.');
diff --git a/app/Livewire/Team/Member.php b/app/Livewire/Team/Member.php
index 680cb901b4..890d640a07 100644
--- a/app/Livewire/Team/Member.php
+++ b/app/Livewire/Team/Member.php
@@ -2,6 +2,7 @@
namespace App\Livewire\Team;
+use App\Enums\Role;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Livewire\Component;
@@ -12,29 +13,66 @@ class Member extends Component
public function makeAdmin()
{
- $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'admin']);
- $this->dispatch('reloadWindow');
+ try {
+ if (Role::from(auth()->user()->role())->lt(Role::ADMIN)
+ || Role::from($this->getMemberRole())->gt(auth()->user()->role())) {
+ throw new \Exception('You are not authorized to perform this action.');
+ }
+ $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => Role::ADMIN->value]);
+ $this->dispatch('reloadWindow');
+ } catch (\Exception $e) {
+ $this->dispatch('error', $e->getMessage());
+ }
}
public function makeOwner()
{
- $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'owner']);
- $this->dispatch('reloadWindow');
+ try {
+ if (Role::from(auth()->user()->role())->lt(Role::OWNER)
+ || Role::from($this->getMemberRole())->gt(auth()->user()->role())) {
+ throw new \Exception('You are not authorized to perform this action.');
+ }
+ $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => Role::OWNER->value]);
+ $this->dispatch('reloadWindow');
+ } catch (\Exception $e) {
+ $this->dispatch('error', $e->getMessage());
+ }
}
public function makeReadonly()
{
- $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'member']);
- $this->dispatch('reloadWindow');
+ try {
+ if (Role::from(auth()->user()->role())->lt(Role::ADMIN)
+ || Role::from($this->getMemberRole())->gt(auth()->user()->role())) {
+ throw new \Exception('You are not authorized to perform this action.');
+ }
+ $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => Role::MEMBER->value]);
+ $this->dispatch('reloadWindow');
+ } catch (\Exception $e) {
+ $this->dispatch('error', $e->getMessage());
+ }
}
public function remove()
{
- $this->member->teams()->detach(currentTeam());
- Cache::forget("team:{$this->member->id}");
- Cache::remember('team:'.$this->member->id, 3600, function () {
- return $this->member->teams()->first();
- });
- $this->dispatch('reloadWindow');
+ try {
+ if (Role::from(auth()->user()->role())->lt(Role::ADMIN)
+ || Role::from($this->getMemberRole())->gt(auth()->user()->role())) {
+ throw new \Exception('You are not authorized to perform this action.');
+ }
+ $this->member->teams()->detach(currentTeam());
+ Cache::forget("team:{$this->member->id}");
+ Cache::remember('team:'.$this->member->id, 3600, function () {
+ return $this->member->teams()->first();
+ });
+ $this->dispatch('reloadWindow');
+ } catch (\Exception $e) {
+ $this->dispatch('error', $e->getMessage());
+ }
+ }
+
+ private function getMemberRole()
+ {
+ return $this->member->teams()->where('teams.id', currentTeam()->id)->first()?->pivot?->role;
}
}
diff --git a/app/Livewire/Upgrade.php b/app/Livewire/Upgrade.php
index dfbd945f59..88ed88cb72 100644
--- a/app/Livewire/Upgrade.php
+++ b/app/Livewire/Upgrade.php
@@ -23,11 +23,9 @@ public function checkUpdate()
try {
$this->latestVersion = get_latest_version_of_coolify();
$this->isUpgradeAvailable = data_get(InstanceSettings::get(), 'new_version_available', false);
-
} catch (\Throwable $e) {
return handleError($e, $this);
}
-
}
public function upgrade()
diff --git a/app/Livewire/VerifyEmail.php b/app/Livewire/VerifyEmail.php
index d1f79c835b..fab3265b65 100644
--- a/app/Livewire/VerifyEmail.php
+++ b/app/Livewire/VerifyEmail.php
@@ -15,10 +15,7 @@ public function again()
$this->rateLimit(1, 300);
auth()->user()->sendVerificationEmail();
$this->dispatch('success', 'Email verification link sent!');
-
} catch (\Exception $e) {
- ray($e);
-
return handleError($e, $this);
}
}
diff --git a/app/Models/Application.php b/app/Models/Application.php
index 07aeb4c5b7..0ef787b2e1 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -114,17 +114,34 @@ class Application extends BaseModel
protected static function booted()
{
static::saving(function ($application) {
- if ($application->fqdn == '') {
+ if ($application->fqdn === '') {
$application->fqdn = null;
}
- $application->forceFill([
- 'fqdn' => $application->fqdn,
- 'install_command' => str($application->install_command)->trim(),
- 'build_command' => str($application->build_command)->trim(),
- 'start_command' => str($application->start_command)->trim(),
- 'base_directory' => str($application->base_directory)->trim(),
- 'publish_directory' => str($application->publish_directory)->trim(),
- ]);
+ $payload = [];
+ if ($application->isDirty('fqdn')) {
+ $payload['fqdn'] = $application->fqdn;
+ }
+ if ($application->isDirty('install_command')) {
+ $payload['install_command'] = str($application->install_command)->trim();
+ }
+ if ($application->isDirty('build_command')) {
+ $payload['build_command'] = str($application->build_command)->trim();
+ }
+ if ($application->isDirty('start_command')) {
+ $payload['start_command'] = str($application->start_command)->trim();
+ }
+ if ($application->isDirty('base_directory')) {
+ $payload['base_directory'] = str($application->base_directory)->trim();
+ }
+ if ($application->isDirty('publish_directory')) {
+ $payload['publish_directory'] = str($application->publish_directory)->trim();
+ }
+ if ($application->isDirty('status')) {
+ $payload['last_online_at'] = now();
+ }
+ if (count($payload) > 0) {
+ $application->forceFill($payload);
+ }
});
static::created(function ($application) {
ApplicationSetting::create([
@@ -155,6 +172,11 @@ public static function ownedByCurrentTeamAPI(int $teamId)
return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
}
+ public static function ownedByCurrentTeam()
+ {
+ return Application::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
+ }
+
public function getContainersToStop(bool $previewDeployments = false): array
{
$containers = $previewDeployments
@@ -221,7 +243,6 @@ public function delete_volumes(?Collection $persistentStorages)
{
if ($this->build_pack === 'dockercompose') {
$server = data_get($this, 'destination.server');
- ray('Deleting volumes');
instant_remote_process(["cd {$this->dirOnServer()} && docker compose down -v"], $server, false);
} else {
if ($persistentStorages->count() === 0) {
@@ -937,7 +958,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
$source_html_url_host = $url['host'];
$source_html_url_scheme = $url['scheme'];
- if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
+ if ($this->source->getMorphClass() === \App\Models\GithubApp::class) {
if ($this->source->is_public) {
$fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$customRepository} {$baseDir}";
@@ -1246,13 +1267,11 @@ public function parseContainerLabels(?ApplicationPreview $preview = null)
return;
}
if (base64_encode(base64_decode($customLabels, true)) !== $customLabels) {
- ray('custom_labels is not base64 encoded');
$this->custom_labels = str($customLabels)->replace(',', "\n");
$this->custom_labels = base64_encode($customLabels);
}
$customLabels = base64_decode($this->custom_labels);
if (mb_detect_encoding($customLabels, 'ASCII', true) === false) {
- ray('custom_labels contains non-ascii characters');
$customLabels = str(implode('|coolify|', generateLabelsApplication($this, $preview)))->replace('|coolify|', "\n");
}
$this->custom_labels = base64_encode($customLabels);
@@ -1400,29 +1419,48 @@ public static function getDomainsByUuid(string $uuid): array
return [];
}
- public function getMetrics(int $mins = 5)
+ public function getCpuMetrics(int $mins = 5)
{
$server = $this->destination->server;
$container_name = $this->uuid;
if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
- if ($error == 'Unauthorized') {
+ if ($error === 'Unauthorized') {
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
}
throw new \Exception($error);
}
- $metrics = str($metrics)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($metrics)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 2);
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
- return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
- });
+ return $parsedCollection->toArray();
+ }
+ }
+
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ if ($server->isMetricsEnabled()) {
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
});
return $parsedCollection->toArray();
@@ -1459,9 +1497,9 @@ public function generateConfig($is_json = false)
return $config;
}
- public function setConfig($config) {
- $config = $config;
+ public function setConfig($config)
+ {
$validator = Validator::make(['config' => $config], [
'config' => 'required|json',
]);
diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php
index 04a0ab27ef..bf2bf05bf1 100644
--- a/app/Models/ApplicationPreview.php
+++ b/app/Models/ApplicationPreview.php
@@ -28,6 +28,11 @@ protected static function booted()
});
}
});
+ static::saving(function ($preview) {
+ if ($preview->isDirty('status')) {
+ $preview->forceFill(['last_online_at' => now()]);
+ }
+ });
}
public static function findPreviewByApplicationAndPullId(int $application_id, int $pull_request_id)
diff --git a/app/Models/Environment.php b/app/Models/Environment.php
index c892d7ba17..71e8bbd218 100644
--- a/app/Models/Environment.php
+++ b/app/Models/Environment.php
@@ -27,10 +27,8 @@ protected static function booted()
static::deleting(function ($environment) {
$shared_variables = $environment->environment_variables();
foreach ($shared_variables as $shared_variable) {
- ray('Deleting environment shared variable: '.$shared_variable->name);
$shared_variable->delete();
}
-
});
}
diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php
index 9f8e4b342f..08f23d7ab1 100644
--- a/app/Models/EnvironmentVariable.php
+++ b/app/Models/EnvironmentVariable.php
@@ -44,7 +44,7 @@ class EnvironmentVariable extends Model
'version' => 'string',
];
- protected $appends = ['real_value', 'is_shared'];
+ protected $appends = ['real_value', 'is_shared', 'is_really_required'];
protected static function booted()
{
@@ -74,6 +74,9 @@ protected static function booted()
'version' => config('version'),
]);
});
+ static::saving(function (EnvironmentVariable $environmentVariable) {
+ $environmentVariable->updateIsShared();
+ });
}
public function service()
@@ -130,6 +133,13 @@ public function realValue(): Attribute
);
}
+ protected function isReallyRequired(): Attribute
+ {
+ return Attribute::make(
+ get: fn () => $this->is_required && str($this->real_value)->isEmpty(),
+ );
+ }
+
protected function isShared(): Attribute
{
return Attribute::make(
@@ -146,13 +156,12 @@ protected function isShared(): Attribute
private function get_real_environment_variables(?string $environment_variable = null, $resource = null)
{
- if ((is_null($environment_variable) && $environment_variable == '') || is_null($resource)) {
+ if ((is_null($environment_variable) && $environment_variable === '') || is_null($resource)) {
return null;
}
$environment_variable = trim($environment_variable);
$sharedEnvsFound = str($environment_variable)->matchAll('/{{(.*?)}}/');
if ($sharedEnvsFound->isEmpty()) {
-
return $environment_variable;
}
@@ -192,7 +201,7 @@ private function get_environment_variables(?string $environment_variable = null)
private function set_environment_variables(?string $environment_variable = null): ?string
{
- if (is_null($environment_variable) && $environment_variable == '') {
+ if (is_null($environment_variable) && $environment_variable === '') {
return null;
}
$environment_variable = trim($environment_variable);
@@ -210,4 +219,11 @@ protected function key(): Attribute
set: fn (string $value) => str($value)->trim()->replace(' ', '_')->value,
);
}
+
+ protected function updateIsShared(): void
+ {
+ $type = str($this->value)->after('{{')->before('.')->value;
+ $isShared = str($this->value)->startsWith('{{'.$type) && str($this->value)->endsWith('}}');
+ $this->is_shared = $isShared;
+ }
}
diff --git a/app/Models/GithubApp.php b/app/Models/GithubApp.php
index 66ecdd9670..0b0e93b128 100644
--- a/app/Models/GithubApp.php
+++ b/app/Models/GithubApp.php
@@ -31,6 +31,11 @@ protected static function booted(): void
});
}
+ public static function ownedByCurrentTeam()
+ {
+ return GithubApp::whereTeamId(currentTeam()->id);
+ }
+
public static function public()
{
return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get();
@@ -60,7 +65,7 @@ public function type(): Attribute
{
return Attribute::make(
get: function () {
- if ($this->getMorphClass() === 'App\Models\GithubApp') {
+ if ($this->getMorphClass() === \App\Models\GithubApp::class) {
return 'github';
}
},
diff --git a/app/Models/GitlabApp.php b/app/Models/GitlabApp.php
index a789a7e658..2112a4a664 100644
--- a/app/Models/GitlabApp.php
+++ b/app/Models/GitlabApp.php
@@ -9,6 +9,11 @@ class GitlabApp extends BaseModel
'app_secret',
];
+ public static function ownedByCurrentTeam()
+ {
+ return GitlabApp::whereTeamId(currentTeam()->id);
+ }
+
public function applications()
{
return $this->morphMany(Application::class, 'source');
diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php
index bb3d1478b6..eeb8039252 100644
--- a/app/Models/InstanceSettings.php
+++ b/app/Models/InstanceSettings.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Jobs\PullHelperImageJob;
use App\Notifications\Channels\SendsEmail;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
@@ -21,8 +22,22 @@ class InstanceSettings extends Model implements SendsEmail
'is_auto_update_enabled' => 'boolean',
'auto_update_frequency' => 'string',
'update_check_frequency' => 'string',
+ 'sentinel_token' => 'encrypted',
];
+ protected static function booted(): void
+ {
+ static::updated(function ($settings) {
+ if ($settings->isDirty('helper_version')) {
+ Server::chunkById(100, function ($servers) {
+ foreach ($servers as $server) {
+ PullHelperImageJob::dispatch($server);
+ }
+ });
+ }
+ });
+ }
+
public function fqdn(): Attribute
{
return Attribute::make(
@@ -86,16 +101,16 @@ public function getTitleDisplayName(): string
return "[{$instanceName}]";
}
- public function helperVersion(): Attribute
- {
- return Attribute::make(
- get: function ($value) {
- if (isDev()) {
- return 'latest';
- }
-
- return $value;
- }
- );
- }
+ // public function helperVersion(): Attribute
+ // {
+ // return Attribute::make(
+ // get: function ($value) {
+ // if (isDev()) {
+ // return 'latest';
+ // }
+
+ // return $value;
+ // }
+ // );
+ // }
}
diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php
index d528099ff8..2c223be779 100644
--- a/app/Models/LocalFileVolume.php
+++ b/app/Models/LocalFileVolume.php
@@ -72,7 +72,6 @@ public function deleteStorageOnServer()
if ($path && $path != '/' && $path != '.' && $path != '..') {
if ($isFile === 'OK') {
$commands->push("rm -rf $path > /dev/null 2>&1 || true");
-
} elseif ($isDir === 'OK') {
$commands->push("rm -rf $path > /dev/null 2>&1 || true");
$commands->push("rmdir $path > /dev/null 2>&1 || true");
@@ -113,15 +112,15 @@ public function saveStorageOnServer()
}
$isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
$isDir = instant_remote_process(["test -d $path && echo OK || echo NOK"], $server);
- if ($isFile == 'OK' && $this->is_directory) {
+ if ($isFile === 'OK' && $this->is_directory) {
$content = instant_remote_process(["cat $path"], $server, false);
$this->is_directory = false;
$this->content = $content;
$this->save();
FileStorageChanged::dispatch(data_get($server, 'team_id'));
throw new \Exception('The following file is a file on the server, but you are trying to mark it as a directory. Please delete the file on the server or mark it as directory.');
- } elseif ($isDir == 'OK' && ! $this->is_directory) {
- if ($path == '/' || $path == '.' || $path == '..' || $path == '' || str($path)->isEmpty() || is_null($path)) {
+ } elseif ($isDir === 'OK' && ! $this->is_directory) {
+ if ($path === '/' || $path === '.' || $path === '..' || $path === '' || str($path)->isEmpty() || is_null($path)) {
$this->is_directory = true;
$this->save();
throw new \Exception('The following file is a directory on the server, but you are trying to mark it as a file.
Please delete the directory on the server or mark it as directory.');
@@ -132,7 +131,7 @@ public function saveStorageOnServer()
], $server, false);
FileStorageChanged::dispatch(data_get($server, 'team_id'));
}
- if ($isDir == 'NOK' && ! $this->is_directory) {
+ if ($isDir === 'NOK' && ! $this->is_directory) {
$chmod = data_get($this, 'chmod');
$chown = data_get($this, 'chown');
if ($content) {
@@ -148,7 +147,7 @@ public function saveStorageOnServer()
if ($chmod) {
$commands->push("chmod $chmod $path");
}
- } elseif ($isDir == 'NOK' && $this->is_directory) {
+ } elseif ($isDir === 'NOK' && $this->is_directory) {
$commands->push("mkdir -p $path > /dev/null 2>&1 || true");
}
diff --git a/app/Models/Project.php b/app/Models/Project.php
index 5a9dd964a5..f27e6c2083 100644
--- a/app/Models/Project.php
+++ b/app/Models/Project.php
@@ -47,7 +47,6 @@ protected static function booted()
$project->settings()->delete();
$shared_variables = $project->environment_variables();
foreach ($shared_variables as $shared_variable) {
- ray('Deleting project shared variable: '.$shared_variable->name);
$shared_variable->delete();
}
});
@@ -123,9 +122,18 @@ public function mariadbs()
return $this->hasManyThrough(StandaloneMariadb::class, Environment::class);
}
- public function resource_count()
+ public function isEmpty()
{
- return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count() + $this->keydbs()->count() + $this->dragonflies()->count() + $this->clickhouses()->count() + $this->services()->count();
+ return $this->applications()->count() == 0 &&
+ $this->redis()->count() == 0 &&
+ $this->postgresqls()->count() == 0 &&
+ $this->mysqls()->count() == 0 &&
+ $this->keydbs()->count() == 0 &&
+ $this->dragonflies()->count() == 0 &&
+ $this->clickhouses()->count() == 0 &&
+ $this->mariadbs()->count() == 0 &&
+ $this->mongodbs()->count() == 0 &&
+ $this->services()->count() == 0;
}
public function databases()
diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php
index 3921e32e4f..473fc7b4bb 100644
--- a/app/Models/ScheduledDatabaseBackup.php
+++ b/app/Models/ScheduledDatabaseBackup.php
@@ -51,7 +51,6 @@ public function server()
}
}
-
return null;
}
}
diff --git a/app/Models/ScheduledTask.php b/app/Models/ScheduledTask.php
index 3cee5a875a..264a04d1f7 100644
--- a/app/Models/ScheduledTask.php
+++ b/app/Models/ScheduledTask.php
@@ -34,21 +34,15 @@ public function server()
{
if ($this->application) {
if ($this->application->destination && $this->application->destination->server) {
- $server = $this->application->destination->server;
-
- return $server;
+ return $this->application->destination->server;
}
} elseif ($this->service) {
if ($this->service->destination && $this->service->destination->server) {
- $server = $this->service->destination->server;
-
- return $server;
+ return $this->service->destination->server;
}
} elseif ($this->database) {
if ($this->database->destination && $this->database->destination->server) {
- $server = $this->database->destination->server;
-
- return $server;
+ return $this->database->destination->server;
}
}
diff --git a/app/Models/Server.php b/app/Models/Server.php
index 0eca3c1684..3076308ade 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -3,13 +3,17 @@
namespace App\Models;
use App\Actions\Server\InstallDocker;
+use App\Actions\Server\StartSentinel;
use App\Enums\ProxyTypes;
-use App\Jobs\PullSentinelImageJob;
+use App\Jobs\CheckAndStartSentinelJob;
+use App\Notifications\Server\Reachable;
+use App\Notifications\Server\Unreachable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
+use Illuminate\Database\Eloquent\SoftDeletes;
+use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
-use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Stringable;
use OpenApi\Attributes as OA;
@@ -43,7 +47,7 @@
class Server extends BaseModel
{
- use SchemalessAttributesTrait;
+ use SchemalessAttributesTrait, SoftDeletes;
public static $batch_counter = 0;
@@ -59,6 +63,11 @@ protected static function booted()
}
$server->forceFill($payload);
});
+ static::saved(function ($server) {
+ if ($server->privateKey->isDirty()) {
+ refresh_server_connection($server->privateKey);
+ }
+ });
static::created(function ($server) {
ServerSetting::create([
'server_id' => $server->id,
@@ -95,7 +104,8 @@ protected static function booted()
}
}
});
- static::deleting(function ($server) {
+
+ static::forceDeleting(function ($server) {
$server->destinations()->each(function ($destination) {
$destination->delete();
});
@@ -103,12 +113,15 @@ protected static function booted()
});
}
- public $casts = [
+ protected $casts = [
'proxy' => SchemalessAttributes::class,
'logdrain_axiom_api_key' => 'encrypted',
'logdrain_newrelic_license_key' => 'encrypted',
'delete_unused_volumes' => 'boolean',
'delete_unused_networks' => 'boolean',
+ 'unreachable_notification_sent' => 'boolean',
+ 'is_build_server' => 'boolean',
+ 'force_disabled' => 'boolean',
];
protected $schemalessAttributes = [
@@ -127,6 +140,11 @@ protected static function booted()
protected $guarded = [];
+ public function type()
+ {
+ return 'server';
+ }
+
public static function isReachable()
{
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true);
@@ -401,7 +419,7 @@ public function setupDynamicProxyConfiguration()
"echo '$base64' | base64 -d | tee $file > /dev/null",
], $this);
- if (config('app.env') == 'local') {
+ if (config('app.env') === 'local') {
// ray($yaml);
}
}
@@ -489,20 +507,6 @@ public static function buildServers($teamId)
return Server::whereTeamId($teamId)->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_build_server', true);
}
- public function skipServer()
- {
- if ($this->ip === '1.2.3.4') {
- // ray('skipping 1.2.3.4');
- return true;
- }
- if ($this->settings->force_disabled === true) {
- // ray('force_disabled');
- return true;
- }
-
- return false;
- }
-
public function isForceDisabled()
{
return $this->settings->force_disabled;
@@ -510,95 +514,83 @@ public function isForceDisabled()
public function forceEnableServer()
{
- $this->settings->update([
- 'force_disabled' => false,
- ]);
+ $this->settings->force_disabled = false;
+ $this->settings->save();
}
public function forceDisableServer()
{
- $this->settings->update([
- 'force_disabled' => true,
- ]);
+ $this->settings->force_disabled = true;
+ $this->settings->save();
$sshKeyFileLocation = "id.root@{$this->uuid}";
Storage::disk('ssh-keys')->delete($sshKeyFileLocation);
Storage::disk('ssh-mux')->delete($this->muxFilename());
}
- public function isSentinelEnabled()
+ public function sentinelHeartbeat(bool $isReset = false)
{
- return $this->isMetricsEnabled() || $this->isServerApiEnabled();
+ $this->sentinel_updated_at = $isReset ? now()->subMinutes(6000) : now();
+ $this->save();
}
- public function isMetricsEnabled()
+ /**
+ * Get the wait time for Sentinel to push before performing an SSH check.
+ *
+ * @return int The wait time in seconds.
+ */
+ public function waitBeforeDoingSshCheck(): int
{
- return $this->settings->is_metrics_enabled;
+ $wait = $this->settings->sentinel_push_interval_seconds * 3;
+ if ($wait < 120) {
+ $wait = 120;
+ }
+
+ return $wait;
}
- public function isServerApiEnabled()
+ public function isSentinelLive()
{
- return $this->settings->is_server_api_enabled;
+ return Carbon::parse($this->sentinel_updated_at)->isAfter(now()->subSeconds($this->waitBeforeDoingSshCheck()));
}
- public function checkServerApi()
+ public function isSentinelEnabled()
{
- if ($this->isServerApiEnabled()) {
- $server_ip = $this->ip;
- if (isDev()) {
- if ($this->id === 0) {
- $server_ip = 'localhost';
- }
- }
- $command = "curl -s http://{$server_ip}:12172/api/health";
- $process = Process::timeout(5)->run($command);
- if ($process->failed()) {
- ray($process->exitCode(), $process->output(), $process->errorOutput());
- throw new \Exception("Server API is not reachable on http://{$server_ip}:12172");
- }
+ return ($this->isMetricsEnabled() || $this->isServerApiEnabled()) && ! $this->isBuildServer();
+ }
- }
+ public function isMetricsEnabled()
+ {
+ return $this->settings->is_metrics_enabled;
+ }
+
+ public function isServerApiEnabled()
+ {
+ return $this->settings->is_sentinel_enabled;
}
public function checkSentinel()
{
- // ray("Checking sentinel on server: {$this->name}");
- if ($this->isSentinelEnabled()) {
- $sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false);
- $sentinel_found = json_decode($sentinel_found, true);
- $status = data_get($sentinel_found, '0.State.Status', 'exited');
- if ($status !== 'running') {
- // ray('Sentinel is not running, starting it...');
- PullSentinelImageJob::dispatch($this);
- } else {
- // ray('Sentinel is running');
- }
- }
+ CheckAndStartSentinelJob::dispatch($this);
}
public function getCpuMetrics(int $mins = 5)
{
if ($this->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->metrics_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false);
+ $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false);
if (str($cpu)->contains('error')) {
$error = json_decode($cpu, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
- if ($error == 'Unauthorized') {
+ if ($error === 'Unauthorized') {
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
}
throw new \Exception($error);
}
- $cpu = str($cpu)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($cpu)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 0);
-
- return [(int) $time, (float) $cpu_usage_percent];
- });
- });
+ $cpu = json_decode($cpu, true);
- return $parsedCollection->toArray();
+ return collect($cpu)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
}
}
@@ -606,98 +598,28 @@ public function getMemoryMetrics(int $mins = 5)
{
if ($this->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->metrics_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false);
+ $memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false);
if (str($memory)->contains('error')) {
$error = json_decode($memory, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
- if ($error == 'Unauthorized') {
+ if ($error === 'Unauthorized') {
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
}
throw new \Exception($error);
}
- $memory = str($memory)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($memory)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $used, $free, $usedPercent] = explode(',', trim($line));
- $usedPercent = number_format($usedPercent, 0);
-
- return [(int) $time, (float) $usedPercent];
- });
+ $memory = json_decode($memory, true);
+ $parsedCollection = collect($memory)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['usedPercent']];
});
return $parsedCollection->toArray();
}
}
- public function isServerReady(int $tries = 3)
- {
- if ($this->skipServer()) {
- return false;
- }
- $serverUptimeCheckNumber = $this->unreachable_count;
- if ($this->unreachable_count < $tries) {
- $serverUptimeCheckNumber = $this->unreachable_count + 1;
- }
- if ($this->unreachable_count > $tries) {
- $serverUptimeCheckNumber = $tries;
- }
-
- $serverUptimeCheckNumberMax = $tries;
-
- // ray('server: ' . $this->name);
- // ray('serverUptimeCheckNumber: ' . $serverUptimeCheckNumber);
- // ray('serverUptimeCheckNumberMax: ' . $serverUptimeCheckNumberMax);
-
- ['uptime' => $uptime] = $this->validateConnection();
- if ($uptime) {
- if ($this->unreachable_notification_sent === true) {
- $this->update(['unreachable_notification_sent' => false]);
- }
-
- return true;
- } else {
- if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
- // Reached max number of retries
- if ($this->unreachable_notification_sent === false) {
- ray('Server unreachable, sending notification...');
- // $this->team?->notify(new Unreachable($this));
- $this->update(['unreachable_notification_sent' => true]);
- }
- if ($this->settings->is_reachable === true) {
- $this->settings()->update([
- 'is_reachable' => false,
- ]);
- }
-
- foreach ($this->applications() as $application) {
- $application->update(['status' => 'exited']);
- }
- foreach ($this->databases() as $database) {
- $database->update(['status' => 'exited']);
- }
- foreach ($this->services()->get() as $service) {
- $apps = $service->applications()->get();
- $dbs = $service->databases()->get();
- foreach ($apps as $app) {
- $app->update(['status' => 'exited']);
- }
- foreach ($dbs as $db) {
- $db->update(['status' => 'exited']);
- }
- }
- } else {
- $this->update([
- 'unreachable_count' => $this->unreachable_count + 1,
- ]);
- }
-
- return false;
- }
- }
-
public function getDiskUsage(): ?string
{
- return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false);
+ return instant_remote_process(['df / --output=pcent | tr -cd 0-9'], $this, false);
+ // return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false);
}
public function definedResources()
@@ -755,7 +677,7 @@ public function getContainers()
}
}
} else {
- $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this, false);
+ $containers = instant_remote_process(["docker container inspect $(docker container ls -aq) --format '{{json .}}'"], $this, false);
$containers = format_docker_command_output_to_json($containers);
$containerReplicates = collect([]);
}
@@ -899,9 +821,7 @@ public function user(): Attribute
{
return Attribute::make(
get: function ($value) {
- $sanitizedValue = preg_replace('/[^A-Za-z0-9\-_]/', '', $value);
-
- return $sanitizedValue;
+ return preg_replace('/[^A-Za-z0-9\-_]/', '', $value);
}
);
}
@@ -945,8 +865,6 @@ public function destinations()
$standalone_docker = $this->hasMany(StandaloneDocker::class)->get();
$swarm_docker = $this->hasMany(SwarmDocker::class)->get();
- // $additional_dockers = $this->belongsToMany(StandaloneDocker::class, 'additional_destinations')->withPivot('server_id')->get();
- // return $standalone_docker->concat($swarm_docker)->concat($additional_dockers);
return $standalone_docker->concat($swarm_docker);
}
@@ -977,18 +895,31 @@ public function team()
public function isProxyShouldRun()
{
- if ($this->proxyType() === ProxyTypes::NONE->value || $this->settings->is_build_server) {
+ // TODO: Do we need "|| $this->proxy->force_stop" here?
+ if ($this->proxyType() === ProxyTypes::NONE->value || $this->isBuildServer()) {
return false;
}
return true;
}
+ public function skipServer()
+ {
+ if ($this->ip === '1.2.3.4') {
+ return true;
+ }
+ if ($this->settings->force_disabled === true) {
+ return true;
+ }
+
+ return false;
+ }
+
public function isFunctional()
{
- $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && ! $this->settings->force_disabled;
+ $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && $this->settings->force_disabled === false && $this->ip !== '1.2.3.4';
- if (! $isFunctional) {
+ if ($isFunctional === false) {
Storage::disk('ssh-mux')->delete($this->muxFilename());
}
@@ -1041,39 +972,110 @@ public function isSwarmWorker()
return data_get($this, 'settings.is_swarm_worker');
}
- public function validateConnection($isManualCheck = true)
+ public function serverStatus(): bool
{
- config()->set('constants.ssh.mux_enabled', ! $isManualCheck);
- // ray('Manual Check: ' . ($isManualCheck ? 'true' : 'false'));
+ if ($this->status() === false) {
+ return false;
+ }
+ if ($this->isFunctional() === false) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function status(): bool
+ {
+ ['uptime' => $uptime] = $this->validateConnection(false);
+ if ($uptime === false) {
+ foreach ($this->applications() as $application) {
+ $application->status = 'exited';
+ $application->save();
+ }
+ foreach ($this->databases() as $database) {
+ $database->status = 'exited';
+ $database->save();
+ }
+ foreach ($this->services() as $service) {
+ $apps = $service->applications()->get();
+ $dbs = $service->databases()->get();
+ foreach ($apps as $app) {
+ $app->status = 'exited';
+ $app->save();
+ }
+ foreach ($dbs as $db) {
+ $db->status = 'exited';
+ $db->save();
+ }
+ }
- $server = Server::find($this->id);
- if (! $server) {
- return ['uptime' => false, 'error' => 'Server not found.'];
+ return false;
}
- if ($server->skipServer()) {
+
+ return true;
+ }
+
+ public function isReachableChanged()
+ {
+ $this->refresh();
+ $unreachableNotificationSent = (bool) $this->unreachable_notification_sent;
+ $isReachable = (bool) $this->settings->is_reachable;
+ // If the server is reachable, send the reachable notification if it was sent before
+ if ($isReachable === true) {
+ if ($unreachableNotificationSent === true) {
+ $this->sendReachableNotification();
+ }
+ } else {
+ // If the server is unreachable, send the unreachable notification if it was not sent before
+ if ($unreachableNotificationSent === false) {
+ $this->sendUnreachableNotification();
+ }
+ }
+ }
+
+ public function sendReachableNotification()
+ {
+ $this->unreachable_notification_sent = false;
+ $this->save();
+ $this->refresh();
+ $this->team->notify(new Reachable($this));
+ }
+
+ public function sendUnreachableNotification()
+ {
+ $this->unreachable_notification_sent = true;
+ $this->save();
+ $this->refresh();
+ $this->team->notify(new Unreachable($this));
+ }
+
+ public function validateConnection(bool $isManualCheck = true, bool $justCheckingNewKey = false)
+ {
+ config()->set('constants.ssh.mux_enabled', ! $isManualCheck);
+
+ if ($this->skipServer()) {
return ['uptime' => false, 'error' => 'Server skipped.'];
}
try {
// Make sure the private key is stored
- if ($server->privateKey) {
- $server->privateKey->storeInFileSystem();
+ if ($this->privateKey) {
+ $this->privateKey->storeInFileSystem();
}
- instant_remote_process(['ls /'], $server);
- $server->settings()->update([
- 'is_reachable' => true,
- ]);
- $server->update([
- 'unreachable_count' => 0,
- ]);
- if (data_get($server, 'unreachable_notification_sent') === true) {
- $server->update(['unreachable_notification_sent' => false]);
+ instant_remote_process(['ls /'], $this);
+ if ($this->settings->is_reachable === false) {
+ $this->settings->is_reachable = true;
+ $this->settings->save();
}
return ['uptime' => true, 'error' => null];
} catch (\Throwable $e) {
- $server->settings()->update([
- 'is_reachable' => false,
- ]);
+ if ($justCheckingNewKey) {
+ return ['uptime' => false, 'error' => 'This key is not valid for this server.'];
+ }
+ if ($this->settings->is_reachable === true) {
+ $this->settings->is_reachable = false;
+ $this->settings->save();
+ }
return ['uptime' => false, 'error' => $e->getMessage()];
}
@@ -1081,9 +1083,7 @@ public function validateConnection($isManualCheck = true)
public function installDocker()
{
- $activity = InstallDocker::run($this);
-
- return $activity;
+ return InstallDocker::run($this);
}
public function validateDockerEngine($throwError = false)
@@ -1228,4 +1228,27 @@ public function isIpv6(): bool
{
return str($this->ip)->contains(':');
}
+
+ public function restartSentinel(bool $async = true)
+ {
+ try {
+ if ($async) {
+ StartSentinel::dispatch($this, true);
+ } else {
+ StartSentinel::run($this, true);
+ }
+ } catch (\Throwable $e) {
+ return handleError($e);
+ }
+ }
+
+ public function url()
+ {
+ return base_url().'/server/'.$this->uuid;
+ }
+
+ public function restartContainer(string $containerName)
+ {
+ return instant_remote_process(['docker restart '.$containerName], $this, false);
+ }
}
diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php
index c44a393b4f..fc2c5a0f4b 100644
--- a/app/Models/ServerSetting.php
+++ b/app/Models/ServerSetting.php
@@ -4,6 +4,7 @@
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Facades\Log;
use OpenApi\Attributes as OA;
#[OA\Schema(
@@ -24,7 +25,7 @@
'is_logdrain_newrelic_enabled' => ['type' => 'boolean'],
'is_metrics_enabled' => ['type' => 'boolean'],
'is_reachable' => ['type' => 'boolean'],
- 'is_server_api_enabled' => ['type' => 'boolean'],
+ 'is_sentinel_enabled' => ['type' => 'boolean'],
'is_swarm_manager' => ['type' => 'boolean'],
'is_swarm_worker' => ['type' => 'boolean'],
'is_usable' => ['type' => 'boolean'],
@@ -35,9 +36,9 @@
'logdrain_highlight_project_id' => ['type' => 'string'],
'logdrain_newrelic_base_uri' => ['type' => 'string'],
'logdrain_newrelic_license_key' => ['type' => 'string'],
- 'metrics_history_days' => ['type' => 'integer'],
- 'metrics_refresh_rate_seconds' => ['type' => 'integer'],
- 'metrics_token' => ['type' => 'string'],
+ 'sentinel_metrics_history_days' => ['type' => 'integer'],
+ 'sentinel_metrics_refresh_rate_seconds' => ['type' => 'integer'],
+ 'sentinel_token' => ['type' => 'string'],
'docker_cleanup_frequency' => ['type' => 'string'],
'docker_cleanup_threshold' => ['type' => 'integer'],
'server_id' => ['type' => 'integer'],
@@ -53,8 +54,85 @@ class ServerSetting extends Model
protected $casts = [
'force_docker_cleanup' => 'boolean',
'docker_cleanup_threshold' => 'integer',
+ 'sentinel_token' => 'encrypted',
+ 'is_reachable' => 'boolean',
+ 'is_usable' => 'boolean',
];
+ protected static function booted()
+ {
+ static::creating(function ($setting) {
+ try {
+ if (str($setting->sentinel_token)->isEmpty()) {
+ $setting->generateSentinelToken(save: false, ignoreEvent: true);
+ }
+ if (str($setting->sentinel_custom_url)->isEmpty()) {
+ $setting->generateSentinelUrl(save: false, ignoreEvent: true);
+ }
+ } catch (\Throwable $e) {
+ Log::error('Error creating server setting: '.$e->getMessage());
+ }
+ });
+ static::updated(function ($settings) {
+ if (
+ $settings->isDirty('sentinel_token') ||
+ $settings->isDirty('sentinel_custom_url') ||
+ $settings->isDirty('sentinel_metrics_refresh_rate_seconds') ||
+ $settings->isDirty('sentinel_metrics_history_days') ||
+ $settings->isDirty('sentinel_push_interval_seconds')
+ ) {
+ $settings->server->restartSentinel();
+ }
+ if ($settings->isDirty('is_reachable')) {
+ $settings->server->isReachableChanged();
+ }
+ });
+ }
+
+ public function generateSentinelToken(bool $save = true, bool $ignoreEvent = false)
+ {
+ $data = [
+ 'server_uuid' => $this->server->uuid,
+ ];
+ $token = json_encode($data);
+ $encrypted = encrypt($token);
+ $this->sentinel_token = $encrypted;
+ if ($save) {
+ if ($ignoreEvent) {
+ $this->saveQuietly();
+ } else {
+ $this->save();
+ }
+ }
+
+ return $token;
+ }
+
+ public function generateSentinelUrl(bool $save = true, bool $ignoreEvent = false)
+ {
+ $domain = null;
+ $settings = InstanceSettings::get();
+ if ($this->server->isLocalhost()) {
+ $domain = 'http://host.docker.internal:8000';
+ } elseif ($settings->fqdn) {
+ $domain = $settings->fqdn;
+ } elseif ($settings->public_ipv4) {
+ $domain = 'http://'.$settings->public_ipv4.':8000';
+ } elseif ($settings->public_ipv6) {
+ $domain = 'http://'.$settings->public_ipv6.':8000';
+ }
+ $this->sentinel_custom_url = $domain;
+ if ($save) {
+ if ($ignoreEvent) {
+ $this->saveQuietly();
+ } else {
+ $this->save();
+ }
+ }
+
+ return $domain;
+ }
+
public function server()
{
return $this->belongsTo(Server::class);
diff --git a/app/Models/Service.php b/app/Models/Service.php
index 0036a9fda4..0c9e081a1e 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -133,6 +133,11 @@ public function tags()
return $this->morphToMany(Tag::class, 'taggable');
}
+ public static function ownedByCurrentTeam()
+ {
+ return Service::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
+ }
+
public function getContainersToStop(): array
{
$containersToStop = [];
@@ -297,7 +302,7 @@ public function extraFields()
'key' => 'CP_DISABLE_HTTPS',
'value' => data_get($disable_https, 'value'),
'rules' => 'required',
- 'customHelper' => "If you want to use https, set this to 0. Variable name: CP_DISABLE_HTTPS",
+ 'customHelper' => 'If you want to use https, set this to 0. Variable name: CP_DISABLE_HTTPS',
],
]);
}
@@ -366,7 +371,6 @@ public function extraFields()
]);
}
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_LANGFUSE')->first();
- ray('password', $password);
if ($password) {
$data = $data->merge([
'Admin Password' => [
@@ -997,8 +1001,8 @@ public function extraFields()
break;
case $image->contains('mysql'):
$userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS', 'MYSQL_USER'];
- $passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD','SERVICE_PASSWORD_64_MYSQL'];
- $rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT','SERVICE_PASSWORD_64_MYSQLROOT'];
+ $passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD', 'SERVICE_PASSWORD_64_MYSQL'];
+ $rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT', 'SERVICE_PASSWORD_64_MYSQLROOT'];
$dbNameVariables = ['MYSQL_DATABASE'];
$mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
$mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
@@ -1096,7 +1100,6 @@ public function extraFields()
}
$fields->put('MariaDB', $data->toArray());
break;
-
}
}
@@ -1108,7 +1111,6 @@ public function saveExtraFields($fields)
foreach ($fields as $field) {
$key = data_get($field, 'key');
$value = data_get($field, 'value');
- ray($key, $value);
$found = $this->environment_variables()->where('key', $key)->first();
if ($found) {
$found->value = $value;
@@ -1232,7 +1234,6 @@ public function scheduled_tasks(): HasMany
public function environment_variables(): HasMany
{
-
return $this->hasMany(EnvironmentVariable::class)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC");
}
@@ -1307,13 +1308,26 @@ public function parse(bool $isNew = false): Collection
} else {
return collect([]);
}
-
}
public function networks()
{
- $networks = getTopLevelNetworks($this);
+ return getTopLevelNetworks($this);
+ }
- return $networks;
+ protected function isDeployable(): Attribute
+ {
+ return Attribute::make(
+ get: function () {
+ $envs = $this->environment_variables()->where('is_required', true)->get();
+ foreach ($envs as $env) {
+ if ($env->is_really_required) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ );
}
}
diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php
index 0e79e1e2e8..5cafc90422 100644
--- a/app/Models/ServiceApplication.php
+++ b/app/Models/ServiceApplication.php
@@ -19,6 +19,11 @@ protected static function booted()
$service->persistentStorages()->delete();
$service->fileStorages()->delete();
});
+ static::saving(function ($service) {
+ if ($service->isDirty('status')) {
+ $service->forceFill(['last_online_at' => now()]);
+ }
+ });
}
public function restart()
@@ -32,6 +37,11 @@ public static function ownedByCurrentTeamAPI(int $teamId)
return ServiceApplication::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name');
}
+ public static function ownedByCurrentTeam()
+ {
+ return ServiceApplication::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->orderBy('name');
+ }
+
public function isRunning()
{
return str($this->status)->contains('running');
diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php
index 9275271182..5fdd52637f 100644
--- a/app/Models/ServiceDatabase.php
+++ b/app/Models/ServiceDatabase.php
@@ -17,6 +17,21 @@ protected static function booted()
$service->persistentStorages()->delete();
$service->fileStorages()->delete();
});
+ static::saving(function ($service) {
+ if ($service->isDirty('status')) {
+ $service->forceFill(['last_online_at' => now()]);
+ }
+ });
+ }
+
+ public static function ownedByCurrentTeamAPI(int $teamId)
+ {
+ return ServiceDatabase::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name');
+ }
+
+ public static function ownedByCurrentTeam()
+ {
+ return ServiceDatabase::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->orderBy('name');
}
public function restart()
diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php
index e4341b1b9d..6d66c68546 100644
--- a/app/Models/StandaloneClickhouse.php
+++ b/app/Models/StandaloneClickhouse.php
@@ -38,6 +38,11 @@ protected static function booted()
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
}
protected function serverStatus(): Attribute
@@ -266,33 +271,48 @@ public function scheduledBackups()
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function getMetrics(int $mins = 5)
+ public function getCpuMetrics(int $mins = 5)
{
$server = $this->destination->server;
$container_name = $this->uuid;
- if ($server->isMetricsEnabled()) {
- $from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
- if (str($metrics)->contains('error')) {
- $error = json_decode($metrics, true);
- $error = data_get($error, 'error', 'Something is not okay, are you okay?');
- if ($error == 'Unauthorized') {
- $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
- }
- throw new \Exception($error);
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
}
- $metrics = str($metrics)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($metrics)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 2);
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
- return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
- });
- });
+ return $parsedCollection->toArray();
+ }
- return $parsedCollection->toArray();
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
}
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
}
public function isBackupSolutionAvailable()
diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php
index 94ab2d7459..f7d83f0a31 100644
--- a/app/Models/StandaloneDragonfly.php
+++ b/app/Models/StandaloneDragonfly.php
@@ -38,6 +38,11 @@ protected static function booted()
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
}
protected function serverStatus(): Attribute
@@ -266,33 +271,48 @@ public function scheduledBackups()
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function getMetrics(int $mins = 5)
+ public function getCpuMetrics(int $mins = 5)
{
$server = $this->destination->server;
$container_name = $this->uuid;
- if ($server->isMetricsEnabled()) {
- $from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
- if (str($metrics)->contains('error')) {
- $error = json_decode($metrics, true);
- $error = data_get($error, 'error', 'Something is not okay, are you okay?');
- if ($error == 'Unauthorized') {
- $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
- }
- throw new \Exception($error);
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
}
- $metrics = str($metrics)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($metrics)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 2);
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
- return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
- });
- });
+ return $parsedCollection->toArray();
+ }
- return $parsedCollection->toArray();
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
}
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
}
public function isBackupSolutionAvailable()
diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php
index 335c8931c9..083c743d95 100644
--- a/app/Models/StandaloneKeydb.php
+++ b/app/Models/StandaloneKeydb.php
@@ -38,6 +38,11 @@ protected static function booted()
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
}
protected function serverStatus(): Attribute
@@ -266,33 +271,48 @@ public function scheduledBackups()
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function getMetrics(int $mins = 5)
+ public function getCpuMetrics(int $mins = 5)
{
$server = $this->destination->server;
$container_name = $this->uuid;
- if ($server->isMetricsEnabled()) {
- $from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
- if (str($metrics)->contains('error')) {
- $error = json_decode($metrics, true);
- $error = data_get($error, 'error', 'Something is not okay, are you okay?');
- if ($error == 'Unauthorized') {
- $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
- }
- throw new \Exception($error);
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
}
- $metrics = str($metrics)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($metrics)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 2);
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
- return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
- });
- });
+ return $parsedCollection->toArray();
+ }
- return $parsedCollection->toArray();
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
}
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
}
public function isBackupSolutionAvailable()
diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php
index c6c08dee5f..833dad6c4f 100644
--- a/app/Models/StandaloneMariadb.php
+++ b/app/Models/StandaloneMariadb.php
@@ -38,6 +38,11 @@ protected static function booted()
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
}
protected function serverStatus(): Attribute
@@ -266,33 +271,48 @@ public function scheduledBackups()
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function getMetrics(int $mins = 5)
+ public function getCpuMetrics(int $mins = 5)
{
$server = $this->destination->server;
$container_name = $this->uuid;
- if ($server->isMetricsEnabled()) {
- $from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
- if (str($metrics)->contains('error')) {
- $error = json_decode($metrics, true);
- $error = data_get($error, 'error', 'Something is not okay, are you okay?');
- if ($error == 'Unauthorized') {
- $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
- }
- throw new \Exception($error);
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
}
- $metrics = str($metrics)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($metrics)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 2);
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
- return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
- });
- });
+ return $parsedCollection->toArray();
+ }
- return $parsedCollection->toArray();
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
}
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
}
public function isBackupSolutionAvailable()
diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php
index 99893b1d13..dd8893180b 100644
--- a/app/Models/StandaloneMongodb.php
+++ b/app/Models/StandaloneMongodb.php
@@ -42,6 +42,11 @@ protected static function booted()
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
}
protected function serverStatus(): Attribute
@@ -286,33 +291,48 @@ public function scheduledBackups()
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function getMetrics(int $mins = 5)
+ public function getCpuMetrics(int $mins = 5)
{
$server = $this->destination->server;
$container_name = $this->uuid;
- if ($server->isMetricsEnabled()) {
- $from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
- if (str($metrics)->contains('error')) {
- $error = json_decode($metrics, true);
- $error = data_get($error, 'error', 'Something is not okay, are you okay?');
- if ($error == 'Unauthorized') {
- $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
- }
- throw new \Exception($error);
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
}
- $metrics = str($metrics)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($metrics)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 2);
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
- return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
- });
- });
+ return $parsedCollection->toArray();
+ }
- return $parsedCollection->toArray();
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
}
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
}
public function isBackupSolutionAvailable()
diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php
index f2a5b5c14e..710fea1bc8 100644
--- a/app/Models/StandaloneMysql.php
+++ b/app/Models/StandaloneMysql.php
@@ -39,6 +39,11 @@ protected static function booted()
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
}
protected function serverStatus(): Attribute
@@ -267,33 +272,48 @@ public function scheduledBackups()
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function getMetrics(int $mins = 5)
+ public function getCpuMetrics(int $mins = 5)
{
$server = $this->destination->server;
$container_name = $this->uuid;
- if ($server->isMetricsEnabled()) {
- $from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
- if (str($metrics)->contains('error')) {
- $error = json_decode($metrics, true);
- $error = data_get($error, 'error', 'Something is not okay, are you okay?');
- if ($error == 'Unauthorized') {
- $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
- }
- throw new \Exception($error);
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
}
- $metrics = str($metrics)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($metrics)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 2);
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
- return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
- });
- });
+ return $parsedCollection->toArray();
+ }
- return $parsedCollection->toArray();
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
}
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
}
public function isBackupSolutionAvailable()
diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php
index 1b18a5ca73..4a457a6cf9 100644
--- a/app/Models/StandalonePostgresql.php
+++ b/app/Models/StandalonePostgresql.php
@@ -39,6 +39,11 @@ protected static function booted()
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
}
public function workdir()
@@ -71,7 +76,6 @@ public function delete_volumes(Collection $persistentStorages)
}
$server = data_get($this, 'destination.server');
foreach ($persistentStorages as $storage) {
- ray('Deleting volume: '.$storage->name);
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
@@ -268,37 +272,52 @@ public function scheduledBackups()
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function getMetrics(int $mins = 5)
+ public function isBackupSolutionAvailable()
+ {
+ return true;
+ }
+
+ public function getCpuMetrics(int $mins = 5)
{
$server = $this->destination->server;
$container_name = $this->uuid;
- if ($server->isMetricsEnabled()) {
- $from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
- if (str($metrics)->contains('error')) {
- $error = json_decode($metrics, true);
- $error = data_get($error, 'error', 'Something is not okay, are you okay?');
- if ($error == 'Unauthorized') {
- $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
- }
- throw new \Exception($error);
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
}
- $metrics = str($metrics)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($metrics)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 2);
-
- return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
- });
- });
-
- return $parsedCollection->toArray();
+ throw new \Exception($error);
}
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
+
+ return $parsedCollection->toArray();
}
- public function isBackupSolutionAvailable()
+ public function getMemoryMetrics(int $mins = 5)
{
- return true;
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
}
}
diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php
index a5868e2438..826bb951c5 100644
--- a/app/Models/StandaloneRedis.php
+++ b/app/Models/StandaloneRedis.php
@@ -34,6 +34,11 @@ protected static function booted()
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
}
protected function serverStatus(): Attribute
@@ -210,7 +215,12 @@ public function databaseType(): Attribute
protected function internalDbUrl(): Attribute
{
return new Attribute(
- get: fn () => "redis://:{$this->redis_password}@{$this->uuid}:6379/0",
+ get: function () {
+ $redis_version = $this->getRedisVersion();
+ $username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
+
+ return "redis://{$username_part}{$this->redis_password}@{$this->uuid}:6379/0";
+ }
);
}
@@ -219,7 +229,10 @@ protected function externalDbUrl(): Attribute
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
- return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
+ $redis_version = $this->getRedisVersion();
+ $username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
+
+ return "redis://{$username_part}{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
}
return null;
@@ -227,6 +240,13 @@ protected function externalDbUrl(): Attribute
);
}
+ public function getRedisVersion()
+ {
+ $image_parts = explode(':', $this->image);
+
+ return $image_parts[1] ?? '0.0';
+ }
+
public function environment()
{
return $this->belongsTo(Environment::class);
@@ -262,37 +282,81 @@ public function scheduledBackups()
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function getMetrics(int $mins = 5)
+ public function getCpuMetrics(int $mins = 5)
{
$server = $this->destination->server;
$container_name = $this->uuid;
- if ($server->isMetricsEnabled()) {
- $from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
- if (str($metrics)->contains('error')) {
- $error = json_decode($metrics, true);
- $error = data_get($error, 'error', 'Something is not okay, are you okay?');
- if ($error == 'Unauthorized') {
- $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
- }
- throw new \Exception($error);
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
}
- $metrics = str($metrics)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($metrics)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 2);
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
- return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
- });
- });
+ return $parsedCollection->toArray();
+ }
- return $parsedCollection->toArray();
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
}
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
}
public function isBackupSolutionAvailable()
{
return false;
}
+
+ public function redisPassword(): Attribute
+ {
+ return new Attribute(
+ get: function () {
+ $password = $this->runtime_environment_variables()->where('key', 'REDIS_PASSWORD')->first();
+ if (! $password) {
+ return null;
+ }
+
+ return $password->value;
+ },
+
+ );
+ }
+
+ public function redisUsername(): Attribute
+ {
+ return new Attribute(
+ get: function () {
+ $username = $this->runtime_environment_variables()->where('key', 'REDIS_USERNAME')->first();
+ if (! $username) {
+ return null;
+ }
+
+ return $username->value;
+ }
+ );
+ }
}
diff --git a/app/Models/Team.php b/app/Models/Team.php
index 3f8e97bc54..db485054b5 100644
--- a/app/Models/Team.php
+++ b/app/Models/Team.php
@@ -34,6 +34,7 @@
'smtp_notifications_status_changes' => ['type' => 'boolean', 'description' => 'Whether to send status change notifications via SMTP.'],
'smtp_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via SMTP.'],
'smtp_notifications_database_backups' => ['type' => 'boolean', 'description' => 'Whether to send database backup notifications via SMTP.'],
+ 'smtp_notifications_server_disk_usage' => ['type' => 'boolean', 'description' => 'Whether to send server disk usage notifications via SMTP.'],
'discord_enabled' => ['type' => 'boolean', 'description' => 'Whether Discord is enabled or not.'],
'discord_webhook_url' => ['type' => 'string', 'description' => 'The Discord webhook URL.'],
'discord_notifications_test' => ['type' => 'boolean', 'description' => 'Whether to send test notifications via Discord.'],
@@ -41,6 +42,7 @@
'discord_notifications_status_changes' => ['type' => 'boolean', 'description' => 'Whether to send status change notifications via Discord.'],
'discord_notifications_database_backups' => ['type' => 'boolean', 'description' => 'Whether to send database backup notifications via Discord.'],
'discord_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via Discord.'],
+ 'discord_notifications_server_disk_usage' => ['type' => 'boolean', 'description' => 'Whether to send server disk usage notifications via Discord.'],
'show_boarding' => ['type' => 'boolean', 'description' => 'Whether to show the boarding screen or not.'],
'resend_enabled' => ['type' => 'boolean', 'description' => 'Whether to enable resending or not.'],
'resend_api_key' => ['type' => 'string', 'description' => 'The resending API key.'],
@@ -56,6 +58,7 @@
'telegram_notifications_deployments_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram deployment message thread ID.'],
'telegram_notifications_status_changes_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram status change message thread ID.'],
'telegram_notifications_database_backups_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram database backup message thread ID.'],
+
'custom_server_limit' => ['type' => 'string', 'description' => 'The custom server limit.'],
'telegram_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via Telegram.'],
'telegram_notifications_scheduled_tasks_thread_id' => ['type' => 'string', 'description' => 'The Telegram scheduled task message thread ID.'],
@@ -90,27 +93,22 @@ protected static function booted()
static::deleting(function ($team) {
$keys = $team->privateKeys;
foreach ($keys as $key) {
- ray('Deleting key: '.$key->name);
$key->delete();
}
$sources = $team->sources();
foreach ($sources as $source) {
- ray('Deleting source: '.$source->name);
$source->delete();
}
$tags = Tag::whereTeamId($team->id)->get();
foreach ($tags as $tag) {
- ray('Deleting tag: '.$tag->name);
$tag->delete();
}
$shared_variables = $team->environment_variables();
foreach ($shared_variables as $shared_variable) {
- ray('Deleting team shared variable: '.$shared_variable->name);
$shared_variable->delete();
}
$s3s = $team->s3s;
foreach ($s3s as $s3) {
- ray('Deleting s3: '.$s3->name);
$s3->delete();
}
});
@@ -133,9 +131,7 @@ public function getRecepients($notification)
{
$recipients = data_get($notification, 'emails', null);
if (is_null($recipients)) {
- $recipients = $this->members()->pluck('email')->toArray();
-
- return $recipients;
+ return $this->members()->pluck('email')->toArray();
}
return explode(',', $recipients);
@@ -164,8 +160,12 @@ public static function serverLimit()
if (currentTeam()->id === 0 && isDev()) {
return 9999999;
}
+ $team = Team::find(currentTeam()->id);
+ if (! $team) {
+ return 0;
+ }
- return Team::find(currentTeam()->id)->limits['serverLimit'];
+ return data_get($team, 'limits', 0);
}
public function limits(): Attribute
@@ -187,9 +187,8 @@ public function limits(): Attribute
} else {
$serverLimit = config('constants.limits.server')[strtolower($subscription)];
}
- $sharedEmailEnabled = config('constants.limits.email')[strtolower($subscription)];
- return ['serverLimit' => $serverLimit, 'sharedEmailEnabled' => $sharedEmailEnabled];
+ return $serverLimit ?? 2;
}
);
@@ -249,9 +248,8 @@ public function sources()
$sources = collect([]);
$github_apps = $this->hasMany(GithubApp::class)->whereisPublic(false)->get();
$gitlab_apps = $this->hasMany(GitlabApp::class)->whereisPublic(false)->get();
- $sources = $sources->merge($github_apps)->merge($gitlab_apps);
- return $sources;
+ return $sources->merge($github_apps)->merge($gitlab_apps);
}
public function s3s()
diff --git a/app/Models/TeamInvitation.php b/app/Models/TeamInvitation.php
index c202710e25..bc1a90d586 100644
--- a/app/Models/TeamInvitation.php
+++ b/app/Models/TeamInvitation.php
@@ -20,11 +20,16 @@ public function team()
return $this->belongsTo(Team::class);
}
+ public static function ownedByCurrentTeam()
+ {
+ return TeamInvitation::whereTeamId(currentTeam()->id);
+ }
+
public function isValid()
{
$createdAt = $this->created_at;
- $diff = $createdAt->diffInMinutes(now());
- if ($diff <= config('constants.invitation.link.expiration')) {
+ $diff = $createdAt->diffInDays(now());
+ if ($diff <= config('constants.invitation.link.expiration_days')) {
return true;
} else {
$this->delete();
diff --git a/app/Models/User.php b/app/Models/User.php
index ecc4ef6b66..25fb33d668 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -10,6 +10,7 @@
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\URL;
@@ -158,7 +159,7 @@ public function isMember()
public function isAdminFromSession()
{
- if (auth()->user()->id === 0) {
+ if (Auth::id() === 0) {
return true;
}
$teams = $this->teams()->get();
@@ -178,9 +179,9 @@ public function isAdminFromSession()
public function isInstanceAdmin()
{
- $found_root_team = auth()->user()->teams->filter(function ($team) {
+ $found_root_team = Auth::user()->teams->filter(function ($team) {
if ($team->id == 0) {
- if (! auth()->user()->isAdmin()) {
+ if (! Auth::user()->isAdmin()) {
return false;
}
@@ -195,9 +196,9 @@ public function isInstanceAdmin()
public function currentTeam()
{
- return Cache::remember('team:'.auth()->user()->id, 3600, function () {
- if (is_null(data_get(session('currentTeam'), 'id')) && auth()->user()->teams->count() > 0) {
- return auth()->user()->teams[0];
+ return Cache::remember('team:'.Auth::id(), 3600, function () {
+ if (is_null(data_get(session('currentTeam'), 'id')) && Auth::user()->teams->count() > 0) {
+ return Auth::user()->teams[0];
}
return Team::find(session('currentTeam')->id);
@@ -206,7 +207,7 @@ public function currentTeam()
public function otherTeams()
{
- return auth()->user()->teams->filter(function ($team) {
+ return Auth::user()->teams->filter(function ($team) {
return $team->id != currentTeam()->id;
});
}
@@ -216,7 +217,7 @@ public function role()
if (data_get($this, 'pivot')) {
return $this->pivot->role;
}
- $user = auth()->user()->teams->where('id', currentTeam()->id)->first();
+ $user = Auth::user()->teams->where('id', currentTeam()->id)->first();
return data_get($user, 'pivot.role');
}
diff --git a/app/Notifications/Application/DeploymentFailed.php b/app/Notifications/Application/DeploymentFailed.php
index 1809da3683..242980e007 100644
--- a/app/Notifications/Application/DeploymentFailed.php
+++ b/app/Notifications/Application/DeploymentFailed.php
@@ -4,6 +4,7 @@
use App\Models\Application;
use App\Models\ApplicationPreview;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -72,14 +73,42 @@ public function toMail(): MailMessage
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
if ($this->preview) {
- $message = 'Coolify: Pull request #'.$this->preview->pull_request_id.' of '.$this->application_name.' ('.$this->preview->fqdn.') deployment failed: ';
- $message .= '[View Deployment Logs]('.$this->deployment_url.')';
+ $message = new DiscordMessage(
+ title: ':cross_mark: Deployment failed',
+ description: 'Pull request: '.$this->preview->pull_request_id,
+ color: DiscordMessage::errorColor(),
+ isCritical: true,
+ );
+
+ $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.')');
+ if ($this->fqdn) {
+ $message->addField('Domain', $this->fqdn, true);
+ }
} else {
- $message = 'Coolify: Deployment failed of '.$this->application_name.' ('.$this->fqdn.'): ';
- $message .= '[View Deployment Logs]('.$this->deployment_url.')';
+ if ($this->fqdn) {
+ $description = '[Open application]('.$this->fqdn.')';
+ } else {
+ $description = '';
+ }
+ $message = new DiscordMessage(
+ title: ':cross_mark: Deployment failed',
+ description: $description,
+ color: DiscordMessage::errorColor(),
+ isCritical: true,
+ );
+
+ $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.')');
}
return $message;
diff --git a/app/Notifications/Application/DeploymentSuccess.php b/app/Notifications/Application/DeploymentSuccess.php
index 5085065c20..946a622ca5 100644
--- a/app/Notifications/Application/DeploymentSuccess.php
+++ b/app/Notifications/Application/DeploymentSuccess.php
@@ -4,6 +4,7 @@
use App\Models\Application;
use App\Models\ApplicationPreview;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -51,7 +52,7 @@ public function via(object $notifiable): array
$channels = setNotificationChannels($notifiable, 'deployments');
if (isCloud()) {
// TODO: Make batch notifications work with email
- $channels = array_diff($channels, ['App\Notifications\Channels\EmailChannel']);
+ $channels = array_diff($channels, [\App\Notifications\Channels\EmailChannel::class]);
}
return $channels;
@@ -78,24 +79,39 @@ public function toMail(): MailMessage
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
if ($this->preview) {
- $message = 'Coolify: New PR'.$this->preview->pull_request_id.' version successfully deployed of '.$this->application_name.'
+ $message = new DiscordMessage(
+ title: ':white_check_mark: Preview deployment successful',
+ description: 'Pull request: '.$this->preview->pull_request_id,
+ color: DiscordMessage::successColor(),
+ );
-';
if ($this->preview->fqdn) {
- $message .= '[Open Application]('.$this->preview->fqdn.') | ';
+ $message->addField('Application', '[Link]('.$this->preview->fqdn.')');
}
- $message .= '[Deployment logs]('.$this->deployment_url.')';
- } else {
- $message = 'Coolify: New version successfully deployed of '.$this->application_name.'
-';
+ $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.')');
+ } else {
if ($this->fqdn) {
- $message .= '[Open Application]('.$this->fqdn.') | ';
+ $description = '[Open application]('.$this->fqdn.')';
+ } else {
+ $description = '';
}
- $message .= '[Deployment logs]('.$this->deployment_url.')';
+ $message = new DiscordMessage(
+ title: ':white_check_mark: New version successfully deployed',
+ description: $description,
+ color: DiscordMessage::successColor(),
+ );
+ $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.')');
}
return $message;
diff --git a/app/Notifications/Application/StatusChanged.php b/app/Notifications/Application/StatusChanged.php
index 53ed8a5896..852c6b5260 100644
--- a/app/Notifications/Application/StatusChanged.php
+++ b/app/Notifications/Application/StatusChanged.php
@@ -3,6 +3,7 @@
namespace App\Notifications\Application;
use App\Models\Application;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -55,14 +56,14 @@ public function toMail(): MailMessage
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = 'Coolify: '.$this->resource_name.' has been stopped.
-
-';
- $message .= '[Open Application in Coolify]('.$this->resource_url.')';
-
- return $message;
+ return new DiscordMessage(
+ title: ':cross_mark: Application stopped',
+ description: '[Open Application in Coolify]('.$this->resource_url.')',
+ color: DiscordMessage::errorColor(),
+ isCritical: true,
+ );
}
public function toTelegram(): array
diff --git a/app/Notifications/Channels/DiscordChannel.php b/app/Notifications/Channels/DiscordChannel.php
index f1706f1380..86276fec96 100644
--- a/app/Notifications/Channels/DiscordChannel.php
+++ b/app/Notifications/Channels/DiscordChannel.php
@@ -12,11 +12,11 @@ class DiscordChannel
*/
public function send(SendsDiscord $notifiable, Notification $notification): void
{
- $message = $notification->toDiscord($notifiable);
+ $message = $notification->toDiscord();
$webhookUrl = $notifiable->routeNotificationForDiscord();
if (! $webhookUrl) {
return;
}
- dispatch(new SendMessageToDiscordJob($message, $webhookUrl));
+ dispatch(new SendMessageToDiscordJob($message, $webhookUrl))->onQueue('high');
}
}
diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php
index 413d3de536..af9af978d0 100644
--- a/app/Notifications/Channels/EmailChannel.php
+++ b/app/Notifications/Channels/EmailChannel.php
@@ -32,7 +32,6 @@ public function send(SendsEmail $notifiable, Notification $notification): void
if ($error === 'No email settings found.') {
throw $e;
}
- ray($e->getMessage());
$message = "EmailChannel error: {$e->getMessage()}. Failed to send email to:";
if (isset($recipients)) {
$message .= implode(', ', $recipients);
diff --git a/app/Notifications/Channels/TelegramChannel.php b/app/Notifications/Channels/TelegramChannel.php
index b1a6076513..b3d4e384bb 100644
--- a/app/Notifications/Channels/TelegramChannel.php
+++ b/app/Notifications/Channels/TelegramChannel.php
@@ -18,29 +18,29 @@ public function send($notifiable, $notification): void
$topicsInstance = get_class($notification);
switch ($topicsInstance) {
- case 'App\Notifications\Test':
+ case \App\Notifications\Test::class:
$topicId = data_get($notifiable, 'telegram_notifications_test_message_thread_id');
break;
- case 'App\Notifications\Application\StatusChanged':
- case 'App\Notifications\Container\ContainerRestarted':
- case 'App\Notifications\Container\ContainerStopped':
+ case \App\Notifications\Application\StatusChanged::class:
+ case \App\Notifications\Container\ContainerRestarted::class:
+ case \App\Notifications\Container\ContainerStopped::class:
$topicId = data_get($notifiable, 'telegram_notifications_status_changes_message_thread_id');
break;
- case 'App\Notifications\Application\DeploymentSuccess':
- case 'App\Notifications\Application\DeploymentFailed':
+ case \App\Notifications\Application\DeploymentSuccess::class:
+ case \App\Notifications\Application\DeploymentFailed::class:
$topicId = data_get($notifiable, 'telegram_notifications_deployments_message_thread_id');
break;
- case 'App\Notifications\Database\BackupSuccess':
- case 'App\Notifications\Database\BackupFailed':
+ case \App\Notifications\Database\BackupSuccess::class:
+ case \App\Notifications\Database\BackupFailed::class:
$topicId = data_get($notifiable, 'telegram_notifications_database_backups_message_thread_id');
break;
- case 'App\Notifications\ScheduledTask\TaskFailed':
+ case \App\Notifications\ScheduledTask\TaskFailed::class:
$topicId = data_get($notifiable, 'telegram_notifications_scheduled_tasks_thread_id');
break;
}
if (! $telegramToken || ! $chatId || ! $message) {
return;
}
- dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId, $topicId));
+ dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId, $topicId))->onQueue('high');
}
}
diff --git a/app/Notifications/Container/ContainerRestarted.php b/app/Notifications/Container/ContainerRestarted.php
index 23f6de2642..182a1f5fc1 100644
--- a/app/Notifications/Container/ContainerRestarted.php
+++ b/app/Notifications/Container/ContainerRestarted.php
@@ -3,6 +3,7 @@
namespace App\Notifications\Container;
use App\Models\Server;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -34,9 +35,17 @@ public function toMail(): MailMessage
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = "Coolify: A resource ({$this->name}) has been restarted automatically on {$this->server->name}";
+ $message = new DiscordMessage(
+ title: ':warning: Resource restarted',
+ description: "{$this->name} has been restarted automatically on {$this->server->name}.",
+ color: DiscordMessage::infoColor(),
+ );
+
+ if ($this->url) {
+ $message->addField('Resource', '[Link]('.$this->url.')');
+ }
return $message;
}
diff --git a/app/Notifications/Container/ContainerStopped.php b/app/Notifications/Container/ContainerStopped.php
index bcf5e67a5c..33a55c65a2 100644
--- a/app/Notifications/Container/ContainerStopped.php
+++ b/app/Notifications/Container/ContainerStopped.php
@@ -3,6 +3,7 @@
namespace App\Notifications\Container;
use App\Models\Server;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -34,9 +35,17 @@ public function toMail(): MailMessage
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = "Coolify: A resource ($this->name) has been stopped unexpectedly on {$this->server->name}";
+ $message = new DiscordMessage(
+ title: ':cross_mark: Resource stopped',
+ description: "{$this->name} has been stopped unexpectedly on {$this->server->name}.",
+ color: DiscordMessage::errorColor(),
+ );
+
+ if ($this->url) {
+ $message->addField('Resource', '[Link]('.$this->url.')');
+ }
return $message;
}
diff --git a/app/Notifications/Database/BackupFailed.php b/app/Notifications/Database/BackupFailed.php
index 77024c05bf..8e2733339d 100644
--- a/app/Notifications/Database/BackupFailed.php
+++ b/app/Notifications/Database/BackupFailed.php
@@ -3,6 +3,7 @@
namespace App\Notifications\Database;
use App\Models\ScheduledDatabaseBackup;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -45,9 +46,19 @@ public function toMail(): MailMessage
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- return "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was FAILED.\n\nReason:\n{$this->output}";
+ $message = new DiscordMessage(
+ title: ':cross_mark: Database backup failed',
+ description: "Database backup for {$this->name} (db:{$this->database_name}) has FAILED.",
+ color: DiscordMessage::errorColor(),
+ isCritical: true,
+ );
+
+ $message->addField('Frequency', $this->frequency, true);
+ $message->addField('Output', $this->output);
+
+ return $message;
}
public function toTelegram(): array
diff --git a/app/Notifications/Database/BackupSuccess.php b/app/Notifications/Database/BackupSuccess.php
index f8dc6eb565..5128c8ed66 100644
--- a/app/Notifications/Database/BackupSuccess.php
+++ b/app/Notifications/Database/BackupSuccess.php
@@ -3,6 +3,7 @@
namespace App\Notifications\Database;
use App\Models\ScheduledDatabaseBackup;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -44,15 +45,22 @@ public function toMail(): MailMessage
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- return "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was successful.";
+ $message = new DiscordMessage(
+ title: ':white_check_mark: Database backup successful',
+ description: "Database backup for {$this->name} (db:{$this->database_name}) was successful.",
+ color: DiscordMessage::successColor(),
+ );
+
+ $message->addField('Frequency', $this->frequency, true);
+
+ return $message;
}
public function toTelegram(): array
{
$message = "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was successful.";
- ray($message);
return [
'message' => $message,
diff --git a/app/Notifications/Database/DailyBackup.php b/app/Notifications/Database/DailyBackup.php
deleted file mode 100644
index a51ac62839..0000000000
--- a/app/Notifications/Database/DailyBackup.php
+++ /dev/null
@@ -1,50 +0,0 @@
-subject('Coolify: Daily backup statuses');
- $mail->view('emails.daily-backup', [
- 'databases' => $this->databases,
- ]);
-
- return $mail;
- }
-
- public function toDiscord(): string
- {
- return 'Coolify: Daily backup statuses';
- }
-
- public function toTelegram(): array
- {
- $message = 'Coolify: Daily backup statuses';
-
- return [
- 'message' => $message,
- ];
- }
-}
diff --git a/app/Notifications/Dto/DiscordMessage.php b/app/Notifications/Dto/DiscordMessage.php
new file mode 100644
index 0000000000..856753dcae
--- /dev/null
+++ b/app/Notifications/Dto/DiscordMessage.php
@@ -0,0 +1,83 @@
+fields[] = [
+ 'name' => $name,
+ 'value' => $value,
+ 'inline' => $inline,
+ ];
+
+ return $this;
+ }
+
+ public function toPayload(): array
+ {
+ $footerText = 'Coolify v'.config('version');
+ if (isCloud()) {
+ $footerText = 'Coolify Cloud';
+ }
+ $payload = [
+ 'embeds' => [
+ [
+ 'title' => $this->title,
+ 'description' => $this->description,
+ 'color' => $this->color,
+ 'fields' => $this->addTimestampToFields($this->fields),
+ 'footer' => [
+ 'text' => $footerText,
+ ],
+ ],
+ ],
+ ];
+ if ($this->isCritical) {
+ $payload['content'] = '@here';
+ }
+
+ return $payload;
+ }
+
+ private function addTimestampToFields(array $fields): array
+ {
+ $fields[] = [
+ 'name' => 'Time',
+ 'value' => 'timestamp.':R>',
+ 'inline' => true,
+ ];
+
+ return $fields;
+ }
+}
diff --git a/app/Notifications/Internal/GeneralNotification.php b/app/Notifications/Internal/GeneralNotification.php
index 1d4d648c86..48e7d8340c 100644
--- a/app/Notifications/Internal/GeneralNotification.php
+++ b/app/Notifications/Internal/GeneralNotification.php
@@ -4,6 +4,7 @@
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\TelegramChannel;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
@@ -32,9 +33,13 @@ public function via(object $notifiable): array
return $channels;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- return $this->message;
+ return new DiscordMessage(
+ title: 'Coolify: General Notification',
+ description: $this->message,
+ color: DiscordMessage::infoColor(),
+ );
}
public function toTelegram(): array
diff --git a/app/Notifications/ScheduledTask/TaskFailed.php b/app/Notifications/ScheduledTask/TaskFailed.php
index 479cc1aa14..c3501a8eb4 100644
--- a/app/Notifications/ScheduledTask/TaskFailed.php
+++ b/app/Notifications/ScheduledTask/TaskFailed.php
@@ -3,6 +3,7 @@
namespace App\Notifications\ScheduledTask;
use App\Models\ScheduledTask;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -29,7 +30,6 @@ public function __construct(public ScheduledTask $task, public string $output)
public function via(object $notifiable): array
{
-
return setNotificationChannels($notifiable, 'scheduled_tasks');
}
@@ -46,9 +46,19 @@ public function toMail(): MailMessage
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- return "Coolify: Scheduled task ({$this->task->name}, [link]({$this->url})) failed with output: {$this->output}";
+ $message = new DiscordMessage(
+ title: ':cross_mark: Scheduled task failed',
+ description: "Scheduled task ({$this->task->name}) failed.",
+ color: DiscordMessage::errorColor(),
+ );
+
+ if ($this->url) {
+ $message->addField('Scheduled task', '[Link]('.$this->url.')');
+ }
+
+ return $message;
}
public function toTelegram(): array
diff --git a/app/Notifications/Server/DockerCleanup.php b/app/Notifications/Server/DockerCleanup.php
index 682ed7a1a7..7ea1b84c2f 100644
--- a/app/Notifications/Server/DockerCleanup.php
+++ b/app/Notifications/Server/DockerCleanup.php
@@ -5,6 +5,7 @@
use App\Models\Server;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\TelegramChannel;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
@@ -49,11 +50,13 @@ public function via(object $notifiable): array
// return $mail;
// }
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = "Coolify: Server '{$this->server->name}' cleanup job done!\n\n{$this->message}";
-
- return $message;
+ return new DiscordMessage(
+ title: ':white_check_mark: Server cleanup job done',
+ description: $this->message,
+ color: DiscordMessage::successColor(),
+ );
}
public function toTelegram(): array
diff --git a/app/Notifications/Server/ForceDisabled.php b/app/Notifications/Server/ForceDisabled.php
index 6377f2f150..a26c803ee6 100644
--- a/app/Notifications/Server/ForceDisabled.php
+++ b/app/Notifications/Server/ForceDisabled.php
@@ -6,6 +6,7 @@
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -50,9 +51,15 @@ public function toMail(): MailMessage
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $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).";
+ $message = new DiscordMessage(
+ title: ':cross_mark: Server disabled',
+ description: "Server ({$this->server->name}) disabled because it is not paid!",
+ color: DiscordMessage::errorColor(),
+ );
+
+ $message->addField('Please update your subscription to enable the server again!', '[Link](https://app.coolify.io/subscriptions)');
return $message;
}
diff --git a/app/Notifications/Server/ForceEnabled.php b/app/Notifications/Server/ForceEnabled.php
index 83594d6434..65b65a10c7 100644
--- a/app/Notifications/Server/ForceEnabled.php
+++ b/app/Notifications/Server/ForceEnabled.php
@@ -6,6 +6,7 @@
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -50,11 +51,13 @@ public function toMail(): MailMessage
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = "Coolify: Server ({$this->server->name}) enabled again!";
-
- return $message;
+ return new DiscordMessage(
+ title: ':white_check_mark: Server enabled',
+ description: "Server '{$this->server->name}' enabled again!",
+ color: DiscordMessage::successColor(),
+ );
}
public function toTelegram(): array
diff --git a/app/Notifications/Server/HighDiskUsage.php b/app/Notifications/Server/HighDiskUsage.php
index 34cb220910..e373abc031 100644
--- a/app/Notifications/Server/HighDiskUsage.php
+++ b/app/Notifications/Server/HighDiskUsage.php
@@ -3,9 +3,7 @@
namespace App\Notifications\Server;
use App\Models\Server;
-use App\Notifications\Channels\DiscordChannel;
-use App\Notifications\Channels\EmailChannel;
-use App\Notifications\Channels\TelegramChannel;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -17,26 +15,11 @@ class HighDiskUsage extends Notification implements ShouldQueue
public $tries = 1;
- public function __construct(public Server $server, public int $disk_usage, public int $docker_cleanup_threshold) {}
+ public function __construct(public Server $server, public int $disk_usage, public int $server_disk_usage_notification_threshold) {}
public function via(object $notifiable): array
{
- $channels = [];
- $isEmailEnabled = isEmailEnabled($notifiable);
- $isDiscordEnabled = data_get($notifiable, 'discord_enabled');
- $isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
-
- if ($isDiscordEnabled) {
- $channels[] = DiscordChannel::class;
- }
- if ($isEmailEnabled) {
- $channels[] = EmailChannel::class;
- }
- if ($isTelegramEnabled) {
- $channels[] = TelegramChannel::class;
- }
-
- return $channels;
+ return setNotificationChannels($notifiable, 'server_disk_usage');
}
public function toMail(): MailMessage
@@ -46,15 +29,25 @@ public function toMail(): MailMessage
$mail->view('emails.high-disk-usage', [
'name' => $this->server->name,
'disk_usage' => $this->disk_usage,
- 'threshold' => $this->docker_cleanup_threshold,
+ 'threshold' => $this->server_disk_usage_notification_threshold,
]);
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->docker_cleanup_threshold}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.";
+ $message = new DiscordMessage(
+ title: ':cross_mark: High disk usage detected',
+ description: "Server '{$this->server->name}' high disk usage detected!",
+ color: DiscordMessage::errorColor(),
+ isCritical: true,
+ );
+
+ $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)');
return $message;
}
@@ -62,7 +55,7 @@ public function toDiscord(): string
public function toTelegram(): array
{
return [
- 'message' => "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->docker_cleanup_threshold}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.",
+ '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.",
];
}
}
diff --git a/app/Notifications/Server/Revived.php b/app/Notifications/Server/Reachable.php
similarity index 64%
rename from app/Notifications/Server/Revived.php
rename to app/Notifications/Server/Reachable.php
index 3f2b3b6962..9b54501d94 100644
--- a/app/Notifications/Server/Revived.php
+++ b/app/Notifications/Server/Reachable.php
@@ -2,35 +2,37 @@
namespace App\Notifications\Server;
-use App\Actions\Docker\GetContainersStatus;
-use App\Jobs\ContainerStatusJob;
use App\Models\Server;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
-use Illuminate\Support\Facades\RateLimiter;
-class Revived extends Notification implements ShouldQueue
+class Reachable extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 1;
+ protected bool $isRateLimited = false;
+
public function __construct(public Server $server)
{
- if ($this->server->unreachable_notification_sent === false) {
- return;
- }
- GetContainersStatus::dispatch($server)->onQueue('high');
- // dispatch(new ContainerStatusJob($server));
+ $this->isRateLimited = isEmailRateLimited(
+ limiterKey: 'server-reachable:'.$this->server->id,
+ );
}
public function via(object $notifiable): array
{
+ if ($this->isRateLimited) {
+ return [];
+ }
+
$channels = [];
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
@@ -45,20 +47,8 @@ public function via(object $notifiable): array
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
- $executed = RateLimiter::attempt(
- 'notification-server-revived-'.$this->server->uuid,
- 1,
- function () use ($channels) {
- return $channels;
- },
- 7200,
- );
-
- if (! $executed) {
- return [];
- }
- return $executed;
+ return $channels;
}
public function toMail(): MailMessage
@@ -72,11 +62,13 @@ public function toMail(): MailMessage
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!";
-
- return $message;
+ return new DiscordMessage(
+ title: ":white_check_mark: Server '{$this->server->name}' revived",
+ description: 'All automations & integrations are turned on again!',
+ color: DiscordMessage::successColor(),
+ );
}
public function toTelegram(): array
diff --git a/app/Notifications/Server/Unreachable.php b/app/Notifications/Server/Unreachable.php
index 2fb83559aa..5bc568e829 100644
--- a/app/Notifications/Server/Unreachable.php
+++ b/app/Notifications/Server/Unreachable.php
@@ -6,11 +6,11 @@
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
-use Illuminate\Support\Facades\RateLimiter;
class Unreachable extends Notification implements ShouldQueue
{
@@ -18,10 +18,21 @@ class Unreachable extends Notification implements ShouldQueue
public $tries = 1;
- public function __construct(public Server $server) {}
+ protected bool $isRateLimited = false;
+
+ public function __construct(public Server $server)
+ {
+ $this->isRateLimited = isEmailRateLimited(
+ limiterKey: 'server-unreachable:'.$this->server->id,
+ );
+ }
public function via(object $notifiable): array
{
+ if ($this->isRateLimited) {
+ return [];
+ }
+
$channels = [];
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
@@ -36,23 +47,11 @@ public function via(object $notifiable): array
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
- $executed = RateLimiter::attempt(
- 'notification-server-unreachable-'.$this->server->uuid,
- 1,
- function () use ($channels) {
- return $channels;
- },
- 7200,
- );
-
- if (! $executed) {
- return [];
- }
- return $executed;
+ return $channels;
}
- public function toMail(): MailMessage
+ public function toMail(): ?MailMessage
{
$mail = new MailMessage;
$mail->subject("Coolify: Your server ({$this->server->name}) is unreachable.");
@@ -63,14 +62,20 @@ public function toMail(): MailMessage
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): ?DiscordMessage
{
- $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.";
+ $message = new DiscordMessage(
+ title: ':cross_mark: Server unreachable',
+ description: "Your server '{$this->server->name}' is unreachable.",
+ color: DiscordMessage::errorColor(),
+ );
+
+ $message->addField('IMPORTANT', 'We automatically try to revive your server and turn on all automations & integrations.');
return $message;
}
- public function toTelegram(): array
+ public function toTelegram(): ?array
{
return [
'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.",
diff --git a/app/Notifications/Test.php b/app/Notifications/Test.php
index 3b46a9a249..a43b1e1533 100644
--- a/app/Notifications/Test.php
+++ b/app/Notifications/Test.php
@@ -2,10 +2,12 @@
namespace App\Notifications;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
+use Illuminate\Queue\Middleware\RateLimited;
class Test extends Notification implements ShouldQueue
{
@@ -20,6 +22,14 @@ public function via(object $notifiable): array
return setNotificationChannels($notifiable, 'test');
}
+ public function middleware(object $notifiable, string $channel)
+ {
+ return match ($channel) {
+ \App\Notifications\Channels\EmailChannel::class => [new RateLimited('email')],
+ default => [],
+ };
+ }
+
public function toMail(): MailMessage
{
$mail = new MailMessage;
@@ -29,11 +39,15 @@ public function toMail(): MailMessage
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = 'Coolify: This is a test Discord notification from Coolify.';
- $message .= "\n\n";
- $message .= '[Go to your dashboard]('.base_url().')';
+ $message = new DiscordMessage(
+ title: ':white_check_mark: Test Success',
+ description: 'This is a test Discord notification from Coolify. :cross_mark: :warning: :information_source:',
+ color: DiscordMessage::successColor(),
+ );
+
+ $message->addField(name: 'Dashboard', value: '[Link]('.base_url().')', inline: true);
return $message;
}
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 8b4c2eef2d..015434bd22 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -5,16 +5,30 @@
use App\Models\PersonalAccessToken;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\ServiceProvider;
+use Illuminate\Validation\Rules\Password;
use Laravel\Sanctum\Sanctum;
class AppServiceProvider extends ServiceProvider
{
- public function register(): void {}
+ public function register(): void
+ {
+ if ($this->app->environment('local')) {
+ $this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class);
+ }
+ }
public function boot(): void
{
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
+ Password::defaults(function () {
+ $rule = Password::min(8);
+
+ return $this->app->isProduction()
+ ? $rule->mixedCase()->letters()->numbers()->symbols()
+ : $rule;
+ });
+
Http::macro('github', function (string $api_url, ?string $github_access_token = null) {
if ($github_access_token) {
return Http::withHeaders([
diff --git a/app/Providers/DuskServiceProvider.php b/app/Providers/DuskServiceProvider.php
new file mode 100644
index 0000000000..07e0e8709f
--- /dev/null
+++ b/app/Providers/DuskServiceProvider.php
@@ -0,0 +1,21 @@
+visit('/login')
+ ->type('email', 'test@example.com')
+ ->type('password', 'password')
+ ->press('Login');
+ });
+ }
+}
diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php
index b916b62340..e8784bab3c 100644
--- a/app/Providers/FortifyServiceProvider.php
+++ b/app/Providers/FortifyServiceProvider.php
@@ -75,7 +75,8 @@ public function boot(): void
});
Fortify::authenticateUsing(function (Request $request) {
- $user = User::where('email', $request->email)->with('teams')->first();
+ $email = strtolower($request->email);
+ $user = User::where('email', $email)->with('teams')->first();
if (
$user &&
Hash::check($request->password, $user->password)
diff --git a/app/View/Components/Forms/Input.php b/app/View/Components/Forms/Input.php
index fbd7b0b158..7283ef20f8 100644
--- a/app/View/Components/Forms/Input.php
+++ b/app/View/Components/Forms/Input.php
@@ -23,6 +23,8 @@ public function __construct(
public bool $isMultiline = false,
public string $defaultClass = 'input',
public string $autocomplete = 'off',
+ public ?int $minlength = null,
+ public ?int $maxlength = null,
) {}
public function render(): View|Closure|string
diff --git a/app/View/Components/Forms/Textarea.php b/app/View/Components/Forms/Textarea.php
index 3f887877c2..6081c2a8ae 100644
--- a/app/View/Components/Forms/Textarea.php
+++ b/app/View/Components/Forms/Textarea.php
@@ -30,7 +30,9 @@ public function __construct(
public bool $realtimeValidation = false,
public bool $allowToPeak = true,
public string $defaultClass = 'input scrollbar font-mono',
- public string $defaultClassInput = 'input'
+ public string $defaultClassInput = 'input',
+ public ?int $minlength = null,
+ public ?int $maxlength = null,
) {
//
}
diff --git a/app/View/Components/Server/Sidebar.php b/app/View/Components/Server/Sidebar.php
deleted file mode 100644
index f968b6d0cf..0000000000
--- a/app/View/Components/Server/Sidebar.php
+++ /dev/null
@@ -1,27 +0,0 @@
-map(function ($d) {
+ return $data->map(function ($d) {
$d = collect($d)->sortKeys();
$created_at = data_get($d, 'created_at');
$updated_at = data_get($d, 'updated_at');
if ($created_at) {
unset($d['created_at']);
$d['created_at'] = $created_at;
-
}
if ($updated_at) {
unset($d['updated_at']);
@@ -50,8 +49,6 @@ function serializeApiResponse($data)
return $d;
});
-
- return $data;
} else {
$d = collect($data)->sortKeys();
$created_at = data_get($d, 'created_at');
@@ -59,7 +56,6 @@ function serializeApiResponse($data)
if ($created_at) {
unset($d['created_at']);
$d['created_at'] = $created_at;
-
}
if ($updated_at) {
unset($d['updated_at']);
diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php
index b3e8011b93..eb331f8c23 100644
--- a/bootstrap/helpers/applications.php
+++ b/bootstrap/helpers/applications.php
@@ -91,7 +91,7 @@ function next_queuable(string $server_id, string $application_id): bool
$server = Server::find($server_id);
$concurrent_builds = $server->settings->concurrent_builds;
- ray("serverId:{$server->id}", "concurrentBuilds:{$concurrent_builds}", "deployments:{$deployments->count()}", "sameApplicationDeployments:{$same_application_deployments->count()}")->green();
+ // ray("serverId:{$server->id}", "concurrentBuilds:{$concurrent_builds}", "deployments:{$deployments->count()}", "sameApplicationDeployments:{$same_application_deployments->count()}")->green();
if ($deployments->count() > $concurrent_builds) {
return false;
diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php
index 950eb67b6e..e12910f82b 100644
--- a/bootstrap/helpers/databases.php
+++ b/bootstrap/helpers/databases.php
@@ -1,5 +1,6 @@
name = generate_database_name('redis');
- $database->redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
+ $redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->environment_id = $environment_id;
$database->destination_id = $destination->id;
$database->destination_type = $destination->getMorphClass();
@@ -57,6 +58,20 @@ function create_standalone_redis($environment_id, $destination_uuid, ?array $oth
}
$database->save();
+ EnvironmentVariable::create([
+ 'key' => 'REDIS_PASSWORD',
+ 'value' => $redis_password,
+ 'standalone_redis_id' => $database->id,
+ 'is_shared' => false,
+ ]);
+
+ EnvironmentVariable::create([
+ 'key' => 'REDIS_USERNAME',
+ 'value' => 'default',
+ 'standalone_redis_id' => $database->id,
+ 'is_shared' => false,
+ ]);
+
return $database;
}
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index 397bce029d..2e583b94d9 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -32,9 +32,8 @@ function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pul
return null;
});
- $containers = $containers->filter();
- return $containers;
+ return $containers->filter();
}
return $containers;
@@ -46,9 +45,8 @@ function getCurrentServiceContainerStatus(Server $server, int $id): Collection
if (! $server->isSwarm()) {
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.serviceId={$id}' --format '{{json .}}' "], $server);
$containers = format_docker_command_output_to_json($containers);
- $containers = $containers->filter();
- return $containers;
+ return $containers->filter();
}
return $containers;
@@ -67,7 +65,7 @@ function format_docker_command_output_to_json($rawOutput): Collection
return $outputLines
->reject(fn ($line) => empty($line))
->map(fn ($outputLine) => json_decode($outputLine, true, flags: JSON_THROW_ON_ERROR));
- } catch (\Throwable $e) {
+ } catch (\Throwable) {
return collect([]);
}
}
@@ -104,7 +102,7 @@ function format_docker_envs_to_json($rawOutput)
return [$env[0] => $env[1]];
});
- } catch (\Throwable $e) {
+ } catch (\Throwable) {
return collect([]);
}
}
@@ -207,12 +205,12 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
}
function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
{
- if ($resource->getMorphClass() === 'App\Models\ServiceApplication') {
+ if ($resource->getMorphClass() === \App\Models\ServiceApplication::class) {
$uuid = data_get($resource, 'uuid');
$server = data_get($resource, 'service.server');
$environment_variables = data_get($resource, 'service.environment_variables');
$type = $resource->serviceType();
- } elseif ($resource->getMorphClass() === 'App\Models\Application') {
+ } elseif ($resource->getMorphClass() === \App\Models\Application::class) {
$uuid = data_get($resource, 'uuid');
$server = data_get($resource, 'destination.server');
$environment_variables = data_get($resource, 'environment_variables');
@@ -279,7 +277,6 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
$labels->push("caddy_ingress_network={$network}");
}
foreach ($domains as $loop => $domain) {
- $loop = $loop;
$url = Url::fromString($domain);
$host = $url->getHost();
$path = $url->getPath();
@@ -335,10 +332,11 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
if (preg_match('/coolify\.traefik\.middlewares=(.*)/', $item, $matches)) {
return explode(',', $matches[1]);
}
+
return null;
})->flatten()
- ->filter()
- ->unique();
+ ->filter()
+ ->unique();
}
foreach ($domains as $loop => $domain) {
try {
@@ -388,7 +386,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
if ($path !== '/') {
// Middleware handling
$middlewares = collect([]);
- if ($is_stripprefix_enabled && !str($image)->contains('ghost')) {
+ if ($is_stripprefix_enabled && ! str($image)->contains('ghost')) {
$labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}");
$middlewares->push("{$https_label}-stripprefix");
}
@@ -402,7 +400,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_non_www);
$middlewares->push($to_non_www_name);
}
- if ($redirect_direction === 'www' && !str($host)->startsWith('www.')) {
+ if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) {
$labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name);
}
@@ -417,7 +415,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$middlewares = collect([]);
if ($is_gzip_enabled) {
$middlewares->push('gzip');
- }
+ }
if (str($image)->contains('ghost')) {
$middlewares->push('redir-ghost');
}
@@ -510,7 +508,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
}
}
}
- } catch (\Throwable $e) {
+ } catch (\Throwable) {
continue;
}
}
@@ -581,7 +579,6 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
redirect_direction: $application->redirect
));
}
-
}
} else {
if (data_get($preview, 'fqdn')) {
@@ -633,7 +630,6 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_stripprefix_enabled: $application->isStripprefixEnabled()
));
}
-
}
return $labels->all();
@@ -658,7 +654,7 @@ function isDatabaseImage(?string $image = null)
return false;
}
-function convert_docker_run_to_compose(?string $custom_docker_run_options = null)
+function convertDockerRunToCompose(?string $custom_docker_run_options = null)
{
$options = [];
$compose_options = collect([]);
@@ -683,9 +679,17 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
'--privileged' => 'privileged',
'--ip' => 'ip',
'--shm-size' => 'shm_size',
+ '--gpus' => 'gpus',
]);
foreach ($matches as $match) {
$option = $match[1];
+ if ($option === '--gpus') {
+ $regexForParsingDeviceIds = '/device=([0-9A-Za-z-,]+)/';
+ preg_match($regexForParsingDeviceIds, $custom_docker_run_options, $device_matches);
+ $value = $device_matches[1] ?? 'all';
+ $options[$option][] = $value;
+ $options[$option] = array_unique($options[$option]);
+ }
if (isset($match[2]) && $match[2] !== '') {
$value = $match[2];
$options[$option][] = $value;
@@ -698,7 +702,6 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
$options = collect($options);
// Easily get mappings from https://github.com/composerize/composerize/blob/master/packages/composerize/src/mappings.js
foreach ($options as $option => $value) {
- // ray($option,$value);
if (! data_get($mapping, $option)) {
continue;
}
@@ -727,6 +730,28 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
if (! is_null($value) && is_array($value) && count($value) > 0) {
$compose_options->put($mapping[$option], $value[0]);
}
+ } elseif ($option === '--gpus') {
+ $payload = [
+ 'driver' => 'nvidia',
+ 'capabilities' => ['gpu'],
+ ];
+ if (! is_null($value) && is_array($value) && count($value) > 0) {
+ if (str($value[0]) != 'all') {
+ if (str($value[0])->contains(',')) {
+ $payload['device_ids'] = str($value[0])->explode(',')->toArray();
+ } else {
+ $payload['device_ids'] = [$value[0]];
+ }
+ }
+ }
+ ray($payload);
+ $compose_options->put('deploy', [
+ 'resources' => [
+ 'reservations' => [
+ 'devices' => [$payload],
+ ],
+ ],
+ ]);
} else {
if ($list_options->contains($option)) {
if ($compose_options->has($mapping[$option])) {
@@ -748,7 +773,7 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
return $compose_options->toArray();
}
-function generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $network)
+function generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $network)
{
$ipv4 = data_get($docker_run_options, 'ip.0');
$ipv6 = data_get($docker_run_options, 'ip6.0');
diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php
index 97deb0b1c8..529ac82b10 100644
--- a/bootstrap/helpers/github.php
+++ b/bootstrap/helpers/github.php
@@ -3,6 +3,7 @@
use App\Models\GithubApp;
use App\Models\GitlabApp;
use Carbon\Carbon;
+use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Lcobucci\JWT\Encoding\ChainedFormatter;
@@ -16,7 +17,7 @@ function generate_github_installation_token(GithubApp $source)
$signingKey = InMemory::plainText($source->privateKey->private_key);
$algorithm = new Sha256;
$tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default()));
- $now = new DateTimeImmutable;
+ $now = CarbonImmutable::now();
$now = $now->setTime($now->format('H'), $now->format('i'));
$issuedToken = $tokenBuilder
->issuedBy($source->app_id)
@@ -40,16 +41,15 @@ function generate_github_jwt_token(GithubApp $source)
$signingKey = InMemory::plainText($source->privateKey->private_key);
$algorithm = new Sha256;
$tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default()));
- $now = new DateTimeImmutable;
+ $now = CarbonImmutable::now();
$now = $now->setTime($now->format('H'), $now->format('i'));
- $issuedToken = $tokenBuilder
+
+ return $tokenBuilder
->issuedBy($source->app_id)
->issuedAt($now->modify('-1 minute'))
->expiresAt($now->modify('+10 minutes'))
->getToken($algorithm, $signingKey)
->toString();
-
- return $issuedToken;
}
function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $method = 'get', ?array $data = null, bool $throwError = true)
@@ -57,7 +57,7 @@ function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $m
if (is_null($source)) {
throw new \Exception('Not implemented yet.');
}
- if ($source->getMorphClass() == 'App\Models\GithubApp') {
+ if ($source->getMorphClass() === \App\Models\GithubApp::class) {
if ($source->is_public) {
$response = Http::github($source->api_url)->$method($endpoint);
} else {
diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php
index 309ccee4a9..a8ef0fe5a7 100644
--- a/bootstrap/helpers/proxy.php
+++ b/bootstrap/helpers/proxy.php
@@ -16,12 +16,10 @@ function collectProxyDockerNetworksByServer(Server $server)
return collect();
}
$networks = instant_remote_process(['docker inspect --format="{{json .NetworkSettings.Networks }}" coolify-proxy'], $server, false);
- $networks = collect($networks)->map(function ($network) {
+
+ return collect($networks)->map(function ($network) {
return collect(json_decode($network))->keys();
})->flatten()->unique();
-
- return $networks;
-
}
function collectDockerNetworksByServer(Server $server)
{
@@ -241,9 +239,11 @@ function generate_default_proxy_configuration(Server $server)
'ports' => [
'80:80',
'443:443',
+ '443:443/udp',
],
'labels' => [
'coolify.managed=true',
+ 'coolify.proxy=true',
],
'volumes' => [
'/var/run/docker.sock:/var/run/docker.sock:ro',
diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php
index 67b60d6b75..c7dd2cb83f 100644
--- a/bootstrap/helpers/remoteProcess.php
+++ b/bootstrap/helpers/remoteProcess.php
@@ -124,7 +124,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
associative: true,
flags: JSON_THROW_ON_ERROR
);
- } catch (\JsonException $exception) {
+ } catch (\JsonException) {
return collect([]);
}
$seenCommands = collect();
@@ -204,7 +204,7 @@ function checkRequiredCommands(Server $server)
}
try {
instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'apt update && apt install -y {$command}'"], $server);
- } catch (\Throwable $e) {
+ } catch (\Throwable) {
break;
}
$commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false);
diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php
index eba88d0002..fd2e1231fb 100644
--- a/bootstrap/helpers/services.php
+++ b/bootstrap/helpers/services.php
@@ -24,7 +24,7 @@ function replaceVariables(string $variable): Stringable
function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Application $oneService, bool $isInit = false)
{
try {
- if ($oneService->getMorphClass() === 'App\Models\Application') {
+ if ($oneService->getMorphClass() === \App\Models\Application::class) {
$workdir = $oneService->workdir();
$server = $oneService->destination->server;
} else {
@@ -51,7 +51,7 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli
// Exists and is a directory
$isDir = instant_remote_process(["test -d $fileLocation && echo OK || echo NOK"], $server);
- if ($isFile == 'OK') {
+ if ($isFile === 'OK') {
// If its a file & exists
$filesystemContent = instant_remote_process(["cat $fileLocation"], $server);
if ($fileVolume->is_based_on_git) {
@@ -59,12 +59,12 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli
}
$fileVolume->is_directory = false;
$fileVolume->save();
- } elseif ($isDir == 'OK') {
+ } elseif ($isDir === 'OK') {
// If its a directory & exists
$fileVolume->content = null;
$fileVolume->is_directory = true;
$fileVolume->save();
- } elseif ($isFile == 'NOK' && $isDir == 'NOK' && ! $fileVolume->is_directory && $isInit && $content) {
+ } elseif ($isFile === 'NOK' && $isDir === 'NOK' && ! $fileVolume->is_directory && $isInit && $content) {
// Does not exists (no dir or file), not flagged as directory, is init, has content
$fileVolume->content = $content;
$fileVolume->is_directory = false;
@@ -75,13 +75,13 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli
"mkdir -p $dir",
"echo '$content' | base64 -d | tee $fileLocation",
], $server);
- } elseif ($isFile == 'NOK' && $isDir == 'NOK' && $fileVolume->is_directory && $isInit) {
+ } elseif ($isFile === 'NOK' && $isDir === 'NOK' && $fileVolume->is_directory && $isInit) {
// Does not exists (no dir or file), flagged as directory, is init
$fileVolume->content = null;
$fileVolume->is_directory = true;
$fileVolume->save();
instant_remote_process(["mkdir -p $fileLocation"], $server);
- } elseif ($isFile == 'NOK' && $isDir == 'NOK' && ! $fileVolume->is_directory && $isInit && is_null($content)) {
+ } elseif ($isFile === 'NOK' && $isDir === 'NOK' && ! $fileVolume->is_directory && $isInit && is_null($content)) {
// Does not exists (no dir or file), not flagged as directory, is init, has no content => create directory
$fileVolume->content = null;
$fileVolume->is_directory = true;
@@ -245,8 +245,5 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource)
}
function serviceKeys()
{
- $services = get_service_templates();
- $serviceKeys = $services->keys();
-
- return $serviceKeys;
+ return get_service_templates()->keys();
}
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index cfdea81fbd..f6875cc814 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -28,17 +28,20 @@
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Internal\GeneralNotification;
+use Carbon\CarbonImmutable;
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Mail\Message;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Process\Pool;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Process;
+use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Validator;
@@ -98,12 +101,12 @@ function isInstanceAdmin()
function currentTeam()
{
- return auth()?->user()?->currentTeam() ?? null;
+ return Auth::user()?->currentTeam() ?? null;
}
function showBoarding(): bool
{
- if (auth()->user()?->isMember()) {
+ if (Auth::user()?->isMember()) {
return false;
}
@@ -112,21 +115,20 @@ function showBoarding(): bool
function refreshSession(?Team $team = null): void
{
if (! $team) {
- if (auth()->user()?->currentTeam()) {
- $team = Team::find(auth()->user()->currentTeam()->id);
+ if (Auth::user()->currentTeam()) {
+ $team = Team::find(Auth::user()->currentTeam()->id);
} else {
- $team = User::find(auth()->user()->id)->teams->first();
+ $team = User::find(Auth::id())->teams->first();
}
}
- Cache::forget('team:'.auth()->user()->id);
- Cache::remember('team:'.auth()->user()->id, 3600, function () use ($team) {
+ Cache::forget('team:'.Auth::id());
+ Cache::remember('team:'.Auth::id(), 3600, function () use ($team) {
return $team;
});
session(['currentTeam' => $team]);
}
function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null)
{
- ray($error);
if ($error instanceof TooManyRequestsException) {
if (isset($livewire)) {
return $livewire->dispatch('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.");
@@ -142,6 +144,10 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
return 'Duplicate entry found. Please use a different name.';
}
+ if ($error instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
+ abort(404);
+ }
+
if ($error instanceof Throwable) {
$message = $error->getMessage();
} else {
@@ -164,14 +170,11 @@ function get_route_parameters(): array
function get_latest_sentinel_version(): string
{
try {
- $response = Http::get('https://cdn.coollabs.io/sentinel/versions.json');
+ $response = Http::get('https://cdn.coollabs.io/coolify/versions.json');
$versions = $response->json();
- return data_get($versions, 'sentinel.version');
- } catch (\Throwable $e) {
- //throw $e;
- ray($e->getMessage());
-
+ return data_get($versions, 'coolify.sentinel.version');
+ } catch (\Throwable) {
return '0.0.0';
}
}
@@ -300,7 +303,7 @@ function getFqdnWithoutPort(string $fqdn)
$path = $url->getPath();
return "$scheme://$host$path";
- } catch (\Throwable $e) {
+ } catch (\Throwable) {
return $fqdn;
}
}
@@ -368,6 +371,9 @@ function translate_cron_expression($expression_to_validate): string
}
function validate_cron_expression($expression_to_validate): bool
{
+ if (empty($expression_to_validate)) {
+ return false;
+ }
$isValid = false;
$expression = new CronExpression($expression_to_validate);
$isValid = $expression->isValid();
@@ -496,9 +502,8 @@ function generateFqdn(Server $server, string $random, bool $forceHttps = false):
if ($forceHttps) {
$scheme = 'https';
}
- $finalFqdn = "$scheme://{$random}.$host$path";
- return $finalFqdn;
+ return "$scheme://{$random}.$host$path";
}
function sslip(Server $server)
{
@@ -536,7 +541,7 @@ function get_service_templates(bool $force = false): Collection
$services = $response->json();
return collect($services);
- } catch (\Throwable $e) {
+ } catch (\Throwable) {
$services = File::get(base_path('templates/service-templates.json'));
return collect(json_decode($services))->sortKeys();
@@ -643,14 +648,13 @@ function queryResourcesByUuid(string $uuid)
return $resource;
}
-function generatTagDeployWebhook($tag_name)
+function generateTagDeployWebhook($tag_name)
{
$baseUrl = base_url();
$api = Url::fromString($baseUrl).'/api/v1';
$endpoint = "/deploy?tag=$tag_name";
- $url = $api.$endpoint;
- return $url;
+ return $api.$endpoint;
}
function generateDeployWebhook($resource)
{
@@ -658,20 +662,18 @@ function generateDeployWebhook($resource)
$api = Url::fromString($baseUrl).'/api/v1';
$endpoint = '/deploy';
$uuid = data_get($resource, 'uuid');
- $url = $api.$endpoint."?uuid=$uuid&force=false";
- return $url;
+ return $api.$endpoint."?uuid=$uuid&force=false";
}
function generateGitManualWebhook($resource, $type)
{
if ($resource->source_id !== 0 && ! is_null($resource->source_id)) {
return null;
}
- if ($resource->getMorphClass() === 'App\Models\Application') {
+ if ($resource->getMorphClass() === \App\Models\Application::class) {
$baseUrl = base_url();
- $api = Url::fromString($baseUrl)."/webhooks/source/$type/events/manual";
- return $api;
+ return Url::fromString($baseUrl)."/webhooks/source/$type/events/manual";
}
return null;
@@ -683,7 +685,7 @@ function removeAnsiColors($text)
function getTopLevelNetworks(Service|Application $resource)
{
- if ($resource->getMorphClass() === 'App\Models\Service') {
+ if ($resource->getMorphClass() === \App\Models\Service::class) {
if ($resource->docker_compose_raw) {
try {
$yaml = Yaml::parse($resource->docker_compose_raw);
@@ -738,7 +740,7 @@ function getTopLevelNetworks(Service|Application $resource)
return $topLevelNetworks->keys();
}
- } elseif ($resource->getMorphClass() === 'App\Models\Application') {
+ } elseif ($resource->getMorphClass() === \App\Models\Application::class) {
try {
$yaml = Yaml::parse($resource->docker_compose_raw);
} catch (\Exception $e) {
@@ -945,7 +947,7 @@ function generateEnvValue(string $command, Service|Application|null $service = n
$key = InMemory::plainText($signingKey);
$algorithm = new Sha256;
$tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default()));
- $now = new DateTimeImmutable;
+ $now = CarbonImmutable::now();
$now = $now->setTime($now->format('H'), $now->format('i'));
$token = $tokenBuilder
->issuedBy('supabase')
@@ -965,7 +967,7 @@ function generateEnvValue(string $command, Service|Application|null $service = n
$key = InMemory::plainText($signingKey);
$algorithm = new Sha256;
$tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default()));
- $now = new DateTimeImmutable;
+ $now = CarbonImmutable::now();
$now = $now->setTime($now->format('H'), $now->format('i'));
$token = $tokenBuilder
->issuedBy('supabase')
@@ -1048,7 +1050,7 @@ function validate_dns_entry(string $fqdn, Server $server)
}
}
}
- } catch (\Exception $e) {
+ } catch (\Exception) {
}
}
ray("Found match: $found_matching_ip");
@@ -1145,7 +1147,7 @@ function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId =
function check_domain_usage(ServiceApplication|Application|null $resource = null, ?string $domain = null)
{
if ($resource) {
- if ($resource->getMorphClass() === 'App\Models\Application' && $resource->build_pack === 'dockercompose') {
+ if ($resource->getMorphClass() === \App\Models\Application::class && $resource->build_pack === 'dockercompose') {
$domains = data_get(json_decode($resource->docker_compose_domains, true), '*.domain');
$domains = collect($domains);
} else {
@@ -1338,13 +1340,6 @@ function isAnyDeploymentInprogress()
exit(0);
}
-function generateSentinelToken()
-{
- $token = Str::random(64);
-
- return $token;
-}
-
function isBase64Encoded($strValue)
{
return base64_encode(base64_decode($strValue, true)) === $strValue;
@@ -1416,7 +1411,7 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull
if ($source->value() === '/tmp' || $source->value() === '/tmp/') {
return $volume;
}
- if (get_class($resource) === "App\Models\Application") {
+ if (get_class($resource) === \App\Models\Application::class) {
$dir = base_configuration_dir().'/applications/'.$resource->uuid;
} else {
$dir = base_configuration_dir().'/services/'.$resource->service->uuid;
@@ -1456,7 +1451,7 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull
}
}
$slugWithoutUuid = Str::slug($source, '-');
- if (get_class($resource) === "App\Models\Application") {
+ if (get_class($resource) === \App\Models\Application::class) {
$name = "{$resource->uuid}_{$slugWithoutUuid}";
} else {
$name = "{$resource->service->uuid}_{$slugWithoutUuid}";
@@ -1499,7 +1494,7 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull
function parseDockerComposeFile(Service|Application $resource, bool $isNew = false, int $pull_request_id = 0, ?int $preview_id = null)
{
- if ($resource->getMorphClass() === 'App\Models\Service') {
+ if ($resource->getMorphClass() === \App\Models\Service::class) {
if ($resource->docker_compose_raw) {
try {
$yaml = Yaml::parse($resource->docker_compose_raw);
@@ -2213,10 +2208,10 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
} else {
return collect([]);
}
- } elseif ($resource->getMorphClass() === 'App\Models\Application') {
+ } elseif ($resource->getMorphClass() === \App\Models\Application::class) {
try {
$yaml = Yaml::parse($resource->docker_compose_raw);
- } catch (\Exception $e) {
+ } catch (\Exception) {
return;
}
$server = $resource->destination->server;
@@ -2962,7 +2957,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
try {
$yaml = Yaml::parse($compose);
- } catch (\Exception $e) {
+ } catch (\Exception) {
return collect([]);
}
@@ -3099,7 +3094,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
}
}
- if ($value && get_class($value) === 'Illuminate\Support\Stringable' && $value->startsWith('/')) {
+ if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) {
$path = $value->value();
if ($path !== '/') {
$fqdn = "$fqdn$path";
@@ -3190,7 +3185,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
'is_build_time' => false,
'is_preview' => false,
]);
-
} else {
$value = generateEnvValue($command, $resource);
$resource->environment_variables()->where('key', $key->value())->where($nameOfId, $resource->id)->firstOrCreate([
@@ -3569,6 +3563,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
]);
} else {
if ($value->startsWith('$')) {
+ $isRequired = false;
if ($value->contains(':-')) {
$value = replaceVariables($value);
$key = $value->before(':');
@@ -3583,11 +3578,13 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$key = $value->before(':');
$value = $value->after(':?');
+ $isRequired = true;
} elseif ($value->contains('?')) {
$value = replaceVariables($value);
$key = $value->before('?');
$value = $value->after('?');
+ $isRequired = true;
}
if ($originalValue->value() === $value->value()) {
// This means the variable does not have a default value, so it needs to be created in Coolify
@@ -3598,6 +3595,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
], [
'is_build_time' => false,
'is_preview' => false,
+ 'is_required' => $isRequired,
]);
// Add the variable to the environment so it will be shown in the deployable compose file
$environment[$parsedKeyValue->value()] = $resource->environment_variables()->where('key', $parsedKeyValue)->where($nameOfId, $resource->id)->first()->value;
@@ -3611,9 +3609,9 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
'value' => $value,
'is_build_time' => false,
'is_preview' => false,
+ 'is_required' => $isRequired,
]);
}
-
}
}
if ($isApplication) {
@@ -3787,7 +3785,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
service_name: $serviceName,
image: $image,
predefinedPort: $predefinedPort
-
));
}
}
@@ -3978,20 +3975,19 @@ function convertComposeEnvironmentToArray($environment)
}
return $convertedServiceVariables;
-
}
function instanceSettings()
{
return InstanceSettings::get();
}
-function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id) {
-
+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();
+ $uuid = new Cuid2;
$cloneCommand = "git clone --no-checkout -b $branch $repository .";
$workdir = rtrim($base_directory, '/');
$fileList = collect([".$workdir/coolify.json"]);
@@ -4008,7 +4004,61 @@ function loadConfigFromGit(string $repository, string $branch, string $base_dire
]);
try {
return instant_remote_process($commands, $server);
- } catch (\Exception $e) {
- // continue
+ } catch (\Exception) {
+ // continue
}
}
+
+function loggy($message = null, array $context = [])
+{
+ if (! isDev()) {
+ return;
+ }
+ if (function_exists('ray') && config('app.debug')) {
+ ray($message, $context);
+ }
+ if (is_null($message)) {
+ return app('log');
+ }
+
+ return app('log')->debug($message, $context);
+}
+function sslipDomainWarning(string $domains)
+{
+ $domains = str($domains)->trim()->explode(',');
+ $showSslipHttpsWarning = false;
+ $domains->each(function ($domain) use (&$showSslipHttpsWarning) {
+ if (str($domain)->contains('https') && str($domain)->contains('sslip')) {
+ $showSslipHttpsWarning = true;
+ }
+ });
+
+ return $showSslipHttpsWarning;
+}
+
+function isEmailRateLimited(string $limiterKey, int $decaySeconds = 3600, ?callable $callbackOnSuccess = null): bool
+{
+ if (isDev()) {
+ $decaySeconds = 120;
+ }
+ $rateLimited = false;
+ $executed = RateLimiter::attempt(
+ $limiterKey,
+ $maxAttempts = 0,
+ function () use (&$rateLimited, &$limiterKey, $callbackOnSuccess) {
+ isDev() && loggy('Rate limit not reached for '.$limiterKey);
+ $rateLimited = false;
+
+ if ($callbackOnSuccess) {
+ $callbackOnSuccess();
+ }
+ },
+ $decaySeconds,
+ );
+ if (! $executed) {
+ isDev() && loggy('Rate limit reached for '.$limiterKey.'. Rate limiter will be disabled for '.$decaySeconds.' seconds.');
+ $rateLimited = true;
+ }
+
+ return $rateLimited;
+}
diff --git a/bootstrap/helpers/socialite.php b/bootstrap/helpers/socialite.php
index a23dc24d3e..cad9de7fa8 100644
--- a/bootstrap/helpers/socialite.php
+++ b/bootstrap/helpers/socialite.php
@@ -7,7 +7,7 @@ function get_socialite_provider(string $provider)
{
$oauth_setting = OauthSetting::firstWhere('provider', $provider);
- if ($provider == 'azure') {
+ if ($provider === 'azure') {
$azure_config = new \SocialiteProviders\Manager\Config(
$oauth_setting->client_id,
$oauth_setting->client_secret,
diff --git a/bootstrap/helpers/subscriptions.php b/bootstrap/helpers/subscriptions.php
index aadd2dd344..8ddb1331c6 100644
--- a/bootstrap/helpers/subscriptions.php
+++ b/bootstrap/helpers/subscriptions.php
@@ -55,12 +55,11 @@ function getStripeCustomerPortalSession(Team $team)
if (! $stripe_customer_id) {
return null;
}
- $session = \Stripe\BillingPortal\Session::create([
+
+ return \Stripe\BillingPortal\Session::create([
'customer' => $stripe_customer_id,
'return_url' => $return_url,
]);
-
- return $session;
}
function allowedPathsForUnsubscribedAccounts()
{
diff --git a/composer.json b/composer.json
index fbd77d0cf9..2bae1149c4 100644
--- a/composer.json
+++ b/composer.json
@@ -1,25 +1,28 @@
{
- "name": "laravel/laravel",
+ "name": "coollabsio/coolify",
+ "description": "The Coolify project.",
+ "license": "Apache-2.0",
"type": "project",
- "description": "The Laravel Framework.",
"keywords": [
- "framework",
- "laravel"
+ "coolify",
+ "deployment",
+ "docker",
+ "self-hosted",
+ "server"
],
- "license": "MIT",
"require": {
"php": "^8.2",
"danharrin/livewire-rate-limiting": "^1.1",
"doctrine/dbal": "^3.6",
"guzzlehttp/guzzle": "^7.5.0",
- "laravel/fortify": "^v1.16.0",
- "laravel/framework": "^v11",
+ "laravel/fortify": "^1.16.0",
+ "laravel/framework": "^11",
"laravel/horizon": "^5.29.1",
+ "laravel/pail": "^1.1",
"laravel/prompts": "^0.1.6",
- "laravel/sanctum": "^v4.0",
- "laravel/socialite": "^v5.14.0",
- "laravel/telescope": "^5.2",
- "laravel/tinker": "^v2.8.1",
+ "laravel/sanctum": "^4.0",
+ "laravel/socialite": "^5.14.0",
+ "laravel/tinker": "^2.8.1",
"laravel/ui": "^4.2",
"lcobucci/jwt": "^5.0.0",
"league/flysystem-aws-s3-v3": "^3.0",
@@ -28,7 +31,7 @@
"log1x/laravel-webfonts": "^1.0",
"lorisleiva/laravel-actions": "^2.7",
"nubs/random-name-generator": "^2.2",
- "phpseclib/phpseclib": "~3.0",
+ "phpseclib/phpseclib": "^3.0",
"pion/laravel-chunk-upload": "^1.5",
"poliander/cron": "^3.0",
"purplepixie/phpdns": "^2.1",
@@ -49,46 +52,65 @@
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.13",
- "fakerphp/faker": "^v1.21.0",
- "laravel/dusk": "^v8.0",
+ "fakerphp/faker": "^1.21.0",
+ "laravel/dusk": "^8.0",
"laravel/pint": "^1.16",
+ "laravel/telescope": "^5.2",
"mockery/mockery": "^1.5.1",
- "nunomaduro/collision": "^v8.1",
+ "nunomaduro/collision": "^8.1",
"pestphp/pest": "^2.16",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.0.19",
- "serversideup/spin": "^v1.1.0",
+ "serversideup/spin": "^1.1.0",
"spatie/laravel-ignition": "^2.1.0",
"symfony/http-client": "^6.2"
},
+ "minimum-stability": "stable",
+ "prefer-stable": true,
"autoload": {
- "files": [
- "bootstrap/includeHelpers.php"
- ],
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
- }
+ },
+ "files": [
+ "bootstrap/includeHelpers.php"
+ ]
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
+ "config": {
+ "allow-plugins": {
+ "pestphp/pest-plugin": true,
+ "php-http/discovery": true
+ },
+ "optimize-autoloader": true,
+ "preferred-install": "dist",
+ "sort-packages": true
+ },
+ "extra": {
+ "laravel": {
+ "dont-discover": [
+ "laravel/telescope"
+ ]
+ }
+ },
"scripts": {
- "post-autoload-dump": [
- "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
- "@php artisan package:discover --ansi"
+ "post-install-cmd": [
+ "cp -r 'hooks/' '.git/hooks/'",
+ "php -r \"copy('hooks/pre-commit', '.git/hooks/pre-commit');\"",
+ "php -r \"chmod('.git/hooks/pre-commit', 0777);\""
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force",
"Illuminate\\Foundation\\ComposerScripts::postUpdate"
],
- "post-install-cmd": [
- "cp -r 'hooks/' '.git/hooks/'",
- "php -r \"copy('hooks/pre-commit', '.git/hooks/pre-commit');\"",
- "php -r \"chmod('.git/hooks/pre-commit', 0777);\""
+ "post-autoload-dump": [
+ "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
+ "@php artisan package:discover --ansi"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
@@ -96,23 +118,5 @@
"post-create-project-cmd": [
"@php artisan key:generate --ansi"
]
- },
- "extra": {
- "laravel": {
- "dont-discover": [
- "laravel/telescope"
- ]
- }
- },
- "config": {
- "optimize-autoloader": true,
- "preferred-install": "dist",
- "sort-packages": true,
- "allow-plugins": {
- "pestphp/pest-plugin": true,
- "php-http/discovery": true
- }
- },
- "minimum-stability": "stable",
- "prefer-stable": true
-}
+ }
+}
\ No newline at end of file
diff --git a/composer.lock b/composer.lock
index 0b8da82d0a..5eb03b5fc3 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "c47adf3684eb727e22503937435c0914",
+ "content-hash": "3f2342fe6b1ba920c8875f8a8fe41962",
"packages": [
{
"name": "amphp/amp",
@@ -3144,6 +3144,83 @@
},
"time": "2024-10-08T18:23:02+00:00"
},
+ {
+ "name": "laravel/pail",
+ "version": "v1.1.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/pail.git",
+ "reference": "b33ad8321416fe86efed7bf398f3306c47b4871b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/pail/zipball/b33ad8321416fe86efed7bf398f3306c47b4871b",
+ "reference": "b33ad8321416fe86efed7bf398f3306c47b4871b",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "illuminate/console": "^10.24|^11.0",
+ "illuminate/contracts": "^10.24|^11.0",
+ "illuminate/log": "^10.24|^11.0",
+ "illuminate/process": "^10.24|^11.0",
+ "illuminate/support": "^10.24|^11.0",
+ "nunomaduro/termwind": "^1.15|^2.0",
+ "php": "^8.2",
+ "symfony/console": "^6.0|^7.0"
+ },
+ "require-dev": {
+ "laravel/pint": "^1.13",
+ "orchestra/testbench": "^8.12|^9.0",
+ "pestphp/pest": "^2.20",
+ "pestphp/pest-plugin-type-coverage": "^2.3",
+ "phpstan/phpstan": "^1.10",
+ "symfony/var-dumper": "^6.3|^7.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.x-dev"
+ },
+ "laravel": {
+ "providers": [
+ "Laravel\\Pail\\PailServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Pail\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ },
+ {
+ "name": "Nuno Maduro",
+ "email": "enunomaduro@gmail.com"
+ }
+ ],
+ "description": "Easily delve into your Laravel application's log files directly from the command line.",
+ "homepage": "https://github.com/laravel/pail",
+ "keywords": [
+ "laravel",
+ "logs",
+ "php",
+ "tail"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/pail/issues",
+ "source": "https://github.com/laravel/pail"
+ },
+ "time": "2024-10-15T20:06:24+00:00"
+ },
{
"name": "laravel/prompts",
"version": "v0.1.25",
@@ -3399,75 +3476,6 @@
},
"time": "2024-09-03T09:46:57+00:00"
},
- {
- "name": "laravel/telescope",
- "version": "v5.2.2",
- "source": {
- "type": "git",
- "url": "https://github.com/laravel/telescope.git",
- "reference": "daaf95dee9fab2dd80f59b5f6611c6c0eff44878"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/laravel/telescope/zipball/daaf95dee9fab2dd80f59b5f6611c6c0eff44878",
- "reference": "daaf95dee9fab2dd80f59b5f6611c6c0eff44878",
- "shasum": ""
- },
- "require": {
- "ext-json": "*",
- "laravel/framework": "^8.37|^9.0|^10.0|^11.0",
- "php": "^8.0",
- "symfony/console": "^5.3|^6.0|^7.0",
- "symfony/var-dumper": "^5.0|^6.0|^7.0"
- },
- "require-dev": {
- "ext-gd": "*",
- "guzzlehttp/guzzle": "^6.0|^7.0",
- "laravel/octane": "^1.4|^2.0|dev-develop",
- "orchestra/testbench": "^6.40|^7.37|^8.17|^9.0",
- "phpstan/phpstan": "^1.10",
- "phpunit/phpunit": "^9.0|^10.5"
- },
- "type": "library",
- "extra": {
- "laravel": {
- "providers": [
- "Laravel\\Telescope\\TelescopeServiceProvider"
- ]
- }
- },
- "autoload": {
- "psr-4": {
- "Laravel\\Telescope\\": "src/",
- "Laravel\\Telescope\\Database\\Factories\\": "database/factories/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Taylor Otwell",
- "email": "taylor@laravel.com"
- },
- {
- "name": "Mohamed Said",
- "email": "mohamed@laravel.com"
- }
- ],
- "description": "An elegant debug assistant for the Laravel framework.",
- "keywords": [
- "debugging",
- "laravel",
- "monitoring"
- ],
- "support": {
- "issues": "https://github.com/laravel/telescope/issues",
- "source": "https://github.com/laravel/telescope/tree/v5.2.2"
- },
- "time": "2024-08-26T12:40:52+00:00"
- },
{
"name": "laravel/tinker",
"version": "v2.10.0",
@@ -9105,16 +9113,16 @@
},
{
"name": "symfony/http-foundation",
- "version": "v7.1.5",
+ "version": "v7.1.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
- "reference": "e30ef73b1e44eea7eb37ba69600a354e553f694b"
+ "reference": "5183b61657807099d98f3367bcccb850238b17a9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e30ef73b1e44eea7eb37ba69600a354e553f694b",
- "reference": "e30ef73b1e44eea7eb37ba69600a354e553f694b",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/5183b61657807099d98f3367bcccb850238b17a9",
+ "reference": "5183b61657807099d98f3367bcccb850238b17a9",
"shasum": ""
},
"require": {
@@ -9162,7 +9170,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-foundation/tree/v7.1.5"
+ "source": "https://github.com/symfony/http-foundation/tree/v7.1.7"
},
"funding": [
{
@@ -9178,7 +9186,7 @@
"type": "tidelift"
}
],
- "time": "2024-09-20T08:28:38+00:00"
+ "time": "2024-11-06T09:02:46+00:00"
},
{
"name": "symfony/http-kernel",
@@ -9376,16 +9384,16 @@
},
{
"name": "symfony/mime",
- "version": "v7.1.5",
+ "version": "v7.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
- "reference": "711d2e167e8ce65b05aea6b258c449671cdd38ff"
+ "reference": "caa1e521edb2650b8470918dfe51708c237f0598"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mime/zipball/711d2e167e8ce65b05aea6b258c449671cdd38ff",
- "reference": "711d2e167e8ce65b05aea6b258c449671cdd38ff",
+ "url": "https://api.github.com/repos/symfony/mime/zipball/caa1e521edb2650b8470918dfe51708c237f0598",
+ "reference": "caa1e521edb2650b8470918dfe51708c237f0598",
"shasum": ""
},
"require": {
@@ -9440,7 +9448,7 @@
"mime-type"
],
"support": {
- "source": "https://github.com/symfony/mime/tree/v7.1.5"
+ "source": "https://github.com/symfony/mime/tree/v7.1.6"
},
"funding": [
{
@@ -9456,7 +9464,7 @@
"type": "tidelift"
}
],
- "time": "2024-09-20T08:28:38+00:00"
+ "time": "2024-10-25T15:11:02+00:00"
},
{
"name": "symfony/options-resolver",
@@ -10243,16 +10251,16 @@
},
{
"name": "symfony/process",
- "version": "v7.1.5",
+ "version": "v7.1.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "5c03ee6369281177f07f7c68252a280beccba847"
+ "reference": "9b8a40b7289767aa7117e957573c2a535efe6585"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/5c03ee6369281177f07f7c68252a280beccba847",
- "reference": "5c03ee6369281177f07f7c68252a280beccba847",
+ "url": "https://api.github.com/repos/symfony/process/zipball/9b8a40b7289767aa7117e957573c2a535efe6585",
+ "reference": "9b8a40b7289767aa7117e957573c2a535efe6585",
"shasum": ""
},
"require": {
@@ -10284,7 +10292,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v7.1.5"
+ "source": "https://github.com/symfony/process/tree/v7.1.7"
},
"funding": [
{
@@ -10300,7 +10308,7 @@
"type": "tidelift"
}
],
- "time": "2024-09-19T21:48:23+00:00"
+ "time": "2024-11-06T09:25:12+00:00"
},
{
"name": "symfony/psr-http-message-bridge",
@@ -12390,6 +12398,75 @@
},
"time": "2024-09-24T17:22:50+00:00"
},
+ {
+ "name": "laravel/telescope",
+ "version": "v5.2.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/telescope.git",
+ "reference": "749369e996611d803e7c1b57929b482dd676008d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/telescope/zipball/749369e996611d803e7c1b57929b482dd676008d",
+ "reference": "749369e996611d803e7c1b57929b482dd676008d",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "laravel/framework": "^8.37|^9.0|^10.0|^11.0",
+ "php": "^8.0",
+ "symfony/console": "^5.3|^6.0|^7.0",
+ "symfony/var-dumper": "^5.0|^6.0|^7.0"
+ },
+ "require-dev": {
+ "ext-gd": "*",
+ "guzzlehttp/guzzle": "^6.0|^7.0",
+ "laravel/octane": "^1.4|^2.0|dev-develop",
+ "orchestra/testbench": "^6.40|^7.37|^8.17|^9.0",
+ "phpstan/phpstan": "^1.10",
+ "phpunit/phpunit": "^9.0|^10.5"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Laravel\\Telescope\\TelescopeServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Telescope\\": "src/",
+ "Laravel\\Telescope\\Database\\Factories\\": "database/factories/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ },
+ {
+ "name": "Mohamed Said",
+ "email": "mohamed@laravel.com"
+ }
+ ],
+ "description": "An elegant debug assistant for the Laravel framework.",
+ "keywords": [
+ "debugging",
+ "laravel",
+ "monitoring"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/telescope/issues",
+ "source": "https://github.com/laravel/telescope/tree/v5.2.4"
+ },
+ "time": "2024-10-29T15:35:13+00:00"
+ },
{
"name": "maximebf/debugbar",
"version": "v1.23.2",
@@ -14897,16 +14974,16 @@
},
{
"name": "symfony/http-client",
- "version": "v6.4.12",
+ "version": "v6.4.14",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
- "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56"
+ "reference": "05d88cbd816ad6e0202edd9a9963cb9d615b8826"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-client/zipball/fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56",
- "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56",
+ "url": "https://api.github.com/repos/symfony/http-client/zipball/05d88cbd816ad6e0202edd9a9963cb9d615b8826",
+ "reference": "05d88cbd816ad6e0202edd9a9963cb9d615b8826",
"shasum": ""
},
"require": {
@@ -14970,7 +15047,7 @@
"http"
],
"support": {
- "source": "https://github.com/symfony/http-client/tree/v6.4.12"
+ "source": "https://github.com/symfony/http-client/tree/v6.4.14"
},
"funding": [
{
@@ -14986,7 +15063,7 @@
"type": "tidelift"
}
],
- "time": "2024-09-20T08:21:33+00:00"
+ "time": "2024-11-05T16:39:55+00:00"
},
{
"name": "symfony/http-client-contracts",
diff --git a/config/app.php b/config/app.php
index 34484fe410..371ac44ecc 100644
--- a/config/app.php
+++ b/config/app.php
@@ -199,8 +199,6 @@
App\Providers\EventServiceProvider::class,
App\Providers\HorizonServiceProvider::class,
App\Providers\RouteServiceProvider::class,
- App\Providers\TelescopeServiceProvider::class,
-
],
/*
diff --git a/config/constants.php b/config/constants.php
index 5792b358c4..1bec2e3bfd 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -1,6 +1,7 @@
'26.0',
'docs' => [
'base_url' => 'https://coolify.io/docs',
'contact' => 'https://coolify.io/docs/contact',
@@ -18,7 +19,7 @@
'invitation' => [
'link' => [
'base_url' => '/invitations/',
- 'expiration' => 10,
+ 'expiration_days' => 3,
],
],
'services' => [
diff --git a/config/coolify.php b/config/coolify.php
index f9878fff7b..225dfe6fa5 100644
--- a/config/coolify.php
+++ b/config/coolify.php
@@ -1,6 +1,7 @@
env('SENTRY_DSN'),
'docs' => 'https://coolify.io/docs/',
'contact' => 'https://coolify.io/docs/contact',
'feedback_discord_webhook' => env('FEEDBACK_DISCORD_WEBHOOK'),
diff --git a/config/debugbar.php b/config/debugbar.php
index eae406ba77..daeea96b6e 100644
--- a/config/debugbar.php
+++ b/config/debugbar.php
@@ -18,6 +18,7 @@
'except' => [
'telescope*',
'horizon*',
+ 'api*',
],
/*
diff --git a/config/horizon.php b/config/horizon.php
index 939d74883d..6086b30dad 100644
--- a/config/horizon.php
+++ b/config/horizon.php
@@ -197,6 +197,7 @@
'production' => [
's6' => [
'autoScalingStrategy' => 'size',
+ 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1),
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6),
'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),
'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1),
@@ -206,6 +207,7 @@
'local' => [
's6' => [
'autoScalingStrategy' => 'size',
+ 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1),
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6),
'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),
'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1),
diff --git a/config/sentry.php b/config/sentry.php
index ade6923ace..33ae36c731 100644
--- a/config/sentry.php
+++ b/config/sentry.php
@@ -3,11 +3,11 @@
return [
// @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/
- 'dsn' => 'https://89552af6db48f4ca6a871ec0fc42964d@o1082494.ingest.us.sentry.io/4505347448045568',
+ 'dsn' => config('coolify.sentry_dsn'),
// The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
- 'release' => '4.0.0-beta.360',
+ 'release' => '4.0.0-beta.361',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),
diff --git a/config/telescope.php b/config/telescope.php
index 24077c24d8..c940bec8a8 100644
--- a/config/telescope.php
+++ b/config/telescope.php
@@ -76,8 +76,8 @@
*/
'queue' => [
- 'connection' => env('TELESCOPE_QUEUE_CONNECTION', null),
- 'queue' => env('TELESCOPE_QUEUE', null),
+ 'connection' => env('TELESCOPE_QUEUE_CONNECTION', 'redis'),
+ 'queue' => env('TELESCOPE_QUEUE', 'default'),
],
/*
@@ -115,7 +115,6 @@
'livewire*',
'nova-api*',
'pulse*',
- 'broadcasting/auth',
],
'ignore_commands' => [
@@ -161,20 +160,20 @@
Watchers\ExceptionWatcher::class => env('TELESCOPE_EXCEPTION_WATCHER', true),
Watchers\GateWatcher::class => [
- 'enabled' => env('TELESCOPE_GATE_WATCHER', false),
+ 'enabled' => env('TELESCOPE_GATE_WATCHER', true),
'ignore_abilities' => [],
'ignore_packages' => true,
'ignore_paths' => [],
],
- Watchers\JobWatcher::class => env('TELESCOPE_JOB_WATCHER', false),
+ Watchers\JobWatcher::class => env('TELESCOPE_JOB_WATCHER', true),
Watchers\LogWatcher::class => [
'enabled' => env('TELESCOPE_LOG_WATCHER', true),
- 'level' => 'debug',
+ 'level' => 'error',
],
- Watchers\MailWatcher::class => env('TELESCOPE_MAIL_WATCHER', false),
+ Watchers\MailWatcher::class => env('TELESCOPE_MAIL_WATCHER', true),
Watchers\ModelWatcher::class => [
'enabled' => env('TELESCOPE_MODEL_WATCHER', true),
@@ -182,7 +181,7 @@
'hydrations' => true,
],
- Watchers\NotificationWatcher::class => env('TELESCOPE_NOTIFICATION_WATCHER', false),
+ Watchers\NotificationWatcher::class => env('TELESCOPE_NOTIFICATION_WATCHER', true),
Watchers\QueryWatcher::class => [
'enabled' => env('TELESCOPE_QUERY_WATCHER', true),
@@ -200,7 +199,7 @@
'ignore_status_codes' => [],
],
- Watchers\ScheduleWatcher::class => env('TELESCOPE_SCHEDULE_WATCHER', false),
+ Watchers\ScheduleWatcher::class => env('TELESCOPE_SCHEDULE_WATCHER', true),
Watchers\ViewWatcher::class => env('TELESCOPE_VIEW_WATCHER', true),
],
];
diff --git a/config/testing.php b/config/testing.php
new file mode 100644
index 0000000000..41b8eadf01
--- /dev/null
+++ b/config/testing.php
@@ -0,0 +1,6 @@
+ env('DUSK_TEST_EMAIL', 'test@example.com'),
+ 'dusk_test_password' => env('DUSK_TEST_PASSWORD', 'password'),
+];
diff --git a/config/version.php b/config/version.php
index 5639fc8a8d..0e83ff40e7 100644
--- a/config/version.php
+++ b/config/version.php
@@ -1,3 +1,3 @@
string('stripe_plan_id')->nullable()->after('stripe_cancel_at_period_end');
-
});
}
diff --git a/database/migrations/2023_08_22_071054_add_stripe_reasons.php b/database/migrations/2023_08_22_071054_add_stripe_reasons.php
index efd611aac9..6ffe37e982 100644
--- a/database/migrations/2023_08_22_071054_add_stripe_reasons.php
+++ b/database/migrations/2023_08_22_071054_add_stripe_reasons.php
@@ -14,7 +14,6 @@ public function up(): void
Schema::table('subscriptions', function (Blueprint $table) {
$table->string('stripe_feedback')->nullable()->after('stripe_cancel_at_period_end');
$table->string('stripe_comment')->nullable()->after('stripe_feedback');
-
});
}
diff --git a/database/migrations/2023_08_22_071059_add_stripe_trial_ended.php b/database/migrations/2023_08_22_071059_add_stripe_trial_ended.php
index c22317e6be..61fcbda6b3 100644
--- a/database/migrations/2023_08_22_071059_add_stripe_trial_ended.php
+++ b/database/migrations/2023_08_22_071059_add_stripe_trial_ended.php
@@ -13,7 +13,6 @@ public function up(): void
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->boolean('stripe_trial_already_ended')->default(false)->after('stripe_cancel_at_period_end');
-
});
}
diff --git a/database/migrations/2023_08_22_071060_change_invitation_link_length.php b/database/migrations/2023_08_22_071060_change_invitation_link_length.php
index 4efb033518..9d14c3f265 100644
--- a/database/migrations/2023_08_22_071060_change_invitation_link_length.php
+++ b/database/migrations/2023_08_22_071060_change_invitation_link_length.php
@@ -13,7 +13,6 @@ public function up(): void
{
Schema::table('team_invitations', function (Blueprint $table) {
$table->text('link')->change();
-
});
}
diff --git a/database/migrations/2023_09_20_082541_update_services_table.php b/database/migrations/2023_09_20_082541_update_services_table.php
index 8c6b350f7f..c70cd28f7c 100644
--- a/database/migrations/2023_09_20_082541_update_services_table.php
+++ b/database/migrations/2023_09_20_082541_update_services_table.php
@@ -16,7 +16,6 @@ public function up(): void
$table->longText('description')->nullable();
$table->longText('docker_compose_raw');
$table->longText('docker_compose')->nullable();
-
});
}
diff --git a/database/migrations/2023_09_20_083549_update_environment_variables_table.php b/database/migrations/2023_09_20_083549_update_environment_variables_table.php
index 40eb6aa445..a96d096bbf 100644
--- a/database/migrations/2023_09_20_083549_update_environment_variables_table.php
+++ b/database/migrations/2023_09_20_083549_update_environment_variables_table.php
@@ -13,7 +13,6 @@ public function up(): void
{
Schema::table('environment_variables', function (Blueprint $table) {
$table->foreignId('service_id')->nullable();
-
});
}
diff --git a/database/migrations/2023_09_23_111809_remove_destination_from_services_table.php b/database/migrations/2023_09_23_111809_remove_destination_from_services_table.php
index 920f44a720..729146a4ad 100644
--- a/database/migrations/2023_09_23_111809_remove_destination_from_services_table.php
+++ b/database/migrations/2023_09_23_111809_remove_destination_from_services_table.php
@@ -14,7 +14,6 @@ public function up(): void
Schema::table('services', function (Blueprint $table) {
$table->dropColumn('destination_type');
$table->dropColumn('destination_id');
-
});
}
diff --git a/database/migrations/2023_09_23_111819_add_server_emails.php b/database/migrations/2023_09_23_111819_add_server_emails.php
index 03c1e6bd22..775e820108 100644
--- a/database/migrations/2023_09_23_111819_add_server_emails.php
+++ b/database/migrations/2023_09_23_111819_add_server_emails.php
@@ -26,6 +26,5 @@ public function down(): void
$table->dropColumn('unreachable_email_sent');
$table->integer('unreachable_count')->default(0);
});
-
}
};
diff --git a/database/migrations/2023_11_16_220647_add_log_drains.php b/database/migrations/2023_11_16_220647_add_log_drains.php
index 05b1ed0541..f5161b3d7f 100644
--- a/database/migrations/2023_11_16_220647_add_log_drains.php
+++ b/database/migrations/2023_11_16_220647_add_log_drains.php
@@ -22,7 +22,6 @@ public function up(): void
$table->boolean('is_logdrain_axiom_enabled')->default(false);
$table->string('logdrain_axiom_dataset_name')->nullable();
$table->string('logdrain_axiom_api_key')->nullable();
-
});
}
diff --git a/database/migrations/2023_12_13_110214_add_soft_deletes.php b/database/migrations/2023_12_13_110214_add_soft_deletes.php
index ab7b562b41..72350b77fb 100644
--- a/database/migrations/2023_12_13_110214_add_soft_deletes.php
+++ b/database/migrations/2023_12_13_110214_add_soft_deletes.php
@@ -66,6 +66,5 @@ public function down(): void
Schema::table('service_databases', function (Blueprint $table) {
$table->dropSoftDeletes();
});
-
}
};
diff --git a/database/migrations/2023_12_17_155616_add_custom_docker_compose_start_command.php b/database/migrations/2023_12_17_155616_add_custom_docker_compose_start_command.php
index eeb2769fe6..f28b2670e3 100644
--- a/database/migrations/2023_12_17_155616_add_custom_docker_compose_start_command.php
+++ b/database/migrations/2023_12_17_155616_add_custom_docker_compose_start_command.php
@@ -14,7 +14,6 @@ public function up(): void
Schema::table('applications', function (Blueprint $table) {
$table->string('docker_compose_custom_start_command')->nullable();
$table->string('docker_compose_custom_build_command')->nullable();
-
});
}
diff --git a/database/migrations/2024_04_09_095517_make_custom_docker_commands_longer.php b/database/migrations/2024_04_09_095517_make_custom_docker_commands_longer.php
index 7df53ec06e..b3f2c1920c 100644
--- a/database/migrations/2024_04_09_095517_make_custom_docker_commands_longer.php
+++ b/database/migrations/2024_04_09_095517_make_custom_docker_commands_longer.php
@@ -13,7 +13,6 @@ public function up(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->text('custom_docker_run_options')->nullable()->change();
-
});
}
diff --git a/database/migrations/2024_04_25_073615_add_docker_network_to_application_settings.php b/database/migrations/2024_04_25_073615_add_docker_network_to_application_settings.php
index aeae6f77de..bea410eab1 100644
--- a/database/migrations/2024_04_25_073615_add_docker_network_to_application_settings.php
+++ b/database/migrations/2024_04_25_073615_add_docker_network_to_application_settings.php
@@ -13,7 +13,6 @@ public function up(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->boolean('connect_to_docker_network')->default(false);
-
});
}
diff --git a/database/migrations/2024_05_23_091713_add_gitea_webhook_to_applications.php b/database/migrations/2024_05_23_091713_add_gitea_webhook_to_applications.php
index 716f1f44c2..b46a072033 100644
--- a/database/migrations/2024_05_23_091713_add_gitea_webhook_to_applications.php
+++ b/database/migrations/2024_05_23_091713_add_gitea_webhook_to_applications.php
@@ -13,7 +13,6 @@ public function up(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->string('manual_webhook_secret_gitea')->nullable();
-
});
}
diff --git a/database/migrations/2024_06_18_105948_move_server_metrics.php b/database/migrations/2024_06_18_105948_move_server_metrics.php
index 26a1d1684e..a6bccd16a1 100644
--- a/database/migrations/2024_06_18_105948_move_server_metrics.php
+++ b/database/migrations/2024_06_18_105948_move_server_metrics.php
@@ -18,7 +18,7 @@ public function up(): void
$table->boolean('is_metrics_enabled')->default(false);
$table->integer('metrics_refresh_rate_seconds')->default(5);
$table->integer('metrics_history_days')->default(30);
- $table->string('metrics_token')->default(generateSentinelToken());
+ $table->string('metrics_token')->nullable();
});
}
diff --git a/database/migrations/2024_06_25_184323_update_db.php b/database/migrations/2024_06_25_184323_update_db.php
index f1b175a9c6..d9cddb15f9 100644
--- a/database/migrations/2024_06_25_184323_update_db.php
+++ b/database/migrations/2024_06_25_184323_update_db.php
@@ -4,6 +4,8 @@
use App\Models\Server;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Visus\Cuid2\Cuid2;
@@ -14,44 +16,45 @@
*/
public function up(): void
{
- Schema::table('applications', function (Blueprint $table) {
- $table->dropColumn('docker_compose_pr_location');
- $table->dropColumn('docker_compose_pr');
- $table->dropColumn('docker_compose_pr_raw');
- });
- Schema::table('subscriptions', function (Blueprint $table) {
- $table->dropColumn('lemon_subscription_id');
- $table->dropColumn('lemon_order_id');
- $table->dropColumn('lemon_product_id');
- $table->dropColumn('lemon_variant_id');
- $table->dropColumn('lemon_variant_name');
- $table->dropColumn('lemon_customer_id');
- $table->dropColumn('lemon_status');
- $table->dropColumn('lemon_renews_at');
- $table->dropColumn('lemon_update_payment_menthod_url');
- $table->dropColumn('lemon_trial_ends_at');
- $table->dropColumn('lemon_ends_at');
- });
- Schema::table('environment_variables', function (Blueprint $table) {
- $table->string('uuid')->nullable()->after('id');
- });
+ try {
+ Schema::table('applications', function (Blueprint $table) {
+ $table->dropColumn('docker_compose_pr_location');
+ $table->dropColumn('docker_compose_pr');
+ $table->dropColumn('docker_compose_pr_raw');
+ });
+ Schema::table('subscriptions', function (Blueprint $table) {
+ $table->dropColumn('lemon_subscription_id');
+ $table->dropColumn('lemon_order_id');
+ $table->dropColumn('lemon_product_id');
+ $table->dropColumn('lemon_variant_id');
+ $table->dropColumn('lemon_variant_name');
+ $table->dropColumn('lemon_customer_id');
+ $table->dropColumn('lemon_status');
+ $table->dropColumn('lemon_renews_at');
+ $table->dropColumn('lemon_update_payment_menthod_url');
+ $table->dropColumn('lemon_trial_ends_at');
+ $table->dropColumn('lemon_ends_at');
+ });
+ Schema::table('environment_variables', function (Blueprint $table) {
+ $table->string('uuid')->nullable()->after('id');
+ });
- EnvironmentVariable::all()->each(function (EnvironmentVariable $environmentVariable) {
- $environmentVariable->update([
- 'uuid' => (string) new Cuid2,
- ]);
- });
- Schema::table('environment_variables', function (Blueprint $table) {
- $table->string('uuid')->nullable(false)->change();
- });
- Schema::table('server_settings', function (Blueprint $table) {
- $table->integer('metrics_history_days')->default(7)->change();
- });
- Server::all()->each(function (Server $server) {
- $server->settings->update([
- 'metrics_history_days' => 7,
- ]);
- });
+ EnvironmentVariable::all()->each(function (EnvironmentVariable $environmentVariable) {
+ $environmentVariable->update([
+ 'uuid' => (string) new Cuid2,
+ ]);
+ });
+ Schema::table('environment_variables', function (Blueprint $table) {
+ $table->string('uuid')->nullable(false)->change();
+ });
+ Schema::table('server_settings', function (Blueprint $table) {
+ $table->integer('metrics_history_days')->default(7)->change();
+ });
+
+ DB::table('server_settings')->update(['metrics_history_days' => 7]);
+ } catch (\Exception $e) {
+ Log::error('Error updating db: '.$e->getMessage());
+ }
}
/**
diff --git a/database/migrations/2024_07_18_123458_add_force_cleanup_server.php b/database/migrations/2024_07_18_123458_add_force_cleanup_server.php
index a33665bd0e..ea3695b3f5 100644
--- a/database/migrations/2024_07_18_123458_add_force_cleanup_server.php
+++ b/database/migrations/2024_07_18_123458_add_force_cleanup_server.php
@@ -12,7 +12,7 @@
public function up(): void
{
Schema::table('server_settings', function (Blueprint $table) {
- $table->boolean('is_force_cleanup_enabled')->default(false)->after('is_sentinel_enabled');
+ $table->boolean('is_force_cleanup_enabled')->default(false);
});
}
diff --git a/database/migrations/2024_08_09_215659_add_server_cleanup_fields_to_server_settings_table.php b/database/migrations/2024_08_09_215659_add_server_cleanup_fields_to_server_settings_table.php
index b5300c9057..e3bdc68c60 100644
--- a/database/migrations/2024_08_09_215659_add_server_cleanup_fields_to_server_settings_table.php
+++ b/database/migrations/2024_08_09_215659_add_server_cleanup_fields_to_server_settings_table.php
@@ -30,7 +30,6 @@ public function up()
$serverSetting->docker_cleanup_threshold = $serverSetting->cleanup_after_percentage;
$serverSetting->save();
}
-
}
/**
diff --git a/database/migrations/2024_09_16_111428_encrypt_existing_private_keys.php b/database/migrations/2024_09_16_111428_encrypt_existing_private_keys.php
index 19274ad9bb..e16181ac7e 100644
--- a/database/migrations/2024_09_16_111428_encrypt_existing_private_keys.php
+++ b/database/migrations/2024_09_16_111428_encrypt_existing_private_keys.php
@@ -20,6 +20,5 @@ public function up()
echo 'Encrypting private keys failed.';
echo $e->getMessage();
}
-
}
}
diff --git a/database/migrations/2024_10_11_114331_add_required_env_variables.php b/database/migrations/2024_10_11_114331_add_required_env_variables.php
new file mode 100644
index 0000000000..4fde0c2bbf
--- /dev/null
+++ b/database/migrations/2024_10_11_114331_add_required_env_variables.php
@@ -0,0 +1,28 @@
+boolean('is_required')->default(false);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('environment_variables', function (Blueprint $table) {
+ $table->dropColumn('is_required');
+ });
+ }
+};
diff --git a/database/migrations/2024_10_14_090416_update_metrics_token_in_server_settings.php b/database/migrations/2024_10_14_090416_update_metrics_token_in_server_settings.php
new file mode 100644
index 0000000000..d5c38501f1
--- /dev/null
+++ b/database/migrations/2024_10_14_090416_update_metrics_token_in_server_settings.php
@@ -0,0 +1,54 @@
+dropColumn('metrics_token');
+ $table->dropColumn('metrics_refresh_rate_seconds');
+ $table->dropColumn('metrics_history_days');
+ $table->dropColumn('is_server_api_enabled');
+
+ $table->boolean('is_sentinel_enabled')->default(false);
+ $table->text('sentinel_token')->nullable();
+ $table->integer('sentinel_metrics_refresh_rate_seconds')->default(10);
+ $table->integer('sentinel_metrics_history_days')->default(7);
+ $table->integer('sentinel_push_interval_seconds')->default(60);
+ $table->string('sentinel_custom_url')->nullable();
+ });
+ Schema::table('servers', function (Blueprint $table) {
+ $table->dateTime('sentinel_updated_at')->default(now());
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('server_settings', function (Blueprint $table) {
+ $table->string('metrics_token')->nullable();
+ $table->integer('metrics_refresh_rate_seconds')->default(5);
+ $table->integer('metrics_history_days')->default(30);
+ $table->boolean('is_server_api_enabled')->default(false);
+
+ $table->dropColumn('is_sentinel_enabled');
+ $table->dropColumn('sentinel_token');
+ $table->dropColumn('sentinel_metrics_refresh_rate_seconds');
+ $table->dropColumn('sentinel_metrics_history_days');
+ $table->dropColumn('sentinel_push_interval_seconds');
+ $table->dropColumn('sentinel_custom_url');
+ });
+ Schema::table('servers', function (Blueprint $table) {
+ $table->dropColumn('sentinel_updated_at');
+ });
+ }
+};
diff --git a/database/migrations/2024_10_15_172139_add_is_shared_to_environment_variables.php b/database/migrations/2024_10_15_172139_add_is_shared_to_environment_variables.php
new file mode 100644
index 0000000000..eb878e2f6b
--- /dev/null
+++ b/database/migrations/2024_10_15_172139_add_is_shared_to_environment_variables.php
@@ -0,0 +1,22 @@
+boolean('is_shared')->default(false);
+ });
+ }
+
+ public function down()
+ {
+ Schema::table('environment_variables', function (Blueprint $table) {
+ $table->dropColumn('is_shared');
+ });
+ }
+}
diff --git a/database/migrations/2024_10_16_120026_move_redis_password_to_envs.php b/database/migrations/2024_10_16_120026_move_redis_password_to_envs.php
new file mode 100644
index 0000000000..fa01e8e85b
--- /dev/null
+++ b/database/migrations/2024_10_16_120026_move_redis_password_to_envs.php
@@ -0,0 +1,41 @@
+where('id', $redis->id)->value('redis_password');
+ EnvironmentVariable::create([
+ 'standalone_redis_id' => $redis->id,
+ 'key' => 'REDIS_PASSWORD',
+ 'value' => $redis_password,
+ ]);
+ EnvironmentVariable::create([
+ 'standalone_redis_id' => $redis->id,
+ 'key' => 'REDIS_USERNAME',
+ 'value' => 'default',
+ ]);
+ }
+ });
+ Schema::table('standalone_redis', function (Blueprint $table) {
+ $table->dropColumn('redis_password');
+ });
+ } catch (\Exception $e) {
+ echo 'Moving Redis passwords to envs failed.';
+ echo $e->getMessage();
+ }
+ }
+}
diff --git a/database/migrations/2024_10_16_192133_add_confirmation_settings_to_instance_settings_table.php b/database/migrations/2024_10_16_192133_add_confirmation_settings_to_instance_settings_table.php
new file mode 100644
index 0000000000..7040daf44a
--- /dev/null
+++ b/database/migrations/2024_10_16_192133_add_confirmation_settings_to_instance_settings_table.php
@@ -0,0 +1,22 @@
+boolean('disable_two_step_confirmation')->default(false);
+ });
+ }
+
+ public function down()
+ {
+ Schema::table('instance_settings', function (Blueprint $table) {
+ $table->dropColumn('disable_two_step_confirmation');
+ });
+ }
+};
diff --git a/database/migrations/2024_10_17_093722_add_soft_delete_to_servers.php b/database/migrations/2024_10_17_093722_add_soft_delete_to_servers.php
new file mode 100644
index 0000000000..7a7f28e249
--- /dev/null
+++ b/database/migrations/2024_10_17_093722_add_soft_delete_to_servers.php
@@ -0,0 +1,28 @@
+softDeletes();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('servers', function (Blueprint $table) {
+ $table->dropSoftDeletes();
+ });
+ }
+};
diff --git a/database/migrations/2024_10_22_105745_add_server_disk_usage_threshold.php b/database/migrations/2024_10_22_105745_add_server_disk_usage_threshold.php
new file mode 100644
index 0000000000..76ccd13529
--- /dev/null
+++ b/database/migrations/2024_10_22_105745_add_server_disk_usage_threshold.php
@@ -0,0 +1,28 @@
+integer('server_disk_usage_notification_threshold')->default(80)->after('docker_cleanup_threshold');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('server_settings', function (Blueprint $table) {
+ $table->dropColumn('server_disk_usage_notification_threshold');
+ });
+ }
+};
diff --git a/database/migrations/2024_10_22_121223_add_server_disk_usage_notification.php b/database/migrations/2024_10_22_121223_add_server_disk_usage_notification.php
new file mode 100644
index 0000000000..a2aa381b7e
--- /dev/null
+++ b/database/migrations/2024_10_22_121223_add_server_disk_usage_notification.php
@@ -0,0 +1,32 @@
+boolean('discord_notifications_server_disk_usage')->default(true)->after('discord_enabled');
+ $table->boolean('smtp_notifications_server_disk_usage')->default(true)->after('smtp_enabled');
+ $table->boolean('telegram_notifications_server_disk_usage')->default(true)->after('telegram_enabled');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('teams', function (Blueprint $table) {
+ $table->dropColumn('discord_notifications_server_disk_usage');
+ $table->dropColumn('smtp_notifications_server_disk_usage');
+ $table->dropColumn('telegram_notifications_server_disk_usage');
+ });
+ }
+};
diff --git a/database/migrations/2024_10_29_093927_add_is_sentinel_debug_enabled_to_server_settings.php b/database/migrations/2024_10_29_093927_add_is_sentinel_debug_enabled_to_server_settings.php
new file mode 100644
index 0000000000..d8ab1313bf
--- /dev/null
+++ b/database/migrations/2024_10_29_093927_add_is_sentinel_debug_enabled_to_server_settings.php
@@ -0,0 +1,28 @@
+boolean('is_sentinel_debug_enabled')->default(false);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('server_settings', function (Blueprint $table) {
+ $table->dropColumn('is_sentinel_debug_enabled');
+ });
+ }
+};
diff --git a/database/migrations/2024_11_02_213214_add_last_online_at_to_resources.php b/database/migrations/2024_11_02_213214_add_last_online_at_to_resources.php
new file mode 100644
index 0000000000..51b8fb3ba8
--- /dev/null
+++ b/database/migrations/2024_11_02_213214_add_last_online_at_to_resources.php
@@ -0,0 +1,96 @@
+timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+ Schema::table('application_previews', function (Blueprint $table) {
+ $table->timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+ Schema::table('service_applications', function (Blueprint $table) {
+ $table->timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+ Schema::table('service_databases', function (Blueprint $table) {
+ $table->timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+ Schema::table('standalone_postgresqls', function (Blueprint $table) {
+ $table->timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+ Schema::table('standalone_redis', function (Blueprint $table) {
+ $table->timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+ Schema::table('standalone_mongodbs', function (Blueprint $table) {
+ $table->timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+ Schema::table('standalone_mysqls', function (Blueprint $table) {
+ $table->timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+ Schema::table('standalone_mariadbs', function (Blueprint $table) {
+ $table->timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+ Schema::table('standalone_keydbs', function (Blueprint $table) {
+ $table->timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+ Schema::table('standalone_dragonflies', function (Blueprint $table) {
+ $table->timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+ Schema::table('standalone_clickhouses', function (Blueprint $table) {
+ $table->timestamp('last_online_at')->default(now())->after('updated_at');
+ });
+
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('applications', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+ Schema::table('application_previews', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+ Schema::table('service_applications', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+ Schema::table('service_databases', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+ Schema::table('standalone_postgresqls', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+ Schema::table('standalone_redis', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+ Schema::table('standalone_mongodbs', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+ Schema::table('standalone_mysqls', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+ Schema::table('standalone_mariadbs', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+ Schema::table('standalone_keydbs', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+ Schema::table('standalone_dragonflies', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+ Schema::table('standalone_clickhouses', function (Blueprint $table) {
+ $table->dropColumn('last_online_at');
+ });
+
+ }
+};
diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php
index be50831087..6e66c64f4a 100644
--- a/database/seeders/DatabaseSeeder.php
+++ b/database/seeders/DatabaseSeeder.php
@@ -26,6 +26,8 @@ public function run(): void
S3StorageSeeder::class,
StandalonePostgresqlSeeder::class,
OauthSettingSeeder::class,
+ DisableTwoStepConfirmationSeeder::class,
+ SentinelSeeder::class,
]);
}
}
diff --git a/database/seeders/DisableTwoStepConfirmationSeeder.php b/database/seeders/DisableTwoStepConfirmationSeeder.php
new file mode 100644
index 0000000000..c43bf1b015
--- /dev/null
+++ b/database/seeders/DisableTwoStepConfirmationSeeder.php
@@ -0,0 +1,20 @@
+updateOrInsert(
+ [],
+ ['disable_two_step_confirmation' => true]
+ );
+ }
+}
diff --git a/database/seeders/PopulateSshKeysDirectorySeeder.php b/database/seeders/PopulateSshKeysDirectorySeeder.php
index e2543ee02e..d528179c0e 100644
--- a/database/seeders/PopulateSshKeysDirectorySeeder.php
+++ b/database/seeders/PopulateSshKeysDirectorySeeder.php
@@ -33,7 +33,6 @@ public function run()
}
} catch (\Throwable $e) {
echo "Error: {$e->getMessage()}\n";
- ray($e->getMessage());
}
}
}
diff --git a/database/seeders/ProductionSeeder.php b/database/seeders/ProductionSeeder.php
index 206f04d6b6..3e820a1629 100644
--- a/database/seeders/ProductionSeeder.php
+++ b/database/seeders/ProductionSeeder.php
@@ -126,7 +126,6 @@ public function run(): void
echo "Your localhost connection won't work until then.";
}
}
-
}
if (config('coolify.is_windows_docker_desktop')) {
PrivateKey::updateOrCreate(
@@ -186,6 +185,6 @@ public function run(): void
$this->call(OauthSettingSeeder::class);
$this->call(PopulateSshKeysDirectorySeeder::class);
-
+ $this->call(SentinelSeeder::class);
}
}
diff --git a/database/seeders/SentinelSeeder.php b/database/seeders/SentinelSeeder.php
new file mode 100644
index 0000000000..3cf9139333
--- /dev/null
+++ b/database/seeders/SentinelSeeder.php
@@ -0,0 +1,32 @@
+settings->sentinel_token)->isEmpty()) {
+ $server->settings->generateSentinelToken(ignoreEvent: true);
+ }
+ if (str($server->settings->sentinel_custom_url)->isEmpty()) {
+ $url = $server->settings->generateSentinelUrl(ignoreEvent: true);
+ if (str($url)->isEmpty()) {
+ $server->settings->is_sentinel_enabled = false;
+ $server->settings->save();
+ }
+ }
+ } catch (\Throwable $e) {
+ Log::error('Error seeding sentinel: '.$e->getMessage());
+ }
+ }
+ });
+ }
+}
diff --git a/docker/coolify-helper/Dockerfile b/docker/coolify-helper/Dockerfile
index 7aa9d8722c..48f401da4a 100644
--- a/docker/coolify-helper/Dockerfile
+++ b/docker/coolify-helper/Dockerfile
@@ -10,7 +10,7 @@ ARG DOCKER_BUILDX_VERSION=0.14.1
# https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=0.35.1
# https://github.com/railwayapp/nixpacks/releases
-ARG NIXPACKS_VERSION=1.28.0
+ARG NIXPACKS_VERSION=1.29.0
USER root
WORKDIR /artifacts
diff --git a/docker/coolify-realtime/package-lock.json b/docker/coolify-realtime/package-lock.json
new file mode 100644
index 0000000000..f5fd1ba18d
--- /dev/null
+++ b/docker/coolify-realtime/package-lock.json
@@ -0,0 +1,190 @@
+{
+ "name": "coolify-realtime",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "dependencies": {
+ "@xterm/addon-fit": "0.10.0",
+ "@xterm/xterm": "5.5.0",
+ "axios": "1.7.5",
+ "cookie": "1.0.1",
+ "dotenv": "16.4.5",
+ "node-pty": "1.0.0",
+ "ws": "8.18.0"
+ }
+ },
+ "node_modules/@xterm/addon-fit": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
+ "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@xterm/xterm": "^5.0.0"
+ }
+ },
+ "node_modules/@xterm/xterm": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
+ "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
+ "license": "MIT"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz",
+ "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.1.tgz",
+ "integrity": "sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.4.5",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
+ "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
+ "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/nan": {
+ "version": "2.22.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz",
+ "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==",
+ "license": "MIT"
+ },
+ "node_modules/node-pty": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz",
+ "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "nan": "^2.17.0"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/ws": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
+ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/docker/coolify-realtime/package.json b/docker/coolify-realtime/package.json
index 90d4f77db8..faeb80f54b 100644
--- a/docker/coolify-realtime/package.json
+++ b/docker/coolify-realtime/package.json
@@ -2,12 +2,12 @@
"private": true,
"type": "module",
"dependencies": {
- "@xterm/addon-fit": "^0.10.0",
- "@xterm/xterm": "^5.5.0",
- "cookie": "^0.6.0",
+ "@xterm/addon-fit": "0.10.0",
+ "@xterm/xterm": "5.5.0",
+ "cookie": "1.0.1",
"axios": "1.7.5",
- "dotenv": "^16.4.5",
- "node-pty": "^1.0.0",
- "ws": "^8.17.0"
+ "dotenv": "16.4.5",
+ "node-pty": "1.0.0",
+ "ws": "8.18.0"
}
}
diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile
index 63832dc36d..d2381f7643 100644
--- a/docker/dev/Dockerfile
+++ b/docker/dev/Dockerfile
@@ -5,34 +5,38 @@ ARG TARGETPLATFORM
ARG CLOUDFLARED_VERSION=2024.4.1
ARG POSTGRES_VERSION=15
-RUN apt-get update
-# Postgres version requirements
-RUN apt install dirmngr ca-certificates software-properties-common gnupg gnupg2 apt-transport-https curl -y
-RUN curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null
-RUN echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ jammy-pgdg main | tee -a /etc/apt/sources.list.d/postgresql.list
+# Use build arguments for caching
+ARG BUILDTIME_DEPS="dirmngr ca-certificates software-properties-common gnupg gnupg2 apt-transport-https curl"
+ARG RUNTIME_DEPS="postgresql-client-$POSTGRES_VERSION php8.2-pgsql openssh-client git git-lfs jq lsof"
-RUN apt-get update
-RUN apt-get install postgresql-client-$POSTGRES_VERSION -y
+# Install dependencies
+RUN --mount=type=cache,target=/var/cache/apt \
+ apt-get update && \
+ apt-get install -y $BUILDTIME_DEPS && \
+ curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null && \
+ echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ jammy-pgdg main | tee -a /etc/apt/sources.list.d/postgresql.list && \
+ apt-get update && \
+ apt-get install -y $RUNTIME_DEPS && \
+ apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
-# Coolify requirements
-RUN apt-get install -y php8.2-pgsql openssh-client git git-lfs jq lsof
-RUN apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
COPY --chmod=755 docker/dev/etc/s6-overlay/ /etc/s6-overlay/
COPY docker/dev/nginx.conf /etc/nginx/conf.d/custom.conf
-RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc
-RUN echo "alias a='php artisan'" >>/etc/bash.bashrc
+RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc && \
+ echo "alias a='php artisan'" >>/etc/bash.bashrc
RUN mkdir -p /usr/local/bin
-RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \
+RUN --mount=type=cache,target=/root/.cache \
+ /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \
echo 'amd64' && \
curl -sSL https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi"
-RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
+RUN --mount=type=cache,target=/root/.cache \
+ /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
echo 'arm64' && \
curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi"
diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile
index d0cebcbca6..37e0481bb9 100644
--- a/docker/prod/Dockerfile
+++ b/docker/prod/Dockerfile
@@ -1,10 +1,10 @@
-FROM serversideup/php:8.2-fpm-nginx-v2.2.1 as base
+FROM serversideup/php:8.2-fpm-nginx-v2.2.1 AS base
WORKDIR /var/www/html
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-interaction --no-plugins --no-scripts --prefer-dist
-FROM node:20 as static-assets
+FROM node:20 AS static-assets
WORKDIR /app
COPY . .
COPY --from=base --chown=9999:9999 /var/www/html .
@@ -45,6 +45,8 @@ RUN composer dump-autoload
COPY --from=static-assets --chown=9999:9999 /app/public/build ./public/build
COPY --chmod=755 docker/prod/etc/s6-overlay/ /etc/s6-overlay/
+RUN php artisan route:clear
+RUN php artisan view:clear
RUN php artisan route:cache
RUN php artisan view:cache
diff --git a/lang/ar.json b/lang/ar.json
index c5ec96c8d7..4b9afbe996 100644
--- a/lang/ar.json
+++ b/lang/ar.json
@@ -26,5 +26,12 @@
"input.code": "الرمز لمرة واحدة",
"input.recovery_code": "رمز الاسترداد",
"button.save": "حفظ",
- "repository.url": "أمثلة للمستودعات العامة، استخدم https://.... للمستودعات الخاصة، استخدم git@....
سيتم تحديد الفرع main لـ https://github.com/coollabsio/coolify-examples سيتم تحديد الفرع nodejs-fastify لـ https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify سيتم تحديد الفرع main لـ https://gitea.com/sedlav/expressjs.git سيتم تحديد الفرع main لـ https://gitlab.com/andrasbacsai/nodejs-example.git."
+ "repository.url": "أمثلة للمستودعات العامة، استخدم https://.... للمستودعات الخاصة، استخدم git@....
سيتم تحديد الفرع main لـ https://github.com/coollabsio/coolify-examples سيتم تحديد الفرع nodejs-fastify لـ https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify سيتم تحديد الفرع main لـ https://gitea.com/sedlav/expressjs.git سيتم تحديد الفرع main لـ https://gitlab.com/andrasbacsai/nodejs-example.git.",
+ "service.stop": "سيتم إيقاف هذه الخدمة.",
+ "resource.docker_cleanup": "قم بتشغيل Docker Cleanup (قم بإزالة الصور غير المستخدمة وذاكرة التخزين المؤقت للمنشئ).",
+ "resource.non_persistent": "سيتم حذف جميع البيانات غير الدائمة.",
+ "resource.delete_volumes": "حذف جميع المجلدات والملفات المرتبطة بهذا المورد بشكل دائم.",
+ "resource.delete_connected_networks": "حذف جميع الشبكات غير المحددة مسبقًا والمرتبطة بهذا المورد بشكل دائم.",
+ "resource.delete_configurations": "حذف جميع ملفات التعريف من الخادم بشكل دائم.",
+ "database.delete_backups_locally": "حذف كافة النسخ الاحتياطية نهائيًا من التخزين المحلي."
}
diff --git a/lang/en.json b/lang/en.json
index fa69c7035a..5ea474b028 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -33,5 +33,6 @@
"resource.delete_volumes": "Permanently delete all volumes associated with this resource.",
"resource.delete_connected_networks": "Permanently delete all non-predefined networks associated with this resource.",
"resource.delete_configurations": "Permanently delete all configuration files from the server.",
- "database.delete_backups_locally": "All backups will be permanently deleted from local storage."
+ "database.delete_backups_locally": "All backups will be permanently deleted from local storage.",
+ "warning.sslipdomain": "Your configuration is saved, but sslip domain with https is NOT recommended, because Let's Encrypt servers with this public domain are rate limited (SSL certificate validation will fail).
Use your own domain instead."
}
diff --git a/lang/ro.json b/lang/ro.json
new file mode 100644
index 0000000000..db1aa85db5
--- /dev/null
+++ b/lang/ro.json
@@ -0,0 +1,37 @@
+{
+ "auth.login": "Autentificare",
+ "auth.login.azure": "Autentificare prin Microsoft",
+ "auth.login.bitbucket": "Autentificare prin Bitbucket",
+ "auth.login.github": "Autentificare prin GitHub",
+ "auth.login.gitlab": "Autentificare prin Gitlab",
+ "auth.login.google": "Autentificare prin Google",
+ "auth.already_registered": "Sunteți deja înregistrat?",
+ "auth.confirm_password": "Confirmați parola",
+ "auth.forgot_password": "Ați uitat parola",
+ "auth.forgot_password_send_email": "Trimiteți e-mail-ul pentru resetarea parolei",
+ "auth.register_now": "Înregistrare",
+ "auth.logout": "Deconectare",
+ "auth.register": "Înregistrare",
+ "auth.registration_disabled": "Înregistrarea este dezactivată. Vă rugăm să contactați administratorul site-ului.",
+ "auth.reset_password": "Resetare parolă",
+ "auth.failed": "Autentificare nereușită. Vă rugăm să verificați datele introduse.",
+ "auth.failed.callback": "A apărut o eroare în timpul autentificării cu furnizorul extern.",
+ "auth.failed.password": "Parola furnizată este incorectă.",
+ "auth.failed.email": "Nu putem găsi un utilizator cu această adresă de e-mail.",
+ "auth.throttle": "Prea multe încercări de autentificare. Vă rugăm să încercați din nou în :seconds secunde.",
+ "input.name": "Nume",
+ "input.email": "E-mail",
+ "input.password": "Parolă",
+ "input.password.again": "Repetați parola",
+ "input.code": "Cod de unică folosință",
+ "input.recovery_code": "Cod de recuperare",
+ "button.save": "Salvare",
+ "repository.url": "Exemple Pentru depozite publice, utilizați https://.... Pentru depozite private, utilizați git@....
https://github.com/coollabsio/coolify-examples va fi selectată ramura main https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify va fi selectată ramura nodejs-fastify. https://gitea.com/sedlav/expressjs.git va fi selectată ramura main. https://gitlab.com/andrasbacsai/nodejs-example.git va fi selectată ramura main.",
+ "service.stop": "Acest serviciu va fi oprit.",
+ "resource.docker_cleanup": "Executați curățarea Docker (eliminați imaginile neutilizate și memoria cache a constructorului).",
+ "resource.non_persistent": "Toate datele nepersistente vor fi șterse.",
+ "resource.delete_volumes": "Ștergeți definitiv toate volumele asociate cu această resursă.",
+ "resource.delete_connected_networks": "Ștergeți definitiv toate rețelele non-predefinite asociate cu această resursă.",
+ "resource.delete_configurations": "Ștergeți definitiv toate fișierele de configurare de pe server.",
+ "database.delete_backups_locally": "Toate copiile de rezervă vor fi șterse definitiv din stocarea locală."
+}
diff --git a/openapi.yaml b/openapi.yaml
index 91d5c14439..d2616e9c6a 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -98,6 +98,10 @@ paths:
is_static:
type: boolean
description: 'The flag to indicate if the application is static.'
+ static_image:
+ type: string
+ enum: ['nginx:alpine']
+ description: 'The static image.'
install_command:
type: string
description: 'The install command.'
@@ -323,6 +327,10 @@ paths:
is_static:
type: boolean
description: 'The flag to indicate if the application is static.'
+ static_image:
+ type: string
+ enum: ['nginx:alpine']
+ description: 'The static image.'
install_command:
type: string
description: 'The install command.'
@@ -548,6 +556,10 @@ paths:
is_static:
type: boolean
description: 'The flag to indicate if the application is static.'
+ static_image:
+ type: string
+ enum: ['nginx:alpine']
+ description: 'The static image.'
install_command:
type: string
description: 'The install command.'
@@ -3093,7 +3105,7 @@ paths:
security:
-
bearerAuth: []
- /healthcheck:
+ /health:
get:
summary: Healthcheck
description: 'Healthcheck endpoint.'
@@ -4959,7 +4971,7 @@ components:
type: boolean
is_reachable:
type: boolean
- is_server_api_enabled:
+ is_sentinel_enabled:
type: boolean
is_swarm_manager:
type: boolean
@@ -4981,11 +4993,11 @@ components:
type: string
logdrain_newrelic_license_key:
type: string
- metrics_history_days:
+ sentinel_metrics_history_days:
type: integer
- metrics_refresh_rate_seconds:
+ sentinel_metrics_refresh_rate_seconds:
type: integer
- metrics_token:
+ sentinel_token:
type: string
docker_cleanup_frequency:
type: string
diff --git a/other/nightly/install.sh b/other/nightly/install.sh
index 04faf50eab..29ce52b77c 100755
--- a/other/nightly/install.sh
+++ b/other/nightly/install.sh
@@ -5,7 +5,7 @@ set -e # Exit immediately if a command exits with a non-zero status
## $1 could be empty, so we need to disable this check
#set -u # Treat unset variables as an error and exit
set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status
-CDN="https://cdn.coollabs.io/coolify-nightly"
+CDN="https://cdn.coollabs.io/coolify"
DATE=$(date +"%Y%m%d-%H%M%S")
VERSION="1.6"
@@ -13,7 +13,7 @@ DOCKER_VERSION="26.0"
# TODO: Ask for a user
CURRENT_USER=$USER
-mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,metrics,logs}
+mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,sentinel}
mkdir -p /data/coolify/ssh/{keys,mux}
mkdir -p /data/coolify/proxy/dynamic
@@ -164,7 +164,6 @@ sles | opensuse-leap | opensuse-tumbleweed)
esac
-
echo -e "2. Check OpenSSH server configuration. "
# Detect OpenSSH server
@@ -262,9 +261,14 @@ if ! [ -x "$(command -v docker)" ]; then
fi
;;
*)
- curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh >/dev/null 2>&1
+ if [ "$OS_TYPE" = "ubuntu" ] && [ "$OS_VERSION" = "24.10" ]; then
+ echo "Docker automated installation is not supported on Ubuntu 24.10 (non-LTS release)."
+ echo "Please install Docker manually."
+ exit 1
+ fi
+ curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh 2>&1
if ! [ -x "$(command -v docker)" ]; then
- curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} >/dev/null 2>&1
+ curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} 2>&1
if ! [ -x "$(command -v docker)" ]; then
echo " - Docker installation failed."
echo " Maybe your OS is not supported?"
@@ -287,7 +291,10 @@ test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json /etc/docker/daemon
"log-opts": {
"max-size": "10m",
"max-file": "3"
- }
+ },
+ "default-address-pools": [
+ {"base":"10.0.0.0/8","size":24}
+ ]
}
EOL
cat >/etc/docker/daemon.json.coolify </etc/docker/daemon.json.coolify </dev/null 2>&1
+bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}"
echo " - Coolify installed successfully."
rm -f $ENV_FILE-$DATE
diff --git a/other/nightly/versions.json b/other/nightly/versions.json
index c04a3dee6c..dbb2955909 100644
--- a/other/nightly/versions.json
+++ b/other/nightly/versions.json
@@ -1,16 +1,19 @@
{
"coolify": {
"v4": {
- "version": "4.0.0-beta.354"
+ "version": "4.0.0-beta.360"
},
"nightly": {
- "version": "4.0.0-beta.355"
+ "version": "4.0.0-beta.362"
},
"helper": {
- "version": "1.0.2"
+ "version": "1.0.3"
},
"realtime": {
- "version": "1.0.3"
+ "version": "1.0.4"
+ },
+ "sentinel": {
+ "version": "0.0.15"
}
}
}
diff --git a/public/svgs/affine.svg b/public/svgs/affine.svg
new file mode 100644
index 0000000000..d8063e9206
--- /dev/null
+++ b/public/svgs/affine.svg
@@ -0,0 +1,88 @@
+
diff --git a/public/svgs/calcom.svg b/public/svgs/calcom.svg
new file mode 100644
index 0000000000..446b16655d
--- /dev/null
+++ b/public/svgs/calcom.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/public/svgs/cloudbeaver.svg b/public/svgs/cloudbeaver.svg
new file mode 100644
index 0000000000..4a76347669
--- /dev/null
+++ b/public/svgs/cloudbeaver.svg
@@ -0,0 +1,7 @@
+
diff --git a/public/svgs/coder.svg b/public/svgs/coder.svg
new file mode 100644
index 0000000000..45b7f795c6
--- /dev/null
+++ b/public/svgs/coder.svg
@@ -0,0 +1,8 @@
+
diff --git a/public/svgs/cryptgeon.png b/public/svgs/cryptgeon.png
new file mode 100644
index 0000000000..be121cfd08
Binary files /dev/null and b/public/svgs/cryptgeon.png differ
diff --git a/public/svgs/dify.png b/public/svgs/dify.png
new file mode 100644
index 0000000000..326acf789c
Binary files /dev/null and b/public/svgs/dify.png differ
diff --git a/public/svgs/edgedb.svg b/public/svgs/edgedb.svg
new file mode 100644
index 0000000000..a906f7f7e1
--- /dev/null
+++ b/public/svgs/edgedb.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/svgs/flowise.png b/public/svgs/flowise.png
new file mode 100644
index 0000000000..6b0be0d2a7
Binary files /dev/null and b/public/svgs/flowise.png differ
diff --git a/public/svgs/foundryvtt.png b/public/svgs/foundryvtt.png
new file mode 100644
index 0000000000..c6a04508f3
Binary files /dev/null and b/public/svgs/foundryvtt.png differ
diff --git a/public/svgs/freshrss.png b/public/svgs/freshrss.png
new file mode 100644
index 0000000000..d1a75118f9
Binary files /dev/null and b/public/svgs/freshrss.png differ
diff --git a/public/svgs/heyform.svg b/public/svgs/heyform.svg
new file mode 100644
index 0000000000..ff29ca6548
--- /dev/null
+++ b/public/svgs/heyform.svg
@@ -0,0 +1,5 @@
+
diff --git a/public/svgs/immich.svg b/public/svgs/immich.svg
new file mode 100644
index 0000000000..9d844a772b
--- /dev/null
+++ b/public/svgs/immich.svg
@@ -0,0 +1,66 @@
+
+
+
diff --git a/public/svgs/jenkins.svg b/public/svgs/jenkins.svg
new file mode 100644
index 0000000000..0529fff1e3
--- /dev/null
+++ b/public/svgs/jenkins.svg
@@ -0,0 +1,283 @@
+
+
+
+
\ No newline at end of file
diff --git a/public/svgs/jitsi.svg b/public/svgs/jitsi.svg
new file mode 100644
index 0000000000..6257659ee5
--- /dev/null
+++ b/public/svgs/jitsi.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/public/svgs/kimai.svg b/public/svgs/kimai.svg
new file mode 100644
index 0000000000..35b1469726
--- /dev/null
+++ b/public/svgs/kimai.svg
@@ -0,0 +1,67 @@
+
\ No newline at end of file
diff --git a/public/svgs/libretranslate.svg b/public/svgs/libretranslate.svg
new file mode 100644
index 0000000000..103d47d609
--- /dev/null
+++ b/public/svgs/libretranslate.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svgs/litequeen.svg b/public/svgs/litequeen.svg
new file mode 100644
index 0000000000..aa0b8e0387
--- /dev/null
+++ b/public/svgs/litequeen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svgs/martin.png b/public/svgs/martin.png
new file mode 100644
index 0000000000..d1a99e1480
Binary files /dev/null and b/public/svgs/martin.png differ
diff --git a/public/svgs/mindsdb.svg b/public/svgs/mindsdb.svg
new file mode 100644
index 0000000000..53799dd1c4
--- /dev/null
+++ b/public/svgs/mindsdb.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/svgs/mosquitto.png b/public/svgs/mosquitto.png
new file mode 100644
index 0000000000..eb287a7cdd
Binary files /dev/null and b/public/svgs/mosquitto.png differ
diff --git a/public/svgs/ntfy.svg b/public/svgs/ntfy.svg
new file mode 100644
index 0000000000..9e5b5136fd
--- /dev/null
+++ b/public/svgs/ntfy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svgs/osticket.png b/public/svgs/osticket.png
new file mode 100644
index 0000000000..65885b71b8
Binary files /dev/null and b/public/svgs/osticket.png differ
diff --git a/public/svgs/owncloud.svg b/public/svgs/owncloud.svg
new file mode 100644
index 0000000000..83631e3f5d
--- /dev/null
+++ b/public/svgs/owncloud.svg
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/svgs/peppermint.png b/public/svgs/peppermint.png
new file mode 100644
index 0000000000..38db83de0c
Binary files /dev/null and b/public/svgs/peppermint.png differ
diff --git a/public/svgs/qbittorrent.svg b/public/svgs/qbittorrent.svg
new file mode 100644
index 0000000000..69d8cf62ae
--- /dev/null
+++ b/public/svgs/qbittorrent.svg
@@ -0,0 +1,16 @@
+
+
+ qbittorrent-new-light
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/svgs/traccar.png b/public/svgs/traccar.png
new file mode 100644
index 0000000000..c747aea054
Binary files /dev/null and b/public/svgs/traccar.png differ
diff --git a/public/svgs/transmission.svg b/public/svgs/transmission.svg
new file mode 100644
index 0000000000..9a11f77f45
--- /dev/null
+++ b/public/svgs/transmission.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svgs/unsend.svg b/public/svgs/unsend.svg
new file mode 100644
index 0000000000..f5ff6fabc6
--- /dev/null
+++ b/public/svgs/unsend.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/svgs/vvveb.svg b/public/svgs/vvveb.svg
new file mode 100644
index 0000000000..2b66b3087b
--- /dev/null
+++ b/public/svgs/vvveb.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svgs/wireguard.svg b/public/svgs/wireguard.svg
new file mode 100644
index 0000000000..81823b3eb7
--- /dev/null
+++ b/public/svgs/wireguard.svg
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/public/svgs/zep.png b/public/svgs/zep.png
new file mode 100644
index 0000000000..7d51b32dc0
Binary files /dev/null and b/public/svgs/zep.png differ
diff --git a/public/svgs/zipline.png b/public/svgs/zipline.png
new file mode 100644
index 0000000000..2b8f6972d5
Binary files /dev/null and b/public/svgs/zipline.png differ
diff --git a/public/vendor/telescope/app.js b/public/vendor/telescope/app.js
index d1c0e7f28d..378d6cf438 100644
--- a/public/vendor/telescope/app.js
+++ b/public/vendor/telescope/app.js
@@ -1,2 +1,2 @@
/*! For license information please see app.js.LICENSE.txt */
-(()=>{var t,e={2110:(t,e,n)=>{"use strict";var o=Object.freeze({}),p=Array.isArray;function M(t){return null==t}function b(t){return null!=t}function c(t){return!0===t}function r(t){return"string"==typeof t||"number"==typeof t||"symbol"==typeof t||"boolean"==typeof t}function z(t){return"function"==typeof t}function a(t){return null!==t&&"object"==typeof t}var i=Object.prototype.toString;function O(t){return"[object Object]"===i.call(t)}function s(t){return"[object RegExp]"===i.call(t)}function A(t){var e=parseFloat(String(t));return e>=0&&Math.floor(e)===e&&isFinite(t)}function u(t){return b(t)&&"function"==typeof t.then&&"function"==typeof t.catch}function l(t){return null==t?"":Array.isArray(t)||O(t)&&t.toString===i?JSON.stringify(t,null,2):String(t)}function d(t){var e=parseFloat(t);return isNaN(e)?t:e}function f(t,e){for(var n=Object.create(null),o=t.split(","),p=0;p-1)return t.splice(o,1)}}var v=Object.prototype.hasOwnProperty;function R(t,e){return v.call(t,e)}function m(t){var e=Object.create(null);return function(n){return e[n]||(e[n]=t(n))}}var g=/-(\w)/g,L=m((function(t){return t.replace(g,(function(t,e){return e?e.toUpperCase():""}))})),y=m((function(t){return t.charAt(0).toUpperCase()+t.slice(1)})),_=/\B([A-Z])/g,N=m((function(t){return t.replace(_,"-$1").toLowerCase()}));var E=Function.prototype.bind?function(t,e){return t.bind(e)}:function(t,e){function n(n){var o=arguments.length;return o?o>1?t.apply(e,arguments):t.call(e,n):t.call(e)}return n._length=t.length,n};function T(t,e){e=e||0;for(var n=t.length-e,o=new Array(n);n--;)o[n]=t[n+e];return o}function B(t,e){for(var n in e)t[n]=e[n];return t}function C(t){for(var e={},n=0;n0,tt=Z&&Z.indexOf("edge/")>0;Z&&Z.indexOf("android");var et=Z&&/iphone|ipad|ipod|ios/.test(Z);Z&&/chrome\/\d+/.test(Z),Z&&/phantomjs/.test(Z);var nt,ot=Z&&Z.match(/firefox\/(\d+)/),pt={}.watch,Mt=!1;if(K)try{var bt={};Object.defineProperty(bt,"passive",{get:function(){Mt=!0}}),window.addEventListener("test-passive",null,bt)}catch(t){}var ct=function(){return void 0===nt&&(nt=!K&&void 0!==n.g&&(n.g.process&&"server"===n.g.process.env.VUE_ENV)),nt},rt=K&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__;function zt(t){return"function"==typeof t&&/native code/.test(t.toString())}var at,it="undefined"!=typeof Symbol&&zt(Symbol)&&"undefined"!=typeof Reflect&&zt(Reflect.ownKeys);at="undefined"!=typeof Set&&zt(Set)?Set:function(){function t(){this.set=Object.create(null)}return t.prototype.has=function(t){return!0===this.set[t]},t.prototype.add=function(t){this.set[t]=!0},t.prototype.clear=function(){this.set=Object.create(null)},t}();var Ot=null;function st(t){void 0===t&&(t=null),t||Ot&&Ot._scope.off(),Ot=t,t&&t._scope.on()}var At=function(){function t(t,e,n,o,p,M,b,c){this.tag=t,this.data=e,this.children=n,this.text=o,this.elm=p,this.ns=void 0,this.context=M,this.fnContext=void 0,this.fnOptions=void 0,this.fnScopeId=void 0,this.key=e&&e.key,this.componentOptions=b,this.componentInstance=void 0,this.parent=void 0,this.raw=!1,this.isStatic=!1,this.isRootInsert=!0,this.isComment=!1,this.isCloned=!1,this.isOnce=!1,this.asyncFactory=c,this.asyncMeta=void 0,this.isAsyncPlaceholder=!1}return Object.defineProperty(t.prototype,"child",{get:function(){return this.componentInstance},enumerable:!1,configurable:!0}),t}(),ut=function(t){void 0===t&&(t="");var e=new At;return e.text=t,e.isComment=!0,e};function lt(t){return new At(void 0,void 0,void 0,String(t))}function dt(t){var e=new At(t.tag,t.data,t.children&&t.children.slice(),t.text,t.elm,t.context,t.componentOptions,t.asyncFactory);return e.ns=t.ns,e.isStatic=t.isStatic,e.key=t.key,e.isComment=t.isComment,e.fnContext=t.fnContext,e.fnOptions=t.fnOptions,e.fnScopeId=t.fnScopeId,e.asyncMeta=t.asyncMeta,e.isCloned=!0,e}var ft=0,qt=[],ht=function(){for(var t=0;t0&&(Vt((o=Kt(o,"".concat(e||"","_").concat(n)))[0])&&Vt(a)&&(i[z]=lt(a.text+o[0].text),o.shift()),i.push.apply(i,o)):r(o)?Vt(a)?i[z]=lt(a.text+o):""!==o&&i.push(lt(o)):Vt(o)&&Vt(a)?i[z]=lt(a.text+o.text):(c(t._isVList)&&b(o.tag)&&M(o.key)&&b(e)&&(o.key="__vlist".concat(e,"_").concat(n,"__")),i.push(o)));return i}var Zt=1,Qt=2;function Jt(t,e,n,o,M,i){return(p(n)||r(n))&&(M=o,o=n,n=void 0),c(i)&&(M=Qt),function(t,e,n,o,M){if(b(n)&&b(n.__ob__))return ut();b(n)&&b(n.is)&&(e=n.is);if(!e)return ut();0;p(o)&&z(o[0])&&((n=n||{}).scopedSlots={default:o[0]},o.length=0);M===Qt?o=$t(o):M===Zt&&(o=function(t){for(var e=0;e0,c=e?!!e.$stable:!b,r=e&&e.$key;if(e){if(e._normalized)return e._normalized;if(c&&p&&p!==o&&r===p.$key&&!b&&!p.$hasNormal)return p;for(var z in M={},e)e[z]&&"$"!==z[0]&&(M[z]=he(t,n,z,e[z]))}else M={};for(var a in n)a in M||(M[a]=We(n,a));return e&&Object.isExtensible(e)&&(e._normalized=M),Y(M,"$stable",c),Y(M,"$key",r),Y(M,"$hasNormal",b),M}function he(t,e,n,o){var M=function(){var e=Ot;st(t);var n=arguments.length?o.apply(null,arguments):o({}),M=(n=n&&"object"==typeof n&&!p(n)?[n]:$t(n))&&n[0];return st(e),n&&(!M||1===n.length&&M.isComment&&!fe(M))?void 0:n};return o.proxy&&Object.defineProperty(e,n,{get:M,enumerable:!0,configurable:!0}),M}function We(t,e){return function(){return t[e]}}function ve(t){return{get attrs(){if(!t._attrsProxy){var e=t._attrsProxy={};Y(e,"_v_attr_proxy",!0),Re(e,t.$attrs,o,t,"$attrs")}return t._attrsProxy},get listeners(){t._listenersProxy||Re(t._listenersProxy={},t.$listeners,o,t,"$listeners");return t._listenersProxy},get slots(){return function(t){t._slotsProxy||ge(t._slotsProxy={},t.$scopedSlots);return t._slotsProxy}(t)},emit:E(t.$emit,t),expose:function(e){e&&Object.keys(e).forEach((function(n){return Ut(t,e,n)}))}}}function Re(t,e,n,o,p){var M=!1;for(var b in e)b in t?e[b]!==n[b]&&(M=!0):(M=!0,me(t,b,o,p));for(var b in t)b in e||(M=!0,delete t[b]);return M}function me(t,e,n,o){Object.defineProperty(t,e,{enumerable:!0,configurable:!0,get:function(){return n[o][e]}})}function ge(t,e){for(var n in e)t[n]=e[n];for(var n in t)n in e||delete t[n]}var Le,ye=null;function _e(t,e){return(t.__esModule||it&&"Module"===t[Symbol.toStringTag])&&(t=t.default),a(t)?e.extend(t):t}function Ne(t){if(p(t))for(var e=0;edocument.createEvent("Event").timeStamp&&(Ye=function(){return $e.now()})}var Ve=function(t,e){if(t.post){if(!e.post)return 1}else if(e.post)return-1;return t.id-e.id};function Ke(){var t,e;for(Ge=Ye(),Fe=!0,De.sort(Ve),He=0;HeHe&&De[n].id>t.id;)n--;De.splice(n+1,0,t)}else De.push(t);je||(je=!0,ln(Ke))}}var Qe="watcher";"".concat(Qe," callback"),"".concat(Qe," getter"),"".concat(Qe," cleanup");var Je;var tn=function(){function t(t){void 0===t&&(t=!1),this.detached=t,this.active=!0,this.effects=[],this.cleanups=[],this.parent=Je,!t&&Je&&(this.index=(Je.scopes||(Je.scopes=[])).push(this)-1)}return t.prototype.run=function(t){if(this.active){var e=Je;try{return Je=this,t()}finally{Je=e}}else 0},t.prototype.on=function(){Je=this},t.prototype.off=function(){Je=this.parent},t.prototype.stop=function(t){if(this.active){var e=void 0,n=void 0;for(e=0,n=this.effects.length;e-1)if(M&&!R(p,"default"))b=!1;else if(""===b||b===N(t)){var r=eo(String,p.type);(r<0||c-1:"string"==typeof t?t.split(",").indexOf(e)>-1:!!s(t)&&t.test(e)}function bo(t,e){var n=t.cache,o=t.keys,p=t._vnode;for(var M in n){var b=n[M];if(b){var c=b.name;c&&!e(c)&&co(n,M,o,p)}}}function co(t,e,n,o){var p=t[e];!p||o&&p.tag===o.tag||p.componentInstance.$destroy(),t[e]=null,W(n,e)}!function(t){t.prototype._init=function(t){var e=this;e._uid=Bn++,e._isVue=!0,e.__v_skip=!0,e._scope=new tn(!0),e._scope._vm=!0,t&&t._isComponent?function(t,e){var n=t.$options=Object.create(t.constructor.options),o=e._parentVnode;n.parent=e.parent,n._parentVnode=o;var p=o.componentOptions;n.propsData=p.propsData,n._parentListeners=p.listeners,n._renderChildren=p.children,n._componentTag=p.tag,e.render&&(n.render=e.render,n.staticRenderFns=e.staticRenderFns)}(e,t):e.$options=Vn(Cn(e.constructor),t||{},e),e._renderProxy=e,e._self=e,function(t){var e=t.$options,n=e.parent;if(n&&!e.abstract){for(;n.$options.abstract&&n.$parent;)n=n.$parent;n.$children.push(t)}t.$parent=n,t.$root=n?n.$root:t,t.$children=[],t.$refs={},t._provided=n?n._provided:Object.create(null),t._watcher=null,t._inactive=null,t._directInactive=!1,t._isMounted=!1,t._isDestroyed=!1,t._isBeingDestroyed=!1}(e),function(t){t._events=Object.create(null),t._hasHookEvent=!1;var e=t.$options._parentListeners;e&&Ce(t,e)}(e),function(t){t._vnode=null,t._staticTrees=null;var e=t.$options,n=t.$vnode=e._parentVnode,p=n&&n.context;t.$slots=le(e._renderChildren,p),t.$scopedSlots=n?qe(t.$parent,n.data.scopedSlots,t.$slots):o,t._c=function(e,n,o,p){return Jt(t,e,n,o,p,!1)},t.$createElement=function(e,n,o,p){return Jt(t,e,n,o,p,!0)};var M=n&&n.data;wt(t,"$attrs",M&&M.attrs||o,null,!0),wt(t,"$listeners",e._parentListeners||o,null,!0)}(e),Ie(e,"beforeCreate",void 0,!1),function(t){var e=Tn(t.$options.inject,t);e&&(Et(!1),Object.keys(e).forEach((function(n){wt(t,n,e[n])})),Et(!0))}(e),gn(e),function(t){var e=t.$options.provide;if(e){var n=z(e)?e.call(t):e;if(!a(n))return;for(var o=en(t),p=it?Reflect.ownKeys(n):Object.keys(n),M=0;M1?T(n):n;for(var o=T(arguments,1),p='event handler for "'.concat(t,'"'),M=0,b=n.length;MparseInt(this.max)&&co(e,n[0],n,this._vnode),this.vnodeToCache=null}}},created:function(){this.cache=Object.create(null),this.keys=[]},destroyed:function(){for(var t in this.cache)co(this.cache,t,this.keys)},mounted:function(){var t=this;this.cacheVNode(),this.$watch("include",(function(e){bo(t,(function(t){return Mo(e,t)}))})),this.$watch("exclude",(function(e){bo(t,(function(t){return!Mo(e,t)}))}))},updated:function(){this.cacheVNode()},render:function(){var t=this.$slots.default,e=Ne(t),n=e&&e.componentOptions;if(n){var o=po(n),p=this.include,M=this.exclude;if(p&&(!o||!Mo(p,o))||M&&o&&Mo(M,o))return e;var b=this.cache,c=this.keys,r=null==e.key?n.Ctor.cid+(n.tag?"::".concat(n.tag):""):e.key;b[r]?(e.componentInstance=b[r].componentInstance,W(c,r),c.push(r)):(this.vnodeToCache=e,this.keyToCache=r),e.data.keepAlive=!0}return e||t&&t[0]}},ao={KeepAlive:zo};!function(t){var e={get:function(){return F}};Object.defineProperty(t,"config",e),t.util={warn:Un,extend:B,mergeOptions:Vn,defineReactive:wt},t.set=St,t.delete=Xt,t.nextTick=ln,t.observable=function(t){return Ct(t),t},t.options=Object.create(null),U.forEach((function(e){t.options[e+"s"]=Object.create(null)})),t.options._base=t,B(t.options.components,ao),function(t){t.use=function(t){var e=this._installedPlugins||(this._installedPlugins=[]);if(e.indexOf(t)>-1)return this;var n=T(arguments,1);return n.unshift(this),z(t.install)?t.install.apply(t,n):z(t)&&t.apply(null,n),e.push(t),this}}(t),function(t){t.mixin=function(t){return this.options=Vn(this.options,t),this}}(t),oo(t),function(t){U.forEach((function(e){t[e]=function(t,n){return n?("component"===e&&O(n)&&(n.name=n.name||t,n=this.options._base.extend(n)),"directive"===e&&z(n)&&(n={bind:n,update:n}),this.options[e+"s"][t]=n,n):this.options[e+"s"][t]}}))}(t)}(no),Object.defineProperty(no.prototype,"$isServer",{get:ct}),Object.defineProperty(no.prototype,"$ssrContext",{get:function(){return this.$vnode&&this.$vnode.ssrContext}}),Object.defineProperty(no,"FunctionalRenderContext",{value:wn}),no.version="2.7.14";var io=f("style,class"),Oo=f("input,textarea,option,select,progress"),so=function(t,e,n){return"value"===n&&Oo(t)&&"button"!==e||"selected"===n&&"option"===t||"checked"===n&&"input"===t||"muted"===n&&"video"===t},Ao=f("contenteditable,draggable,spellcheck"),uo=f("events,caret,typing,plaintext-only"),lo=function(t,e){return vo(e)||"false"===e?"false":"contenteditable"===t&&uo(e)?e:"true"},fo=f("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,truespeed,typemustmatch,visible"),qo="http://www.w3.org/1999/xlink",ho=function(t){return":"===t.charAt(5)&&"xlink"===t.slice(0,5)},Wo=function(t){return ho(t)?t.slice(6,t.length):""},vo=function(t){return null==t||!1===t};function Ro(t){for(var e=t.data,n=t,o=t;b(o.componentInstance);)(o=o.componentInstance._vnode)&&o.data&&(e=mo(o.data,e));for(;b(n=n.parent);)n&&n.data&&(e=mo(e,n.data));return function(t,e){if(b(t)||b(e))return go(t,Lo(e));return""}(e.staticClass,e.class)}function mo(t,e){return{staticClass:go(t.staticClass,e.staticClass),class:b(t.class)?[t.class,e.class]:e.class}}function go(t,e){return t?e?t+" "+e:t:e||""}function Lo(t){return Array.isArray(t)?function(t){for(var e,n="",o=0,p=t.length;o