feat(security): implement server patching functionality
- Add CheckUpdates and UpdatePackage actions for managing server updates. - Create ServerPackageUpdated event for broadcasting update status. - Introduce Patches Livewire component for user interface to check and apply updates. - Update navigation and sidebar to include security patching options.
This commit is contained in:
		
							
								
								
									
										227
									
								
								app/Actions/Server/CheckUpdates.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										227
									
								
								app/Actions/Server/CheckUpdates.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,227 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace App\Actions\Server;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use App\Models\Server;
 | 
				
			||||||
 | 
					use Lorisleiva\Actions\Concerns\AsAction;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CheckUpdates
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    use AsAction;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public string $jobQueue = 'high';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function handle(Server $server)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            if ($server->serverStatus() === false) {
 | 
				
			||||||
 | 
					                return [
 | 
				
			||||||
 | 
					                    'error' => 'Server is not reachable or not ready.',
 | 
				
			||||||
 | 
					                ];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Try first method - using instant_remote_process
 | 
				
			||||||
 | 
					            $output = instant_remote_process(['cat /etc/os-release'], $server);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Parse os-release into an associative array
 | 
				
			||||||
 | 
					            $osInfo = [];
 | 
				
			||||||
 | 
					            foreach (explode("\n", $output) as $line) {
 | 
				
			||||||
 | 
					                if (empty($line)) {
 | 
				
			||||||
 | 
					                    continue;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if (strpos($line, '=') === false) {
 | 
				
			||||||
 | 
					                    continue;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                [$key, $value] = explode('=', $line, 2);
 | 
				
			||||||
 | 
					                $osInfo[$key] = trim($value, '"');
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Get the main OS identifier
 | 
				
			||||||
 | 
					            $osId = $osInfo['ID'] ?? '';
 | 
				
			||||||
 | 
					            // $osIdLike = $osInfo['ID_LIKE'] ?? '';
 | 
				
			||||||
 | 
					            // $versionId = $osInfo['VERSION_ID'] ?? '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Normalize OS types based on install.sh logic
 | 
				
			||||||
 | 
					            switch ($osId) {
 | 
				
			||||||
 | 
					                case 'manjaro':
 | 
				
			||||||
 | 
					                case 'manjaro-arm':
 | 
				
			||||||
 | 
					                case 'endeavouros':
 | 
				
			||||||
 | 
					                    $osType = 'arch';
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                case 'pop':
 | 
				
			||||||
 | 
					                case 'linuxmint':
 | 
				
			||||||
 | 
					                case 'zorin':
 | 
				
			||||||
 | 
					                    $osType = 'ubuntu';
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                case 'fedora-asahi-remix':
 | 
				
			||||||
 | 
					                    $osType = 'fedora';
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                default:
 | 
				
			||||||
 | 
					                    $osType = $osId;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Determine package manager based on OS type
 | 
				
			||||||
 | 
					            $packageManager = match ($osType) {
 | 
				
			||||||
 | 
					                'arch' => 'pacman',
 | 
				
			||||||
 | 
					                'alpine' => 'apk',
 | 
				
			||||||
 | 
					                'ubuntu', 'debian', 'raspbian' => 'apt',
 | 
				
			||||||
 | 
					                'centos', 'fedora', 'rhel', 'ol', 'rocky', 'almalinux', 'amzn' => 'dnf',
 | 
				
			||||||
 | 
					                'sles', 'opensuse-leap', 'opensuse-tumbleweed' => 'zypper',
 | 
				
			||||||
 | 
					                default => null
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            switch ($packageManager) {
 | 
				
			||||||
 | 
					                case 'zypper':
 | 
				
			||||||
 | 
					                    $output = instant_remote_process(['LANG=C zypper -tx list-updates'], $server);
 | 
				
			||||||
 | 
					                    $out = $this->parseZypperOutput($output);
 | 
				
			||||||
 | 
					                    $out['osId'] = $osId;
 | 
				
			||||||
 | 
					                    $out['package_manager'] = $packageManager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    return $out;
 | 
				
			||||||
 | 
					                case 'dnf':
 | 
				
			||||||
 | 
					                    $output = instant_remote_process(['LANG=C dnf list -q --updates --refresh'], $server);
 | 
				
			||||||
 | 
					                    $out = $this->parseDnfOutput($output);
 | 
				
			||||||
 | 
					                    $out['osId'] = $osId;
 | 
				
			||||||
 | 
					                    $out['package_manager'] = $packageManager;
 | 
				
			||||||
 | 
					                    $rebootRequired = instant_remote_process(['LANG=C dnf needs-restarting -r'], $server);
 | 
				
			||||||
 | 
					                    $out['reboot_required'] = $rebootRequired === '0' ? true : false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    return $out;
 | 
				
			||||||
 | 
					                case 'apt':
 | 
				
			||||||
 | 
					                    instant_remote_process(['apt-get update -qq'], $server);
 | 
				
			||||||
 | 
					                    $output = instant_remote_process(['LANG=C apt list --upgradable 2>/dev/null'], $server);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    $out = $this->parseAptOutput($output);
 | 
				
			||||||
 | 
					                    $out['osId'] = $osId;
 | 
				
			||||||
 | 
					                    $out['package_manager'] = $packageManager;
 | 
				
			||||||
 | 
					                    $rebootRequired = instant_remote_process(['LANG=C test -f /var/run/reboot-required && echo "YES" || echo "NO"'], $server);
 | 
				
			||||||
 | 
					                    $out['reboot_required'] = $rebootRequired === 'YES' ? true : false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    return $out;
 | 
				
			||||||
 | 
					                default:
 | 
				
			||||||
 | 
					                    return [
 | 
				
			||||||
 | 
					                        'osId' => $osId,
 | 
				
			||||||
 | 
					                        'error' => 'Unsupported package manager',
 | 
				
			||||||
 | 
					                        'package_manager' => $packageManager,
 | 
				
			||||||
 | 
					                    ];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (\Throwable $e) {
 | 
				
			||||||
 | 
					            ray('Error:', $e->getMessage());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return [
 | 
				
			||||||
 | 
					                'osId' => $osId,
 | 
				
			||||||
 | 
					                'package_manager' => $packageManager,
 | 
				
			||||||
 | 
					                'error' => $e->getMessage(),
 | 
				
			||||||
 | 
					                'trace' => $e->getTraceAsString(),
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function parseZypperOutput(string $output): array
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $updates = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            $xml = simplexml_load_string($output);
 | 
				
			||||||
 | 
					            if ($xml === false) {
 | 
				
			||||||
 | 
					                return [
 | 
				
			||||||
 | 
					                    'total_updates' => 0,
 | 
				
			||||||
 | 
					                    'updates' => [],
 | 
				
			||||||
 | 
					                    'error' => 'Failed to parse XML output',
 | 
				
			||||||
 | 
					                ];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Navigate to the update-list node
 | 
				
			||||||
 | 
					            $updateList = $xml->xpath('//update-list/update');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            foreach ($updateList as $update) {
 | 
				
			||||||
 | 
					                $updates[] = [
 | 
				
			||||||
 | 
					                    'package' => (string) $update['name'],
 | 
				
			||||||
 | 
					                    'new_version' => (string) $update['edition'],
 | 
				
			||||||
 | 
					                    'current_version' => (string) $update['edition-old'],
 | 
				
			||||||
 | 
					                    'architecture' => (string) $update['arch'],
 | 
				
			||||||
 | 
					                    'repository' => (string) $update->source['alias'],
 | 
				
			||||||
 | 
					                    'summary' => (string) $update->summary,
 | 
				
			||||||
 | 
					                    'description' => (string) $update->description,
 | 
				
			||||||
 | 
					                ];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return [
 | 
				
			||||||
 | 
					                'total_updates' => count($updates),
 | 
				
			||||||
 | 
					                'updates' => $updates,
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					        } catch (\Throwable $e) {
 | 
				
			||||||
 | 
					            return [
 | 
				
			||||||
 | 
					                'total_updates' => 0,
 | 
				
			||||||
 | 
					                'updates' => [],
 | 
				
			||||||
 | 
					                'error' => 'Error parsing zypper output: '.$e->getMessage(),
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function parseDnfOutput(string $output): array
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $updates = [];
 | 
				
			||||||
 | 
					        $lines = explode("\n", $output);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        foreach ($lines as $line) {
 | 
				
			||||||
 | 
					            if (empty($line)) {
 | 
				
			||||||
 | 
					                continue;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Split by multiple spaces/tabs and filter out empty elements
 | 
				
			||||||
 | 
					            $parts = array_values(array_filter(preg_split('/\s+/', $line)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (count($parts) >= 3) {
 | 
				
			||||||
 | 
					                $package = $parts[0];
 | 
				
			||||||
 | 
					                $new_version = $parts[1];
 | 
				
			||||||
 | 
					                $repository = $parts[2];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Extract architecture from package name (e.g., "cloud-init.noarch" -> "noarch")
 | 
				
			||||||
 | 
					                $architecture = str_contains($package, '.') ? explode('.', $package)[1] : 'noarch';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                $updates[] = [
 | 
				
			||||||
 | 
					                    'package' => $package,
 | 
				
			||||||
 | 
					                    'new_version' => $new_version,
 | 
				
			||||||
 | 
					                    'repository' => $repository,
 | 
				
			||||||
 | 
					                    'architecture' => $architecture,
 | 
				
			||||||
 | 
					                    'current_version' => 'unknown', // DNF doesn't show current version in check-update output
 | 
				
			||||||
 | 
					                ];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					            'total_updates' => count($updates),
 | 
				
			||||||
 | 
					            'updates' => $updates,
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function parseAptOutput(string $output): array
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $updates = [];
 | 
				
			||||||
 | 
					        $lines = explode("\n", $output);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        foreach ($lines as $line) {
 | 
				
			||||||
 | 
					            // Skip the "Listing... Done" line and empty lines
 | 
				
			||||||
 | 
					            if (empty($line) || str_contains($line, 'Listing...')) {
 | 
				
			||||||
 | 
					                continue;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Example line: package/stable 2.0-1 amd64 [upgradable from: 1.0-1]
 | 
				
			||||||
 | 
					            if (preg_match('/^(.+?)\/(\S+)\s+(\S+)\s+(\S+)\s+\[upgradable from: (.+?)\]/', $line, $matches)) {
 | 
				
			||||||
 | 
					                $updates[] = [
 | 
				
			||||||
 | 
					                    'package' => $matches[1],
 | 
				
			||||||
 | 
					                    'repository' => $matches[2],
 | 
				
			||||||
 | 
					                    'new_version' => $matches[3],
 | 
				
			||||||
 | 
					                    'architecture' => $matches[4],
 | 
				
			||||||
 | 
					                    'current_version' => $matches[5],
 | 
				
			||||||
 | 
					                ];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					            'total_updates' => count($updates),
 | 
				
			||||||
 | 
					            'updates' => $updates,
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										81
									
								
								app/Actions/Server/UpdatePackage.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								app/Actions/Server/UpdatePackage.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace App\Actions\Server;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use App\Models\Server;
 | 
				
			||||||
 | 
					use Lorisleiva\Actions\Concerns\AsAction;
 | 
				
			||||||
 | 
					use Spatie\Activitylog\Contracts\Activity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class UpdatePackage
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    use AsAction;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public string $jobQueue = 'high';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function handle(Server $server, string $osId, ?string $package = null, ?string $packageManager = null, bool $all = false): Activity|array
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            if ($server->serverStatus() === false) {
 | 
				
			||||||
 | 
					                return [
 | 
				
			||||||
 | 
					                    'error' => 'Server is not reachable or not ready.',
 | 
				
			||||||
 | 
					                ];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            switch ($packageManager) {
 | 
				
			||||||
 | 
					                case 'zypper':
 | 
				
			||||||
 | 
					                    $commandAll = 'zypper update -y';
 | 
				
			||||||
 | 
					                    $commandInstall = 'zypper install -y '.$package;
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                case 'dnf':
 | 
				
			||||||
 | 
					                    $commandAll = 'dnf update -y';
 | 
				
			||||||
 | 
					                    $commandInstall = 'dnf update -y '.$package;
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                case 'apt':
 | 
				
			||||||
 | 
					                    $commandAll = 'apt update && apt upgrade -y';
 | 
				
			||||||
 | 
					                    $commandInstall = 'apt install -y '.$package;
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                default:
 | 
				
			||||||
 | 
					                    return [
 | 
				
			||||||
 | 
					                        'error' => 'OS not supported',
 | 
				
			||||||
 | 
					                    ];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if ($all) {
 | 
				
			||||||
 | 
					                return remote_process([$commandAll], $server);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return remote_process([$commandInstall], $server);
 | 
				
			||||||
 | 
					        } catch (\Exception $e) {
 | 
				
			||||||
 | 
					            return [
 | 
				
			||||||
 | 
					                'error' => $e->getMessage(),
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function parseAptOutput(string $output): array
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $updates = [];
 | 
				
			||||||
 | 
					        $lines = explode("\n", $output);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        foreach ($lines as $line) {
 | 
				
			||||||
 | 
					            // Skip the "Listing... Done" line and empty lines
 | 
				
			||||||
 | 
					            if (empty($line) || str_contains($line, 'Listing...')) {
 | 
				
			||||||
 | 
					                continue;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Example line: package/stable 2.0-1 amd64 [upgradable from: 1.0-1]
 | 
				
			||||||
 | 
					            if (preg_match('/^(.+?)\/(\S+)\s+(\S+)\s+(\S+)\s+\[upgradable from: (.+?)\]/', $line, $matches)) {
 | 
				
			||||||
 | 
					                $updates[] = [
 | 
				
			||||||
 | 
					                    'package' => $matches[1],
 | 
				
			||||||
 | 
					                    'repository' => $matches[2],
 | 
				
			||||||
 | 
					                    'new_version' => $matches[3],
 | 
				
			||||||
 | 
					                    'architecture' => $matches[4],
 | 
				
			||||||
 | 
					                    'current_version' => $matches[5],
 | 
				
			||||||
 | 
					                ];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					            'total_updates' => count($updates),
 | 
				
			||||||
 | 
					            'updates' => $updates,
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										35
									
								
								app/Events/ServerPackageUpdated.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								app/Events/ServerPackageUpdated.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace App\Events;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Illuminate\Broadcasting\InteractsWithSockets;
 | 
				
			||||||
 | 
					use Illuminate\Broadcasting\PrivateChannel;
 | 
				
			||||||
 | 
					use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
 | 
				
			||||||
 | 
					use Illuminate\Foundation\Events\Dispatchable;
 | 
				
			||||||
 | 
					use Illuminate\Queue\SerializesModels;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ServerPackageUpdated implements ShouldBroadcast
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    use Dispatchable, InteractsWithSockets, SerializesModels;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public ?int $teamId = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function __construct($teamId = null)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
 | 
				
			||||||
 | 
					            $teamId = auth()->user()->currentTeam()->id;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        $this->teamId = $teamId;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function broadcastOn(): array
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (is_null($this->teamId)) {
 | 
				
			||||||
 | 
					            return [];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					            new PrivateChannel("team.{$this->teamId}"),
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										92
									
								
								app/Livewire/Server/Security/Patches.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								app/Livewire/Server/Security/Patches.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace App\Livewire\Server\Security;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use App\Actions\Server\CheckUpdates;
 | 
				
			||||||
 | 
					use App\Actions\Server\UpdatePackage;
 | 
				
			||||||
 | 
					use App\Events\ServerPackageUpdated;
 | 
				
			||||||
 | 
					use App\Models\Server;
 | 
				
			||||||
 | 
					use Livewire\Component;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Patches extends Component
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public array $parameters;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public Server $server;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public ?int $totalUpdates = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public ?array $updates = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public ?string $error = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public ?string $osId = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public ?string $packageManager = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function getListeners()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $teamId = auth()->user()->currentTeam()->id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					            "echo-private:team.{$teamId},ServerPackageUpdated" => 'checkForUpdatesDispatch',
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function mount()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (! auth()->user()->isAdmin()) {
 | 
				
			||||||
 | 
					            abort(403);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        $this->parameters = get_route_parameters();
 | 
				
			||||||
 | 
					        $this->server = Server::ownedByCurrentTeam()->whereUuid($this->parameters['server_uuid'])->firstOrFail();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function checkForUpdatesDispatch()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->totalUpdates = null;
 | 
				
			||||||
 | 
					        $this->updates = null;
 | 
				
			||||||
 | 
					        $this->error = null;
 | 
				
			||||||
 | 
					        $this->osId = null;
 | 
				
			||||||
 | 
					        $this->packageManager = null;
 | 
				
			||||||
 | 
					        $this->dispatch('checkForUpdatesDispatch');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function checkForUpdates()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $job = CheckUpdates::run($this->server);
 | 
				
			||||||
 | 
					        if (isset($job['error'])) {
 | 
				
			||||||
 | 
					            $this->error = data_get($job, 'error', 'Something went wrong.');
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            $this->totalUpdates = data_get($job, 'total_updates', 0);
 | 
				
			||||||
 | 
					            $this->updates = data_get($job, 'updates', []);
 | 
				
			||||||
 | 
					            $this->osId = data_get($job, 'osId', null);
 | 
				
			||||||
 | 
					            $this->packageManager = data_get($job, 'package_manager', null);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function updateAllPackages()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            $activity = UpdatePackage::run(server: $this->server, packageManager: $this->packageManager, osId: $this->osId, all: true);
 | 
				
			||||||
 | 
					            $this->dispatch('activityMonitor', $activity->id, ServerPackageUpdated::class);
 | 
				
			||||||
 | 
					        } catch (\Exception $e) {
 | 
				
			||||||
 | 
					            $this->dispatch('error', message: $e->getMessage());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function updatePackage($package)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            $activity = UpdatePackage::run(server: $this->server, packageManager: $this->packageManager, osId: $this->osId, package: $package);
 | 
				
			||||||
 | 
					            $this->dispatch('activityMonitor', $activity->id, ServerPackageUpdated::class);
 | 
				
			||||||
 | 
					        } catch (\Exception $e) {
 | 
				
			||||||
 | 
					            $this->dispatch('error', message: $e->getMessage());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function render()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return view('livewire.server.security.patches');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -45,6 +45,12 @@
 | 
				
			|||||||
                ]) }}">
 | 
					                ]) }}">
 | 
				
			||||||
                <button>Terminal</button>
 | 
					                <button>Terminal</button>
 | 
				
			||||||
            </a>
 | 
					            </a>
 | 
				
			||||||
 | 
					            <a class="{{ request()->routeIs('server.security.patches') ? 'dark:text-white' : '' }}"
 | 
				
			||||||
 | 
					                href="{{ route('server.security.patches', [
 | 
				
			||||||
 | 
					                    'server_uuid' => data_get($server, 'uuid'),
 | 
				
			||||||
 | 
					                ]) }}">
 | 
				
			||||||
 | 
					                <button>Security</button>
 | 
				
			||||||
 | 
					            </a>
 | 
				
			||||||
        </nav>
 | 
					        </nav>
 | 
				
			||||||
        <div class="order-first sm:order-last">
 | 
					        <div class="order-first sm:order-last">
 | 
				
			||||||
            <livewire:server.proxy.deploy :server="$server" />
 | 
					            <livewire:server.proxy.deploy :server="$server" />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					<div class="flex flex-col items-start gap-2 min-w-fit">
 | 
				
			||||||
 | 
					    <a class="{{ request()->routeIs('server.security.patches') ? 'menu-item menu-item-active' : 'menu-item' }}"
 | 
				
			||||||
 | 
					        href="{{ route('server.security.patches', $parameters) }}">
 | 
				
			||||||
 | 
					        <button>Server Patching</button>
 | 
				
			||||||
 | 
					    </a>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
							
								
								
									
										118
									
								
								resources/views/livewire/server/security/patches.blade.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								resources/views/livewire/server/security/patches.blade.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,118 @@
 | 
				
			|||||||
 | 
					<div>
 | 
				
			||||||
 | 
					    <x-slot:title>
 | 
				
			||||||
 | 
					        {{ data_get_str($server, 'name')->limit(10) }} > Security | Coolify
 | 
				
			||||||
 | 
					    </x-slot>
 | 
				
			||||||
 | 
					    <x-server.navbar :server="$server" />
 | 
				
			||||||
 | 
					    <x-slide-over closeWithX fullScreen @startupdate.window="slideOverOpen = true">
 | 
				
			||||||
 | 
					        <x-slot:title>Updating Packages</x-slot:title>
 | 
				
			||||||
 | 
					        <x-slot:content>
 | 
				
			||||||
 | 
					            <livewire:activity-monitor header="Logs" />
 | 
				
			||||||
 | 
					        </x-slot:content>
 | 
				
			||||||
 | 
					    </x-slide-over>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex flex-col h-full gap-8 sm:flex-row">
 | 
				
			||||||
 | 
					        <x-server.sidebar-security :server="$server" :parameters="$parameters" />
 | 
				
			||||||
 | 
					        <form wire:submit='submit' class="w-full">
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					                <div class="flex items-center gap-2">
 | 
				
			||||||
 | 
					                    <h2>Server Patching</h2>
 | 
				
			||||||
 | 
					                    <x-forms.button type="button" wire:click="$dispatch('checkForUpdatesDispatch')">Manually
 | 
				
			||||||
 | 
					                        Check</x-forms.button>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div>Check if your server has updates available.</div>
 | 
				
			||||||
 | 
					                <div class="text-xs pt-1">(only available for apt, dnf and zypper package managers atm, more coming
 | 
				
			||||||
 | 
					                    soon as well as more features...)
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div>
 | 
				
			||||||
 | 
					                    <div class="flex flex-col gap-6 pt-4">
 | 
				
			||||||
 | 
					                        <div class="flex flex-col">
 | 
				
			||||||
 | 
					                            <div>
 | 
				
			||||||
 | 
					                                <div wire:target="checkForUpdates" wire:loading>
 | 
				
			||||||
 | 
					                                    Checking for updates. It may take a few minutes. <x-loading />
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                                @if ($error)
 | 
				
			||||||
 | 
					                                    <div class="text-red-500">{{ $error }}</div>
 | 
				
			||||||
 | 
					                                @else
 | 
				
			||||||
 | 
					                                    @if ($totalUpdates === 0)
 | 
				
			||||||
 | 
					                                        <div class="text-green-500">Your server is up to date.</div>
 | 
				
			||||||
 | 
					                                    @endif
 | 
				
			||||||
 | 
					                                    @if (isset($updates) && count($updates) > 0)
 | 
				
			||||||
 | 
					                                        <x-modal-confirmation title="Confirm package update?"
 | 
				
			||||||
 | 
					                                            buttonTitle="Update All
 | 
				
			||||||
 | 
					                                            Packages"
 | 
				
			||||||
 | 
					                                            isHighlightedButton submitAction="updateAllPackages" dispatchAction
 | 
				
			||||||
 | 
					                                            :actions="[
 | 
				
			||||||
 | 
					                                                'All packages will be updated to the latest version.',
 | 
				
			||||||
 | 
					                                                'This action could restart your currently running containers if docker will be updated.',
 | 
				
			||||||
 | 
					                                            ]" confirmationText="Update All Packages"
 | 
				
			||||||
 | 
					                                            confirmationLabel="Please confirm the execution of the actions by entering the name below"
 | 
				
			||||||
 | 
					                                            shortConfirmationLabel="Name" :confirmWithPassword=false
 | 
				
			||||||
 | 
					                                            step2ButtonText="Update All
 | 
				
			||||||
 | 
					                                            Packages" />
 | 
				
			||||||
 | 
					                                        <table>
 | 
				
			||||||
 | 
					                                            <thead>
 | 
				
			||||||
 | 
					                                                <tr>
 | 
				
			||||||
 | 
					                                                    <th>Package</th>
 | 
				
			||||||
 | 
					                                                    @if ($packageManager !== 'dnf')
 | 
				
			||||||
 | 
					                                                        <th>Current Version</th>
 | 
				
			||||||
 | 
					                                                    @endif
 | 
				
			||||||
 | 
					                                                    <th>New Version</th>
 | 
				
			||||||
 | 
					                                                    <th>Action</th>
 | 
				
			||||||
 | 
					                                                </tr>
 | 
				
			||||||
 | 
					                                            </thead>
 | 
				
			||||||
 | 
					                                            <tbody>
 | 
				
			||||||
 | 
					                                                @foreach ($updates as $update)
 | 
				
			||||||
 | 
					                                                    <tr>
 | 
				
			||||||
 | 
					                                                        <td class="inline-flex gap-2 justify-center items-center">
 | 
				
			||||||
 | 
					                                                            @if (data_get_str($update, 'package')->contains('docker'))
 | 
				
			||||||
 | 
					                                                                <x-helper :helper="'This package will restart your currently running containers'">
 | 
				
			||||||
 | 
					                                                                    <x-slot:icon>
 | 
				
			||||||
 | 
					                                                                        <svg class="w-4 h-4 text-red-500 block"
 | 
				
			||||||
 | 
					                                                                            viewBox="0 0 256 256"
 | 
				
			||||||
 | 
					                                                                            xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					                                                                            <path fill="currentColor"
 | 
				
			||||||
 | 
					                                                                                d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16">
 | 
				
			||||||
 | 
					                                                                            </path>
 | 
				
			||||||
 | 
					                                                                        </svg>
 | 
				
			||||||
 | 
					                                                                    </x-slot:icon>
 | 
				
			||||||
 | 
					                                                                </x-helper>
 | 
				
			||||||
 | 
					                                                            @endif
 | 
				
			||||||
 | 
					                                                            {{ data_get($update, 'package') }}
 | 
				
			||||||
 | 
					                                                        </td>
 | 
				
			||||||
 | 
					                                                        @if ($packageManager !== 'dnf')
 | 
				
			||||||
 | 
					                                                            <td>{{ data_get($update, 'current_version') }}</td>
 | 
				
			||||||
 | 
					                                                        @endif
 | 
				
			||||||
 | 
					                                                        <td>{{ data_get($update, 'new_version') }}</td>
 | 
				
			||||||
 | 
					                                                        <td>
 | 
				
			||||||
 | 
					                                                            <x-forms.button type="button"
 | 
				
			||||||
 | 
					                                                                wire:click="$dispatch('updatePackage', { package: '{{ data_get($update, 'package') }}' })">Update</x-forms.button>
 | 
				
			||||||
 | 
					                                                        </td>
 | 
				
			||||||
 | 
					                                                    </tr>
 | 
				
			||||||
 | 
					                                                @endforeach
 | 
				
			||||||
 | 
					                                            </tbody>
 | 
				
			||||||
 | 
					                                        </table>
 | 
				
			||||||
 | 
					                                    @endif
 | 
				
			||||||
 | 
					                                @endif
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </form>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    @script
 | 
				
			||||||
 | 
					        <script>
 | 
				
			||||||
 | 
					            $wire.on('updateAllPackages', () => {
 | 
				
			||||||
 | 
					                window.dispatchEvent(new CustomEvent('startupdate'));
 | 
				
			||||||
 | 
					                $wire.$call('updateAllPackages');
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            $wire.on('updatePackage', (data) => {
 | 
				
			||||||
 | 
					                window.dispatchEvent(new CustomEvent('startupdate'));
 | 
				
			||||||
 | 
					                $wire.$call('updatePackage', data.package);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            $wire.on('checkForUpdatesDispatch', () => {
 | 
				
			||||||
 | 
					                $wire.$call('checkForUpdates');
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        </script>
 | 
				
			||||||
 | 
					    @endscript
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -49,6 +49,7 @@ use App\Livewire\Server\Proxy\DynamicConfigurations as ProxyDynamicConfiguration
 | 
				
			|||||||
use App\Livewire\Server\Proxy\Logs as ProxyLogs;
 | 
					use App\Livewire\Server\Proxy\Logs as ProxyLogs;
 | 
				
			||||||
use App\Livewire\Server\Proxy\Show as ProxyShow;
 | 
					use App\Livewire\Server\Proxy\Show as ProxyShow;
 | 
				
			||||||
use App\Livewire\Server\Resources as ResourcesShow;
 | 
					use App\Livewire\Server\Resources as ResourcesShow;
 | 
				
			||||||
 | 
					use App\Livewire\Server\Security\Patches;
 | 
				
			||||||
use App\Livewire\Server\Show as ServerShow;
 | 
					use App\Livewire\Server\Show as ServerShow;
 | 
				
			||||||
use App\Livewire\Settings\Index as SettingsIndex;
 | 
					use App\Livewire\Settings\Index as SettingsIndex;
 | 
				
			||||||
use App\Livewire\SettingsBackup;
 | 
					use App\Livewire\SettingsBackup;
 | 
				
			||||||
@@ -252,6 +253,8 @@ Route::middleware(['auth', 'verified'])->group(function () {
 | 
				
			|||||||
        Route::get('/proxy/logs', ProxyLogs::class)->name('server.proxy.logs');
 | 
					        Route::get('/proxy/logs', ProxyLogs::class)->name('server.proxy.logs');
 | 
				
			||||||
        Route::get('/terminal', ExecuteContainerCommand::class)->name('server.command');
 | 
					        Route::get('/terminal', ExecuteContainerCommand::class)->name('server.command');
 | 
				
			||||||
        Route::get('/docker-cleanup', DockerCleanup::class)->name('server.docker-cleanup');
 | 
					        Route::get('/docker-cleanup', DockerCleanup::class)->name('server.docker-cleanup');
 | 
				
			||||||
 | 
					        Route::get('/security', fn () => redirect(route('dashboard')));
 | 
				
			||||||
 | 
					        Route::get('/security/patches', Patches::class)->name('server.security.patches');
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    Route::get('/destinations', DestinationIndex::class)->name('destination.index');
 | 
					    Route::get('/destinations', DestinationIndex::class)->name('destination.index');
 | 
				
			||||||
    Route::get('/destination/{destination_uuid}', DestinationShow::class)->name('destination.show');
 | 
					    Route::get('/destination/{destination_uuid}', DestinationShow::class)->name('destination.show');
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user