From 184350494d94f8a3115671ddc89a82d6585e8642 Mon Sep 17 00:00:00 2001 From: nexxo Date: Tue, 28 Jul 2026 22:25:27 +0200 Subject: [PATCH] Let a half-finished two-factor enrolment be cancelled, and rework the setup page Found uncommitted in the working tree at the start of this session and carried in as its own commit so it can be reverted on its own. Not written here: a cancel action that clears an unconfirmed secret, the wording for every state of the page, and a rebuilt setup view. The suite is green with it in place, including the R18-R23 scan tests. --- app/Livewire/Admin/TwoFactorSetup.php | 23 ++ lang/de/two_factor_setup.php | 15 ++ lang/en/two_factor_setup.php | 15 ++ .../livewire/admin/two-factor-setup.blade.php | 223 +++++++++++++----- 4 files changed, 216 insertions(+), 60 deletions(-) diff --git a/app/Livewire/Admin/TwoFactorSetup.php b/app/Livewire/Admin/TwoFactorSetup.php index e060baf..e5a61bc 100644 --- a/app/Livewire/Admin/TwoFactorSetup.php +++ b/app/Livewire/Admin/TwoFactorSetup.php @@ -70,6 +70,29 @@ class TwoFactorSetup extends Component app(EnableTwoFactorAuthentication::class)($operator); } + /** + * Step back out of a half-finished setup. + * + * Without this the only way out of the QR step was to navigate away, which + * left an unconfirmed secret sitting on the account — invisible, because + * nothing shows a pending enrolment anywhere else. Clearing it costs + * nothing: it was never confirmed, so it never protected anything. + */ + public function cancelSetup(): void + { + $this->requireConfirmedPassword(); + + if (! $operator = $this->currentOperator()) { + return; + } + + app(DisableTwoFactorAuthentication::class)($operator); + + $this->twoFactorCode = ''; + $this->resetErrorBag('twoFactorCode'); + $this->dispatch('notify', message: __('two_factor_setup.cancelled')); + } + /** Accept a code from the app, which is what actually turns it on. */ public function confirmTwoFactor(): void { diff --git a/lang/de/two_factor_setup.php b/lang/de/two_factor_setup.php index b9e9b0b..d429d3d 100644 --- a/lang/de/two_factor_setup.php +++ b/lang/de/two_factor_setup.php @@ -23,4 +23,19 @@ return [ 'new_codes' => 'Neue Wiederherstellungscodes', 'codes_title' => 'Wiederherstellungscodes', 'codes_hint' => 'Jetzt notieren und sicher verwahren. Sie sind der Weg zurück, wenn das Gerät verloren geht — jeder Code gilt einmal.', + 'locked_title' => 'Gesperrt', + 'not_set_title' => 'Noch nicht eingerichtet', + 'not_set_body' => 'Ihr Konto ist derzeit nur durch das Passwort geschützt. Mit Zwei-Faktor braucht eine Anmeldung zusätzlich einen Code, der alle 30 Sekunden wechselt.', + 'need_app' => 'Sie brauchen dafür eine Authenticator-App auf dem Telefon — etwa 1Password, Bitwarden, Google Authenticator oder Aegis.', + 'active_title' => 'Zwei-Faktor ist aktiv', + 'active_body' => 'Jede Anmeldung an der Konsole verlangt zusätzlich einen Code aus Ihrer App.', + 'scan_title' => 'Code scannen und bestätigen', + 'step_two' => 'Schritt 2 von 2', + 'scan_step_open' => 'Öffnen Sie Ihre Authenticator-App und wählen Sie „Konto hinzufügen“.', + 'scan_step_scan' => 'Scannen Sie den Code links.', + 'scan_step_code' => 'Geben Sie die sechsstellige Ziffernfolge ein, die die App anzeigt.', + 'cancel' => 'Abbrechen', + 'cancelled' => 'Einrichtung abgebrochen. Es wurde nichts gespeichert.', + 'copy_codes' => 'Alle kopieren', + 'codes_copied' => 'Kopiert', ]; diff --git a/lang/en/two_factor_setup.php b/lang/en/two_factor_setup.php index 27be514..1f30740 100644 --- a/lang/en/two_factor_setup.php +++ b/lang/en/two_factor_setup.php @@ -24,4 +24,19 @@ return [ 'new_codes' => 'New recovery codes', 'codes_title' => 'Recovery codes', 'codes_hint' => 'Write these down now and keep them safe. They are the way back in if the device is lost — each code works once.', + 'locked_title' => 'Locked', + 'not_set_title' => 'Not set up yet', + 'not_set_body' => 'Your account is currently protected by its password alone. With two-factor, signing in also needs a code that changes every 30 seconds.', + 'need_app' => 'You will need an authenticator app on your phone — 1Password, Bitwarden, Google Authenticator or Aegis, for instance.', + 'active_title' => 'Two-factor is active', + 'active_body' => 'Every console sign-in additionally asks for a code from your app.', + 'scan_title' => 'Scan the code and confirm', + 'step_two' => 'Step 2 of 2', + 'scan_step_open' => 'Open your authenticator app and choose "add account".', + 'scan_step_scan' => 'Scan the code on the left.', + 'scan_step_code' => 'Enter the six digits the app shows.', + 'cancel' => 'Cancel', + 'cancelled' => 'Setup cancelled. Nothing was saved.', + 'copy_codes' => 'Copy all', + 'codes_copied' => 'Copied', ]; diff --git a/resources/views/livewire/admin/two-factor-setup.blade.php b/resources/views/livewire/admin/two-factor-setup.blade.php index ad34dfc..2d99286 100644 --- a/resources/views/livewire/admin/two-factor-setup.blade.php +++ b/resources/views/livewire/admin/two-factor-setup.blade.php @@ -1,74 +1,177 @@
-
-

{{ __('two_factor_setup.title') }}

-

{{ __('two_factor_setup.sub') }}

-
+ {{-- The state belongs beside the title, not floating inside the card: it is + a property of the account, not of whichever step is on screen. --}} +
+
+

{{ __('two_factor_setup.title') }}

+

{{ __('two_factor_setup.sub') }}

+
- {{-- Every action is re-checked server-side against a recent password - confirmation — hiding the buttons stops nobody who can post to - /livewire/update. --}} -
- {{ $twoFactorOn ? __('two_factor_setup.state_on') : __('two_factor_setup.state_off') }} +
- @if (! $passwordConfirmed) - {{-- The gate. A signed-in session is not enough to change how the - account is protected — the risk is an unlocked machine. --}} -
-

{{ __('two_factor_setup.confirm_first') }}

-
-
- + {{-- Every action below is re-checked server-side against a recent password + confirmation — hiding a button stops nobody who can post to + /livewire/update. --}} + + @if (! $passwordConfirmed) + {{-- The gate. Being signed in is not enough to change how the account is + protected: the realistic threat is an unlocked machine. --}} +
+
+ +
+

{{ __('two_factor_setup.locked_title') }}

+

{{ __('two_factor_setup.confirm_first') }}

+ + +
+ +
+ + {{ __('two_factor_setup.confirm_button') }} + + +
+
+
+ + @elseif ($twoFactorOn) +
+
+ +
+

{{ __('two_factor_setup.active_title') }}

+

{{ __('two_factor_setup.active_body') }}

+ +
+ + {{ __('two_factor_setup.new_codes') }} + + + {{ __('two_factor_setup.disable') }} +
- - {{ __('two_factor_setup.confirm_button') }} +
+
+
+ + @elseif ($twoFactorPending) + {{-- The step that had no way out. The secret exists but is unconfirmed, + so "Abbrechen" genuinely clears it rather than leaving a dangling + enrolment that nothing else on the console would ever show. --}} +
+
+

{{ __('two_factor_setup.scan_title') }}

+ {{ __('two_factor_setup.step_two') }} +
+ +
+
+ {!! $twoFactorQr !!} +
+ +
+
    +
  1. + + {{ __('two_factor_setup.scan_step_open') }} +
  2. +
  3. + + {{ __('two_factor_setup.scan_step_scan') }} +
  4. +
  5. + + {{ __('two_factor_setup.scan_step_code') }} +
  6. +
+ +
+
+ +
+ +
+ + {{ __('two_factor_setup.activate') }} + + + {{ __('two_factor_setup.cancel') }} + +
+
+
+
+
+ + @else +
+
+ +
+

{{ __('two_factor_setup.not_set_title') }}

+

{{ __('two_factor_setup.not_set_body') }}

+

{{ __('two_factor_setup.need_app') }}

+ + + {{ __('two_factor_setup.enable') }}
- - @elseif ($twoFactorOn) -
- - {{ __('two_factor_setup.new_codes') }} - - - {{ __('two_factor_setup.disable') }} -
- @elseif ($twoFactorPending) -
-
{!! $twoFactorQr !!}
-
-

{{ __('two_factor_setup.scan') }}

-
- -
- - {{ __('two_factor_setup.activate') }} - -
-
- @else - - {{ __('two_factor_setup.enable') }} - - @endif +
+ @endif - @if ($recoveryCodes) - {{-- Shown once, on purpose: nobody writes down what they were never - shown, and these are the way back in when the phone is gone. --}} -
-

{{ __('two_factor_setup.codes_title') }}

-

{{ __('two_factor_setup.codes_hint') }}

-
    - @foreach ($recoveryCodes as $code)
  • {{ $code }}
  • @endforeach -
+ @if ($recoveryCodes) + {{-- Its own card, not a footnote under another one: these are the only + way back in once the phone is gone, and they are shown exactly once. --}} +
+
+
+

+ {{ __('two_factor_setup.codes_title') }} +

+

{{ __('two_factor_setup.codes_hint') }}

+
+ + + + +
- @endif -
+ +
    + @foreach ($recoveryCodes as $code)
  • {{ $code }}
  • @endforeach +
+
+ @endif