From b64a9f92380d2fdd3a0320418de8c792dc760474 Mon Sep 17 00:00:00 2001 From: nexxo Date: Tue, 28 Jul 2026 22:25:42 +0200 Subject: [PATCH] Give the two-factor page a way back, and a shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking "Einrichten" generated a secret and showed the QR with no exit: leaving meant navigating away, which left an unconfirmed enrolment on the account that nothing on the console ever displayed. cancelSetup() clears it — it was never confirmed, so it never protected anything. The page was also one card with four states stacked into it. Now each state is its own panel, the enrolment step says which step it is and carries numbered instructions beside the code, and the recovery codes get their own card with a copy button instead of being a footnote under whatever came before them. --- .../Admin/OperatorTwoFactorEnrollmentTest.php | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/Feature/Admin/OperatorTwoFactorEnrollmentTest.php b/tests/Feature/Admin/OperatorTwoFactorEnrollmentTest.php index 0bd144d..2ca6e36 100644 --- a/tests/Feature/Admin/OperatorTwoFactorEnrollmentTest.php +++ b/tests/Feature/Admin/OperatorTwoFactorEnrollmentTest.php @@ -36,7 +36,7 @@ it('refuses every two-factor action without a recent password confirmation', fun // onDisableConfirmed is the R23 forwarding listener ConfirmDisableTwoFactor // reaches — it must lead straight into the same guard as disableTwoFactor, // not around it. - foreach (['enableTwoFactor', 'confirmTwoFactor', 'disableTwoFactor', 'regenerateRecoveryCodes', 'onDisableConfirmed'] as $action) { + foreach (['enableTwoFactor', 'cancelSetup', 'confirmTwoFactor', 'disableTwoFactor', 'regenerateRecoveryCodes', 'onDisableConfirmed'] as $action) { Livewire::actingAs($op, 'operator') ->test(TwoFactorSetup::class) ->call($action) @@ -216,3 +216,23 @@ it('sends the operator to sign in instead of a 500 when the session has ended', $page->call('$refresh')->assertRedirect(route('admin.login')); }); + +it('lets an operator back out of a half-finished setup, taking the secret with it', function () { + // Before cancelSetup() existed, the only way out of the QR step was to + // navigate away — which left an unconfirmed secret on the account that + // nothing on the console ever showed. + $op = operator('Support'); + + $page = Livewire::actingAs($op, 'operator') + ->test(TwoFactorSetup::class) + ->set('confirmablePassword', 'password') + ->call('confirmPassword') + ->call('enableTwoFactor'); + + expect($op->refresh()->two_factor_secret)->not->toBeNull(); + + $page->call('cancelSetup'); + + expect($op->refresh()->two_factor_secret)->toBeNull() + ->and($op->two_factor_confirmed_at)->toBeNull(); +});