diff --git a/app/Livewire/Server/New/ByHetzner.php b/app/Livewire/Server/New/ByHetzner.php index 6f7e915c9..e344524ea 100644 --- a/app/Livewire/Server/New/ByHetzner.php +++ b/app/Livewire/Server/New/ByHetzner.php @@ -42,12 +42,16 @@ class ByHetzner extends Component public array $serverTypes = []; + public array $hetznerSshKeys = []; + public ?string $selected_location = null; public ?int $selected_image = null; public ?string $selected_server_type = null; + public array $selectedHetznerSshKeyIds = []; + public string $server_name = ''; public ?int $private_key_id = null; @@ -110,6 +114,8 @@ protected function rules(): array 'selected_image' => 'required|integer', 'selected_server_type' => 'required|string', 'private_key_id' => 'required|integer|exists:private_keys,id,team_id,'.currentTeam()->id, + 'selectedHetznerSshKeyIds' => 'nullable|array', + 'selectedHetznerSshKeyIds.*' => 'integer', ]); } @@ -224,6 +230,14 @@ private function loadHetznerData(string $token) 'debian_images' => collect($this->images)->filter(fn ($img) => str_contains($img['name'] ?? '', 'debian'))->values(), ]); + // Load SSH keys from Hetzner + $this->hetznerSshKeys = $hetznerService->getSshKeys(); + + ray('Hetzner SSH Keys', [ + 'total_count' => count($this->hetznerSshKeys), + 'keys' => $this->hetznerSshKeys, + ]); + $this->loading_data = false; } catch (\Throwable $e) { $this->loading_data = false; @@ -365,6 +379,16 @@ private function createHetznerServer(string $token): array // Normalize server name to lowercase for RFC 1123 compliance $normalizedServerName = strtolower(trim($this->server_name)); + // Prepare SSH keys array: Coolify key + user-selected Hetzner keys + $sshKeys = array_merge( + [$sshKeyId], // Coolify key (always included) + $this->selectedHetznerSshKeyIds // User-selected Hetzner keys + ); + + // Remove duplicates in case the Coolify key was also selected + $sshKeys = array_unique($sshKeys); + $sshKeys = array_values($sshKeys); // Re-index array + // Prepare server creation parameters $params = [ 'name' => $normalizedServerName, @@ -372,7 +396,7 @@ private function createHetznerServer(string $token): array 'image' => $this->selected_image, 'location' => $this->selected_location, 'start_after_create' => true, - 'ssh_keys' => [$sshKeyId], + 'ssh_keys' => $sshKeys, ]; ray('Server creation parameters', $params); diff --git a/app/View/Components/Forms/Datalist.php b/app/View/Components/Forms/Datalist.php index 25643753d..33e264e37 100644 --- a/app/View/Components/Forms/Datalist.php +++ b/app/View/Components/Forms/Datalist.php @@ -4,7 +4,7 @@ use Closure; use Illuminate\Contracts\View\View; -use Illuminate\Support\Str; +use Illuminate\Support\Facades\Gate; use Illuminate\View\Component; use Visus\Cuid2\Cuid2; @@ -19,9 +19,27 @@ public function __construct( public ?string $label = null, public ?string $helper = null, public bool $required = false, - public string $defaultClass = 'input' + public bool $disabled = false, + public bool $readonly = false, + public bool $multiple = false, + public string|bool $instantSave = false, + public ?string $value = null, + public ?string $placeholder = null, + public bool $autofocus = false, + public string $defaultClass = 'input', + public ?string $canGate = null, + public mixed $canResource = null, + public bool $autoDisable = true, ) { - // + // Handle authorization-based disabling + if ($this->canGate && $this->canResource && $this->autoDisable) { + $hasPermission = Gate::allows($this->canGate, $this->canResource); + + if (! $hasPermission) { + $this->disabled = true; + $this->instantSave = false; // Disable instant save for unauthorized users + } + } } /** @@ -36,8 +54,6 @@ public function render(): View|Closure|string $this->name = $this->id; } - $this->label = Str::title($this->label); - return view('components.forms.datalist'); } } diff --git a/resources/views/components/forms/datalist.blade.php b/resources/views/components/forms/datalist.blade.php index c9710b728..8ce19773e 100644 --- a/resources/views/components/forms/datalist.blade.php +++ b/resources/views/components/forms/datalist.blade.php @@ -1,6 +1,6 @@
- + @endif + @error($id) @enderror - {{-- --}}
diff --git a/resources/views/livewire/server/new/by-hetzner.blade.php b/resources/views/livewire/server/new/by-hetzner.blade.php index e95c85887..b8191593d 100644 --- a/resources/views/livewire/server/new/by-hetzner.blade.php +++ b/resources/views/livewire/server/new/by-hetzner.blade.php @@ -99,6 +99,22 @@ +
+ + @foreach ($hetznerSshKeys as $sshKey) + + @endforeach + +
+
diff --git a/tests/Feature/HetznerServerCreationTest.php b/tests/Feature/HetznerServerCreationTest.php new file mode 100644 index 000000000..815462ffa --- /dev/null +++ b/tests/Feature/HetznerServerCreationTest.php @@ -0,0 +1,49 @@ +toBe([123]) + ->and(count($sshKeys))->toBe(1); +}); + +it('validates SSH key array merging with additional Hetzner keys', function () { + $coolifyKeyId = 123; + $selectedHetznerKeys = [456, 789]; + + $sshKeys = array_merge( + [$coolifyKeyId], + $selectedHetznerKeys + ); + $sshKeys = array_unique($sshKeys); + $sshKeys = array_values($sshKeys); + + expect($sshKeys)->toBe([123, 456, 789]) + ->and(count($sshKeys))->toBe(3); +}); + +it('validates deduplication when Coolify key is also in selected keys', function () { + $coolifyKeyId = 123; + $selectedHetznerKeys = [123, 456, 789]; + + $sshKeys = array_merge( + [$coolifyKeyId], + $selectedHetznerKeys + ); + $sshKeys = array_unique($sshKeys); + $sshKeys = array_values($sshKeys); + + expect($sshKeys)->toBe([123, 456, 789]) + ->and(count($sshKeys))->toBe(3); +}); diff --git a/tests/Unit/DatalistComponentTest.php b/tests/Unit/DatalistComponentTest.php new file mode 100644 index 000000000..12699c30a --- /dev/null +++ b/tests/Unit/DatalistComponentTest.php @@ -0,0 +1,68 @@ +required)->toBeFalse() + ->and($component->disabled)->toBeFalse() + ->and($component->readonly)->toBeFalse() + ->and($component->multiple)->toBeFalse() + ->and($component->instantSave)->toBeFalse() + ->and($component->defaultClass)->toBe('input'); +}); + +it('uses provided id', function () { + $component = new Datalist(id: 'test-datalist'); + + expect($component->id)->toBe('test-datalist'); +}); + +it('accepts multiple selection mode', function () { + $component = new Datalist(multiple: true); + + expect($component->multiple)->toBeTrue(); +}); + +it('accepts instantSave parameter', function () { + $component = new Datalist(instantSave: 'customSave'); + + expect($component->instantSave)->toBe('customSave'); +}); + +it('accepts placeholder', function () { + $component = new Datalist(placeholder: 'Select an option...'); + + expect($component->placeholder)->toBe('Select an option...'); +}); + +it('accepts autofocus', function () { + $component = new Datalist(autofocus: true); + + expect($component->autofocus)->toBeTrue(); +}); + +it('accepts disabled state', function () { + $component = new Datalist(disabled: true); + + expect($component->disabled)->toBeTrue(); +}); + +it('accepts readonly state', function () { + $component = new Datalist(readonly: true); + + expect($component->readonly)->toBeTrue(); +}); + +it('accepts authorization properties', function () { + $component = new Datalist( + canGate: 'update', + canResource: 'resource', + autoDisable: false + ); + + expect($component->canGate)->toBe('update') + ->and($component->canResource)->toBe('resource') + ->and($component->autoDisable)->toBeFalse(); +}); diff --git a/tests/Unit/HetznerSshKeysTest.php b/tests/Unit/HetznerSshKeysTest.php new file mode 100644 index 000000000..06c6b06e6 --- /dev/null +++ b/tests/Unit/HetznerSshKeysTest.php @@ -0,0 +1,53 @@ +toBe([123, 456, 789]) + ->and(count($sshKeys))->toBe(3); +}); + +it('removes duplicate SSH key IDs', function () { + $coolifyKeyId = 123; + $selectedHetznerKeys = [123, 456, 789]; // User also selected Coolify key + + // Simulate the merge and deduplication logic + $sshKeys = array_merge( + [$coolifyKeyId], + $selectedHetznerKeys + ); + $sshKeys = array_unique($sshKeys); + $sshKeys = array_values($sshKeys); + + expect($sshKeys)->toBe([123, 456, 789]) + ->and(count($sshKeys))->toBe(3); +}); + +it('works with no selected Hetzner keys', function () { + $coolifyKeyId = 123; + $selectedHetznerKeys = []; + + // Simulate the merge logic + $sshKeys = array_merge( + [$coolifyKeyId], + $selectedHetznerKeys + ); + + expect($sshKeys)->toBe([123]) + ->and(count($sshKeys))->toBe(1); +}); + +it('validates SSH key IDs are integers', function () { + $selectedHetznerKeys = [456, 789, 1011]; + + foreach ($selectedHetznerKeys as $keyId) { + expect($keyId)->toBeInt(); + } +});