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 754f0929b..0a6c5bae6 100644
--- a/app/Livewire/Settings/Index.php
+++ b/app/Livewire/Settings/Index.php
@@ -5,62 +5,95 @@ namespace App\Livewire\Settings;
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\Rule;
use Livewire\Component;
class Index extends Component
{
public InstanceSettings $settings;
- public bool $do_not_track;
-
- public bool $is_auto_update_enabled;
-
- public bool $is_registration_enabled;
-
- public bool $is_dns_validation_enabled;
-
- public bool $is_api_enabled;
-
- public string $auto_update_frequency;
-
- public string $update_check_frequency;
-
- protected string $dynamic_config_path = '/data/coolify/proxy/dynamic';
-
protected Server $server;
- 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',
- ];
-
+ #[Locked]
public $timezones;
+ #[Rule('boolean')]
+ public bool $is_auto_update_enabled;
+
+ #[Rule('nullable|string|max:255')]
+ public ?string $fqdn = null;
+
+ #[Rule('nullable|string|max:255')]
+ public ?string $resale_license = null;
+
+ #[Rule('required|integer|min:1025|max:65535')]
+ public int $public_port_min;
+
+ #[Rule('required|integer|min:1025|max:65535')]
+ public int $public_port_max;
+
+ #[Rule('nullable|string')]
+ public ?string $custom_dns_servers = null;
+
+ #[Rule('nullable|string|max:255')]
+ public ?string $instance_name = null;
+
+ #[Rule('nullable|string')]
+ public ?string $allowed_ips = null;
+
+ #[Rule('nullable|string')]
+ public ?string $public_ipv4 = null;
+
+ #[Rule('nullable|string')]
+ public ?string $public_ipv6 = null;
+
+ #[Rule('string')]
+ public string $auto_update_frequency;
+
+ #[Rule('string')]
+ public string $update_check_frequency;
+
+ #[Rule('required|string|timezone')]
+ public string $instance_timezone;
+
+ #[Rule('boolean')]
+ public bool $do_not_track;
+
+ #[Rule('boolean')]
+ public bool $is_registration_enabled;
+
+ #[Rule('boolean')]
+ public bool $is_dns_validation_enabled;
+
+ #[Rule('boolean')]
+ public bool $is_api_enabled;
+
+ #[Rule('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 @@ class Index extends Component
$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 @@ class Index extends Component
$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 @@ class Index extends Component
$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 @@ class Index extends Component
}
}
- 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 ca0c9c1ae..79f8269f3 100644
--- a/app/Livewire/Settings/License.php
+++ b/app/Livewire/Settings/License.php
@@ -28,6 +28,9 @@ class License extends Component
if (! isCloud()) {
abort(404);
}
+ if (! isInstanceAdmin()) {
+ return redirect()->route('home');
+ }
$this->instance_id = config('app.id');
$this->settings = instanceSettings();
}
@@ -47,7 +50,6 @@ class License extends Component
$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 9240aa96d..38f7e548a 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\Rule;
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',
+ #[Rule(['required'])]
+ public string $uuid;
- ];
+ #[Rule(['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',
- ];
+ #[Rule(['nullable'])]
+ public ?string $description = null;
+
+ #[Rule(['required'])]
+ public string $postgres_user;
+
+ #[Rule(['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 @@ class SettingsBackup extends Component
}
$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 @@ class SettingsBackup extends Component
'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 @@ class SettingsBackup extends Component
'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 @@ class SettingsBackup extends Component
}
}
- 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 4515df9a7..2b515bf68 100644
--- a/app/Livewire/SettingsEmail.php
+++ b/app/Livewire/SettingsEmail.php
@@ -3,7 +3,6 @@
namespace App\Livewire;
use App\Models\InstanceSettings;
-use App\Notifications\TransactionalEmails\Test;
use Livewire\Component;
class SettingsEmail extends Component
@@ -124,10 +123,4 @@ class SettingsEmail extends Component
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 c3884589f..17b3b89a3 100644
--- a/app/Livewire/SettingsOauth.php
+++ b/app/Livewire/SettingsOauth.php
@@ -24,6 +24,9 @@ class SettingsOauth extends Component
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 193b650ff..7df6f968f 100644
--- a/app/Livewire/Source/Github/Change.php
+++ b/app/Livewire/Source/Github/Change.php
@@ -93,52 +93,55 @@ class Change extends Component
// }
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');
- } 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');
+ $this->name = str($this->github_app->name)->kebab();
+ $this->fqdn = $settings->fqdn;
- return redirect()->route($back, [
- 'environment_name' => $environment_name,
- 'project_uuid' => $project_uuid,
- 'type' => $type,
- 'destination' => $destination,
- ]);
+ 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 {
+ $this->webhook_endpoint = $this->ipv4;
+ $this->is_system_wide = $this->github_app->is_system_wide;
+ }
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
- $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;
- }
+
}
public function submit()
diff --git a/app/Livewire/Source/Github/Create.php b/app/Livewire/Source/Github/Create.php
index f85e8646e..103c5c9fb 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/Tags/Deployments.php b/app/Livewire/Tags/Deployments.php
index 270aa176a..e4afa5b60 100644
--- a/app/Livewire/Tags/Deployments.php
+++ b/app/Livewire/Tags/Deployments.php
@@ -7,19 +7,19 @@ use Livewire\Component;
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 @@ class Deployments extends Component
'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
index a01d00a70..642b2bded 100644
--- a/app/Livewire/Tags/Index.php
+++ b/app/Livewire/Tags/Index.php
@@ -5,9 +5,11 @@ namespace App\Livewire\Tags;
use App\Http\Controllers\Api\DeployController;
use App\Models\Tag;
use Illuminate\Support\Collection;
+use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
+#[Title('Tags | Coolify')]
class Index extends Component
{
#[Url()]
@@ -21,33 +23,47 @@ class Index extends Component
public $webhook = null;
- public $deployments_per_tag_per_server = [];
+ public $deploymentsPerTagPerServer = [];
- protected $listeners = ['deployments' => 'update_deployments'];
+ protected $listeners = ['deployments' => 'updateDeployments'];
- public function update_deployments($deployments)
+ public function render()
{
- $this->deployments_per_tag_per_server = $deployments;
+ return view('livewire.tags.index');
}
- public function tag_updated()
+ public function mount()
+ {
+ $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name');
+ if ($this->tag) {
+ $this->tagUpdated();
+ }
+ }
+
+ public function updateDeployments($deployments)
+ {
+ $this->deploymentsPerTagPerServer = $deployments;
+ }
+
+ public function tagUpdated()
{
if ($this->tag == '') {
return;
}
- $tag = $this->tags->where('name', $this->tag)->first();
+ $sanitizedTag = htmlspecialchars($this->tag, ENT_QUOTES, 'UTF-8');
+ $tag = $this->tags->where('name', $sanitizedTag)->first();
if (! $tag) {
- $this->dispatch('error', "Tag ({$this->tag}) not found.");
+ $this->dispatch('error', 'Tag ('.e($sanitizedTag).') not found.');
$this->tag = '';
return;
}
- $this->webhook = generatTagDeployWebhook($tag->name);
+ $this->webhook = generateTagDeployWebhook($tag->name);
$this->applications = $tag->applications()->get();
$this->services = $tag->services()->get();
}
- public function redeploy_all()
+ public function redeployAll()
{
try {
$this->applications->each(function ($resource) {
@@ -63,17 +79,4 @@ class Index extends Component
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 668101edb..0dffcce57 100644
--- a/app/Livewire/Tags/Show.php
+++ b/app/Livewire/Tags/Show.php
@@ -5,8 +5,10 @@ namespace App\Livewire\Tags;
use App\Http\Controllers\Api\DeployController;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Tag;
+use Livewire\Attributes\Title;
use Livewire\Component;
+#[Title('Tags | Coolify')]
class Show extends Component
{
public $tags;
@@ -28,7 +30,7 @@ class Show extends Component
if (! $tag) {
return redirect()->route('tags.index');
}
- $this->webhook = generatTagDeployWebhook($tag->name);
+ $this->webhook = generateTagDeployWebhook($tag->name);
$this->applications = $tag->applications()->get();
$this->services = $tag->services()->get();
$this->tag = $tag;
diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php
index 3026cb297..c9dabcb5c 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,27 @@ class AdminView extends Component
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 (! 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 +87,23 @@ class AdminView extends Component
$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 +112,19 @@ class AdminView extends Component
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/Invitations.php b/app/Livewire/Team/Invitations.php
index 6a32a1d16..043c4e2e8 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 $e) {
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 6c9e405fc..25f8a1ff5 100644
--- a/app/Livewire/Team/InviteLink.php
+++ b/app/Livewire/Team/InviteLink.php
@@ -41,6 +41,9 @@ class InviteLink extends Component
{
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 680cb901b..890d640a0 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/VerifyEmail.php b/app/Livewire/VerifyEmail.php
index d1f79c835..9d1fdab98 100644
--- a/app/Livewire/VerifyEmail.php
+++ b/app/Livewire/VerifyEmail.php
@@ -17,8 +17,6 @@ class VerifyEmail extends Component
$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 07aeb4c5b..c747c75c5 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -221,7 +221,6 @@ class Application extends BaseModel
{
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 +936,7 @@ class Application extends BaseModel
$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 +1245,11 @@ class Application extends BaseModel
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,13 +1397,13 @@ class Application extends BaseModel
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?');
@@ -1415,14 +1412,33 @@ class Application extends BaseModel
}
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 +1475,9 @@ class Application extends BaseModel
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/Environment.php b/app/Models/Environment.php
index c892d7ba1..d0bb5d2a6 100644
--- a/app/Models/Environment.php
+++ b/app/Models/Environment.php
@@ -27,7 +27,6 @@ class Environment extends Model
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 9f8e4b342..f77d73db8 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 @@ class EnvironmentVariable extends Model
'version' => config('version'),
]);
});
+ static::saving(function (EnvironmentVariable $environmentVariable) {
+ $environmentVariable->updateIsShared();
+ });
}
public function service()
@@ -130,6 +133,13 @@ class EnvironmentVariable extends Model
);
}
+ protected function isReallyRequired(): Attribute
+ {
+ return Attribute::make(
+ get: fn () => $this->is_required && str($this->real_value)->isEmpty(),
+ );
+ }
+
protected function isShared(): Attribute
{
return Attribute::make(
@@ -210,4 +220,11 @@ class EnvironmentVariable extends Model
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 66ecdd967..0b0e93b12 100644
--- a/app/Models/GithubApp.php
+++ b/app/Models/GithubApp.php
@@ -31,6 +31,11 @@ class GithubApp extends BaseModel
});
}
+ 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 @@ class GithubApp extends BaseModel
{
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 a789a7e65..2112a4a66 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 bb3d1478b..339daed2a 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,23 @@ 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 +102,16 @@ class InstanceSettings extends Model implements SendsEmail
return "[{$instanceName}]";
}
- public function helperVersion(): Attribute
- {
- return Attribute::make(
- get: function ($value) {
- if (isDev()) {
- return 'latest';
- }
+ // public function helperVersion(): Attribute
+ // {
+ // return Attribute::make(
+ // get: function ($value) {
+ // if (isDev()) {
+ // return 'latest';
+ // }
- return $value;
- }
- );
- }
+ // return $value;
+ // }
+ // );
+ // }
}
diff --git a/app/Models/Project.php b/app/Models/Project.php
index 5a9dd964a..3a09b0b8f 100644
--- a/app/Models/Project.php
+++ b/app/Models/Project.php
@@ -47,7 +47,6 @@ class Project extends BaseModel
$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();
}
});
diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php
index ce5d3a87f..473fc7b4b 100644
--- a/app/Models/ScheduledDatabaseBackup.php
+++ b/app/Models/ScheduledDatabaseBackup.php
@@ -39,9 +39,14 @@ class ScheduledDatabaseBackup extends BaseModel
public function server()
{
if ($this->database) {
- if ($this->database->destination && $this->database->destination->server) {
- $server = $this->database->destination->server;
-
+ if ($this->database instanceof ServiceDatabase) {
+ $destination = data_get($this->database->service, 'destination');
+ $server = data_get($destination, 'server');
+ } else {
+ $destination = data_get($this->database, 'destination');
+ $server = data_get($destination, 'server');
+ }
+ if ($server) {
return $server;
}
}
diff --git a/app/Models/Server.php b/app/Models/Server.php
index 8864deef1..a9e3acde4 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -3,10 +3,16 @@
namespace App\Models;
use App\Actions\Server\InstallDocker;
+use App\Actions\Server\StartSentinel;
use App\Enums\ProxyTypes;
-use App\Jobs\PullSentinelImageJob;
+use App\Helpers\SshMultiplexingHelper;
+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;
@@ -43,7 +49,7 @@ use Symfony\Component\Yaml\Yaml;
class Server extends BaseModel
{
- use SchemalessAttributesTrait;
+ use SchemalessAttributesTrait, SoftDeletes;
public static $batch_counter = 0;
@@ -59,6 +65,11 @@ class Server extends BaseModel
}
$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 +106,8 @@ class Server extends BaseModel
}
}
});
- static::deleting(function ($server) {
+
+ static::forceDeleting(function ($server) {
$server->destinations()->each(function ($destination) {
$destination->delete();
});
@@ -103,12 +115,15 @@ class Server extends BaseModel
});
}
- 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 +142,11 @@ class Server extends BaseModel
protected $guarded = [];
+ public function type()
+ {
+ return 'server';
+ }
+
public static function isReachable()
{
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true);
@@ -209,10 +229,13 @@ respond 404
1 => 'https',
],
'service' => 'noop',
- 'rule' => 'HostRegexp(`{catchall:.*}`)',
+ 'rule' => 'HostRegexp(`.+`)',
+ 'tls' => [
+ 'certResolver' => 'letsencrypt',
+ ],
'priority' => 1,
'middlewares' => [
- 0 => 'redirect-regexp@file',
+ 0 => 'redirect-regexp',
],
],
],
@@ -507,24 +530,48 @@ $schema://$host {
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 sentinelHeartbeat(bool $isReset = false)
+ {
+ $this->sentinel_updated_at = $isReset ? now()->subMinutes(6000) : now();
+ $this->save();
+ }
+
+ /**
+ * Get the wait time for Sentinel to push before performing an SSH check.
+ *
+ * @return int The wait time in seconds.
+ */
+ public function waitBeforeDoingSshCheck(): int
+ {
+ $wait = $this->settings->sentinel_push_interval_seconds * 3;
+ if ($wait < 120) {
+ $wait = 120;
+ }
+
+ return $wait;
+ }
+
+ public function isSentinelLive()
+ {
+ return Carbon::parse($this->sentinel_updated_at)->isAfter(now()->subSeconds($this->waitBeforeDoingSshCheck()));
+ }
+
public function isSentinelEnabled()
{
- return $this->isMetricsEnabled() || $this->isServerApiEnabled();
+ return ($this->isMetricsEnabled() || $this->isServerApiEnabled()) && ! $this->isBuildServer();
}
public function isMetricsEnabled()
@@ -534,49 +581,19 @@ $schema://$host {
public function isServerApiEnabled()
{
- return $this->settings->is_server_api_enabled;
- }
-
- public function checkServerApi()
- {
- 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->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?');
@@ -585,17 +602,12 @@ $schema://$host {
}
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);
+ $parsedCollection = collect($cpu)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
});
- return $parsedCollection->toArray();
+ return $parsedCollection;
}
}
@@ -603,7 +615,7 @@ $schema://$host {
{
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?');
@@ -612,89 +624,19 @@ $schema://$host {
}
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()
@@ -974,7 +916,8 @@ $schema://$host {
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;
}
@@ -1038,39 +981,115 @@ $schema://$host {
return data_get($this, 'settings.is_swarm_worker');
}
- public function validateConnection($isManualCheck = true)
+ public function serverStatus(): bool
+ {
+ if ($this->status() === false) {
+ return false;
+ }
+ if ($this->isFunctional() === false) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function status(): bool
+ {
+ if ($this->skipServer()) {
+ return false;
+ }
+ ['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();
+ }
+ }
+
+ return false;
+ }
+
+ 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);
- // ray('Manual Check: ' . ($isManualCheck ? 'true' : 'false'));
- $server = Server::find($this->id);
- if (! $server) {
- return ['uptime' => false, 'error' => 'Server not found.'];
- }
- if ($server->skipServer()) {
+ SshMultiplexingHelper::removeMuxFile($this);
+
+ 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()];
}
@@ -1225,4 +1244,27 @@ $schema://$host {
{
return str($this->ip)->contains(':');
}
+
+ public function restartSentinel(bool $async = true): void
+ {
+ try {
+ if ($async) {
+ StartSentinel::dispatch($this, true);
+ } else {
+ StartSentinel::run($this, true);
+ }
+ } catch (\Throwable $e) {
+ loggy('Error restarting Sentinel: '.$e->getMessage());
+ }
+ }
+
+ 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 c44a393b4..bca16536e 100644
--- a/app/Models/ServerSetting.php
+++ b/app/Models/ServerSetting.php
@@ -24,7 +24,7 @@ use OpenApi\Attributes as OA;
'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 +35,9 @@ use OpenApi\Attributes as OA;
'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 +53,77 @@ 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);
+ }
+ if (str($setting->sentinel_custom_url)->isEmpty()) {
+ $setting->generateSentinelUrl(save: false);
+ }
+ } catch (\Throwable $e) {
+ loggy('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)
+ {
+ $data = [
+ 'server_uuid' => $this->server->uuid,
+ ];
+ $token = json_encode($data);
+ $encrypted = encrypt($token);
+ $this->sentinel_token = $encrypted;
+ if ($save) {
+ $this->save();
+ }
+
+ return $token;
+ }
+
+ public function generateSentinelUrl(bool $save = true)
+ {
+ $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) {
+ $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 bcdb74f8c..c4f4c2d3c 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -288,6 +288,21 @@ class Service extends BaseModel
continue;
}
switch ($image) {
+ case $image->contains('castopod'):
+ $data = collect([]);
+ $disable_https = $this->environment_variables()->where('key', 'CP_DISABLE_HTTPS')->first();
+ if ($disable_https) {
+ $data = $data->merge([
+ 'Disable HTTPS' => [
+ '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',
+ ],
+ ]);
+ }
+ $fields->put('Castopod', $data->toArray());
+ break;
case $image->contains('label-studio'):
$data = collect([]);
$username = $this->environment_variables()->where('key', 'LABEL_STUDIO_USERNAME')->first();
@@ -304,7 +319,7 @@ class Service extends BaseModel
if ($password) {
$data = $data->merge([
'Password' => [
- 'key' => 'LABEL_STUDIO_PASSWORD',
+ 'key' => data_get($password, 'key'),
'value' => data_get($password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -344,18 +359,17 @@ class Service extends BaseModel
if ($email) {
$data = $data->merge([
'Admin Email' => [
- 'key' => 'LANGFUSE_INIT_USER_EMAIL',
+ 'key' => data_get($email, 'key'),
'value' => data_get($email, 'value'),
'rules' => 'required|email',
],
]);
}
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_LANGFUSE')->first();
- ray('password', $password);
if ($password) {
$data = $data->merge([
'Admin Password' => [
- 'key' => 'LANGFUSE_INIT_USER_PASSWORD',
+ 'key' => data_get($password, 'key'),
'value' => data_get($password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -369,7 +383,7 @@ class Service extends BaseModel
$email = $this->environment_variables()->where('key', 'IN_USER_EMAIL')->first();
$data = $data->merge([
'Email' => [
- 'key' => 'IN_USER_EMAIL',
+ 'key' => data_get($email, 'key'),
'value' => data_get($email, 'value'),
'rules' => 'required|email',
],
@@ -377,7 +391,7 @@ class Service extends BaseModel
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_INVOICENINJAUSER')->first();
$data = $data->merge([
'Password' => [
- 'key' => 'IN_PASSWORD',
+ 'key' => data_get($password, 'key'),
'value' => data_get($password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -472,7 +486,7 @@ class Service extends BaseModel
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
- 'key' => 'SERVICE_PASSWORD_TOLGEE',
+ 'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -519,7 +533,7 @@ class Service extends BaseModel
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
- 'key' => 'SERVICE_PASSWORD_UNLEASH',
+ 'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -542,7 +556,7 @@ class Service extends BaseModel
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
- 'key' => 'GF_SECURITY_ADMIN_PASSWORD',
+ 'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -904,7 +918,7 @@ class Service extends BaseModel
if ($admin_user) {
$data = $data->merge([
'User' => [
- 'key' => 'SERVICE_USER_ADMIN',
+ 'key' => data_get($admin_user, 'key'),
'value' => data_get($admin_user, 'value', 'admin'),
'readonly' => true,
'rules' => 'required',
@@ -914,7 +928,7 @@ class Service extends BaseModel
if ($admin_password) {
$data = $data->merge([
'Password' => [
- 'key' => 'SERVICE_PASSWORD_ADMIN',
+ 'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -924,7 +938,7 @@ class Service extends BaseModel
if ($admin_email) {
$data = $data->merge([
'Email' => [
- 'key' => 'ADMIN_EMAIL',
+ 'key' => data_get($admin_email, 'key'),
'value' => data_get($admin_email, 'value'),
'rules' => 'required|email',
],
@@ -982,8 +996,8 @@ class Service extends BaseModel
break;
case $image->contains('mysql'):
$userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS', 'MYSQL_USER'];
- $passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD'];
- $rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT'];
+ $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();
@@ -1216,7 +1230,6 @@ class Service extends BaseModel
public function environment_variables(): HasMany
{
-
return $this->hasMany(EnvironmentVariable::class)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC");
}
@@ -1300,4 +1313,20 @@ class Service extends BaseModel
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/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php
index e4341b1b9..c9d3ea031 100644
--- a/app/Models/StandaloneClickhouse.php
+++ b/app/Models/StandaloneClickhouse.php
@@ -266,33 +266,48 @@ class StandaloneClickhouse extends BaseModel
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);
-
- 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 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 94ab2d745..2bde51080 100644
--- a/app/Models/StandaloneDragonfly.php
+++ b/app/Models/StandaloneDragonfly.php
@@ -266,33 +266,48 @@ class StandaloneDragonfly extends BaseModel
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);
-
- 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 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 335c8931c..fbee9789a 100644
--- a/app/Models/StandaloneKeydb.php
+++ b/app/Models/StandaloneKeydb.php
@@ -266,33 +266,48 @@ class StandaloneKeydb extends BaseModel
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);
-
- 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 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 c6c08dee5..00f635faf 100644
--- a/app/Models/StandaloneMariadb.php
+++ b/app/Models/StandaloneMariadb.php
@@ -266,33 +266,48 @@ class StandaloneMariadb extends BaseModel
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);
-
- 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 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 99893b1d1..c225011c5 100644
--- a/app/Models/StandaloneMongodb.php
+++ b/app/Models/StandaloneMongodb.php
@@ -286,33 +286,48 @@ class StandaloneMongodb extends BaseModel
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);
-
- 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 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 f2a5b5c14..52725ffc7 100644
--- a/app/Models/StandaloneMysql.php
+++ b/app/Models/StandaloneMysql.php
@@ -267,33 +267,48 @@ class StandaloneMysql extends BaseModel
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);
-
- 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 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 1b18a5ca7..3dd13b633 100644
--- a/app/Models/StandalonePostgresql.php
+++ b/app/Models/StandalonePostgresql.php
@@ -71,7 +71,6 @@ class StandalonePostgresql extends BaseModel
}
$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 +267,52 @@ class StandalonePostgresql extends BaseModel
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function getMetrics(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);
- }
- $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();
- }
- }
-
public function isBackupSolutionAvailable()
{
return true;
}
+
+ public function getCpuMetrics(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}/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.';
+ }
+ 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 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();
+ }
}
diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php
index a5868e243..5b6993b9a 100644
--- a/app/Models/StandaloneRedis.php
+++ b/app/Models/StandaloneRedis.php
@@ -210,7 +210,12 @@ class StandaloneRedis extends BaseModel
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 +224,10 @@ class StandaloneRedis extends BaseModel
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 +235,13 @@ class StandaloneRedis extends BaseModel
);
}
+ public function getRedisVersion()
+ {
+ $image_parts = explode(':', $this->image);
+
+ return $image_parts[1] ?? '0.0';
+ }
+
public function environment()
{
return $this->belongsTo(Environment::class);
@@ -262,37 +277,81 @@ class StandaloneRedis extends BaseModel
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);
-
- 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 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 3f8e97bc5..49b019725 100644
--- a/app/Models/Team.php
+++ b/app/Models/Team.php
@@ -34,6 +34,7 @@ use OpenApi\Attributes as OA;
'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 @@ use OpenApi\Attributes as OA;
'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 @@ use OpenApi\Attributes as OA;
'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 @@ class Team extends Model implements SendsDiscord, SendsEmail
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();
}
});
@@ -164,8 +162,12 @@ class Team extends Model implements SendsDiscord, SendsEmail
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.serverLimit', 0);
}
public function limits(): Attribute
diff --git a/app/Models/TeamInvitation.php b/app/Models/TeamInvitation.php
index c202710e2..0f298a829 100644
--- a/app/Models/TeamInvitation.php
+++ b/app/Models/TeamInvitation.php
@@ -20,6 +20,11 @@ class TeamInvitation extends Model
return $this->belongsTo(Team::class);
}
+ public static function ownedByCurrentTeam()
+ {
+ return TeamInvitation::whereTeamId(currentTeam()->id);
+ }
+
public function isValid()
{
$createdAt = $this->created_at;
diff --git a/app/Notifications/Application/DeploymentFailed.php b/app/Notifications/Application/DeploymentFailed.php
index 1809da368..242980e00 100644
--- a/app/Notifications/Application/DeploymentFailed.php
+++ b/app/Notifications/Application/DeploymentFailed.php
@@ -4,6 +4,7 @@ namespace App\Notifications\Application;
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 @@ class DeploymentFailed extends Notification implements ShouldQueue
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 5085065c2..946a622ca 100644
--- a/app/Notifications/Application/DeploymentSuccess.php
+++ b/app/Notifications/Application/DeploymentSuccess.php
@@ -4,6 +4,7 @@ namespace App\Notifications\Application;
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 @@ class DeploymentSuccess extends Notification implements ShouldQueue
$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 @@ class DeploymentSuccess extends Notification implements ShouldQueue
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 53ed8a589..a080fcabe 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,12 +56,14 @@ class StatusChanged extends Notification implements ShouldQueue
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.')';
+ $message = new DiscordMessage(
+ title: ':cross_mark: Application stopped',
+ description: '[Open Application in Coolify]('.$this->resource_url.')',
+ color: DiscordMessage::errorColor(),
+ isCritical: true,
+ );
return $message;
}
diff --git a/app/Notifications/Channels/DiscordChannel.php b/app/Notifications/Channels/DiscordChannel.php
index f1706f138..3a33d8902 100644
--- a/app/Notifications/Channels/DiscordChannel.php
+++ b/app/Notifications/Channels/DiscordChannel.php
@@ -12,7 +12,7 @@ class DiscordChannel
*/
public function send(SendsDiscord $notifiable, Notification $notification): void
{
- $message = $notification->toDiscord($notifiable);
+ $message = $notification->toDiscord();
$webhookUrl = $notifiable->routeNotificationForDiscord();
if (! $webhookUrl) {
return;
diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php
index 413d3de53..af9af978d 100644
--- a/app/Notifications/Channels/EmailChannel.php
+++ b/app/Notifications/Channels/EmailChannel.php
@@ -32,7 +32,6 @@ class EmailChannel
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 b1a607651..4b1fa49dd 100644
--- a/app/Notifications/Channels/TelegramChannel.php
+++ b/app/Notifications/Channels/TelegramChannel.php
@@ -18,23 +18,23 @@ class TelegramChannel
$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;
}
diff --git a/app/Notifications/Container/ContainerRestarted.php b/app/Notifications/Container/ContainerRestarted.php
index 23f6de264..182a1f5fc 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 @@ class ContainerRestarted extends Notification implements ShouldQueue
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 bcf5e67a5..33a55c65a 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 @@ class ContainerStopped extends Notification implements ShouldQueue
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 77024c05b..8e2733339 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 @@ class BackupFailed extends Notification implements ShouldQueue
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 f8dc6eb56..5128c8ed6 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 @@ class BackupSuccess extends Notification implements ShouldQueue
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 a51ac6283..000000000
--- 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 000000000..856753dca
--- /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 1d4d648c8..48e7d8340 100644
--- a/app/Notifications/Internal/GeneralNotification.php
+++ b/app/Notifications/Internal/GeneralNotification.php
@@ -4,6 +4,7 @@ namespace App\Notifications\Internal;
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 @@ class GeneralNotification extends Notification implements ShouldQueue
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 479cc1aa1..2cc33f2ba 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;
@@ -46,9 +47,19 @@ class TaskFailed extends Notification implements ShouldQueue
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 682ed7a1a..7ea1b84c2 100644
--- a/app/Notifications/Server/DockerCleanup.php
+++ b/app/Notifications/Server/DockerCleanup.php
@@ -5,6 +5,7 @@ namespace App\Notifications\Server;
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 @@ class DockerCleanup extends Notification implements ShouldQueue
// 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 6377f2f15..a26c803ee 100644
--- a/app/Notifications/Server/ForceDisabled.php
+++ b/app/Notifications/Server/ForceDisabled.php
@@ -6,6 +6,7 @@ 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;
@@ -50,9 +51,15 @@ class ForceDisabled extends Notification implements ShouldQueue
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 83594d643..65b65a10c 100644
--- a/app/Notifications/Server/ForceEnabled.php
+++ b/app/Notifications/Server/ForceEnabled.php
@@ -6,6 +6,7 @@ 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;
@@ -50,11 +51,13 @@ class ForceEnabled extends Notification implements ShouldQueue
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 34cb22091..e373abc03 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 @@ class HighDiskUsage extends Notification implements ShouldQueue
$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 @@ class HighDiskUsage extends Notification implements ShouldQueue
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 3f2b3b696..9b54501d9 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 @@ class Revived extends Notification implements ShouldQueue
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 @@ class Revived extends Notification implements ShouldQueue
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 2fb83559a..5bc568e82 100644
--- a/app/Notifications/Server/Unreachable.php
+++ b/app/Notifications/Server/Unreachable.php
@@ -6,11 +6,11 @@ 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 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 @@ class Unreachable extends Notification implements ShouldQueue
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 @@ class Unreachable extends Notification implements ShouldQueue
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 3b46a9a24..a43b1e153 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 @@ class Test extends Notification implements ShouldQueue
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 @@ class Test extends Notification implements ShouldQueue
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 8b4c2eef2..1fffd0399 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -5,6 +5,7 @@ namespace App\Providers;
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
@@ -15,6 +16,14 @@ class AppServiceProvider extends ServiceProvider
{
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 000000000..07e0e8709
--- /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/View/Components/Server/Sidebar.php b/app/View/Components/Server/Sidebar.php
deleted file mode 100644
index f968b6d0c..000000000
--- a/app/View/Components/Server/Sidebar.php
+++ /dev/null
@@ -1,27 +0,0 @@
- 'string',
'build_pack' => Rule::enum(BuildPackTypes::class),
'is_static' => 'boolean',
+ 'static_image' => Rule::enum(StaticImageTypes::class),
'domains' => 'string',
'redirect' => Rule::enum(RedirectTypes::class),
'git_commit_sha' => 'string',
@@ -176,4 +178,5 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
$request->offsetUnset('github_app_uuid');
$request->offsetUnset('private_key_uuid');
$request->offsetUnset('use_build_server');
+ $request->offsetUnset('is_static');
}
diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php
index d8dc26a48..303fcab8e 100644
--- a/bootstrap/helpers/constants.php
+++ b/bootstrap/helpers/constants.php
@@ -40,6 +40,7 @@ const DATABASE_DOCKER_IMAGES = [
];
const SPECIFIC_SERVICES = [
'quay.io/minio/minio',
+ 'minio/minio',
'svhd/logto',
];
diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php
index 950eb67b6..e12910f82 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 397bce029..2795ae295 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -207,12 +207,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 +279,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 +334,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 +388,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 +402,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 +417,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');
}
diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php
index 97deb0b1c..0b5f7034b 100644
--- a/bootstrap/helpers/github.php
+++ b/bootstrap/helpers/github.php
@@ -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 5d1ad5390..496017217 100644
--- a/bootstrap/helpers/proxy.php
+++ b/bootstrap/helpers/proxy.php
@@ -164,6 +164,7 @@ function generate_default_proxy_configuration(Server $server)
'ports' => [
'80:80',
'443:443',
+ '443:443/udp',
'8080:8080',
],
'healthcheck' => [
@@ -187,6 +188,7 @@ function generate_default_proxy_configuration(Server $server)
'--entryPoints.http.http2.maxConcurrentStreams=50',
'--entrypoints.https.http.encodequerysemicolons=true',
'--entryPoints.https.http2.maxConcurrentStreams=50',
+ '--entrypoints.https.http3',
'--providers.docker.exposedbydefault=false',
'--providers.file.directory=/traefik/dynamic/',
'--providers.file.watch=true',
@@ -239,9 +241,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/services.php b/bootstrap/helpers/services.php
index eba88d000..94c3c5f45 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 {
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index 487a6f107..89462215c 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -39,6 +39,7 @@ 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;
@@ -126,7 +127,6 @@ function refreshSession(?Team $team = null): void
}
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 +142,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 +168,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');
+ return data_get($versions, 'coolify.sentinel.version');
} catch (\Throwable $e) {
- //throw $e;
- ray($e->getMessage());
-
return '0.0.0';
}
}
@@ -368,6 +369,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();
@@ -643,7 +647,7 @@ function queryResourcesByUuid(string $uuid)
return $resource;
}
-function generatTagDeployWebhook($tag_name)
+function generateTagDeployWebhook($tag_name)
{
$baseUrl = base_url();
$api = Url::fromString($baseUrl).'/api/v1';
@@ -667,7 +671,7 @@ 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";
@@ -683,7 +687,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 +742,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) {
@@ -1145,7 +1149,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 {
@@ -1174,10 +1178,10 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null
if ($domains->contains($naked_domain)) {
if (data_get($resource, 'uuid')) {
if ($resource->uuid !== $app->uuid) {
- throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:
{$app->name}.");
+ throw new \RuntimeException("Domain $naked_domain is already in use by another resource:
Link: {$app->name}");
}
} elseif ($domain) {
- throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:
{$app->name}.");
+ throw new \RuntimeException("Domain $naked_domain is already in use by another resource:
Link: {$app->name}");
}
}
}
@@ -1193,10 +1197,10 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null
if ($domains->contains($naked_domain)) {
if (data_get($resource, 'uuid')) {
if ($resource->uuid !== $app->uuid) {
- throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:
{$app->name}.");
+ throw new \RuntimeException("Domain $naked_domain is already in use by another resource:
Link: {$app->service->name}");
}
} elseif ($domain) {
- throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:
{$app->name}.");
+ throw new \RuntimeException("Domain $naked_domain is already in use by another resource:
سيتم تحديد الفرع 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 fa69c7035..5ea474b02 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/fr.json b/lang/fr.json
index cb089812e..dbd5a1bf7 100644
--- a/lang/fr.json
+++ b/lang/fr.json
@@ -1,30 +1,37 @@
{
- "auth.login": "Connexion",
- "auth.login.azure": "Connexion avec Microsoft",
- "auth.login.bitbucket": "Connexion avec Bitbucket",
- "auth.login.github": "Connexion avec GitHub",
- "auth.login.gitlab": "Connexion avec Gitlab",
- "auth.login.google": "Connexion avec Google",
- "auth.already_registered": "Déjà enregistré ?",
- "auth.confirm_password": "Confirmer le mot de passe",
- "auth.forgot_password": "Mot de passe oublié",
- "auth.forgot_password_send_email": "Envoyer l'email de réinitialisation de mot de passe",
- "auth.register_now": "S'enregistrer",
- "auth.logout": "Déconnexion",
- "auth.register": "S'enregistrer",
- "auth.registration_disabled": "L'enregistrement est désactivé. Merci de contacter l'administateur.",
- "auth.reset_password": "Réinitialiser le mot de passe",
- "auth.failed": "Aucune correspondance n'a été trouvée pour les informations d'identification renseignées.",
- "auth.failed.callback": "Erreur lors du processus de retour de la plateforme de connexion.",
- "auth.failed.password": "Le mot de passe renseigné est incorrect.",
- "auth.failed.email": "Aucun utilisateur avec cette adresse email n'a été trouvé.",
- "auth.throttle": "Trop de tentatives de connexion. Merci de réessayer dans :seconds secondes.",
- "input.name": "Nom",
- "input.email": "Email",
- "input.password": "Mot de passe",
- "input.password.again": "Mot de passe identique",
- "input.code": "Code à usage unique",
- "input.recovery_code": "Code de récupération",
- "button.save": "Sauvegarder",
- "repository.url": "Exemples Pour les dépôts publiques, utilisez https://.... Pour les dépôts privés, utilisez git@....
https://github.com/coollabsio/coolify-examples main sera la branche selectionnée https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify sera la branche selectionnée. https://gitea.com/sedlav/expressjs.git main sera la branche selectionnée. https://gitlab.com/andrasbacsai/nodejs-example.git main sera la branche selectionnée."
+ "auth.login": "Connexion",
+ "auth.login.azure": "Connexion avec Microsoft",
+ "auth.login.bitbucket": "Connexion avec Bitbucket",
+ "auth.login.github": "Connexion avec GitHub",
+ "auth.login.gitlab": "Connexion avec Gitlab",
+ "auth.login.google": "Connexion avec Google",
+ "auth.already_registered": "Déjà enregistré ?",
+ "auth.confirm_password": "Confirmer le mot de passe",
+ "auth.forgot_password": "Mot de passe oublié",
+ "auth.forgot_password_send_email": "Envoyer l'email de réinitialisation de mot de passe",
+ "auth.register_now": "S'enregistrer",
+ "auth.logout": "Déconnexion",
+ "auth.register": "S'enregistrer",
+ "auth.registration_disabled": "L'enregistrement est désactivé. Merci de contacter l'administrateur.",
+ "auth.reset_password": "Réinitialiser le mot de passe",
+ "auth.failed": "Aucune correspondance n'a été trouvée pour les informations d'identification renseignées.",
+ "auth.failed.callback": "Erreur lors du processus de retour de la plateforme de connexion.",
+ "auth.failed.password": "Le mot de passe renseigné est incorrect.",
+ "auth.failed.email": "Aucun utilisateur avec cette adresse email n'a été trouvé.",
+ "auth.throttle": "Trop de tentatives de connexion. Merci de réessayer dans :seconds secondes.",
+ "input.name": "Nom",
+ "input.email": "Email",
+ "input.password": "Mot de passe",
+ "input.password.again": "Mot de passe identique",
+ "input.code": "Code à usage unique",
+ "input.recovery_code": "Code de récupération",
+ "button.save": "Sauvegarder",
+ "repository.url": "Exemples Pour les dépôts publiques, utilisez https://.... Pour les dépôts privés, utilisez git@....
https://github.com/coollabsio/coolify-examples main sera la branche selectionnée https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify sera la branche selectionnée. https://gitea.com/sedlav/expressjs.git main sera la branche selectionnée. https://gitlab.com/andrasbacsai/nodejs-example.git main sera la branche selectionnée.",
+ "service.stop": "Ce service sera arrêté.",
+ "resource.docker_cleanup": "Exécuter le nettoyage Docker (supprimer les images inutilisées et le cache du builder).",
+ "resource.non_persistent": "Toutes les données non persistantes seront supprimées.",
+ "resource.delete_volumes": "Supprimer définitivement tous les volumes associés à cette ressource.",
+ "resource.delete_connected_networks": "Supprimer définitivement tous les réseaux non-prédéfinis associés à cette ressource.",
+ "resource.delete_configurations": "Supprimer définitivement tous les fichiers de configuration du serveur.",
+ "database.delete_backups_locally": "Toutes les sauvegardes seront définitivement supprimées du stockage local."
}
diff --git a/lang/ro.json b/lang/ro.json
new file mode 100644
index 000000000..db1aa85db
--- /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 91d5c1443..d2616e9c6 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/public/svgs/affine.svg b/public/svgs/affine.svg
new file mode 100644
index 000000000..d8063e920
--- /dev/null
+++ b/public/svgs/affine.svg
@@ -0,0 +1,88 @@
+
diff --git a/public/svgs/audiobookshelf.svg b/public/svgs/audiobookshelf.svg
new file mode 100644
index 000000000..d641b765b
--- /dev/null
+++ b/public/svgs/audiobookshelf.svg
@@ -0,0 +1,25 @@
+
\ No newline at end of file
diff --git a/public/svgs/azimutt.png b/public/svgs/azimutt.png
new file mode 100644
index 000000000..ef69062cd
Binary files /dev/null and b/public/svgs/azimutt.png differ
diff --git a/public/svgs/bookstack.png b/public/svgs/bookstack.png
new file mode 100644
index 000000000..d10b3ca43
Binary files /dev/null and b/public/svgs/bookstack.png differ
diff --git a/public/svgs/calcom.svg b/public/svgs/calcom.svg
new file mode 100644
index 000000000..446b16655
--- /dev/null
+++ b/public/svgs/calcom.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/public/svgs/castopod.svg b/public/svgs/castopod.svg
new file mode 100644
index 000000000..c73008400
--- /dev/null
+++ b/public/svgs/castopod.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/public/svgs/cloudbeaver.svg b/public/svgs/cloudbeaver.svg
new file mode 100644
index 000000000..4a7634766
--- /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 000000000..45b7f795c
--- /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 000000000..be121cfd0
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 000000000..326acf789
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 000000000..a906f7f7e
--- /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 000000000..6b0be0d2a
Binary files /dev/null and b/public/svgs/flowise.png differ
diff --git a/public/svgs/forgejo.svg b/public/svgs/forgejo.svg
new file mode 100644
index 000000000..804b05e28
--- /dev/null
+++ b/public/svgs/forgejo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svgs/foundryvtt.png b/public/svgs/foundryvtt.png
new file mode 100644
index 000000000..c6a04508f
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 000000000..d1a75118f
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 000000000..ff29ca654
--- /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 000000000..9d844a772
--- /dev/null
+++ b/public/svgs/immich.svg
@@ -0,0 +1,66 @@
+
+
+
diff --git a/public/svgs/joplin.png b/public/svgs/joplin.png
new file mode 100644
index 000000000..d17a1d2c1
Binary files /dev/null and b/public/svgs/joplin.png differ
diff --git a/public/svgs/keycloak.svg b/public/svgs/keycloak.svg
new file mode 100644
index 000000000..849ac2759
--- /dev/null
+++ b/public/svgs/keycloak.svg
@@ -0,0 +1,55 @@
+
\ No newline at end of file
diff --git a/public/svgs/kimai.svg b/public/svgs/kimai.svg
new file mode 100644
index 000000000..35b146972
--- /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 000000000..103d47d60
--- /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 000000000..aa0b8e038
--- /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 000000000..d1a99e148
Binary files /dev/null and b/public/svgs/martin.png differ
diff --git a/public/svgs/mattermost.svg b/public/svgs/mattermost.svg
new file mode 100644
index 000000000..b01d38eb7
--- /dev/null
+++ b/public/svgs/mattermost.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/svgs/mautic.svg b/public/svgs/mautic.svg
new file mode 100644
index 000000000..b528f72ef
--- /dev/null
+++ b/public/svgs/mautic.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/public/svgs/mindsdb.svg b/public/svgs/mindsdb.svg
new file mode 100644
index 000000000..53799dd1c
--- /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 000000000..eb287a7cd
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 000000000..9e5b5136f
--- /dev/null
+++ b/public/svgs/ntfy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svgs/onedev.svg b/public/svgs/onedev.svg
new file mode 100644
index 000000000..fb9c9c060
--- /dev/null
+++ b/public/svgs/onedev.svg
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/svgs/osticket.png b/public/svgs/osticket.png
new file mode 100644
index 000000000..65885b71b
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 000000000..83631e3f5
--- /dev/null
+++ b/public/svgs/owncloud.svg
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/svgs/paperless.svg b/public/svgs/paperless.svg
new file mode 100644
index 000000000..347b1e759
--- /dev/null
+++ b/public/svgs/paperless.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/public/svgs/peppermint.png b/public/svgs/peppermint.png
new file mode 100644
index 000000000..38db83de0
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 000000000..69d8cf62a
--- /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 000000000..c747aea05
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 000000000..9a11f77f4
--- /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 000000000..f5ff6fabc
--- /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 000000000..2b66b3087
--- /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 000000000..81823b3eb
--- /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 000000000..7d51b32dc
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 000000000..2b8f6972d
Binary files /dev/null and b/public/svgs/zipline.png differ
diff --git a/resources/js/terminal.js b/resources/js/terminal.js
index 4cdab0e07..59c9a79a8 100644
--- a/resources/js/terminal.js
+++ b/resources/js/terminal.js
@@ -184,10 +184,6 @@ export function initializeTerminalComponent() {
// Copy and paste functionality
this.term.attachCustomKeyEventHandler((arg) => {
if (arg.ctrlKey && arg.code === "KeyV" && arg.type === "keydown") {
- navigator.clipboard.readText()
- .then(text => {
- this.socket.send(JSON.stringify({ message: text }));
- });
return false;
}
diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php
index 7d615885f..d6c3edf84 100644
--- a/resources/views/auth/login.blade.php
+++ b/resources/views/auth/login.blade.php
@@ -5,6 +5,13 @@
Coolify