From 178e6665f8d05cfc7e7c31ca0e02a96cab4957ef Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 28 Sep 2026 08:40:01 +0200 Subject: [PATCH] fix: restore security and broken-feature fixes lost in the revert Security: - Settings email: authorize the SMTP and Resend toggles. - Server navbar: show Sentinel status only to users who can view Sentinel. - GitHub source: permission gates on Sync name and Delete. Broken features: - Profile: avatar upload (missing import and file uploads trait). - API tokens: keep the "Never" expiry choice. - Environment variables: show validation errors instead of a generic error. - Servers API: read the server uuid from the route in domains_by_server, so ?uuid= filters instead of returning 404. - Cloud servers: Power On for stopped Hetzner, DigitalOcean and Vultr servers. Co-Authored-By: Claude Opus 5.5 --- .../Controllers/Api/ServersController.php | 2 +- app/Livewire/Profile/Index.php | 4 +++ .../Shared/EnvironmentVariable/Show.php | 4 ++- app/Livewire/Security/ApiTokens.php | 10 ++++++-- app/Livewire/SettingsEmail.php | 2 ++ .../views/livewire/server/navbar.blade.php | 4 ++- .../views/livewire/server/show.blade.php | 18 +++++++++++++ .../livewire/source/github/change.blade.php | 6 +++-- .../SettingsEmailProviderExclusivityTest.php | 25 +++++++++++++++++++ 9 files changed, 68 insertions(+), 7 deletions(-) diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index 76e36fe873..f886bef8c8 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -305,7 +305,7 @@ class ServersController extends Controller if (is_null($teamId)) { return invalidTokenResponse(); } - $server = ModelsServer::whereTeamId($teamId)->whereUuid($request->uuid)->first(); + $server = ModelsServer::whereTeamId($teamId)->whereUuid($request->route('uuid'))->first(); if (is_null($server)) { return response()->json(['message' => 'Server not found.'], 404); } diff --git a/app/Livewire/Profile/Index.php b/app/Livewire/Profile/Index.php index 9b1ef42ce2..a69424cffd 100644 --- a/app/Livewire/Profile/Index.php +++ b/app/Livewire/Profile/Index.php @@ -2,15 +2,19 @@ namespace App\Livewire\Profile; +use App\Services\AvatarStorageService; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\RateLimiter; use Illuminate\Validation\Rules\Password; use Livewire\Attributes\Validate; use Livewire\Component; +use Livewire\WithFileUploads; class Index extends Component { + use WithFileUploads; + public int $userId; public string $email; diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index e47d3818fe..694f18c9c9 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -17,6 +17,7 @@ use App\Traits\HasSecretManagerAutocomplete; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Illuminate\Validation\ValidationException; use Livewire\Attributes\Computed; use Livewire\Component; @@ -188,7 +189,6 @@ class Show extends Component } private function syncData(bool $toModel = false): void - { if ($toModel) { $this->key = ValidationPatterns::normalizeEnvironmentVariableKey($this->key); @@ -330,6 +330,8 @@ class Show extends Component if ($this->is_required && $this->resource instanceof Service) { event(new ApplicationConfigurationChanged($this->resource->team()->id)); } + } catch (ValidationException $e) { + throw $e; } catch (\Exception $e) { return handleError($e); } diff --git a/app/Livewire/Security/ApiTokens.php b/app/Livewire/Security/ApiTokens.php index 7a530fe297..ca98698450 100644 --- a/app/Livewire/Security/ApiTokens.php +++ b/app/Livewire/Security/ApiTokens.php @@ -15,7 +15,13 @@ class ApiTokens extends Component public ?string $description = null; - public ?int $expiresInDays = 30; + /** + * Untyped on purpose: Livewire skips hydrating null into typed properties, which + * would silently revert a "Never" (null) choice back to the default on the next request. + * + * @var int|string|null + */ + public $expiresInDays = 30; public $tokens = []; @@ -143,7 +149,7 @@ class ApiTokens extends Component throw new \Exception('You do not have permission to create tokens with read:sensitive permissions.'); } - $expiresAt = $this->expiresInDays ? now()->addDays($this->expiresInDays) : null; + $expiresAt = filled($validated['expiresInDays'] ?? null) ? now()->addDays((int) $validated['expiresInDays']) : null; $token = auth()->user()->createToken($this->description, $permissions, $expiresAt); auditLog('ui.api_token.created', [ 'team_id' => currentTeam()->id, diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php index 975ce9a241..320285970d 100644 --- a/app/Livewire/SettingsEmail.php +++ b/app/Livewire/SettingsEmail.php @@ -163,6 +163,7 @@ class SettingsEmail extends Component public function toggleSmtp() { try { + $this->authorize('update', $this->settings); $this->resetErrorBag(); if ($this->smtpEnabled) { @@ -185,6 +186,7 @@ class SettingsEmail extends Component public function toggleResend() { try { + $this->authorize('update', $this->settings); $this->resetErrorBag(); if ($this->resendEnabled) { diff --git a/resources/views/livewire/server/navbar.blade.php b/resources/views/livewire/server/navbar.blade.php index 41aedc45ae..01cc64891c 100644 --- a/resources/views/livewire/server/navbar.blade.php +++ b/resources/views/livewire/server/navbar.blade.php @@ -85,7 +85,9 @@ $activeServerMenuItem['route'], $serverRouteParameters, ); - $showSentinelStatus = $server->isFunctional() && $server->isSentinelEnabled(); + $showSentinelStatus = $server->isFunctional() + && $server->isSentinelEnabled() + && auth()->user()?->can('viewSentinel', $server); $proxyCanBeStopped = in_array($proxyStatus, ['running', 'starting', 'restarting'], true); @endphp diff --git a/resources/views/livewire/server/show.blade.php b/resources/views/livewire/server/show.blade.php index 4edbf479a3..6ae3c02aa8 100644 --- a/resources/views/livewire/server/show.blade.php +++ b/resources/views/livewire/server/show.blade.php @@ -88,6 +88,24 @@ @endif + @if ($server->cloudProviderToken) + @if ($provider === 'Hetzner' && !$server->isFunctional() && $hetznerServerStatus === 'off') + + Power On + + @elseif ($provider === 'DigitalOcean' && $digitalOceanDropletStatus === 'off') + + Power On + + @elseif ($provider === 'Vultr' && $vultrInstanceStatus === 'stopped') + + Power On + + @endif + @endif @endif @if ($server->server_metadata) @endif - + Sync name @@ -197,7 +198,8 @@ shortConfirmationLabel="GitHub App Name" :confirmWithPassword="false" step2ButtonText="Permanently Delete" /> @else - + Delete @endcan diff --git a/tests/Feature/SettingsEmailProviderExclusivityTest.php b/tests/Feature/SettingsEmailProviderExclusivityTest.php index 7e6b6ff232..c63df9fe40 100644 --- a/tests/Feature/SettingsEmailProviderExclusivityTest.php +++ b/tests/Feature/SettingsEmailProviderExclusivityTest.php @@ -62,3 +62,28 @@ test('enabling Resend disables SMTP in storage', function () { expect($this->settings->resend_enabled)->toBeTrue(); expect($this->settings->smtp_enabled)->toBeFalse(); }); + +test('a user who is no longer an instance admin cannot toggle email providers', function (string $action, string $column) { + $this->settings->update([ + 'smtp_enabled' => $column === 'smtp_enabled', + 'resend_enabled' => $column === 'resend_enabled', + 'resend_api_key' => 're_test_key', + 'smtp_host' => 'smtp.example.com', + 'smtp_port' => '587', + 'smtp_from_address' => 'from@example.com', + 'smtp_from_name' => 'Coolify', + ]); + + $component = Livewire::test(SettingsEmail::class); + + $member = User::factory()->create(); + $member->teams()->attach($this->rootTeam, ['role' => 'member']); + $this->actingAs($member); + + $component->call($action)->assertDispatched('error'); + + expect($this->settings->refresh()->{$column})->toBeTrue(); +})->with([ + 'SMTP' => ['toggleSmtp', 'smtp_enabled'], + 'Resend' => ['toggleResend', 'resend_enabled'], +]);