diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index 4626a9135..7a4a1c480 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -5,9 +5,12 @@ use App\Actions\Server\StartSentinel; use App\Actions\Server\StopSentinel; use App\Events\ServerReachabilityChanged; +use App\Models\CloudProviderToken; use App\Models\Server; +use App\Services\HetznerService; use App\Support\ValidationPatterns; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Illuminate\Support\Collection; use Livewire\Attributes\Computed; use Livewire\Attributes\Locked; use Livewire\Component; @@ -73,6 +76,17 @@ class Show extends Component public bool $isValidating = false; + // Hetzner linking properties + public Collection $availableHetznerTokens; + + public ?int $selectedHetznerTokenId = null; + + public ?array $matchedHetznerServer = null; + + public ?string $hetznerSearchError = null; + + public bool $hetznerNoMatchFound = false; + public function getListeners() { $teamId = $this->server->team_id ?? auth()->user()->currentTeam()->id; @@ -150,6 +164,9 @@ public function mount(string $server_uuid) $this->hetznerServerStatus = $this->server->hetzner_server_status; $this->isValidating = $this->server->is_validating ?? false; + // Load Hetzner tokens for linking + $this->loadHetznerTokens(); + } catch (\Throwable $e) { return handleError($e, $this); } @@ -465,6 +482,98 @@ public function submit() } } + public function loadHetznerTokens(): void + { + $this->availableHetznerTokens = CloudProviderToken::ownedByCurrentTeam() + ->where('provider', 'hetzner') + ->get(); + } + + public function searchHetznerServer(): void + { + $this->hetznerSearchError = null; + $this->hetznerNoMatchFound = false; + $this->matchedHetznerServer = null; + + if (! $this->selectedHetznerTokenId) { + $this->hetznerSearchError = 'Please select a Hetzner token.'; + + return; + } + + try { + $this->authorize('update', $this->server); + + $token = $this->availableHetznerTokens->firstWhere('id', $this->selectedHetznerTokenId); + if (! $token) { + $this->hetznerSearchError = 'Invalid token selected.'; + + return; + } + + $hetznerService = new HetznerService($token->token); + $matched = $hetznerService->findServerByIp($this->server->ip); + + if ($matched) { + $this->matchedHetznerServer = $matched; + } else { + $this->hetznerNoMatchFound = true; + } + } catch (\Throwable $e) { + $this->hetznerSearchError = 'Failed to search Hetzner servers: '.$e->getMessage(); + } + } + + public function linkToHetzner() + { + if (! $this->matchedHetznerServer) { + $this->dispatch('error', 'No Hetzner server selected.'); + + return; + } + + try { + $this->authorize('update', $this->server); + + $token = $this->availableHetznerTokens->firstWhere('id', $this->selectedHetznerTokenId); + if (! $token) { + $this->dispatch('error', 'Invalid token selected.'); + + return; + } + + // Verify the server exists and is accessible with the token + $hetznerService = new HetznerService($token->token); + $serverData = $hetznerService->getServer($this->matchedHetznerServer['id']); + + if (empty($serverData)) { + $this->dispatch('error', 'Could not find Hetzner server with ID: '.$this->matchedHetznerServer['id']); + + return; + } + + // Update the server with Hetzner details + $this->server->update([ + 'cloud_provider_token_id' => $this->selectedHetznerTokenId, + 'hetzner_server_id' => $this->matchedHetznerServer['id'], + 'hetzner_server_status' => $serverData['status'] ?? null, + ]); + + $this->hetznerServerStatus = $serverData['status'] ?? null; + + // Clear the linking state + $this->matchedHetznerServer = null; + $this->selectedHetznerTokenId = null; + $this->hetznerNoMatchFound = false; + $this->hetznerSearchError = null; + + $this->dispatch('success', 'Server successfully linked to Hetzner Cloud!'); + $this->dispatch('refreshServerShow'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function render() { return view('livewire.server.show'); diff --git a/app/Services/HetznerService.php b/app/Services/HetznerService.php index f7855090a..1de7eb2b1 100644 --- a/app/Services/HetznerService.php +++ b/app/Services/HetznerService.php @@ -161,4 +161,30 @@ public function deleteServer(int $serverId): void { $this->request('delete', "/servers/{$serverId}"); } + + public function getServers(): array + { + return $this->requestPaginated('get', '/servers', 'servers'); + } + + public function findServerByIp(string $ip): ?array + { + $servers = $this->getServers(); + + foreach ($servers as $server) { + // Check IPv4 + $ipv4 = data_get($server, 'public_net.ipv4.ip'); + if ($ipv4 === $ip) { + return $server; + } + + // Check IPv6 (Hetzner returns the full /64 block) + $ipv6 = data_get($server, 'public_net.ipv6.ip'); + if ($ipv6 && str_starts_with($ip, rtrim($ipv6, '/'))) { + return $server; + } + } + + return null; + } } diff --git a/resources/views/livewire/server/show.blade.php b/resources/views/livewire/server/show.blade.php index f9311bb83..a8344df05 100644 --- a/resources/views/livewire/server/show.blade.php +++ b/resources/views/livewire/server/show.blade.php @@ -320,6 +320,64 @@ class="w-full input opacity-50 cursor-not-allowed" + @if (!$server->hetzner_server_id && $availableHetznerTokens->isNotEmpty()) +
+ Link this server to a Hetzner Cloud instance to enable power controls and status monitoring. +
+ +{{ $hetznerSearchError }}
++ No Hetzner server found matching IP: {{ $server->ip }} +
++ Try a different token or verify the server IP is correct. +
+