init of cloud providers

This commit is contained in:
Andras Bacsai committed 2025-10-08 20:47:50 +02:00
1 parent b803a137f6
commit c1bcc41546
13 files changed
+548 -2

No files matched your search

@@ -0,0 +1,119 @@
<?php
namespace App\Livewire\Security;
use App\Models\CloudProviderToken;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Http;
use Livewire\Component;
class CloudProviderTokens extends Component
{
use AuthorizesRequests;
public $tokens;
public string $provider = 'hetzner';
public string $token = '';
public string $name = '';
public function mount()
{
$this->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');
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace App\Livewire\Security;
use Livewire\Component;
class CloudTokens extends Component
{
public function render()
{
return view('livewire.security.cloud-tokens');
}
}
+128
View File
@@ -0,0 +1,128 @@
<?php
namespace App\Livewire\Server\New;
use App\Models\CloudProviderToken;
use App\Models\Server;
use App\Models\Team;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
use Livewire\Attributes\Locked;
use Livewire\Component;
class ByHetzner extends Component
{
use AuthorizesRequests;
#[Locked]
public Collection $available_tokens;
#[Locked]
public $private_keys;
#[Locked]
public $limit_reached;
public ?int $selected_token_id = null;
public string $hetzner_token = '';
public bool $save_token = false;
public ?string $token_name = null;
public function mount()
{
$this->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');
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class CloudProviderToken extends Model
{
protected $guarded = [];
protected $casts = [
'token' => '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);
}
}
+5
View File
@@ -258,6 +258,11 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
return $this->hasMany(PrivateKey::class);
}
public function cloudProviderTokens()
{
return $this->hasMany(CloudProviderToken::class);
}
public function sources()
{
$sources = collect([]);
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace App\Policies;
use App\Models\CloudProviderToken;
use App\Models\User;
class CloudProviderTokenPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->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();
}
}