improved hetzner features

This commit is contained in:
Andras Bacsai committed 2025-10-09 12:53:57 +02:00
1 parent c9e6418542
commit 704ddf2968
17 files changed
+514 -199

No files matched your search

@@ -0,0 +1,144 @@
<?php
namespace App\Livewire\Server\CloudProviderToken;
use App\Models\CloudProviderToken;
use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class Show extends Component
{
use AuthorizesRequests;
public Server $server;
public $cloudProviderTokens = [];
public $parameters = [];
public function mount(string $server_uuid)
{
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
$this->loadTokens();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function getListeners()
{
return [
'tokenAdded' => 'handleTokenAdded',
];
}
public function loadTokens()
{
$this->cloudProviderTokens = CloudProviderToken::ownedByCurrentTeam()
->where('provider', 'hetzner')
->get();
}
public function handleTokenAdded($tokenId)
{
$this->loadTokens();
}
public function setCloudProviderToken($tokenId)
{
$ownedToken = CloudProviderToken::ownedByCurrentTeam()->find($tokenId);
if (is_null($ownedToken)) {
$this->dispatch('error', 'You are not allowed to use this token.');
return;
}
try {
$this->authorize('update', $this->server);
// Validate the token works and can access this specific server
$validationResult = $this->validateTokenForServer($ownedToken);
if (! $validationResult['valid']) {
$this->dispatch('error', $validationResult['error']);
return;
}
$this->server->cloudProviderToken()->associate($ownedToken);
$this->server->save();
$this->dispatch('success', 'Hetzner token updated successfully.');
$this->dispatch('refreshServerShow');
} catch (\Exception $e) {
$this->server->refresh();
$this->dispatch('error', $e->getMessage());
}
}
private function validateTokenForServer(CloudProviderToken $token): array
{
try {
// First, validate the token itself
$response = \Illuminate\Support\Facades\Http::withHeaders([
'Authorization' => 'Bearer '.$token->token,
])->timeout(10)->get('https://api.hetzner.cloud/v1/servers');
if (! $response->successful()) {
return [
'valid' => false,
'error' => 'This token is invalid or has insufficient permissions.',
];
}
// Check if this token can access the specific Hetzner server
if ($this->server->hetzner_server_id) {
$serverResponse = \Illuminate\Support\Facades\Http::withHeaders([
'Authorization' => 'Bearer '.$token->token,
])->timeout(10)->get("https://api.hetzner.cloud/v1/servers/{$this->server->hetzner_server_id}");
if (! $serverResponse->successful()) {
return [
'valid' => false,
'error' => 'This token cannot access this server. It may belong to a different Hetzner project.',
];
}
}
return ['valid' => true];
} catch (\Throwable $e) {
return [
'valid' => false,
'error' => 'Failed to validate token: '.$e->getMessage(),
];
}
}
public function validateToken()
{
try {
$token = $this->server->cloudProviderToken;
if (! $token) {
$this->dispatch('error', 'No Hetzner token is associated with this server.');
return;
}
$response = \Illuminate\Support\Facades\Http::withHeaders([
'Authorization' => 'Bearer '.$token->token,
])->timeout(10)->get('https://api.hetzner.cloud/v1/servers');
if ($response->successful()) {
$this->dispatch('success', 'Hetzner token is valid and working.');
} else {
$this->dispatch('error', 'Hetzner token is invalid or has insufficient permissions.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.server.cloud-provider-token.show');
}
}
+8
View File
@@ -2,6 +2,7 @@
namespace App\Livewire\Server;
use App\Models\CloudProviderToken;
use App\Models\PrivateKey;
use App\Models\Team;
use Livewire\Component;
@@ -12,6 +13,8 @@ class Create extends Component
public bool $limit_reached = false;
public bool $has_hetzner_tokens = false;
public function mount()
{
$this->private_keys = PrivateKey::ownedByCurrentTeam()->get();
@@ -21,6 +24,11 @@ class Create extends Component
return;
}
$this->limit_reached = Team::serverLimitReached();
// Check if user has Hetzner tokens
$this->has_hetzner_tokens = CloudProviderToken::ownedByCurrentTeam()
->where('provider', 'hetzner')
->exists();
}
public function render()
+42 -57
View File
@@ -34,12 +34,6 @@ class ByHetzner extends Component
// Step 1: Token selection
public ?int $selected_token_id = null;
public string $hetzner_token = '';
public bool $save_token = false;
public ?string $token_name = null;
// Step 2: Server configuration
public array $locations = [];
@@ -64,31 +58,50 @@ class ByHetzner extends Component
public function mount()
{
$this->authorize('viewAny', CloudProviderToken::class);
$this->available_tokens = CloudProviderToken::ownedByCurrentTeam()
->where('provider', 'hetzner')
->get();
$this->loadTokens();
$this->server_name = generate_random_name();
if ($this->private_keys->count() > 0) {
$this->private_key_id = $this->private_keys->first()->id;
}
}
public function getListeners()
{
return [
'tokenAdded' => 'handleTokenAdded',
'modalClosed' => 'resetSelection',
];
}
public function resetSelection()
{
$this->selected_token_id = null;
$this->current_step = 1;
}
public function loadTokens()
{
$this->available_tokens = CloudProviderToken::ownedByCurrentTeam()
->where('provider', 'hetzner')
->get();
}
public function handleTokenAdded($tokenId)
{
// Refresh token list
$this->loadTokens();
// Auto-select the new token
$this->selected_token_id = $tokenId;
// Automatically proceed to next step
$this->nextStep();
}
protected function rules(): array
{
$rules = [
'selected_token_id' => 'nullable|integer',
'hetzner_token' => 'required_without:selected_token_id|string',
'save_token' => 'boolean',
'token_name' => [
'nullable',
'string',
'max:255',
function ($attribute, $value, $fail) {
if ($this->save_token && ! empty($this->hetzner_token) && empty($value)) {
$fail('Please provide a name for the token.');
}
},
],
'selected_token_id' => 'required|integer|exists:cloud_provider_tokens,id',
];
if ($this->current_step === 2) {
@@ -108,8 +121,8 @@ class ByHetzner extends Component
protected function messages(): array
{
return [
'hetzner_token.required_without' => 'Please provide a Hetzner API token or select a saved token.',
'token_name.required_if' => 'Please provide a name for the token.',
'selected_token_id.required' => 'Please select a Hetzner token.',
'selected_token_id.exists' => 'Selected token not found.',
];
}
@@ -139,50 +152,21 @@ class ByHetzner extends Component
return $token ? $token->token : '';
}
return $this->hetzner_token;
return '';
}
public function nextStep()
{
// Validate step 1
// Validate step 1 - just need a token selected
$this->validate([
'selected_token_id' => 'nullable|integer',
'hetzner_token' => 'required_without:selected_token_id|string',
'save_token' => 'boolean',
'token_name' => [
'nullable',
'string',
'max:255',
function ($attribute, $value, $fail) {
if ($this->save_token && ! empty($this->hetzner_token) && empty($value)) {
$fail('Please provide a name for the token.');
}
},
],
'selected_token_id' => 'required|integer|exists:cloud_provider_tokens,id',
]);
try {
$hetznerToken = $this->getHetznerToken();
if (! $hetznerToken) {
return $this->dispatch('error', 'Please provide a valid Hetzner API token.');
}
// Validate token if it's a new one
if (! $this->selected_token_id) {
if (! $this->validateHetznerToken($hetznerToken)) {
return $this->dispatch('error', 'Invalid Hetzner API token. Please check your token and try again.');
}
// Save token if requested
if ($this->save_token) {
CloudProviderToken::create([
'team_id' => currentTeam()->id,
'provider' => 'hetzner',
'token' => $this->hetzner_token,
'name' => $this->token_name,
]);
}
return $this->dispatch('error', 'Please select a valid Hetzner token.');
}
// Load Hetzner data
@@ -424,6 +408,7 @@ class ByHetzner extends Component
'port' => 22,
'team_id' => currentTeam()->id,
'private_key_id' => $this->private_key_id,
'cloud_provider_token_id' => $this->selected_token_id,
'hetzner_server_id' => $hetznerServer['id'],
]);