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