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 }}
+