From f6a65827fd4c57fd8e7e9d647c689db9176a7476 Mon Sep 17 00:00:00 2001 From: boban Date: Sun, 14 Jun 2026 16:57:44 +0200 Subject: [PATCH] feat(auth): clusev:reset-admin CLI lockout fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shell-only emergency recovery: resets the admin password (prints a random one if omitted) and, with --disable-2fa, clears the 2FA secret + backup codes so the operator can re-enroll. Requires server access — the safe last resort. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/Console/Commands/ResetAdmin.php | 60 +++++++++++++++++++++++++ tests/Feature/ResetAdminCommandTest.php | 36 +++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 app/Console/Commands/ResetAdmin.php create mode 100644 tests/Feature/ResetAdminCommandTest.php diff --git a/app/Console/Commands/ResetAdmin.php b/app/Console/Commands/ResetAdmin.php new file mode 100644 index 0000000..d3b4739 --- /dev/null +++ b/app/Console/Commands/ResetAdmin.php @@ -0,0 +1,60 @@ +option('email'); + $user = $email !== '' + ? User::where('email', $email)->first() + : (User::count() === 1 ? User::first() : null); + + if (! $user) { + $this->error($email !== '' ? "Kein Benutzer mit E-Mail {$email}." : 'Kein eindeutiger Benutzer — bitte --email angeben.'); + + return self::FAILURE; + } + + $password = (string) ($this->option('password') ?: Str::password(20)); + $user->forceFill(['password' => Hash::make($password), 'must_change_password' => false]); + + if ($this->option('disable-2fa')) { + $user->forceFill([ + 'two_factor_secret' => null, + 'two_factor_confirmed_at' => null, + 'two_factor_recovery_codes' => null, + ]); + } + + $user->save(); + + $this->info("Passwort für {$user->email} zurückgesetzt."); + if (! $this->option('password')) { + $this->warn("Einmal-Passwort (jetzt notieren): {$password}"); + } + if ($this->option('disable-2fa')) { + $this->info('2FA + Backup-Codes entfernt — beim nächsten Login neu einrichten.'); + } + + return self::SUCCESS; + } +} diff --git a/tests/Feature/ResetAdminCommandTest.php b/tests/Feature/ResetAdminCommandTest.php new file mode 100644 index 0000000..8264a8f --- /dev/null +++ b/tests/Feature/ResetAdminCommandTest.php @@ -0,0 +1,36 @@ +create([ + 'email' => 'admin@clusev.local', + 'two_factor_secret' => 'x', 'two_factor_confirmed_at' => now(), + ]); + $user->replaceRecoveryCodes(); + + $this->artisan('clusev:reset-admin', ['--email' => 'admin@clusev.local', '--password' => 'BrandNew1234', '--disable-2fa' => true]) + ->assertExitCode(0); + + $user->refresh(); + $this->assertTrue(Hash::check('BrandNew1234', $user->password)); + $this->assertNull($user->two_factor_secret); + $this->assertNull($user->two_factor_confirmed_at); + $this->assertFalse($user->hasRecoveryCodes()); + } + + public function test_unknown_email_fails(): void + { + $this->artisan('clusev:reset-admin', ['--email' => 'nope@x.test'])->assertExitCode(1); + } +}