From c1bcc415463b46f055b5db4ab470a06557eb18c7 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 8 Oct 2025 20:47:50 +0200 Subject: [PATCH] init of cloud providers --- app/Livewire/Security/CloudProviderTokens.php | 119 ++++++++++++++++ app/Livewire/Security/CloudTokens.php | 13 ++ app/Livewire/Server/New/ByHetzner.php | 128 ++++++++++++++++++ app/Models/CloudProviderToken.php | 31 +++++ app/Models/Team.php | 5 + app/Policies/CloudProviderTokenPolicy.php | 65 +++++++++ ...125_create_cloud_provider_tokens_table.php | 33 +++++ .../components/security/navbar.blade.php | 7 +- .../security/cloud-provider-tokens.blade.php | 66 +++++++++ .../livewire/security/cloud-tokens.blade.php | 7 + .../views/livewire/server/create.blade.php | 22 ++- .../livewire/server/new/by-hetzner.blade.php | 52 +++++++ routes/web.php | 2 + 13 files changed, 548 insertions(+), 2 deletions(-) create mode 100644 app/Livewire/Security/CloudProviderTokens.php create mode 100644 app/Livewire/Security/CloudTokens.php create mode 100644 app/Livewire/Server/New/ByHetzner.php create mode 100644 app/Models/CloudProviderToken.php create mode 100644 app/Policies/CloudProviderTokenPolicy.php create mode 100644 database/migrations/2025_10_08_181125_create_cloud_provider_tokens_table.php create mode 100644 resources/views/livewire/security/cloud-provider-tokens.blade.php create mode 100644 resources/views/livewire/security/cloud-tokens.blade.php create mode 100644 resources/views/livewire/server/new/by-hetzner.blade.php diff --git a/app/Livewire/Security/CloudProviderTokens.php b/app/Livewire/Security/CloudProviderTokens.php new file mode 100644 index 000000000..f35a3a806 --- /dev/null +++ b/app/Livewire/Security/CloudProviderTokens.php @@ -0,0 +1,119 @@ +authorize('viewAny', CloudProviderToken::class); + $this->loadTokens(); + } + + protected function rules(): array + { + return [ + 'provider' => 'required|string|in:hetzner,digitalocean', + 'token' => 'required|string', + 'name' => 'required|string|max:255', + ]; + } + + protected function messages(): array + { + return [ + 'provider.required' => 'Please select a cloud provider.', + 'provider.in' => 'Invalid cloud provider selected.', + 'token.required' => 'API token is required.', + 'name.required' => 'Token name is required.', + ]; + } + + public function loadTokens() + { + $this->tokens = CloudProviderToken::ownedByCurrentTeam()->get(); + } + + private function validateToken(string $provider, string $token): bool + { + try { + if ($provider === 'hetzner') { + $response = Http::withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->timeout(10)->get('https://api.hetzner.cloud/v1/servers'); + + return $response->successful(); + } + + // Add other providers here in the future + // if ($provider === 'digitalocean') { ... } + + return false; + } catch (\Throwable $e) { + return false; + } + } + + public function addNewToken() + { + $this->validate(); + + try { + $this->authorize('create', CloudProviderToken::class); + + // Validate the token with the provider's API + if (! $this->validateToken($this->provider, $this->token)) { + return $this->dispatch('error', 'Invalid API token. Please check your token and try again.'); + } + + CloudProviderToken::create([ + 'team_id' => currentTeam()->id, + 'provider' => $this->provider, + 'token' => $this->token, + 'name' => $this->name, + ]); + + $this->reset(['token', 'name']); + $this->loadTokens(); + + $this->dispatch('success', 'Cloud provider token added successfully.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function deleteToken(int $tokenId) + { + try { + $token = CloudProviderToken::findOrFail($tokenId); + $this->authorize('delete', $token); + + $token->delete(); + $this->loadTokens(); + + $this->dispatch('success', 'Cloud provider token deleted successfully.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.security.cloud-provider-tokens'); + } +} diff --git a/app/Livewire/Security/CloudTokens.php b/app/Livewire/Security/CloudTokens.php new file mode 100644 index 000000000..d6d1333f1 --- /dev/null +++ b/app/Livewire/Security/CloudTokens.php @@ -0,0 +1,13 @@ +authorize('viewAny', CloudProviderToken::class); + $this->available_tokens = CloudProviderToken::ownedByCurrentTeam() + ->where('provider', 'hetzner') + ->get(); + } + + protected function rules(): array + { + return [ + 'selected_token_id' => 'nullable|integer', + 'hetzner_token' => 'required_without:selected_token_id|string', + 'save_token' => 'boolean', + 'token_name' => 'required_if:save_token,true|nullable|string|max:255', + ]; + } + + 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.', + ]; + } + + public function selectToken(int $tokenId) + { + $this->selected_token_id = $tokenId; + } + + private function validateHetznerToken(string $token): bool + { + try { + $response = Http::withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->timeout(10)->get('https://api.hetzner.cloud/v1/servers'); + + return $response->successful(); + } catch (\Throwable $e) { + return false; + } + } + + public function submit() + { + $this->validate(); + + try { + $this->authorize('create', Server::class); + + if (Team::serverLimitReached()) { + return $this->dispatch('error', 'You have reached the server limit for your subscription.'); + } + + // Determine which token to use + if ($this->selected_token_id) { + $token = $this->available_tokens->firstWhere('id', $this->selected_token_id); + if (! $token) { + return $this->dispatch('error', 'Selected token not found.'); + } + $hetznerToken = $token->token; + } else { + $hetznerToken = $this->hetzner_token; + + // Validate the new token before saving + if (! $this->validateHetznerToken($hetznerToken)) { + return $this->dispatch('error', 'Invalid Hetzner API token. Please check your token and try again.'); + } + + // If saving the new token + if ($this->save_token) { + CloudProviderToken::create([ + 'team_id' => currentTeam()->id, + 'provider' => 'hetzner', + 'token' => $this->hetzner_token, + 'name' => $this->token_name, + ]); + } + } + + // TODO: Actual Hetzner server provisioning will be implemented in future phase + // The $hetznerToken variable contains the token to use + return $this->dispatch('success', 'Hetzner token validated successfully! Server provisioning coming soon.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.server.new.by-hetzner'); + } +} diff --git a/app/Models/CloudProviderToken.php b/app/Models/CloudProviderToken.php new file mode 100644 index 000000000..9ce216b25 --- /dev/null +++ b/app/Models/CloudProviderToken.php @@ -0,0 +1,31 @@ + 'encrypted', + ]; + + public function team() + { + return $this->belongsTo(Team::class); + } + + public static function ownedByCurrentTeam(array $select = ['*']) + { + $selectArray = collect($select)->concat(['id']); + + return self::whereTeamId(currentTeam()->id)->select($selectArray->all()); + } + + public function scopeForProvider($query, string $provider) + { + return $query->where('provider', $provider); + } +} diff --git a/app/Models/Team.php b/app/Models/Team.php index 51fdeffa4..6945bb918 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -258,6 +258,11 @@ public function privateKeys() return $this->hasMany(PrivateKey::class); } + public function cloudProviderTokens() + { + return $this->hasMany(CloudProviderToken::class); + } + public function sources() { $sources = collect([]); diff --git a/app/Policies/CloudProviderTokenPolicy.php b/app/Policies/CloudProviderTokenPolicy.php new file mode 100644 index 000000000..b7b108ba8 --- /dev/null +++ b/app/Policies/CloudProviderTokenPolicy.php @@ -0,0 +1,65 @@ +isAdmin(); + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, CloudProviderToken $cloudProviderToken): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, CloudProviderToken $cloudProviderToken): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, CloudProviderToken $cloudProviderToken): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, CloudProviderToken $cloudProviderToken): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, CloudProviderToken $cloudProviderToken): bool + { + return $user->isAdmin(); + } +} diff --git a/database/migrations/2025_10_08_181125_create_cloud_provider_tokens_table.php b/database/migrations/2025_10_08_181125_create_cloud_provider_tokens_table.php new file mode 100644 index 000000000..2c92b0e19 --- /dev/null +++ b/database/migrations/2025_10_08_181125_create_cloud_provider_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('team_id')->constrained()->onDelete('cascade'); + $table->string('provider'); + $table->text('token'); + $table->string('name')->nullable(); + $table->timestamps(); + + $table->index(['team_id', 'provider']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cloud_provider_tokens'); + } +}; diff --git a/resources/views/components/security/navbar.blade.php b/resources/views/components/security/navbar.blade.php index e389ad6b7..b0dfdd242 100644 --- a/resources/views/components/security/navbar.blade.php +++ b/resources/views/components/security/navbar.blade.php @@ -6,8 +6,13 @@ + @can('viewAny', App\Models\CloudProviderToken::class) + + + + @endcan - + diff --git a/resources/views/livewire/security/cloud-provider-tokens.blade.php b/resources/views/livewire/security/cloud-provider-tokens.blade.php new file mode 100644 index 000000000..d29631c4c --- /dev/null +++ b/resources/views/livewire/security/cloud-provider-tokens.blade.php @@ -0,0 +1,66 @@ +