Testergebnis erscheint am Feld statt am Seitenende; DNS-Token bekommt einen Testknopf
parent
b8a774b604
commit
f91726d078
|
|
@ -106,6 +106,15 @@ class Integrations extends Component
|
|||
|
||||
public ?array $check = null;
|
||||
|
||||
/**
|
||||
* Zu welchem Eintrag `$check` gehört.
|
||||
*
|
||||
* Ohne das erschien das Ergebnis eines DNS-Tests unter der Stripe-Karte —
|
||||
* beide Prüfungen teilen sich eine Eigenschaft, und die Anzeige braucht
|
||||
* den Bezug.
|
||||
*/
|
||||
public ?string $checkedKey = null;
|
||||
|
||||
// Plain settings — App\Support\ProvisioningSettings' full list.
|
||||
public string $dnsZone = '';
|
||||
|
||||
|
|
@ -297,6 +306,7 @@ class Integrations extends Component
|
|||
|
||||
$this->entered[$field] = '';
|
||||
$this->check = null;
|
||||
$this->checkedKey = null;
|
||||
$this->dispatch('notify', message: __('secrets.saved'));
|
||||
}
|
||||
|
||||
|
|
@ -332,6 +342,7 @@ class Integrations extends Component
|
|||
|
||||
app(SecretVault::class)->forget($key);
|
||||
$this->check = null;
|
||||
$this->checkedKey = null;
|
||||
$this->dispatch('notify', message: __('secrets.removed'));
|
||||
}
|
||||
|
||||
|
|
@ -435,6 +446,7 @@ class Integrations extends Component
|
|||
|
||||
$candidate = trim((string) ($this->entered[self::field($key)] ?? '')) ?: null;
|
||||
$this->check = app($checker)->run($candidate);
|
||||
$this->checkedKey = $key;
|
||||
}
|
||||
|
||||
/** The dotless form key for a registry key (a dot means nesting to Livewire). */
|
||||
|
|
@ -540,6 +552,7 @@ class Integrations extends Component
|
|||
$this->lockAgain();
|
||||
|
||||
$this->check = null;
|
||||
$this->checkedKey = null;
|
||||
}
|
||||
|
||||
private function guardInfra(): void
|
||||
|
|
@ -668,6 +681,9 @@ class Integrations extends Component
|
|||
// Everything else here is issued by somebody else (Stripe,
|
||||
// Hetzner, the mail host) and can only be pasted in.
|
||||
'generatable' => $key === 'ssh.private_key',
|
||||
// Das Ergebnis GEHÖRT zu diesem Eintrag, sonst null. So
|
||||
// steht die Antwort unter dem Knopf, der sie ausgelöst hat.
|
||||
'check' => $this->checkedKey === $key ? $this->check : null,
|
||||
])
|
||||
->keyBy('key'),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Services\Secrets;
|
||||
|
||||
use App\Models\Operator;
|
||||
use App\Services\Dns\DnsTokenCheck;
|
||||
use App\Services\Stripe\StripeCheck;
|
||||
use App\Support\OperatingMode;
|
||||
use Illuminate\Contracts\Encryption\DecryptException;
|
||||
|
|
@ -115,6 +116,12 @@ final class SecretVault
|
|||
'dns.token' => [
|
||||
'config' => 'provisioning.dns.token',
|
||||
'label' => 'secrets.item.dns_token',
|
||||
// Die Prüfung gab es längst — sie lief nur auf der
|
||||
// Bereitschaftsseite und nicht an der Karte, auf der der Token
|
||||
// eingetippt wird. Ein Wert prüfen zu können, BEVOR er gespeichert
|
||||
// ist, ist genau der Punkt dieses Knopfes: ein falscher Token
|
||||
// fällt sonst erst auf, wenn eine Bereitstellung an ihm stirbt.
|
||||
'check' => DnsTokenCheck::class,
|
||||
'env_key' => 'HETZNER_DNS_TOKEN',
|
||||
],
|
||||
'monitoring.token' => [
|
||||
|
|
|
|||
|
|
@ -72,6 +72,12 @@ return [
|
|||
'saved' => 'Gespeichert. Der neue Wert gilt ab sofort.',
|
||||
'removed' => 'Entfernt. Es gilt wieder der Wert aus der Serverdatei.',
|
||||
'empty' => 'Bitte einen Wert eingeben.',
|
||||
'check_writable' => 'Der Token darf schreiben. Ein Probeeintrag wurde angelegt und wieder entfernt.',
|
||||
'check_read_only' => 'Der Token darf lesen, aber nicht schreiben. Damit scheitert der A-Eintrag jeder neuen Instanz — nach der Bezahlung.',
|
||||
'check_write_failed' => 'Der Schreibversuch wurde abgelehnt. Über die Berechtigung des Tokens sagt das nichts.',
|
||||
'check_zone_not_found' => 'Die eingetragene Zone liegt nicht in diesem Konto. Einzelheiten auf der Bereitschaftsseite.',
|
||||
'check_zone_list_failed' => 'Die Zonenliste kam nicht. Über die Zonen dieses Kontos sagt das nichts.',
|
||||
'check_zone_list_unreadable' => 'Die Antwort war keine Zonenliste. Da hat etwas anderes geantwortet.',
|
||||
|
||||
'check_title' => 'Ergebnis der Prüfung',
|
||||
'check_missing' => 'Es ist kein Schlüssel hinterlegt und auch keiner in der Serverdatei.',
|
||||
|
|
|
|||
|
|
@ -70,6 +70,12 @@ return [
|
|||
'saved' => 'Saved. The new value applies immediately.',
|
||||
'removed' => 'Removed. The server file applies again.',
|
||||
'empty' => 'Enter a value.',
|
||||
'check_writable' => 'The token may write. A probe record was created and removed again.',
|
||||
'check_read_only' => 'The token may read but not write. Every new instance\'s A record then fails — after payment.',
|
||||
'check_write_failed' => 'The write was rejected. That says nothing about the token\'s permissions.',
|
||||
'check_zone_not_found' => 'The configured zone is not in this account. Details on the readiness page.',
|
||||
'check_zone_list_failed' => 'The zone list did not arrive. That says nothing about this account\'s zones.',
|
||||
'check_zone_list_unreadable' => 'The answer was not a zone list. Something else replied.',
|
||||
|
||||
'check_title' => 'Result',
|
||||
'check_missing' => 'No key is stored here and none is in the server file.',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
@props(['check', 'key'])
|
||||
{{--
|
||||
Die Antwort einer Prüfung, direkt unter dem Knopf, der sie ausgelöst hat.
|
||||
|
||||
Vorher stand dieser Block ganz unten auf der Seite — hinter allen Karten und
|
||||
hinter dem Speichern-Knopf. Der Testknopf sitzt oben in der Stripe-Karte;
|
||||
wer ihn drückte, sah die Antwort nie, weil sie ausserhalb des Bildschirms
|
||||
erschien. Gemeldet wurde das als „der Knopf tut nichts", und genau so sah es
|
||||
aus.
|
||||
|
||||
Und der Block ging von einem STRIPE-Ergebnis aus: `account`, `live`,
|
||||
`webhooks`. Ein DnsTokenCheck-Ergebnis hat davon nichts — und sein Erfolg
|
||||
heisst `writable`, lief also in genau diesen Zweig. Deshalb wird hier nach
|
||||
dem geprüften Eintrag verzweigt statt nach `ok`.
|
||||
--}}
|
||||
<div class="mt-4 rounded-lg border border-line bg-surface-2 p-4">
|
||||
<h4 class="text-sm font-semibold text-ink">{{ __('secrets.check_title') }}</h4>
|
||||
|
||||
@if (! $check['ok'])
|
||||
<x-ui.alert variant="danger" class="mt-3">{{ __('secrets.check_'.$check['reason']) }}</x-ui.alert>
|
||||
@elseif ($key !== 'stripe.secret')
|
||||
{{-- Jede andere Prüfung sagt genau eine Sache: es geht. Was sie im
|
||||
Fehlerfall zu sagen hat, steht ausführlich auf der
|
||||
Bereitschaftsseite, die dafür die Stelle ist. --}}
|
||||
<x-ui.alert variant="success" class="mt-3">{{ __('secrets.check_'.$check['reason']) }}</x-ui.alert>
|
||||
@else
|
||||
<dl class="mt-3 space-y-1 text-sm">
|
||||
<div class="flex gap-2"><dt class="text-muted">{{ __('secrets.check_account') }}:</dt>
|
||||
<dd class="font-mono text-body">{{ $check['account'] }}{{ $check['business'] ? ' · '.$check['business'] : '' }}</dd></div>
|
||||
<div class="flex gap-2"><dt class="text-muted">{{ __('secrets.check_mode') }}:</dt>
|
||||
<dd class="{{ $check['live'] ? 'font-medium text-warning' : 'text-body' }}">
|
||||
{{ $check['live'] ? __('secrets.mode_live') : __('secrets.mode_test') }}{{ $check['restricted'] ? ' · '.__('secrets.mode_restricted') : '' }}
|
||||
</dd></div>
|
||||
</dl>
|
||||
|
||||
<h5 class="mt-4 text-sm font-semibold text-ink">{{ __('secrets.check_webhooks') }}</h5>
|
||||
@if ($check['webhooks'] === null)
|
||||
<p class="mt-1 text-sm text-muted">{{ __('secrets.check_webhooks_unknown') }}</p>
|
||||
@elseif ($check['webhooks'] === [])
|
||||
<x-ui.alert variant="warning" class="mt-2">{{ __('secrets.check_webhooks_none') }}</x-ui.alert>
|
||||
@else
|
||||
<ul class="mt-2 space-y-3">
|
||||
@foreach ($check['webhooks'] as $hook)
|
||||
<li class="rounded-lg border border-line bg-surface px-4 py-3">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<span class="font-mono text-xs text-body">{{ $hook['url'] }}</span>
|
||||
<span class="text-xs {{ $hook['status'] === 'enabled' ? 'text-success' : 'text-warning' }}">{{ $hook['status'] }}</span>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-wrap gap-1.5">
|
||||
@foreach ($hook['events'] as $event)
|
||||
<span class="rounded border border-line px-1.5 py-0.5 font-mono text-[11px] text-muted">{{ $event }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
|
@ -130,5 +130,9 @@
|
|||
</x-ui.button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($entry['check'] !== null)
|
||||
<x-admin.check-result :check="$entry['check']" :key="$entry['key']" />
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -368,49 +368,6 @@
|
|||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Stripe's own answer, under the Stripe card it belongs to. --}}
|
||||
@if ($tab === 'services' && $canSecrets && $unlocked && $check !== null)
|
||||
<div class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise">
|
||||
<h2 class="font-semibold text-ink">{{ __('secrets.check_title') }}</h2>
|
||||
|
||||
@if (! $check['ok'])
|
||||
<x-ui.alert variant="danger" class="mt-3">{{ __('secrets.check_'.$check['reason']) }}</x-ui.alert>
|
||||
@else
|
||||
<dl class="mt-3 space-y-1 text-sm">
|
||||
<div class="flex gap-2"><dt class="text-muted">{{ __('secrets.check_account') }}:</dt>
|
||||
<dd class="font-mono text-body">{{ $check['account'] }}{{ $check['business'] ? ' · '.$check['business'] : '' }}</dd></div>
|
||||
<div class="flex gap-2"><dt class="text-muted">{{ __('secrets.check_mode') }}:</dt>
|
||||
<dd class="{{ $check['live'] ? 'font-medium text-warning' : 'text-body' }}">
|
||||
{{ $check['live'] ? __('secrets.mode_live') : __('secrets.mode_test') }}{{ $check['restricted'] ? ' · '.__('secrets.mode_restricted') : '' }}
|
||||
</dd></div>
|
||||
</dl>
|
||||
|
||||
<h3 class="mt-5 text-sm font-semibold text-ink">{{ __('secrets.check_webhooks') }}</h3>
|
||||
@if ($check['webhooks'] === null)
|
||||
<p class="mt-1 text-sm text-muted">{{ __('secrets.check_webhooks_unknown') }}</p>
|
||||
@elseif ($check['webhooks'] === [])
|
||||
<x-ui.alert variant="warning" class="mt-2">{{ __('secrets.check_webhooks_none') }}</x-ui.alert>
|
||||
@else
|
||||
<ul class="mt-2 space-y-3">
|
||||
@foreach ($check['webhooks'] as $hook)
|
||||
<li class="rounded-lg border border-line bg-surface-2 px-4 py-3">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<span class="font-mono text-xs text-body">{{ $hook['url'] }}</span>
|
||||
<span class="text-xs {{ $hook['status'] === 'enabled' ? 'text-success' : 'text-warning' }}">{{ $hook['status'] }}</span>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-wrap gap-1.5">
|
||||
@foreach ($hook['events'] as $event)
|
||||
<span class="rounded border border-line px-1.5 py-0.5 font-mono text-[11px] text-muted">{{ $event }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Part B: the raw .env editor. Everything above has a field; everything
|
||||
that does not still needs to be reachable from the console, or an
|
||||
operator needing it needs a shell and this whole page misses the
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
use App\Livewire\Admin\Integrations;
|
||||
use App\Support\Settings;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Livewire\Livewire;
|
||||
|
||||
/**
|
||||
* Gemeldet: „Verbindung testen" gibt keine Rückmeldung.
|
||||
*/
|
||||
beforeEach(function () {
|
||||
config()->set('admin_access.secrets_key', 'base64:'.base64_encode(random_bytes(32)));
|
||||
withStripeSecret();
|
||||
Http::preventStrayRequests();
|
||||
});
|
||||
|
||||
it('produces a result at all when the button is pressed', function () {
|
||||
Http::fake(['api.stripe.com/v1/account' => Http::response([
|
||||
'id' => 'acct_1', 'settings' => ['dashboard' => ['display_name' => 'CluPilot']],
|
||||
])]);
|
||||
|
||||
$page = Livewire::actingAs(operator('Owner'), 'operator')
|
||||
->test(Integrations::class)
|
||||
->set('confirmablePassword', 'password')
|
||||
->call('confirmPassword')
|
||||
->call('test', 'stripe.secret');
|
||||
|
||||
// Erst: kommt überhaupt ein Ergebnis an? Wenn ja, ist es keine kaputte
|
||||
// Prüfung, sondern eine Anzeige, die niemand findet.
|
||||
expect($page->get('check'))->not->toBeNull();
|
||||
});
|
||||
|
||||
it('does not fall over when the DNS token is the one tested', function () {
|
||||
// Der Ergebnisblock las `$check['account']`, `$check['live']`,
|
||||
// `$check['webhooks']` — alles nur bei Stripe vorhanden. Ein
|
||||
// DnsTokenCheck-Ergebnis hat davon nichts, und `writable` ist obendrein
|
||||
// ein ERFOLG, laeuft also in genau diesen Zweig.
|
||||
Settings::set('provisioning.dns_zone', 'probe.example');
|
||||
Http::fake([
|
||||
'api.hetzner.cloud/v1/zones' => Http::response(['zones' => [['id' => 42, 'name' => 'probe.example']]]),
|
||||
'api.hetzner.cloud/v1/zones/probe.example/rrsets' => Http::response(['rrset' => ['id' => 'x/TXT']], 201),
|
||||
'api.hetzner.cloud/v1/zones/probe.example/rrsets/*' => Http::response([], 201),
|
||||
]);
|
||||
|
||||
Livewire::actingAs(operator('Owner'), 'operator')
|
||||
->test(Integrations::class)
|
||||
->set('confirmablePassword', 'password')
|
||||
->call('confirmPassword')
|
||||
->set('entered.dns_token', 'irgendein-token')
|
||||
->call('test', 'dns.token')
|
||||
->assertOk()
|
||||
->assertSee(__('secrets.check_writable'));
|
||||
});
|
||||
|
||||
it('shows the result beside the key it was run for, not at the end of the page', function () {
|
||||
// Der eigentliche Fund: das Ergebnis stand ganz unten, hinter allen
|
||||
// Karten und hinter dem Speichern-Knopf. Der Knopf sitzt oben in der
|
||||
// Stripe-Karte — wer ihn drückt, sieht die Antwort nicht, weil sie
|
||||
// ausserhalb des Bildschirms erscheint. Für den Betreiber ist das
|
||||
// ununterscheidbar von „der Knopf tut nichts".
|
||||
$markup = file_get_contents(base_path('resources/views/components/admin/secret-field.blade.php'));
|
||||
|
||||
expect($markup)->toContain('$entry[\'check\']');
|
||||
});
|
||||
Loading…
Reference in New Issue