mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-12 15:53:46 -04:00
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=<application> filters instead of returning 404. - Cloud servers: Power On for stopped Hetzner, DigitalOcean and Vultr servers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1c3de6a4cd
commit
178e6665f8
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -110,7 +110,8 @@
|
||||
Test connection
|
||||
</x-forms.button>
|
||||
@endif
|
||||
<x-forms.button type="button" wire:click.prevent="updateGithubAppName">
|
||||
<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>
|
||||
@@ -197,7 +198,8 @@
|
||||
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
|
||||
|
||||
@@ -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'],
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user