refactor(proxy-status): refactored how the proxy status is handled on the UI and on the backend

feat(cloudflare): improved cloudflare tunnel automated installation
This commit is contained in:
Andras Bacsai
2025-06-06 14:47:54 +02:00
parent 8e8400f595
commit ddcb14500d
51 changed files with 1277 additions and 829 deletions

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Livewire\Server;
use App\Actions\Server\ConfigureCloudflared;
use App\Models\Server;
use Livewire\Attributes\Validate;
use Livewire\Component;
class CloudflareTunnel extends Component
{
public Server $server;
#[Validate(['required', 'string'])]
public string $cloudflare_token;
#[Validate(['required', 'string'])]
public string $ssh_domain;
#[Validate(['required', 'boolean'])]
public bool $isCloudflareTunnelsEnabled;
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'refresh',
];
}
public function refresh()
{
$this->mount($this->server->uuid);
}
public function mount(string $server_uuid)
{
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
if ($this->server->isLocalhost()) {
return redirect()->route('server.show', ['server_uuid' => $server_uuid]);
}
$this->isCloudflareTunnelsEnabled = $this->server->settings->is_cloudflare_tunnel;
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function toggleCloudflareTunnels()
{
try {
remote_process(['docker rm -f coolify-cloudflared'], $this->server, false, 10);
$this->isCloudflareTunnelsEnabled = false;
$this->server->settings->is_cloudflare_tunnel = false;
$this->server->settings->save();
if ($this->server->ip_previous) {
$this->server->update(['ip' => $this->server->ip_previous]);
$this->dispatch('success', 'Cloudflare Tunnel disabled.<br><br>Manually updated the server IP address to its previous IP address.');
} else {
$this->dispatch('success', 'Cloudflare Tunnel disabled. Please update the server IP address to its real IP address in the server settings.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function manualCloudflareConfig()
{
$this->isCloudflareTunnelsEnabled = true;
$this->server->settings->is_cloudflare_tunnel = true;
$this->server->settings->save();
$this->server->refresh();
$this->dispatch('success', 'Cloudflare Tunnel enabled.');
}
public function automatedCloudflareConfig()
{
try {
if (str($this->ssh_domain)->contains('https://')) {
$this->ssh_domain = str($this->ssh_domain)->replace('https://', '')->replace('http://', '')->trim();
$this->ssh_domain = str($this->ssh_domain)->replace('/', '');
}
$activity = ConfigureCloudflared::run($this->server, $this->cloudflare_token, $this->ssh_domain);
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.server.cloudflare-tunnel');
}
}

View File

@@ -1,54 +0,0 @@
<?php
namespace App\Livewire\Server;
use App\Models\Server;
use Livewire\Attributes\Validate;
use Livewire\Component;
class CloudflareTunnels extends Component
{
public Server $server;
#[Validate(['required', 'boolean'])]
public bool $isCloudflareTunnelsEnabled;
public function mount(string $server_uuid)
{
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
if ($this->server->isLocalhost()) {
return redirect()->route('server.show', ['server_uuid' => $server_uuid]);
}
$this->isCloudflareTunnelsEnabled = $this->server->settings->is_cloudflare_tunnel;
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
try {
$this->validate();
$this->server->settings->is_cloudflare_tunnel = $this->isCloudflareTunnelsEnabled;
$this->server->settings->save();
$this->dispatch('success', 'Server updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function manualCloudflareConfig()
{
$this->isCloudflareTunnelsEnabled = true;
$this->server->settings->is_cloudflare_tunnel = true;
$this->server->settings->save();
$this->server->refresh();
$this->dispatch('success', 'Cloudflare Tunnels enabled.');
}
public function render()
{
return view('livewire.server.cloudflare-tunnels');
}
}

View File

@@ -1,54 +0,0 @@
<?php
namespace App\Livewire\Server;
use App\Actions\Server\ConfigureCloudflared;
use App\Models\Server;
use Livewire\Component;
class ConfigureCloudflareTunnels extends Component
{
public $server_id;
public string $cloudflare_token;
public string $ssh_domain;
public function alreadyConfigured()
{
try {
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
$server->settings->is_cloudflare_tunnel = true;
$server->settings->save();
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
$this->dispatch('refreshServerShow');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {
if (str($this->ssh_domain)->contains('https://')) {
$this->ssh_domain = str($this->ssh_domain)->replace('https://', '')->replace('http://', '')->trim();
// remove / from the end
$this->ssh_domain = str($this->ssh_domain)->replace('/', '');
}
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
ConfigureCloudflared::dispatch($server, $this->cloudflare_token);
$server->settings->is_cloudflare_tunnel = true;
$server->ip = $this->ssh_domain;
$server->save();
$server->settings->save();
$this->dispatch('info', 'Cloudflare Tunnels configuration started.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.server.configure-cloudflare-tunnels');
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace App\Livewire\Server;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Actions\Proxy\StopProxy;
use App\Jobs\RestartProxyJob;
use App\Models\Server;
use Livewire\Component;
class Navbar extends Component
{
public Server $server;
public bool $isChecking = false;
public ?string $currentRoute = null;
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ProxyStatusChangedUI" => 'showNotification',
];
}
public function mount(Server $server)
{
$this->server = $server;
$this->currentRoute = request()->route()->getName();
}
public function restart()
{
try {
RestartProxyJob::dispatch($this->server);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function checkProxy()
{
try {
CheckProxy::run($this->server, true);
$this->dispatch('startProxy')->self();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function startProxy()
{
try {
$activity = StartProxy::run($this->server, force: true);
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function stop(bool $forceStop = true)
{
try {
StopProxy::dispatch($this->server, $forceStop);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function checkProxyStatus()
{
if ($this->isChecking) {
return;
}
try {
$this->isChecking = true;
CheckProxy::run($this->server, true);
$this->showNotification();
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->isChecking = false;
}
}
public function showNotification()
{
$status = $this->server->proxy->status ?? 'unknown';
$forceStop = $this->server->proxy->force_stop ?? false;
switch ($status) {
case 'running':
$this->dispatch('success', 'Proxy is running.');
break;
case 'restarting':
$this->dispatch('info', 'Initiating proxy restart.');
break;
case 'exited':
if ($forceStop) {
$this->dispatch('info', 'Proxy is stopped manually.');
} else {
$this->dispatch('info', 'Proxy is stopped manually. Starting in a moment.');
}
break;
default:
$this->dispatch('warning', 'Proxy is not running.');
break;
}
}
public function render()
{
return view('livewire.server.navbar');
}
}

View File

@@ -19,7 +19,7 @@ class Proxy extends Component
public ?string $redirect_url = null;
protected $listeners = ['proxyStatusUpdated', 'saveConfiguration' => 'submit'];
protected $listeners = ['saveConfiguration' => 'submit'];
protected $rules = [
'server.settings.generate_exact_labels' => 'required|boolean',
@@ -32,10 +32,10 @@ class Proxy extends Component
$this->redirect_url = data_get($this->server, 'proxy.redirect_url');
}
public function proxyStatusUpdated()
{
$this->dispatch('refresh')->self();
}
// public function proxyStatusUpdated()
// {
// $this->dispatch('refresh')->self();
// }
public function changeProxy()
{

View File

@@ -1,106 +0,0 @@
<?php
namespace App\Livewire\Server\Proxy;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Actions\Proxy\StopProxy;
use App\Events\ProxyStatusChanged;
use App\Jobs\RestartProxyJob;
use App\Models\Server;
use Livewire\Component;
class Deploy extends Component
{
public Server $server;
public bool $traefikDashboardAvailable = false;
public ?string $currentRoute = null;
public ?string $serverIp = null;
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ProxyStatusChanged" => 'proxyStarted',
'proxyStatusUpdated',
'traefikDashboardAvailable',
'serverRefresh' => 'proxyStatusUpdated',
'checkProxy',
'startProxy',
'proxyChanged' => 'proxyStatusUpdated',
];
}
public function mount()
{
if ($this->server->id === 0) {
$this->serverIp = base_ip();
} else {
$this->serverIp = $this->server->ip;
}
$this->currentRoute = request()->route()->getName();
}
public function traefikDashboardAvailable(bool $data)
{
$this->traefikDashboardAvailable = $data;
}
public function proxyStarted()
{
CheckProxy::run($this->server, true);
$this->dispatch('proxyStatusUpdated');
}
public function proxyStatusUpdated()
{
$this->server->refresh();
}
public function restart()
{
try {
RestartProxyJob::dispatch($this->server);
$this->dispatch('checkProxy');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function checkProxy()
{
try {
CheckProxy::run($this->server, true);
$this->dispatch('startProxyPolling');
$this->dispatch('proxyChecked');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function startProxy()
{
try {
$this->server->proxy->force_stop = false;
$this->server->save();
$activity = StartProxy::run($this->server, force: true);
$this->dispatch('activityMonitor', $activity->id, ProxyStatusChanged::class);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function stop(bool $forceStop = true)
{
try {
StopProxy::run($this->server, $forceStop);
$this->dispatch('proxyStatusUpdated');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -19,7 +19,7 @@ class DynamicConfigurations extends Component
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ProxyStatusChanged" => 'loadDynamicConfigurations',
"echo-private:team.{$teamId},ProxyStatusChangedUI" => 'loadDynamicConfigurations',
'loadDynamicConfigurations',
];
}

View File

@@ -11,12 +11,12 @@ class Show extends Component
public $parameters = [];
protected $listeners = ['proxyStatusUpdated', 'proxyChanged' => 'proxyStatusUpdated'];
// protected $listeners = ['proxyStatusUpdated'];
public function proxyStatusUpdated()
{
$this->server->refresh();
}
// public function proxyStatusUpdated()
// {
// $this->server->refresh();
// }
public function mount()
{

View File

@@ -1,77 +0,0 @@
<?php
namespace App\Livewire\Server\Proxy;
use App\Actions\Docker\GetContainersStatus;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Models\Server;
use Livewire\Component;
class Status extends Component
{
public Server $server;
public bool $polling = false;
public int $numberOfPolls = 0;
protected $listeners = [
'proxyStatusUpdated',
'startProxyPolling',
];
public function startProxyPolling()
{
$this->checkProxy();
}
public function proxyStatusUpdated()
{
$this->server->refresh();
}
public function checkProxy(bool $notification = false)
{
try {
if ($this->polling) {
if ($this->numberOfPolls >= 10) {
$this->polling = false;
$this->numberOfPolls = 0;
$notification && $this->dispatch('error', 'Proxy is not running.');
return;
}
$this->numberOfPolls++;
}
$shouldStart = CheckProxy::run($this->server, true);
if ($shouldStart) {
StartProxy::run($this->server, false);
}
$this->dispatch('proxyStatusUpdated');
if ($this->server->proxy->status === 'running') {
$this->polling = false;
$notification && $this->dispatch('success', 'Proxy is running.');
} elseif ($this->server->proxy->status === 'exited' and ! $this->server->proxy->force_stop) {
$notification && $this->dispatch('error', 'Proxy has exited.');
} elseif ($this->server->proxy->force_stop) {
$notification && $this->dispatch('error', 'Proxy is stopped manually.');
} else {
$notification && $this->dispatch('error', 'Proxy is not running.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function getProxyStatus()
{
try {
GetContainersStatus::run($this->server);
// dispatch_sync(new ContainerStatusJob($this->server));
$this->dispatch('proxyStatusUpdated');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -86,10 +86,7 @@ class Show extends Component
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'refresh',
'refreshServerShow' => 'refresh',
];
}
@@ -211,7 +208,6 @@ class Show extends Component
$this->server->settings->is_usable = $this->isUsable = true;
$this->server->settings->save();
ServerReachabilityChanged::dispatch($this->server);
$this->dispatch('proxyStatusUpdated');
} else {
$this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br>Error: '.$error);