From a32082b03fe956bbe6b257375cb0e36022e4e0be Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 28 Sep 2026 00:18:09 +0200 Subject: [PATCH] fix: regressions found by the test suite - TrustHosts: validate the Host header also when the FQDN cache is cold. - Settings email: authorize the SMTP and Resend toggles. - Server navbar: show Sentinel status only to users who can view Sentinel. - GitHub source: restore permission gates on Sync name and Delete, and the Test connection button. - Profile: make avatar upload work (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. - Configuration diff: report all settings when no earlier snapshot exists. - Cloud servers: restore Power On for stopped servers; do not repeat the provider error prefix. - HasSecretManager: do not stop later deleting listeners. - GitLab webhook: use new_public_id() like the other webhooks. - Dev: show the Lima VM start command on the localhost server page. Co-Authored-By: Claude Opus 5.5 --- .../Controllers/Api/ServersController.php | 2 +- app/Http/Controllers/Webhook/Gitlab.php | 7 ++- app/Http/Middleware/TrustHosts.php | 52 +++++++++++-------- app/Livewire/Profile/Index.php | 4 ++ .../Shared/EnvironmentVariable/Show.php | 4 +- app/Livewire/Security/ApiTokens.php | 10 +++- app/Livewire/Server/New/ByDigitalOcean.php | 4 +- app/Livewire/Server/New/ByVultr.php | 4 +- app/Livewire/SettingsEmail.php | 2 + .../ConfigurationDiffer.php | 4 +- app/Traits/HasSecretManager.php | 5 +- .../views/livewire/server/navbar.blade.php | 4 +- .../partials/localhost-general.blade.php | 7 +++ .../views/livewire/server/show.blade.php | 18 +++++++ .../livewire/source/github/change.blade.php | 9 +++- 15 files changed, 98 insertions(+), 38 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/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php index 27138135ef..62199ba6ec 100644 --- a/app/Http/Controllers/Webhook/Gitlab.php +++ b/app/Http/Controllers/Webhook/Gitlab.php @@ -17,7 +17,6 @@ use Exception; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; -use Visus\Cuid2\Cuid2; class Gitlab extends Controller { @@ -185,7 +184,7 @@ class Gitlab extends Controller continue; } - $deployment_uuid = new Cuid2; + $deployment_uuid = new_public_id(); $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, @@ -203,7 +202,7 @@ class Gitlab extends Controller 'mode' => 'app', 'application_uuid' => $application->uuid, 'application_name' => $application->name, - 'deployment_uuid' => $deployment_uuid->toString(), + 'deployment_uuid' => $deployment_uuid, 'commit' => $commit, ]); @@ -275,7 +274,7 @@ class Gitlab extends Controller continue; } - $deployment_uuid = new Cuid2; + $deployment_uuid = new_public_id(); $found = ApplicationPreview::where('application_id', $application->id) ->where('pull_request_id', $pull_request_id) ->first(); diff --git a/app/Http/Middleware/TrustHosts.php b/app/Http/Middleware/TrustHosts.php index 350d5359c1..a12feccd34 100644 --- a/app/Http/Middleware/TrustHosts.php +++ b/app/Http/Middleware/TrustHosts.php @@ -28,8 +28,7 @@ class TrustHosts extends Middleware } // Skip host validation if no FQDN is configured (initial setup) - $fqdnHost = Cache::get('instance_settings_fqdn_host'); - if ($fqdnHost === '' || $fqdnHost === null) { + if ($this->configuredFqdnHost() === null) { return $next($request); } @@ -46,27 +45,7 @@ class TrustHosts extends Middleware { $trustedHosts = []; - // Trust the configured FQDN from InstanceSettings (cached to avoid DB query on every request) - // Use empty string as sentinel value instead of null so negative results are cached - $fqdnHost = Cache::remember('instance_settings_fqdn_host', 300, function () { - try { - $settings = InstanceSettings::get(); - if ($settings && $settings->fqdn) { - $url = Url::fromString($settings->fqdn); - $host = $url->getHost(); - - return $host ?: ''; - } - } catch (\Exception $e) { - // If instance settings table doesn't exist yet (during installation), - // return empty string (sentinel) so this result is cached - } - - return ''; - }); - - // Convert sentinel value back to null for consumption - $fqdnHost = $fqdnHost !== '' ? $fqdnHost : null; + $fqdnHost = $this->configuredFqdnHost(); if ($fqdnHost) { $trustedHosts[] = $fqdnHost; @@ -97,4 +76,31 @@ class TrustHosts extends Middleware return array_filter($trustedHosts); } + + /** + * Resolve the configured instance FQDN host, cached to avoid a DB query on every request. + * + * An empty string is cached as the "no FQDN" sentinel so negative results are cached too. + */ + private function configuredFqdnHost(): ?string + { + $fqdnHost = Cache::remember('instance_settings_fqdn_host', 300, function () { + try { + $settings = InstanceSettings::get(); + if ($settings && $settings->fqdn) { + $url = Url::fromString($settings->fqdn); + $host = $url->getHost(); + + return $host ?: ''; + } + } catch (\Exception $e) { + // If instance settings table doesn't exist yet (during installation), + // return empty string (sentinel) so this result is cached + } + + return ''; + }); + + return $fqdnHost !== '' ? $fqdnHost : null; + } } 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/Server/New/ByDigitalOcean.php b/app/Livewire/Server/New/ByDigitalOcean.php index d05532776e..1cfcc56634 100644 --- a/app/Livewire/Server/New/ByDigitalOcean.php +++ b/app/Livewire/Server/New/ByDigitalOcean.php @@ -296,7 +296,9 @@ class ByDigitalOcean extends Component $details = data_get($e->response->json(), $jsonMessageKey) ?: $e->response->body() ?: $details; } - return "{$providerName} API error: {$details}"; + $prefix = "{$providerName} API error: "; + + return str_starts_with($details, $prefix) ? $details : $prefix.$details; } public function getAvailableSizesProperty(): array diff --git a/app/Livewire/Server/New/ByVultr.php b/app/Livewire/Server/New/ByVultr.php index 3a6ce532df..321d8f6588 100644 --- a/app/Livewire/Server/New/ByVultr.php +++ b/app/Livewire/Server/New/ByVultr.php @@ -375,7 +375,9 @@ class ByVultr extends Component $details = data_get($e->response->json(), $jsonMessageKey) ?: $e->response->body() ?: $details; } - return "{$providerName} API error: {$details}"; + $prefix = "{$providerName} API error: "; + + return str_starts_with($details, $prefix) ? $details : $prefix.$details; } private function createVultrServer(VultrService $vultrService): array 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/app/Services/DeploymentConfiguration/ConfigurationDiffer.php b/app/Services/DeploymentConfiguration/ConfigurationDiffer.php index ace4888338..688db3f0a4 100644 --- a/app/Services/DeploymentConfiguration/ConfigurationDiffer.php +++ b/app/Services/DeploymentConfiguration/ConfigurationDiffer.php @@ -39,7 +39,9 @@ class ConfigurationDiffer $previous = $previousItems[$key] ?? null; $current = $currentItems[$key] ?? null; - if ($previous === null && ! in_array(data_get($current, 'section'), self::DYNAMIC_SECTIONS, true)) { + // Static settings missing from an older snapshot were newly tracked, not changed. + // Without any previous snapshot (legacy fallback), every setting is reported. + if ($previous === null && $previousItems !== [] && ! in_array(data_get($current, 'section'), self::DYNAMIC_SECTIONS, true)) { continue; } diff --git a/app/Traits/HasSecretManager.php b/app/Traits/HasSecretManager.php index 8b3e50b7bd..a788ae823e 100644 --- a/app/Traits/HasSecretManager.php +++ b/app/Traits/HasSecretManager.php @@ -15,7 +15,10 @@ trait HasSecretManager public static function bootHasSecretManager(): void { - static::deleting(fn ($resource) => $resource->secretManagerLink()->delete()); + // Return nothing: a non-null result halts the "deleting" event and skips later listeners. + static::deleting(function ($resource): void { + $resource->secretManagerLink()->delete(); + }); } public function secretManagerLink(): MorphOne 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/partials/localhost-general.blade.php b/resources/views/livewire/server/partials/localhost-general.blade.php index fef1609998..3876b4edbe 100644 --- a/resources/views/livewire/server/partials/localhost-general.blade.php +++ b/resources/views/livewire/server/partials/localhost-general.blade.php @@ -13,6 +13,13 @@ :type="$server->isFunctional() ? 'success' : 'warning'" /> + @if ($this->limaStartCommand) + + {{ $this->limaStartCommand }} + + @endif +
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) - + @if ($isConnected) + + Test connection + + @endif + Sync name @@ -191,7 +196,7 @@ shortConfirmationLabel="GitHub App Name" :confirmWithPassword="false" step2ButtonText="Permanently Delete" /> @else - + Delete @endcan