diff --git a/app/Livewire/Auth/RecoveryCodes.php b/app/Livewire/Modals/RecoveryCodes.php
similarity index 51%
rename from app/Livewire/Auth/RecoveryCodes.php
rename to app/Livewire/Modals/RecoveryCodes.php
index e0fefcc..a6c7edb 100644
--- a/app/Livewire/Auth/RecoveryCodes.php
+++ b/app/Livewire/Modals/RecoveryCodes.php
@@ -1,15 +1,28 @@
*/
+ public function codes(): array
+ {
+ return Auth::user()->recoveryCodes();
+ }
+
/** Regenerate the current user's backup codes (invalidates the old set). */
public function regenerate(): void
{
@@ -28,8 +41,6 @@ class RecoveryCodes extends Component
public function render()
{
- return view('livewire.auth.recovery-codes', [
- 'codes' => Auth::user()->recoveryCodes(),
- ])->title(__('auth.title_recovery'));
+ return view('livewire.modals.recovery-codes', ['codes' => $this->codes()]);
}
}
diff --git a/lang/de/auth.php b/lang/de/auth.php
index 53f6ec7..da4ae13 100644
--- a/lang/de/auth.php
+++ b/lang/de/auth.php
@@ -74,7 +74,6 @@ return [
// ── 2FA backup (recovery) codes ──────────────────────────────────────
'challenge_recovery_hint' => 'Authenticator verloren? Gib einen deiner Backup-Codes ein.',
- 'title_recovery' => 'Backup-Codes — Clusev',
'recovery_heading' => 'Backup-Codes',
'recovery_subtitle' => 'Mit diesen Einmal-Codes meldest du dich an, wenn du keinen Authenticator hast.',
'recovery_warning' => 'Speichere diese Codes jetzt sicher ab — jeder Code funktioniert genau einmal. Lade sie herunter, solange sie angezeigt werden.',
diff --git a/lang/en/auth.php b/lang/en/auth.php
index e01e9f8..3567f8a 100644
--- a/lang/en/auth.php
+++ b/lang/en/auth.php
@@ -74,7 +74,6 @@ return [
// ── 2FA backup (recovery) codes ──────────────────────────────────────
'challenge_recovery_hint' => 'Lost your authenticator? Enter one of your backup codes.',
- 'title_recovery' => 'Backup codes — Clusev',
'recovery_heading' => 'Backup codes',
'recovery_subtitle' => 'Use these one-time codes to sign in when you do not have your authenticator.',
'recovery_warning' => 'Save these codes somewhere safe now — each works exactly once. Download them while they are shown.',
diff --git a/resources/views/livewire/auth/recovery-codes.blade.php b/resources/views/livewire/auth/recovery-codes.blade.php
deleted file mode 100644
index 09dab08..0000000
--- a/resources/views/livewire/auth/recovery-codes.blade.php
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
{{ __('auth.two_factor') }}
-
{{ __('auth.recovery_heading') }}
-
{{ __('auth.recovery_subtitle') }}
-
-
-
-
-
{{ __('auth.recovery_warning') }}
-
-
-
- @foreach ($codes as $c)
- {{ $c }}
- @endforeach
-
-
-
-
- {{ __('auth.recovery_download') }}
-
-
- {{ __('auth.recovery_regenerate') }}
-
- {{ __('auth.recovery_done') }}
-
-
diff --git a/resources/views/livewire/modals/recovery-codes.blade.php b/resources/views/livewire/modals/recovery-codes.blade.php
new file mode 100644
index 0000000..12c185c
--- /dev/null
+++ b/resources/views/livewire/modals/recovery-codes.blade.php
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
{{ __('auth.recovery_heading') }}
+
{{ __('auth.recovery_subtitle') }}
+
+
+
+
+
+
{{ __('auth.recovery_warning') }}
+
+
+
+ @foreach ($codes as $c)
+ {{ $c }}
+ @endforeach
+
+
+
+
+ {{ __('auth.recovery_download') }}
+
+
+ {{ __('auth.recovery_regenerate') }}
+
+ {{ __('auth.recovery_done') }}
+
+
diff --git a/resources/views/livewire/settings/index.blade.php b/resources/views/livewire/settings/index.blade.php
index d765d1c..a277b13 100644
--- a/resources/views/livewire/settings/index.blade.php
+++ b/resources/views/livewire/settings/index.blade.php
@@ -107,7 +107,7 @@
@if ($twoFactorEnabled)
- {{ __('auth.recovery_manage') }}
+ {{ __('auth.recovery_manage') }}
{{ __('common.disable') }}
@else
diff --git a/routes/web.php b/routes/web.php
index 5ecadfa..0692d6e 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -53,9 +53,7 @@ Route::middleware('auth')->group(function () {
Route::get('/password', Auth\PasswordChange::class)->name('password.change');
Route::get('/two-factor-setup', Auth\TwoFactorSetup::class)->name('two-factor.setup');
- // 2FA backup codes — shown right after enrolling (before the onboarding gate) and
- // manageable later from Settings.
- Route::get('/two-factor/recovery-codes', Auth\RecoveryCodes::class)->name('two-factor.recovery');
+ // 2FA backup-codes download — the codes themselves are shown in a modal (Modals\RecoveryCodes).
Route::get('/two-factor/recovery-codes/download', function () {
$codes = AuthFacade::user()->recoveryCodes();
@@ -74,7 +72,7 @@ Route::middleware('auth')->group(function () {
return redirect()->route('login');
})->name('logout');
- // The panel — requires completed onboarding (password rotated + 2FA enrolled).
+ // The panel — requires completed onboarding (seeded password rotated).
Route::middleware(EnsureSecurityOnboarded::class)->group(function () {
Route::get('/', Dashboard::class)->name('dashboard');
diff --git a/tests/Feature/RecoveryCodesModalTest.php b/tests/Feature/RecoveryCodesModalTest.php
new file mode 100644
index 0000000..39bc95d
--- /dev/null
+++ b/tests/Feature/RecoveryCodesModalTest.php
@@ -0,0 +1,43 @@
+create();
+ $codes = $user->replaceRecoveryCodes();
+
+ Livewire::actingAs($user)->test(RecoveryCodes::class)
+ ->assertSee($codes[0])
+ ->assertSee($codes[7]);
+ }
+
+ public function test_regenerate_replaces_the_codes(): void
+ {
+ $user = User::factory()->create();
+ $old = $user->replaceRecoveryCodes();
+
+ Livewire::actingAs($user)->test(RecoveryCodes::class)
+ ->call('regenerate')
+ ->assertDontSee($old[0]);
+
+ $this->assertNotEquals($old, $user->fresh()->recoveryCodes());
+ }
+
+ public function test_old_recovery_route_is_gone(): void
+ {
+ $this->assertFalse(Route::has('two-factor.recovery'));
+ $this->assertTrue(Route::has('two-factor.recovery.download'));
+ }
+}
diff --git a/tests/Feature/RecoveryCodesViewTest.php b/tests/Feature/RecoveryCodesViewTest.php
deleted file mode 100644
index 93dcaae..0000000
--- a/tests/Feature/RecoveryCodesViewTest.php
+++ /dev/null
@@ -1,55 +0,0 @@
-create(['two_factor_secret' => null, 'two_factor_confirmed_at' => null]);
- $this->actingAs($user);
- $secret = (new Google2FA)->generateSecretKey();
- $code = (new Google2FA)->getCurrentOtp($secret);
-
- Livewire::test(TwoFactorSetup::class)
- ->set('secret', $secret)
- ->set('code', $code)
- ->call('confirm')
- ->assertRedirect(route('two-factor.recovery'));
-
- $this->assertCount(8, $user->fresh()->recoveryCodes());
- }
-
- public function test_regenerate_replaces_codes(): void
- {
- $user = User::factory()->create(['two_factor_secret' => 'x', 'two_factor_confirmed_at' => now()]);
- $old = $user->replaceRecoveryCodes();
- $this->actingAs($user);
-
- Livewire::test(RecoveryCodes::class)->call('regenerate');
-
- $this->assertNotSame($old, $user->fresh()->recoveryCodes());
- $this->assertCount(8, $user->fresh()->recoveryCodes());
- }
-
- public function test_download_returns_codes_as_text(): void
- {
- $user = User::factory()->create();
- $user->replaceRecoveryCodes();
- $this->actingAs($user);
-
- $res = $this->get(route('two-factor.recovery.download'));
- $res->assertOk();
- $this->assertStringContainsString($user->recoveryCodes()[0], $res->streamedContent());
- }
-}