diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index 8a369edc1d..0bd347ea78 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -2,20 +2,13 @@ namespace App\Livewire\Server; -use App\Actions\Proxy\DeleteTraefikAcmeBackup; -use App\Actions\Proxy\DeleteTraefikCertificate; use App\Actions\Proxy\GetProxyConfiguration; -use App\Actions\Proxy\GetTraefikCertificates; -use App\Actions\Proxy\ListTraefikAcmeBackups; -use App\Actions\Proxy\RestoreTraefikAcmeBackup; use App\Actions\Proxy\SaveProxyConfiguration; use App\Enums\ProxyTypes; -use App\Jobs\RestartProxyJob; use App\Models\Server; use App\Rules\SafeExternalUrl; use App\Traits\ListensToTeamChannel; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; -use Illuminate\Support\Facades\Gate; use Livewire\Component; class Proxy extends Component @@ -35,13 +28,6 @@ class Proxy extends Component public bool $generateExactLabels = false; - public array $traefikCertificates = []; - - public bool $traefikCertificatesLoaded = false; - - /** @var array */ - public array $traefikAcmeBackups = []; - /** * Cache the versions.json file data in memory for this component instance. * This avoids multiple file reads during a single request/render cycle. @@ -237,114 +223,6 @@ class Proxy extends Component return $this->server->outdatedCaddyProxyImage(); } - public function loadTraefikCertificates(): void - { - $this->traefikCertificates = []; - - try { - $this->authorize('view', $this->server); - $this->traefikCertificates = GetTraefikCertificates::run($this->server); - $this->traefikCertificatesLoaded = true; - } catch (\Throwable $e) { - $this->traefikCertificatesLoaded = true; - handleError($e, $this); - } - - $this->loadTraefikAcmeBackups(); - } - - private function loadTraefikAcmeBackups(): void - { - $this->traefikAcmeBackups = []; - - try { - if (Gate::allows('manageProxy', $this->server)) { - $this->traefikAcmeBackups = ListTraefikAcmeBackups::run($this->server); - } - } catch (\Throwable $e) { - handleError($e, $this); - } - } - - /** Default of the "Restart the proxy now" option in the acme.json restore dialog. */ - public bool $restartProxyAfterAcmeRestore = true; - - /** - * @param array $selectedActions checkbox ids selected in the restore dialog - */ - public function restoreTraefikAcmeBackup(string $backupName, string $password = '', array $selectedActions = []): void - { - try { - $this->authorize('manageProxy', $this->server); - RestoreTraefikAcmeBackup::run($this->server, $backupName); - auditLog('ui.proxy.acme_backup_restored', [ - 'team_id' => $this->server->team_id, - 'server_uuid' => $this->server->uuid, - 'server_name' => $this->server->name, - 'backup' => $backupName, - ]); - $this->loadTraefikCertificates(); - - // A running Traefik keeps its certificates in memory and can write them back to acme.json. - if (in_array('restartProxyAfterAcmeRestore', $selectedActions, true)) { - RestartProxyJob::dispatch($this->server); - auditLog('ui.proxy.restarted', [ - 'team_id' => $this->server->team_id, - 'server_uuid' => $this->server->uuid, - 'server_name' => $this->server->name, - ]); - $this->dispatch('refreshServerShow'); - $this->dispatch('success', 'acme.json restored. The proxy is restarting to load the restored certificates.'); - - return; - } - - $this->dispatch('refreshServerShow'); - $this->dispatch('success', 'acme.json restored. Restart the proxy to load the restored certificates.'); - } catch (\Throwable $e) { - handleError($e, $this); - } - } - - public function deleteTraefikAcmeBackup(string $backupName, string $password = ''): void - { - try { - $this->authorize('manageProxy', $this->server); - DeleteTraefikAcmeBackup::run($this->server, $backupName); - auditLog('ui.proxy.acme_backup_deleted', [ - 'team_id' => $this->server->team_id, - 'server_uuid' => $this->server->uuid, - 'server_name' => $this->server->name, - 'backup' => $backupName, - ]); - $this->loadTraefikAcmeBackups(); - $this->dispatch('success', 'acme.json backup deleted.'); - } catch (\Throwable $e) { - handleError($e, $this); - } - } - - public function deleteTraefikCertificate(string $certificateId, string $password = ''): void - { - try { - $this->authorize('update', $this->server); - $certificate = DeleteTraefikCertificate::run($this->server, $certificateId); - auditLog('ui.proxy.certificate_deleted', [ - 'team_id' => $this->server->team_id, - 'server_uuid' => $this->server->uuid, - 'server_name' => $this->server->name, - 'domain' => $certificate['main_domain'], - 'resolver' => $certificate['resolver'], - ]); - $this->traefikCertificates = GetTraefikCertificates::run($this->server); - $this->loadTraefikAcmeBackups(); - $this->dispatch('refreshServerShow'); - $this->dispatch('success', 'TLS certificate deleted. Restart Traefik to remove it from the running proxy.'); - } catch (\Throwable $e) { - handleError($e, $this); - } - } - /** * Get the latest Traefik version for this server's current branch. * diff --git a/app/Livewire/Server/Proxy/Certificates.php b/app/Livewire/Server/Proxy/Certificates.php new file mode 100644 index 0000000000..93c16ae083 --- /dev/null +++ b/app/Livewire/Server/Proxy/Certificates.php @@ -0,0 +1,142 @@ + */ + public array $traefikAcmeBackups = []; + + public function loadTraefikCertificates(): void + { + $this->traefikCertificates = []; + + try { + $this->authorize('view', $this->server); + $this->traefikCertificates = GetTraefikCertificates::run($this->server); + $this->traefikCertificatesLoaded = true; + } catch (\Throwable $e) { + $this->traefikCertificatesLoaded = true; + handleError($e, $this); + } + + $this->loadTraefikAcmeBackups(); + } + + private function loadTraefikAcmeBackups(): void + { + $this->traefikAcmeBackups = []; + + try { + if (Gate::allows('manageProxy', $this->server)) { + $this->traefikAcmeBackups = ListTraefikAcmeBackups::run($this->server); + } + } catch (\Throwable $e) { + handleError($e, $this); + } + } + + /** Default of the "Restart the proxy now" option in the acme.json restore dialog. */ + public bool $restartProxyAfterAcmeRestore = true; + + /** + * @param array $selectedActions checkbox ids selected in the restore dialog + */ + public function restoreTraefikAcmeBackup(string $backupName, string $password = '', array $selectedActions = []): void + { + try { + $this->authorize('manageProxy', $this->server); + RestoreTraefikAcmeBackup::run($this->server, $backupName); + auditLog('ui.proxy.acme_backup_restored', [ + 'team_id' => $this->server->team_id, + 'server_uuid' => $this->server->uuid, + 'server_name' => $this->server->name, + 'backup' => $backupName, + ]); + $this->loadTraefikCertificates(); + + // A running Traefik keeps its certificates in memory and can write them back to acme.json. + if (in_array('restartProxyAfterAcmeRestore', $selectedActions, true)) { + RestartProxyJob::dispatch($this->server); + auditLog('ui.proxy.restarted', [ + 'team_id' => $this->server->team_id, + 'server_uuid' => $this->server->uuid, + 'server_name' => $this->server->name, + ]); + $this->dispatch('refreshServerShow'); + $this->dispatch('success', 'acme.json restored. The proxy is restarting to load the restored certificates.'); + + return; + } + + $this->dispatch('refreshServerShow'); + $this->dispatch('success', 'acme.json restored. Restart the proxy to load the restored certificates.'); + } catch (\Throwable $e) { + handleError($e, $this); + } + } + + public function deleteTraefikAcmeBackup(string $backupName, string $password = ''): void + { + try { + $this->authorize('manageProxy', $this->server); + DeleteTraefikAcmeBackup::run($this->server, $backupName); + auditLog('ui.proxy.acme_backup_deleted', [ + 'team_id' => $this->server->team_id, + 'server_uuid' => $this->server->uuid, + 'server_name' => $this->server->name, + 'backup' => $backupName, + ]); + $this->loadTraefikAcmeBackups(); + $this->dispatch('success', 'acme.json backup deleted.'); + } catch (\Throwable $e) { + handleError($e, $this); + } + } + + public function deleteTraefikCertificate(string $certificateId, string $password = ''): void + { + try { + $this->authorize('update', $this->server); + $certificate = DeleteTraefikCertificate::run($this->server, $certificateId); + auditLog('ui.proxy.certificate_deleted', [ + 'team_id' => $this->server->team_id, + 'server_uuid' => $this->server->uuid, + 'server_name' => $this->server->name, + 'domain' => $certificate['main_domain'], + 'resolver' => $certificate['resolver'], + ]); + $this->traefikCertificates = GetTraefikCertificates::run($this->server); + $this->loadTraefikAcmeBackups(); + $this->dispatch('refreshServerShow'); + $this->dispatch('success', 'TLS certificate deleted. Restart Traefik to remove it from the running proxy.'); + } catch (\Throwable $e) { + handleError($e, $this); + } + } + + public function render(): View + { + return view('livewire.server.proxy.certificates'); + } +} diff --git a/app/Livewire/Server/Proxy/CertificatesShow.php b/app/Livewire/Server/Proxy/CertificatesShow.php new file mode 100644 index 0000000000..53c56d2b05 --- /dev/null +++ b/app/Livewire/Server/Proxy/CertificatesShow.php @@ -0,0 +1,26 @@ +server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + $this->authorize('view', $this->server); + } + + public function render(): View + { + return view('livewire.server.proxy.certificates-show'); + } +} diff --git a/resources/views/components/server/sidebar.blade.php b/resources/views/components/server/sidebar.blade.php index 5418012621..f444210e00 100644 --- a/resources/views/components/server/sidebar.blade.php +++ b/resources/views/components/server/sidebar.blade.php @@ -70,6 +70,7 @@ 'children' => [ ['label' => 'Configuration', 'route' => 'server.proxy', 'active' => $activeSubMenu === 'configuration', 'icon' => 'settings'], ['label' => 'Dynamic Configurations', 'route' => 'server.proxy.dynamic-confs', 'active' => $activeSubMenu === 'dynamic-confs', 'icon' => 'sliders', 'visible' => $server->proxySet()], + ['label' => 'TLS Certificates', 'route' => 'server.proxy.certificates', 'active' => $activeSubMenu === 'certificates', 'icon' => 'shield-star', 'visible' => $server->proxyType() === \App\Enums\ProxyTypes::TRAEFIK->value], ['label' => 'Logs', 'route' => 'server.proxy.logs', 'active' => $activeSubMenu === 'logs', 'icon' => 'file-content', 'visible' => $server->proxySet(), 'navigate' => false], ], ], diff --git a/resources/views/livewire/server/proxy.blade.php b/resources/views/livewire/server/proxy.blade.php index 1fe88d5248..037466932a 100644 --- a/resources/views/livewire/server/proxy.blade.php +++ b/resources/views/livewire/server/proxy.blade.php @@ -83,164 +83,6 @@ - @if ($server->proxyType() === ProxyTypes::TRAEFIK->value) - - - - Refresh - - - -
- -
- -
- @if ($traefikCertificatesLoaded && count($traefikCertificates) === 0) - - @elseif (count($traefikCertificates) > 0) -
-
- - - - - - - - - - - - @foreach ($traefikCertificates as $certificate) - - - - - - - - @endforeach - -
DomainResolverAlternative namesExpiresActions
- {{ $certificate['main_domain'] }} - - {{ $certificate['resolver'] }} - @if ($certificate['store']) - - ({{ $certificate['store'] }}) - - @endif - - @if (count($certificate['sans']) > 0) -
- @foreach ($certificate['sans'] as $domain) - - {{ $domain }} - - @endforeach -
- @else - None - @endif -
- {{ $certificate['expires_at'] ?? 'Unknown' }} - - @can('update', $server) - - @endcan -
-
-
- @endif - - @can('manageProxy', $server) - @if (count($traefikAcmeBackups) > 0) -
-
-

acme.json backups

-

- Coolify saves a copy of acme.json before it changes the file and keeps the - last {{ \App\Actions\Proxy\ListTraefikAcmeBackups::KEEP }} copies. -

-
-
-
- - - - - - - - - - - @foreach ($traefikAcmeBackups as $backup) - - - - - - - @endforeach - -
BackupCreatedSizeActions
- {{ $backup['name'] }} - {{ $backup['created_at'] }}{{ formatBytes($backup['size']) }} -
- - -
-
-
-
-
- @endif - @endcan -
-
- @endif - @php $proxyTitle = $server->proxyType() === ProxyTypes::TRAEFIK->value diff --git a/resources/views/livewire/server/proxy/certificates-show.blade.php b/resources/views/livewire/server/proxy/certificates-show.blade.php new file mode 100644 index 0000000000..a58ce1d6fd --- /dev/null +++ b/resources/views/livewire/server/proxy/certificates-show.blade.php @@ -0,0 +1,24 @@ +
+ + TLS Certificates | Coolify + + +
+ + @if ($server->isFunctional() && $server->proxyType() === \App\Enums\ProxyTypes::TRAEFIK->value) +
+ +
+ @else +
+ + + +
+ @endif +
+
diff --git a/resources/views/livewire/server/proxy/certificates.blade.php b/resources/views/livewire/server/proxy/certificates.blade.php new file mode 100644 index 0000000000..8afcaa228c --- /dev/null +++ b/resources/views/livewire/server/proxy/certificates.blade.php @@ -0,0 +1,167 @@ +@php use App\Enums\ProxyTypes; @endphp + +
+ @if ($server->hasPendingProxyConfiguration()) + + Restart the proxy to apply TLS certificate changes. + + @endif + @if ($server->proxyType() === ProxyTypes::TRAEFIK->value) + + + + Refresh + + + +
+ +
+ +
+ @if ($traefikCertificatesLoaded && count($traefikCertificates) === 0) + + @elseif (count($traefikCertificates) > 0) +
+
+ + + + + + + + + + + + @foreach ($traefikCertificates as $certificate) + + + + + + + + @endforeach + +
DomainResolverAlternative namesExpiresActions
+ {{ $certificate['main_domain'] }} + + {{ $certificate['resolver'] }} + @if ($certificate['store']) + + ({{ $certificate['store'] }}) + + @endif + + @if (count($certificate['sans']) > 0) +
+ @foreach ($certificate['sans'] as $domain) + + {{ $domain }} + + @endforeach +
+ @else + None + @endif +
+ {{ $certificate['expires_at'] ?? 'Unknown' }} + + @can('update', $server) + + @endcan +
+
+
+ @endif + + @can('manageProxy', $server) + @if (count($traefikAcmeBackups) > 0) +
+
+

acme.json backups

+

+ Coolify saves a copy of acme.json before it changes the file and keeps the + last {{ \App\Actions\Proxy\ListTraefikAcmeBackups::KEEP }} copies. +

+
+
+
+ + + + + + + + + + + @foreach ($traefikAcmeBackups as $backup) + + + + + + + @endforeach + +
BackupCreatedSizeActions
+ {{ $backup['name'] }} + {{ $backup['created_at'] }}{{ formatBytes($backup['size']) }} +
+ + +
+
+
+
+
+ @endif + @endcan +
+
+ @endif + +
diff --git a/routes/web.php b/routes/web.php index 4300ff674f..65a9cb4d48 100644 --- a/routes/web.php +++ b/routes/web.php @@ -70,6 +70,7 @@ use App\Livewire\Server\GithubRunners; use App\Livewire\Server\Index as ServerIndex; use App\Livewire\Server\LogDrains; use App\Livewire\Server\PrivateKey\Show as PrivateKeyShow; +use App\Livewire\Server\Proxy\CertificatesShow as ProxyCertificatesShow; use App\Livewire\Server\Proxy\DynamicConfigurations as ProxyDynamicConfigurations; use App\Livewire\Server\Proxy\Logs as ProxyLogs; use App\Livewire\Server\Proxy\Show as ProxyShow; @@ -397,6 +398,7 @@ Route::middleware(['auth', 'verified'])->group(function () { Route::get('/danger', DeleteServer::class)->name('server.delete'); Route::get('/transfer', ServerTransfer::class)->name('server.transfer'); Route::get('/proxy', ProxyShow::class)->name('server.proxy'); + Route::get('/proxy/certificates', ProxyCertificatesShow::class)->name('server.proxy.certificates'); Route::get('/proxy/dynamic', ProxyDynamicConfigurations::class)->name('server.proxy.dynamic-confs'); Route::get('/proxy/logs', ProxyLogs::class)->name('server.proxy.logs'); Route::get('/terminal', ExecuteContainerCommand::class)->name('server.command')->middleware('can.access.terminal'); diff --git a/tests/Feature/ProxyAcmeCertificateUiTest.php b/tests/Feature/ProxyAcmeCertificateUiTest.php index d562511ceb..f3fab3d44b 100644 --- a/tests/Feature/ProxyAcmeCertificateUiTest.php +++ b/tests/Feature/ProxyAcmeCertificateUiTest.php @@ -1,8 +1,8 @@ toContain('submitAction="deleteTraefikCertificate') diff --git a/tests/Feature/TraefikAcmeBackupTest.php b/tests/Feature/TraefikAcmeBackupTest.php index 25d17a4976..841cd351fe 100644 --- a/tests/Feature/TraefikAcmeBackupTest.php +++ b/tests/Feature/TraefikAcmeBackupTest.php @@ -8,7 +8,7 @@ use App\Actions\Proxy\RestoreTraefikAcmeBackup; use App\Actions\Proxy\SaveTraefikAcmeFile; use App\Enums\ProxyTypes; use App\Jobs\RestartProxyJob; -use App\Livewire\Server\Proxy; +use App\Livewire\Server\Proxy\Certificates; use App\Models\AuditEvent; use App\Models\InstanceSettings; use App\Models\PrivateKey; @@ -219,7 +219,7 @@ it('lets an admin see and restore acme.json backups from the proxy page', functi actingAsAcmeBackupUser($this->server->team, 'admin'); createAcmeBackup($this->proxyDirectory, 'acme.json.backup-20260101T000000Z-00000001', '{}'); - Livewire::test(Proxy::class, ['server' => $this->server]) + Livewire::test(Certificates::class, ['server' => $this->server]) ->call('loadTraefikCertificates') ->assertSee('acme.json.backup-20260101T000000Z-00000001') ->assertSee('2026-01-01 00:00:00 UTC') @@ -245,7 +245,7 @@ it('restarts the proxy after a restore when the restart option is selected', fun actingAsAcmeBackupUser($this->server->team, 'admin'); createAcmeBackup($this->proxyDirectory, 'acme.json.backup-20260101T000000Z-00000001', '{}'); - Livewire::test(Proxy::class, ['server' => $this->server]) + Livewire::test(Certificates::class, ['server' => $this->server]) ->assertSet('restartProxyAfterAcmeRestore', true) ->call('restoreTraefikAcmeBackup', 'acme.json.backup-20260101T000000Z-00000001', '', ['restartProxyAfterAcmeRestore']) ->assertDispatched('success'); @@ -260,7 +260,7 @@ it('does not restart the proxy after a restore when the restart option is cleare actingAsAcmeBackupUser($this->server->team, 'admin'); createAcmeBackup($this->proxyDirectory, 'acme.json.backup-20260101T000000Z-00000001', '{}'); - Livewire::test(Proxy::class, ['server' => $this->server]) + Livewire::test(Certificates::class, ['server' => $this->server]) ->call('restoreTraefikAcmeBackup', 'acme.json.backup-20260101T000000Z-00000001', '') ->assertDispatched('success'); @@ -271,7 +271,7 @@ it('lets an admin delete an acme.json backup from the proxy page', function () { actingAsAcmeBackupUser($this->server->team, 'admin'); createAcmeBackup($this->proxyDirectory, 'acme.json.backup-20260101T000000Z-00000001', '{}'); - Livewire::test(Proxy::class, ['server' => $this->server]) + Livewire::test(Certificates::class, ['server' => $this->server]) ->call('deleteTraefikAcmeBackup', 'acme.json.backup-20260101T000000Z-00000001') ->assertDispatched('success') ->assertSet('traefikAcmeBackups', []); @@ -283,7 +283,7 @@ it('does not let a member list, restore, or delete acme.json backups', function actingAsAcmeBackupUser($this->server->team, 'member'); createAcmeBackup($this->proxyDirectory, 'acme.json.backup-20260101T000000Z-00000001', '{}'); - Livewire::test(Proxy::class, ['server' => $this->server]) + Livewire::test(Certificates::class, ['server' => $this->server]) ->call('loadTraefikCertificates') ->assertSet('traefikCertificates', fn (array $certificates): bool => count($certificates) === 2) ->assertSet('traefikAcmeBackups', []) @@ -305,7 +305,7 @@ it('does not list, restore, or delete acme.json backups of another team', functi actingAsAcmeBackupUser(Team::factory()->create(), 'owner'); createAcmeBackup($this->proxyDirectory, 'acme.json.backup-20260101T000000Z-00000001', '{}'); - Livewire::test(Proxy::class, ['server' => $this->server]) + Livewire::test(Certificates::class, ['server' => $this->server]) ->call('loadTraefikCertificates') ->assertSet('traefikAcmeBackups', []) ->call('restoreTraefikAcmeBackup', 'acme.json.backup-20260101T000000Z-00000001') diff --git a/tests/Feature/TraefikCertificateRemoteFileTest.php b/tests/Feature/TraefikCertificateRemoteFileTest.php index f5f80efbbb..972d0f4986 100644 --- a/tests/Feature/TraefikCertificateRemoteFileTest.php +++ b/tests/Feature/TraefikCertificateRemoteFileTest.php @@ -4,7 +4,7 @@ use App\Actions\Development\SeedDevelopmentTraefikCertificates; use App\Actions\Proxy\DeleteTraefikCertificate; use App\Actions\Proxy\GetTraefikCertificates; use App\Enums\ProxyTypes; -use App\Livewire\Server\Proxy; +use App\Livewire\Server\Proxy\Certificates; use App\Models\AuditEvent; use App\Models\InstanceSettings; use App\Models\PrivateKey; @@ -99,7 +99,7 @@ it('shows the restart warning after deleting a certificate from the proxy page', session(['currentTeam' => $this->server->team]); $certificateId = GetTraefikCertificates::run($this->server)[0]['id']; - Livewire::test(Proxy::class, ['server' => $this->server]) + Livewire::test(Certificates::class, ['server' => $this->server]) ->assertDontSee('Restart the proxy to apply TLS certificate changes.') ->call('deleteTraefikCertificate', $certificateId) ->assertDispatched('refreshServerShow') @@ -122,7 +122,7 @@ it('lets an admin delete a certificate and records an audit event', function () actingAsTraefikCertificateUser($this->server->team, 'admin'); $certificate = GetTraefikCertificates::run($this->server)[0]; - Livewire::test(Proxy::class, ['server' => $this->server]) + Livewire::test(Certificates::class, ['server' => $this->server]) ->call('deleteTraefikCertificate', $certificate['id']) ->assertDispatched('success'); @@ -140,7 +140,7 @@ it('lets a member list certificates but not delete them', function () { actingAsTraefikCertificateUser($this->server->team, 'member'); $certificateId = GetTraefikCertificates::run($this->server)[0]['id']; - Livewire::test(Proxy::class, ['server' => $this->server]) + Livewire::test(Certificates::class, ['server' => $this->server]) ->call('loadTraefikCertificates') ->assertSet('traefikCertificates', fn (array $certificates): bool => count($certificates) === 40) ->assertDontSeeHtml('deleteTraefikCertificate(') @@ -157,7 +157,7 @@ it('does not show or delete certificates of another team', function () { actingAsTraefikCertificateUser(Team::factory()->create(), 'owner'); $certificateId = app(TraefikAcmeService::class)->certificates($this->acmeContents)[0]['id']; - Livewire::test(Proxy::class, ['server' => $this->server]) + Livewire::test(Certificates::class, ['server' => $this->server]) ->call('loadTraefikCertificates') ->assertSet('traefikCertificates', []) ->assertDontSee('app1.example.com') @@ -186,3 +186,38 @@ it('refuses to add example certificates outside development', function () { expect($this->uploadedContents)->toBeNull(); }); + +it('serves the certificate page for team users', function (string $role) { + $this->server->team->update(['show_boarding' => false]); + $this->server->settings->update(['is_reachable' => true, 'is_usable' => true]); + actingAsTraefikCertificateUser($this->server->team, $role); + + $this->get(route('server.proxy.certificates', ['server_uuid' => $this->server->uuid])) + ->assertOk() + ->assertSeeLivewire(Certificates::class); +})->with(['admin', 'member']); + +it('does not serve the certificate page of another team', function () { + actingAsTraefikCertificateUser(Team::factory()->create(['show_boarding' => false]), 'owner'); + + $this->get(route('server.proxy.certificates', ['server_uuid' => $this->server->uuid])) + ->assertNotFound(); +}); + +it('requires login to open the certificate page', function () { + $this->get(route('server.proxy.certificates', ['server_uuid' => $this->server->uuid])) + ->assertRedirect(route('login')); +}); + +it('does not load certificate controls for a Caddy server', function () { + actingAsTraefikCertificateUser($this->server->team, 'admin'); + $this->server->team->update(['show_boarding' => false]); + $this->server->settings->update(['is_reachable' => true, 'is_usable' => true]); + $this->server->proxy = ['type' => ProxyTypes::CADDY->value, 'status' => 'running']; + $this->server->save(); + + $this->get(route('server.proxy.certificates', ['server_uuid' => $this->server->uuid])) + ->assertOk() + ->assertDontSeeLivewire(Certificates::class) + ->assertSee('Traefik required'); +});