From 8d7f2da8d07098de8e034e5c510d52c83cf7e035 Mon Sep 17 00:00:00 2001 From: boban Date: Sun, 14 Jun 2026 22:09:59 +0200 Subject: [PATCH] =?UTF-8?q?feat(ssh):=20provision=20modal=20+=20hardening-?= =?UTF-8?q?row=20action=20=E2=80=94=20confirm,=20run,=20reveal=20key=20onc?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- app/Livewire/Modals/SshKeyProvision.php | 76 +++++++++++++++++++ lang/de/modals.php | 20 +++++ lang/de/servers.php | 1 + lang/en/modals.php | 20 +++++ lang/en/servers.php | 1 + .../modals/ssh-key-provision.blade.php | 58 ++++++++++++++ .../views/livewire/servers/show.blade.php | 17 +++-- tests/Feature/SshKeyProvisionModalTest.php | 60 +++++++++++++++ 8 files changed, 248 insertions(+), 5 deletions(-) create mode 100644 app/Livewire/Modals/SshKeyProvision.php create mode 100644 resources/views/livewire/modals/ssh-key-provision.blade.php create mode 100644 tests/Feature/SshKeyProvisionModalTest.php diff --git a/app/Livewire/Modals/SshKeyProvision.php b/app/Livewire/Modals/SshKeyProvision.php new file mode 100644 index 0000000..1f334d7 --- /dev/null +++ b/app/Livewire/Modals/SshKeyProvision.php @@ -0,0 +1,76 @@ +serverId = $serverId; + $this->serverName = Server::find($serverId)?->name ?? ''; + } + + public static function modalMaxWidth(): string + { + return 'lg'; + } + + public function run(SshKeyProvisioner $provisioner): void + { + if ($this->done) { + return; + } + + $server = Server::find($this->serverId); + if (! $server) { + $this->error = __('common.server_not_found'); + + return; + } + + $result = $provisioner->enableKeyOnlyAccess($server); + + $this->done = true; + $this->ok = $result['ok']; + $this->privateKey = $result['privateKey'] ?? null; + $this->publicKey = $result['publicKey'] ?? null; + $this->error = $result['ok'] ? null : ($result['error'] ?? __('modals.ssh_key_provision.error_unknown')); + + // A credential switch and/or a hardening change happened whenever a key was generated or + // password auth was turned off — refresh the page state either way. + $this->dispatch('hardeningApplied'); + $this->dispatch('credentialChanged'); + + $this->dispatch('notify', message: $this->ok + ? __('modals.ssh_key_provision.notify_ok', ['server' => $this->serverName]) + : __('modals.ssh_key_provision.notify_failed')); + } + + public function render() + { + return view('livewire.modals.ssh-key-provision'); + } +} diff --git a/lang/de/modals.php b/lang/de/modals.php index 96b2c89..cb55a2e 100644 --- a/lang/de/modals.php +++ b/lang/de/modals.php @@ -164,6 +164,26 @@ return [ 'notify_failed' => 'Aktualisierung fehlgeschlagen.', ], + // ── SSH key provision ───────────────────────────────────────────────── + 'ssh_key_provision' => [ + 'title' => 'SSH-Key erstellen & Passwort-Login deaktivieren', + 'subtitle' => 'Für :server wird ein SSH-Key erzeugt und installiert; Passwort-Login wird erst danach abgeschaltet.', + 'steps_title' => 'Ablauf', + 'step_generate' => 'Key erzeugen und auf dem Server hinterlegen.', + 'step_verify' => 'Key-Login prüfen — Passwort-Login bleibt bis dahin aktiv.', + 'step_disable' => 'Erst nach erfolgreicher Prüfung: Passwort-Login deaktivieren.', + 'safety_note' => 'Schlägt die Prüfung fehl, bleibt Passwort-Login aktiv — kein Aussperren.', + 'confirm' => 'Erstellen & deaktivieren', + 'running' => 'Wird ausgeführt …', + 'result_ok' => 'Passwort-Login deaktiviert — Zugang nur noch per Key.', + 'key_warning' => 'Diesen Private-Key jetzt sichern — er wird nur einmal angezeigt.', + 'download' => 'Key herunterladen', + 'done' => 'Fertig', + 'notify_ok' => 'SSH-Key eingerichtet, Passwort-Login deaktiviert (:server).', + 'notify_failed' => 'SSH-Key-Einrichtung fehlgeschlagen.', + 'error_unknown' => 'Unbekannter Fehler.', + ], + // ── Generic confirm dialog defaults (ConfirmAction) ─────────────────── 'confirm_action' => [ 'default_heading' => 'Aktion bestätigen', diff --git a/lang/de/servers.php b/lang/de/servers.php index 65fb2e4..2ff282b 100644 --- a/lang/de/servers.php +++ b/lang/de/servers.php @@ -85,6 +85,7 @@ return [ 'check_secure' => 'sicher', 'check_open' => 'offen', 'fail2ban_configure' => 'fail2ban konfigurieren', + 'ssh_key_provision_action' => 'Sicher deaktivieren', // ── Show: firewall rules ────────────────────────────────────────────── 'firewall_title' => 'Firewall-Regeln', diff --git a/lang/en/modals.php b/lang/en/modals.php index 0f839e4..90f0f3b 100644 --- a/lang/en/modals.php +++ b/lang/en/modals.php @@ -163,6 +163,26 @@ return [ 'notify_failed' => 'Update failed.', ], + // ── SSH key provision ───────────────────────────────────────────────── + 'ssh_key_provision' => [ + 'title' => 'Create SSH key & disable password login', + 'subtitle' => 'An SSH key will be generated and installed on :server; password login is disabled only afterwards.', + 'steps_title' => 'Process', + 'step_generate' => 'Generate key and store it on the server.', + 'step_verify' => 'Verify key login — password login stays active until then.', + 'step_disable' => 'Only after successful verification: disable password login.', + 'safety_note' => 'If verification fails, password login remains active — no lockout.', + 'confirm' => 'Create & disable', + 'running' => 'Running…', + 'result_ok' => 'Password login disabled — access via key only.', + 'key_warning' => 'Save this private key now — it is shown only once.', + 'download' => 'Download key', + 'done' => 'Done', + 'notify_ok' => 'SSH key set up, password login disabled (:server).', + 'notify_failed' => 'SSH key setup failed.', + 'error_unknown' => 'Unknown error.', + ], + // ── Generic confirm dialog defaults (ConfirmAction) ─────────────────── 'confirm_action' => [ 'default_heading' => 'Confirm action', diff --git a/lang/en/servers.php b/lang/en/servers.php index daefc42..e390bec 100644 --- a/lang/en/servers.php +++ b/lang/en/servers.php @@ -85,6 +85,7 @@ return [ 'check_secure' => 'secure', 'check_open' => 'open', 'fail2ban_configure' => 'Configure fail2ban', + 'ssh_key_provision_action' => 'Disable safely', // ── Show: firewall rules ────────────────────────────────────────────── 'firewall_title' => 'Firewall rules', diff --git a/resources/views/livewire/modals/ssh-key-provision.blade.php b/resources/views/livewire/modals/ssh-key-provision.blade.php new file mode 100644 index 0000000..e3a57e8 --- /dev/null +++ b/resources/views/livewire/modals/ssh-key-provision.blade.php @@ -0,0 +1,58 @@ +
+
+ + + +
+

{{ __('modals.ssh_key_provision.title') }}

+

{{ __('modals.ssh_key_provision.subtitle', ['server' => $serverName]) }}

+
+
+ + @if (! $done) +
+

{{ __('modals.ssh_key_provision.steps_title') }}

+
    +
  1. {{ __('modals.ssh_key_provision.step_generate') }}
  2. +
  3. {{ __('modals.ssh_key_provision.step_verify') }}
  4. +
  5. {{ __('modals.ssh_key_provision.step_disable') }}
  6. +
+

+ {{ __('modals.ssh_key_provision.safety_note') }} +

+
+ +
+ {{ __('common.cancel') }} + + + {{ __('modals.ssh_key_provision.confirm') }} + {{ __('modals.ssh_key_provision.running') }} + +
+ @else + @if ($ok) +

{{ __('modals.ssh_key_provision.result_ok') }}

+ @else +

{{ $error }}

+ @endif + + @if ($privateKey) +
+

{{ __('modals.ssh_key_provision.key_warning') }}

+ +
+ + {{ __('modals.ssh_key_provision.download') }} + +
+
+ @endif + +
+ {{ __('modals.ssh_key_provision.done') }} +
+ @endif +
diff --git a/resources/views/livewire/servers/show.blade.php b/resources/views/livewire/servers/show.blade.php index b6f50fc..67281c6 100644 --- a/resources/views/livewire/servers/show.blade.php +++ b/resources/views/livewire/servers/show.blade.php @@ -224,11 +224,18 @@ @endif - {{-- Uniform style across all rows (R10): always the bordered secondary button. --}} - - {{ $check['featureOn'] ? __('common.disable') : __('common.enable') }} - + @if ($check['key'] === 'ssh_password' && $check['featureOn']) + {{-- Safe auto-flow: generate+install a key, verify, switch the panel credential, THEN disable. --}} + + {{ __('servers.ssh_key_provision_action') }} + + @else + + {{ $check['featureOn'] ? __('common.disable') : __('common.enable') }} + + @endif @endif @endforeach diff --git a/tests/Feature/SshKeyProvisionModalTest.php b/tests/Feature/SshKeyProvisionModalTest.php new file mode 100644 index 0000000..4e1c58d --- /dev/null +++ b/tests/Feature/SshKeyProvisionModalTest.php @@ -0,0 +1,60 @@ +actingAs(User::factory()->create()); + $server = Server::create(['name' => 'box', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']); + + $svc = Mockery::mock(SshKeyProvisioner::class); + $svc->shouldReceive('enableKeyOnlyAccess')->once() + ->andReturn(['ok' => true, 'privateKey' => 'PRIVATE-PEM', 'publicKey' => 'ssh-ed25519 AAAA']); + app()->instance(SshKeyProvisioner::class, $svc); + + Livewire::test(SshKeyProvision::class, ['serverId' => $server->id]) + ->call('run') + ->assertSet('done', true) + ->assertSet('ok', true) + ->assertSet('privateKey', 'PRIVATE-PEM') + ->assertDispatched('hardeningApplied') + ->assertSee('PRIVATE-PEM'); + } + + public function test_run_surfaces_an_error_without_revealing_a_key(): void + { + $this->actingAs(User::factory()->create()); + $server = Server::create(['name' => 'box', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']); + + $svc = Mockery::mock(SshKeyProvisioner::class); + $svc->shouldReceive('enableKeyOnlyAccess')->once() + ->andReturn(['ok' => false, 'error' => 'verify failed']); + app()->instance(SshKeyProvisioner::class, $svc); + + Livewire::test(SshKeyProvision::class, ['serverId' => $server->id]) + ->call('run') + ->assertSet('done', true) + ->assertSet('ok', false) + ->assertSet('privateKey', null) + ->assertSee('verify failed'); + } +}