feat: add token validation functionality for Hetzner and DigitalOcean providers

This commit is contained in:
Andras Bacsai 2025-10-29 23:21:38 +01:00
parent c95e297f39
commit 2a8fbb3f6e
3 changed files with 82 additions and 18 deletions

View file

@ -30,6 +30,60 @@ public function loadTokens()
$this->tokens = CloudProviderToken::ownedByCurrentTeam()->get();
}
public function validateToken(int $tokenId)
{
try {
$token = CloudProviderToken::ownedByCurrentTeam()->findOrFail($tokenId);
$this->authorize('view', $token);
if ($token->provider === 'hetzner') {
$isValid = $this->validateHetznerToken($token->token);
if ($isValid) {
$this->dispatch('success', 'Hetzner token is valid.');
} else {
$this->dispatch('error', 'Hetzner token validation failed. Please check the token.');
}
} elseif ($token->provider === 'digitalocean') {
$isValid = $this->validateDigitalOceanToken($token->token);
if ($isValid) {
$this->dispatch('success', 'DigitalOcean token is valid.');
} else {
$this->dispatch('error', 'DigitalOcean token validation failed. Please check the token.');
}
} else {
$this->dispatch('error', 'Unknown provider.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
private function validateHetznerToken(string $token): bool
{
try {
$response = \Illuminate\Support\Facades\Http::withToken($token)
->timeout(10)
->get('https://api.hetzner.cloud/v1/servers?per_page=1');
return $response->successful();
} catch (\Throwable $e) {
return false;
}
}
private function validateDigitalOceanToken(string $token): bool
{
try {
$response = \Illuminate\Support\Facades\Http::withToken($token)
->timeout(10)
->get('https://api.digitalocean.com/v2/account');
return $response->successful();
} catch (\Throwable $e) {
return false;
}
}
public function deleteToken(int $tokenId)
{
try {

View file

@ -14,13 +14,14 @@
<x-forms.input required id="name" label="Token Name"
placeholder="e.g., Production Hetzner. tip: add Hetzner project name to identify easier" />
<x-forms.input required type="password" id="token" label="API Token" placeholder="Enter your API token" />
<x-forms.input required type="password" id="token" label="API Token"
placeholder="Enter your API token" />
@if (auth()->user()->currentTeam()->cloudProviderTokens->where('provider', $provider)->isEmpty())
<div class="text-sm text-neutral-500 dark:text-neutral-400">
Create an API token in the <a
href='{{ $provider === 'hetzner' ? 'https://console.hetzner.com/projects' : '#' }}' target='_blank'
class='underline dark:text-white'>{{ ucfirst($provider) }} Console</a> choose
href='{{ $provider === 'hetzner' ? 'https://console.hetzner.com/projects' : '#' }}'
target='_blank' class='underline dark:text-white'>{{ ucfirst($provider) }} Console</a> choose
Project Security API Tokens.
@if ($provider === 'hetzner')
<br><br>
@ -28,12 +29,12 @@ class='underline dark:text-white'>{{ ucfirst($provider) }} Console</a> → choos
class='underline dark:text-white'>Sign up here</a>
<br>
<span class="text-xs">(Coolify's affiliate link, only new accounts - supports us (€10)
and gives you €20)</span>
and gives you €20)</span>
@endif
</div>
@endif
<x-forms.button type="submit" wire:target="addToken">Validate & Add Token</x-forms.button>
<x-forms.button type="submit">Validate & Add Token</x-forms.button>
@else
{{-- Full page layout: horizontal, spacious --}}
<div class="flex gap-2 items-end flex-wrap">
@ -49,7 +50,8 @@ class='underline dark:text-white'>Sign up here</a>
</div>
</div>
<div class="flex-1 min-w-64">
<x-forms.input required type="password" id="token" label="API Token" placeholder="Enter your API token" />
<x-forms.input required type="password" id="token" label="API Token"
placeholder="Enter your API token" />
@if (auth()->user()->currentTeam()->cloudProviderTokens->where('provider', $provider)->isEmpty())
<div class="text-sm text-neutral-500 dark:text-neutral-400 mt-2">
Create an API token in the <a href='https://console.hetzner.com/projects' target='_blank'
@ -60,11 +62,11 @@ class='underline dark:text-white'>Hetzner Console</a> → choose Project → Sec
class='underline dark:text-white'>Sign up here</a>
<br>
<span class="text-xs">(Coolify's affiliate link, only new accounts - supports us (€10)
and gives you €20)</span>
and gives you €20)</span>
</div>
@endif
</div>
<x-forms.button type="submit" wire:target="addToken">Validate & Add Token</x-forms.button>
<x-forms.button type="submit">Validate & Add Token</x-forms.button>
@endif
</form>
</div>

View file

@ -20,16 +20,24 @@ class="flex flex-col gap-1 p-2 border dark:border-coolgray-200 hover:no-underlin
</div>
<div class="text-sm">Created: {{ $savedToken->created_at->diffForHumans() }}</div>
@can('delete', $savedToken)
<x-modal-confirmation title="Confirm Token Deletion?" isErrorButton buttonTitle="Delete Token"
submitAction="deleteToken({{ $savedToken->id }})" :actions="[
'This cloud provider token will be permanently deleted.',
'Any servers using this token will need to be reconfigured.',
]"
confirmationText="{{ $savedToken->name }}"
confirmationLabel="Please confirm the deletion by entering the token name below"
shortConfirmationLabel="Token Name" :confirmWithPassword="false" step2ButtonText="Delete Token" />
@endcan
<div class="flex gap-2 pt-2">
@can('view', $savedToken)
<x-forms.button wire:click="validateToken({{ $savedToken->id }})" type="button">
Validate Token
</x-forms.button>
@endcan
@can('delete', $savedToken)
<x-modal-confirmation title="Confirm Token Deletion?" isErrorButton buttonTitle="Delete Token"
submitAction="deleteToken({{ $savedToken->id }})" :actions="[
'This cloud provider token will be permanently deleted.',
'Any servers using this token will need to be reconfigured.',
]"
confirmationText="{{ $savedToken->name }}"
confirmationLabel="Please confirm the deletion by entering the token name below"
shortConfirmationLabel="Token Name" :confirmWithPassword="false" step2ButtonText="Delete Token" />
@endcan
</div>
</div>
@empty
<div>