mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-14 22:17:19 +00:00
Merge remote-tracking branch 'origin/next' into audit-policies
# Conflicts: # app/Http/Controllers/Api/SecurityController.php # app/Http/Controllers/Api/ServersController.php # app/Livewire/Admin/Index.php # app/Livewire/Destination/Show.php # app/Livewire/NavbarDeleteTeam.php # app/Livewire/Project/Application/Previews.php # app/Livewire/Project/DeleteProject.php # app/Livewire/Project/Shared/ResourceOperations.php # app/Livewire/Server/Resources.php # app/Livewire/Server/ValidateAndInstall.php # app/Livewire/Storage/Show.php # resources/views/livewire/dashboard.blade.php # resources/views/livewire/project/application/heading.blade.php # resources/views/livewire/project/database/heading.blade.php # resources/views/livewire/project/service/heading.blade.php # resources/views/livewire/project/shared/environment-variable/show.blade.php # resources/views/livewire/project/shared/scheduled-task/show.blade.php # tests/Feature/Security/TrustHostsMiddlewareTest.php
This commit is contained in:
commit
43f33d94ad
685 files changed
+41904
-5674
No files matched your search
@@ -15,17 +15,17 @@ class Advanced extends Component
|
||||
#[Validate(['string'])]
|
||||
public string $serverDiskUsageCheckFrequency = '0 23 * * *';
|
||||
|
||||
#[Validate(['integer', 'min:1', 'max:99'])]
|
||||
public int $serverDiskUsageNotificationThreshold = 50;
|
||||
#[Validate(['required', 'integer', 'min:1', 'max:99'])]
|
||||
public int|string $serverDiskUsageNotificationThreshold = 50;
|
||||
|
||||
#[Validate(['integer', 'min:1'])]
|
||||
public int $concurrentBuilds = 1;
|
||||
#[Validate(['required', 'integer', 'min:1'])]
|
||||
public int|string $concurrentBuilds = 1;
|
||||
|
||||
#[Validate(['integer', 'min:1'])]
|
||||
public int $dynamicTimeout = 1;
|
||||
#[Validate(['required', 'integer', 'min:1'])]
|
||||
public int|string $dynamicTimeout = 1;
|
||||
|
||||
#[Validate(['integer', 'min:1'])]
|
||||
public int $deploymentQueueLimit = 25;
|
||||
#[Validate(['required', 'integer', 'min:1'])]
|
||||
public int|string $deploymentQueueLimit = 25;
|
||||
|
||||
public function mount(string $server_uuid)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Actions\Server\DeleteServer;
|
||||
use App\Jobs\DeleteResourceJob;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
@@ -15,6 +16,8 @@ class Delete extends Component
|
||||
|
||||
public bool $delete_from_hetzner = false;
|
||||
|
||||
public bool $force_delete_resources = false;
|
||||
|
||||
public function mount(string $server_uuid)
|
||||
{
|
||||
try {
|
||||
@@ -24,19 +27,30 @@ class Delete extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($password)
|
||||
public function delete($password, $selectedActions = [])
|
||||
{
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
return 'The provided password is incorrect.';
|
||||
}
|
||||
|
||||
if (! empty($selectedActions)) {
|
||||
$this->delete_from_hetzner = in_array('delete_from_hetzner', $selectedActions);
|
||||
$this->force_delete_resources = in_array('force_delete_resources', $selectedActions);
|
||||
}
|
||||
try {
|
||||
$this->authorize('delete', $this->server);
|
||||
if ($this->server->hasDefinedResources()) {
|
||||
$this->dispatch('error', 'Server has defined resources. Please delete them first.');
|
||||
if ($this->server->hasDefinedResources() && ! $this->force_delete_resources) {
|
||||
$this->dispatch('error', 'Server has defined resources. Please delete them first or select "Delete all resources".');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->force_delete_resources) {
|
||||
foreach ($this->server->definedResources() as $resource) {
|
||||
DeleteResourceJob::dispatch($resource);
|
||||
}
|
||||
}
|
||||
|
||||
$this->server->delete();
|
||||
DeleteServer::dispatch(
|
||||
$this->server->id,
|
||||
@@ -56,6 +70,15 @@ class Delete extends Component
|
||||
{
|
||||
$checkboxes = [];
|
||||
|
||||
if ($this->server->hasDefinedResources()) {
|
||||
$resourceCount = $this->server->definedResources()->count();
|
||||
$checkboxes[] = [
|
||||
'id' => 'force_delete_resources',
|
||||
'label' => "Delete all resources ({$resourceCount} total)",
|
||||
'default_warning' => 'Server cannot be deleted while it has resources.',
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->server->hetzner_server_id) {
|
||||
$checkboxes[] = [
|
||||
'id' => 'delete_from_hetzner',
|
||||
|
||||
@@ -24,16 +24,16 @@ class LogDrains extends Component
|
||||
#[Validate(['boolean'])]
|
||||
public bool $isLogDrainAxiomEnabled = false;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
#[Validate(['string', 'nullable', 'regex:/^[a-zA-Z0-9_\-\.]+$/'])]
|
||||
public ?string $logDrainNewRelicLicenseKey = null;
|
||||
|
||||
#[Validate(['url', 'nullable'])]
|
||||
public ?string $logDrainNewRelicBaseUri = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
#[Validate(['string', 'nullable', 'regex:/^[a-zA-Z0-9_\-\.]+$/'])]
|
||||
public ?string $logDrainAxiomDatasetName = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
#[Validate(['string', 'nullable', 'regex:/^[a-zA-Z0-9_\-\.]+$/'])]
|
||||
public ?string $logDrainAxiomApiKey = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
@@ -127,7 +127,7 @@ class LogDrains extends Component
|
||||
if ($this->isLogDrainNewRelicEnabled) {
|
||||
try {
|
||||
$this->validate([
|
||||
'logDrainNewRelicLicenseKey' => ['required'],
|
||||
'logDrainNewRelicLicenseKey' => ['required', 'regex:/^[a-zA-Z0-9_\-\.]+$/'],
|
||||
'logDrainNewRelicBaseUri' => ['required', 'url'],
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
@@ -138,8 +138,8 @@ class LogDrains extends Component
|
||||
} elseif ($this->isLogDrainAxiomEnabled) {
|
||||
try {
|
||||
$this->validate([
|
||||
'logDrainAxiomDatasetName' => ['required'],
|
||||
'logDrainAxiomApiKey' => ['required'],
|
||||
'logDrainAxiomDatasetName' => ['required', 'regex:/^[a-zA-Z0-9_\-\.]+$/'],
|
||||
'logDrainAxiomApiKey' => ['required', 'regex:/^[a-zA-Z0-9_\-\.]+$/'],
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
$this->isLogDrainAxiomEnabled = false;
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Models\CloudProviderToken;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Rules\ValidCloudInitYaml;
|
||||
use App\Rules\ValidHostname;
|
||||
use App\Services\HetznerService;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
@@ -165,7 +166,7 @@ class ByHetzner extends Component
|
||||
'selectedHetznerSshKeyIds.*' => 'integer',
|
||||
'enable_ipv4' => 'required|boolean',
|
||||
'enable_ipv6' => 'required|boolean',
|
||||
'cloud_init_script' => ['nullable', 'string', new \App\Rules\ValidCloudInitYaml],
|
||||
'cloud_init_script' => ['nullable', 'string', new ValidCloudInitYaml],
|
||||
'save_cloud_init_script' => 'boolean',
|
||||
'cloud_init_script_name' => 'nullable|string|max:255',
|
||||
'selected_cloud_init_script_id' => 'nullable|integer|exists:cloud_init_scripts,id',
|
||||
@@ -299,11 +300,6 @@ class ByHetzner extends Component
|
||||
|
||||
public function getAvailableServerTypesProperty()
|
||||
{
|
||||
ray('Getting available server types', [
|
||||
'selected_location' => $this->selected_location,
|
||||
'total_server_types' => count($this->serverTypes),
|
||||
]);
|
||||
|
||||
if (! $this->selected_location) {
|
||||
return $this->serverTypes;
|
||||
}
|
||||
@@ -326,21 +322,11 @@ class ByHetzner extends Component
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
ray('Filtered server types', [
|
||||
'selected_location' => $this->selected_location,
|
||||
'filtered_count' => count($filtered),
|
||||
]);
|
||||
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
public function getAvailableImagesProperty()
|
||||
{
|
||||
ray('Getting available images', [
|
||||
'selected_server_type' => $this->selected_server_type,
|
||||
'total_images' => count($this->images),
|
||||
'images' => $this->images,
|
||||
]);
|
||||
|
||||
if (! $this->selected_server_type) {
|
||||
return $this->images;
|
||||
@@ -348,10 +334,7 @@ class ByHetzner extends Component
|
||||
|
||||
$serverType = collect($this->serverTypes)->firstWhere('name', $this->selected_server_type);
|
||||
|
||||
ray('Server type data', $serverType);
|
||||
|
||||
if (! $serverType || ! isset($serverType['architecture'])) {
|
||||
ray('No architecture in server type, returning all');
|
||||
|
||||
return $this->images;
|
||||
}
|
||||
@@ -363,11 +346,6 @@ class ByHetzner extends Component
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
ray('Filtered images', [
|
||||
'architecture' => $architecture,
|
||||
'filtered_count' => count($filtered),
|
||||
]);
|
||||
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
@@ -390,8 +368,6 @@ class ByHetzner extends Component
|
||||
|
||||
public function updatedSelectedLocation($value)
|
||||
{
|
||||
ray('Location selected', $value);
|
||||
|
||||
// Reset server type and image when location changes
|
||||
$this->selected_server_type = null;
|
||||
$this->selected_image = null;
|
||||
@@ -399,15 +375,13 @@ class ByHetzner extends Component
|
||||
|
||||
public function updatedSelectedServerType($value)
|
||||
{
|
||||
ray('Server type selected', $value);
|
||||
|
||||
// Reset image when server type changes
|
||||
$this->selected_image = null;
|
||||
}
|
||||
|
||||
public function updatedSelectedImage($value)
|
||||
{
|
||||
ray('Image selected', $value);
|
||||
//
|
||||
}
|
||||
|
||||
public function updatedSelectedCloudInitScriptId($value)
|
||||
@@ -437,18 +411,10 @@ class ByHetzner extends Component
|
||||
$publicKey = $privateKey->getPublicKey();
|
||||
$md5Fingerprint = PrivateKey::generateMd5Fingerprint($privateKey->private_key);
|
||||
|
||||
ray('Private Key Info', [
|
||||
'private_key_id' => $this->private_key_id,
|
||||
'sha256_fingerprint' => $privateKey->fingerprint,
|
||||
'md5_fingerprint' => $md5Fingerprint,
|
||||
]);
|
||||
|
||||
// Check if SSH key already exists on Hetzner by comparing MD5 fingerprints
|
||||
$existingSshKeys = $hetznerService->getSshKeys();
|
||||
$existingKey = null;
|
||||
|
||||
ray('Existing SSH Keys on Hetzner', $existingSshKeys);
|
||||
|
||||
foreach ($existingSshKeys as $key) {
|
||||
if ($key['fingerprint'] === $md5Fingerprint) {
|
||||
$existingKey = $key;
|
||||
@@ -459,12 +425,10 @@ class ByHetzner extends Component
|
||||
// Upload SSH key if it doesn't exist
|
||||
if ($existingKey) {
|
||||
$sshKeyId = $existingKey['id'];
|
||||
ray('Using existing SSH key', ['ssh_key_id' => $sshKeyId]);
|
||||
} else {
|
||||
$sshKeyName = $privateKey->name;
|
||||
$uploadedKey = $hetznerService->uploadSshKey($sshKeyName, $publicKey);
|
||||
$sshKeyId = $uploadedKey['id'];
|
||||
ray('Uploaded new SSH key', ['ssh_key_id' => $sshKeyId, 'name' => $sshKeyName]);
|
||||
}
|
||||
|
||||
// Normalize server name to lowercase for RFC 1123 compliance
|
||||
@@ -499,13 +463,9 @@ class ByHetzner extends Component
|
||||
$params['user_data'] = $this->cloud_init_script;
|
||||
}
|
||||
|
||||
ray('Server creation parameters', $params);
|
||||
|
||||
// Create server on Hetzner
|
||||
$hetznerServer = $hetznerService->createServer($params);
|
||||
|
||||
ray('Hetzner server created', $hetznerServer);
|
||||
|
||||
return $hetznerServer;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,8 @@ class Show extends Component
|
||||
$this->dispatch('success', 'Server is reachable.');
|
||||
$this->dispatch('refreshServerShow');
|
||||
} else {
|
||||
$this->dispatch('error', 'Server is not reachable.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.<br><br>Error: '.$error);
|
||||
$sanitizedError = htmlspecialchars($error ?? '', ENT_QUOTES, 'UTF-8');
|
||||
$this->dispatch('error', 'Server is not reachable.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.<br><br>Error: '.$sanitizedError);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Actions\Proxy\GetProxyConfiguration;
|
||||
use App\Actions\Proxy\SaveProxyConfiguration;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Models\Server;
|
||||
use App\Rules\SafeExternalUrl;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -41,9 +42,13 @@ class Proxy extends Component
|
||||
];
|
||||
}
|
||||
|
||||
protected $rules = [
|
||||
'generateExactLabels' => 'required|boolean',
|
||||
];
|
||||
protected function rules()
|
||||
{
|
||||
return [
|
||||
'generateExactLabels' => 'required|boolean',
|
||||
'redirectUrl' => ['nullable', new SafeExternalUrl],
|
||||
];
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
@@ -51,6 +56,7 @@ class Proxy extends Component
|
||||
$this->redirectEnabled = data_get($this->server, 'proxy.redirect_enabled', true);
|
||||
$this->redirectUrl = data_get($this->server, 'proxy.redirect_url');
|
||||
$this->syncData(false);
|
||||
$this->loadProxyConfiguration();
|
||||
}
|
||||
|
||||
private function syncData(bool $toModel = false): void
|
||||
@@ -150,6 +156,7 @@ class Proxy extends Component
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
$this->validate();
|
||||
SaveProxyConfiguration::run($this->server, $this->proxySettings);
|
||||
$this->server->proxy->redirect_url = $this->redirectUrl;
|
||||
$this->server->save();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -31,6 +32,11 @@ class Resources extends Component
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
if (! ValidationPatterns::isValidContainerName($id)) {
|
||||
$this->dispatch('error', 'Invalid container identifier.');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->server->startUnmanaged($id);
|
||||
$this->dispatch('success', 'Container started.');
|
||||
$this->loadUnmanagedContainers();
|
||||
@@ -43,6 +49,11 @@ class Resources extends Component
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
if (! ValidationPatterns::isValidContainerName($id)) {
|
||||
$this->dispatch('error', 'Invalid container identifier.');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->server->restartUnmanaged($id);
|
||||
$this->dispatch('success', 'Container restarted.');
|
||||
$this->loadUnmanagedContainers();
|
||||
@@ -55,6 +66,11 @@ class Resources extends Component
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
if (! ValidationPatterns::isValidContainerName($id)) {
|
||||
$this->dispatch('error', 'Invalid container identifier.');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->server->stopUnmanaged($id);
|
||||
$this->dispatch('success', 'Container stopped.');
|
||||
$this->loadUnmanagedContainers();
|
||||
|
||||
@@ -31,7 +31,7 @@ class TerminalAccess extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function toggleTerminal($password)
|
||||
public function toggleTerminal($password, $selectedActions = [])
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
@@ -43,7 +43,7 @@ class TerminalAccess extends Component
|
||||
|
||||
// Verify password
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
return 'The provided password is incorrect.';
|
||||
}
|
||||
|
||||
// Toggle the terminal setting
|
||||
@@ -55,6 +55,8 @@ class TerminalAccess extends Component
|
||||
|
||||
$status = $this->isTerminalEnabled ? 'enabled' : 'disabled';
|
||||
$this->dispatch('success', "Terminal access has been {$status}.");
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -19,19 +19,19 @@ class Sentinel extends Component
|
||||
|
||||
public bool $isMetricsEnabled;
|
||||
|
||||
#[Validate(['required'])]
|
||||
#[Validate(['required', 'string', 'max:500', 'regex:/\A[a-zA-Z0-9._\-+=\/]+\z/'])]
|
||||
public string $sentinelToken;
|
||||
|
||||
public ?string $sentinelUpdatedAt = null;
|
||||
|
||||
#[Validate(['required', 'integer', 'min:1'])]
|
||||
public int $sentinelMetricsRefreshRateSeconds;
|
||||
public int|string $sentinelMetricsRefreshRateSeconds;
|
||||
|
||||
#[Validate(['required', 'integer', 'min:1'])]
|
||||
public int $sentinelMetricsHistoryDays;
|
||||
public int|string $sentinelMetricsHistoryDays;
|
||||
|
||||
#[Validate(['required', 'integer', 'min:10'])]
|
||||
public int $sentinelPushIntervalSeconds;
|
||||
public int|string $sentinelPushIntervalSeconds;
|
||||
|
||||
#[Validate(['nullable', 'url'])]
|
||||
public ?string $sentinelCustomUrl = null;
|
||||
|
||||
@@ -489,6 +489,22 @@ class Show extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function refreshServerMetadata(): void
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
$result = $this->server->gatherServerMetadata();
|
||||
if ($result) {
|
||||
$this->server->refresh();
|
||||
$this->dispatch('success', 'Server details refreshed.');
|
||||
} else {
|
||||
$this->dispatch('error', 'Could not fetch server details. Is the server reachable?');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -94,7 +94,8 @@ class ValidateAndInstall extends Component
|
||||
$this->authorize('update', $this->server);
|
||||
['uptime' => $this->uptime, 'error' => $error] = $this->server->validateConnection();
|
||||
if (! $this->uptime) {
|
||||
$this->error = 'Server is not reachable. Please validate your configuration and connection.<br>Check this <a target="_blank" class="text-black underline dark:text-white" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br><div class="text-error">Error: '.$error.'</div>';
|
||||
$sanitizedError = htmlspecialchars($error ?? '', ENT_QUOTES, 'UTF-8');
|
||||
$this->error = 'Server is not reachable. Please validate your configuration and connection.<br>Check this <a target="_blank" class="text-black underline dark:text-white" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br><div class="text-error">Error: '.$sanitizedError.'</div>';
|
||||
$this->server->update([
|
||||
'validation_logs' => $this->error,
|
||||
]);
|
||||
@@ -206,6 +207,9 @@ class ValidateAndInstall extends Component
|
||||
// Mark validation as complete
|
||||
$this->server->update(['is_validating' => false]);
|
||||
|
||||
// Auto-fetch server details now that validation passed
|
||||
$this->server->gatherServerMetadata();
|
||||
|
||||
$this->dispatch('refreshServerShow');
|
||||
$this->dispatch('refreshBoardingIndex');
|
||||
ServerValidated::dispatch($this->server->team_id, $this->server->uuid);
|
||||
|
||||
Reference in new issue
Block a user