mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
1e66c9f9bd
commit
a32082b03f
15 files changed
+98
-38
No files matched your search
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -13,6 +13,13 @@
|
||||
:type="$server->isFunctional() ? 'success' : 'warning'" />
|
||||
</x-slot:actions>
|
||||
|
||||
@if ($this->limaStartCommand)
|
||||
<x-callout type="info" title="Start this Lima VM locally" class="mb-4">
|
||||
<code
|
||||
class="mt-2 block overflow-x-auto rounded-lg bg-neutral-950 px-3 py-2 font-mono text-[11px] text-neutral-200">{{ $this->limaStartCommand }}</code>
|
||||
</x-callout>
|
||||
@endif
|
||||
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-neutral-100 text-neutral-600 dark:bg-white/[0.06] dark:text-fg-dim">
|
||||
|
||||
@@ -88,6 +88,24 @@
|
||||
<x-reicon name="refresh" class="size-3.5" />
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@if ($server->cloudProviderToken)
|
||||
@if ($provider === 'Hetzner' && !$server->isFunctional() && $hetznerServerStatus === 'off')
|
||||
<x-forms.button type="button" wire:click.prevent="startHetznerServer" isHighlighted
|
||||
canGate="update" :canResource="$server">
|
||||
Power On
|
||||
</x-forms.button>
|
||||
@elseif ($provider === 'DigitalOcean' && $digitalOceanDropletStatus === 'off')
|
||||
<x-forms.button type="button" wire:click.prevent="startDigitalOceanDroplet"
|
||||
isHighlighted canGate="update" :canResource="$server">
|
||||
Power On
|
||||
</x-forms.button>
|
||||
@elseif ($provider === 'Vultr' && $vultrInstanceStatus === 'stopped')
|
||||
<x-forms.button type="button" wire:click.prevent="startVultrInstance" isHighlighted
|
||||
canGate="update" :canResource="$server">
|
||||
Power On
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@endif
|
||||
@endif
|
||||
@if ($server->server_metadata)
|
||||
<x-forms.button type="button" class="size-8! px-0!"
|
||||
|
||||
@@ -104,7 +104,12 @@
|
||||
<x-application.settings-section title="General"
|
||||
description="Connection and authentication settings for this private GitHub source.">
|
||||
<x-slot:actions>
|
||||
<x-forms.button type="button" wire:click.prevent="updateGithubAppName">
|
||||
@if ($isConnected)
|
||||
<x-forms.button type="button" canGate="view" :canResource="$github_app" wire:click.prevent="testConnection">
|
||||
Test connection
|
||||
</x-forms.button>
|
||||
@endif
|
||||
<x-forms.button type="button" canGate="update" :canResource="$github_app" wire:click.prevent="updateGithubAppName">
|
||||
<x-reicon name="refresh" class="size-3.5" />
|
||||
Sync name
|
||||
</x-forms.button>
|
||||
@@ -191,7 +196,7 @@
|
||||
shortConfirmationLabel="GitHub App Name" :confirmWithPassword="false"
|
||||
step2ButtonText="Permanently Delete" />
|
||||
@else
|
||||
<x-forms.button isError disabled tooltip="You do not have permission to delete this GitHub App.">
|
||||
<x-forms.button isError disabled canGate="delete" :canResource="$github_app" tooltip="You do not have permission to delete this GitHub App.">
|
||||
Delete
|
||||
</x-forms.button>
|
||||
@endcan
|
||||
|
||||
Reference in new issue
Block a user