diff --git a/README.md b/README.md index a1a1754..c1e7c9e 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,24 @@ docker compose -f docker-compose.prod.yml exec -u app app php artisan clusev:ins --- +## Account recovery / Wiederherstellung + +Forgot the admin password? The panel offers two self-service paths on the **forgot-password** +screen: an **e-mail reset link** (15 minutes valid) when SMTP is configured, and an inline +**2FA-proof reset** (e-mail + TOTP or a backup code + new password) as a fallback. + +Locked out with **no 2FA and no SMTP**? As a last resort, SSH to the host and reset the admin +from the container: + +```bash +docker compose -f docker-compose.prod.yml exec app php artisan clusev:reset-admin +``` + +This recovery command is documented in-panel under **Settings → Security** (admin-only) — it is +deliberately not advertised on the public, pre-login forgot-password screen. + +--- + ## License AGPL core + commercial Pro modules (open-core). Multi-server fleet management is always free. diff --git a/app/Livewire/Auth/ForgotPassword.php b/app/Livewire/Auth/ForgotPassword.php index 559491a..e221166 100644 --- a/app/Livewire/Auth/ForgotPassword.php +++ b/app/Livewire/Auth/ForgotPassword.php @@ -23,6 +23,9 @@ class ForgotPassword extends Component public string $password_confirmation = ''; + /** Reveals the secondary inline 2FA-proof form when a mailer is configured. */ + public bool $showCodeReset = false; + /** True when a real mailer is configured (then the email-link option is offered too). */ public function mailEnabled(): bool { diff --git a/config/auth.php b/config/auth.php index d7568ff..98d36ec 100644 --- a/config/auth.php +++ b/config/auth.php @@ -96,7 +96,7 @@ return [ 'users' => [ 'provider' => 'users', 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), - 'expire' => 60, + 'expire' => 15, 'throttle' => 60, ], ], diff --git a/lang/de/auth.php b/lang/de/auth.php index 514fa5e..3ed8cb1 100644 --- a/lang/de/auth.php +++ b/lang/de/auth.php @@ -96,15 +96,16 @@ return [ 'forgot_link' => 'Passwort vergessen?', 'forgot_heading' => 'Passwort zurücksetzen', 'forgot_subtitle' => 'Setze dein Passwort mit deinem 2FA-Code oder einem Backup-Code zurück.', + 'forgot_subtitle_mail' => 'Wir senden dir einen Reset-Link (15 Minuten gültig).', 'forgot_code' => '2FA-Code oder Backup-Code', 'forgot_code_hint' => '6-stelliger Authenticator-Code oder einer deiner Backup-Codes.', 'forgot_submit' => 'Passwort zurücksetzen', - 'forgot_send_email' => 'Reset-Link per E-Mail senden', + 'forgot_send_email' => 'Reset-Link senden', + 'forgot_use_code_toggle' => 'Kein E-Mail-Zugang? Mit 2FA-Code zurücksetzen', 'reset_invalid' => 'E-Mail oder Code ungültig.', 'reset_done' => 'Passwort geändert. Bitte neu anmelden.', 'reset_link_sent' => 'Falls die E-Mail existiert, wurde ein Reset-Link versendet.', 'reset_token_invalid' => 'Reset-Link ungültig oder abgelaufen.', - 'reset_no_2fa_note' => 'Ohne 2FA ist die Wiederherstellung nur per E-Mail-Link (falls SMTP konfiguriert) oder über den Befehl clusev:reset-admin möglich.', // ── WebAuthn / Security-Keys ───────────────────────────────────────── 'webauthn_login' => 'Mit Security-Key anmelden', diff --git a/lang/de/settings.php b/lang/de/settings.php index 76454a0..503d1a2 100644 --- a/lang/de/settings.php +++ b/lang/de/settings.php @@ -40,6 +40,9 @@ return [ 'twofa_recommended' => '2FA ist optional, aber empfohlen — sichere dein Konto mit einem Authenticator oder Security-Key.', 'twofa_remove_totp' => 'Authenticator entfernen', + // Account recovery (last-resort) note — admin-only, post-login + 'recovery_note' => 'Konto-Wiederherstellung (letzter Ausweg): per SSH auf den Host einloggen und `clusev:reset-admin` ausführen.', + // Notifications 'profile_saved' => 'Profil gespeichert.', 'password_changed' => 'Passwort geändert.', diff --git a/lang/en/auth.php b/lang/en/auth.php index 0f35ca9..11f363d 100644 --- a/lang/en/auth.php +++ b/lang/en/auth.php @@ -96,15 +96,16 @@ return [ 'forgot_link' => 'Forgot password?', 'forgot_heading' => 'Reset password', 'forgot_subtitle' => 'Reset your password with your 2FA code or a backup code.', + 'forgot_subtitle_mail' => 'We will send you a reset link (valid for 15 minutes).', 'forgot_code' => '2FA code or backup code', 'forgot_code_hint' => 'Your 6-digit authenticator code or one of your backup codes.', 'forgot_submit' => 'Reset password', - 'forgot_send_email' => 'Email me a reset link', + 'forgot_send_email' => 'Send reset link', + 'forgot_use_code_toggle' => 'No email access? Reset with a 2FA code', 'reset_invalid' => 'Invalid email or code.', 'reset_done' => 'Password changed. Please sign in again.', 'reset_link_sent' => 'If the email exists, a reset link has been sent.', 'reset_token_invalid' => 'Reset link is invalid or expired.', - 'reset_no_2fa_note' => 'Without 2FA, recovery is only possible via the email link (if SMTP is configured) or the clusev:reset-admin command.', // ── WebAuthn / security keys ───────────────────────────────────────── 'webauthn_login' => 'Sign in with a security key', diff --git a/lang/en/settings.php b/lang/en/settings.php index 56124a3..f46e2ce 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -40,6 +40,9 @@ return [ 'twofa_recommended' => '2FA is optional but recommended — secure your account with an authenticator or a security key.', 'twofa_remove_totp' => 'Remove authenticator', + // Account recovery (last-resort) note — admin-only, post-login + 'recovery_note' => 'Account recovery (last resort): sign in to the host over SSH and run `clusev:reset-admin`.', + // Notifications 'profile_saved' => 'Profile saved.', 'password_changed' => 'Password changed.', diff --git a/resources/views/livewire/auth/forgot-password.blade.php b/resources/views/livewire/auth/forgot-password.blade.php index 02bb928..b94e983 100644 --- a/resources/views/livewire/auth/forgot-password.blade.php +++ b/resources/views/livewire/auth/forgot-password.blade.php @@ -2,60 +2,82 @@ $label = 'mb-1.5 block font-mono text-[11px] uppercase tracking-wider text-ink-3'; $field = 'h-11 w-full rounded-md border border-line bg-inset px-3 text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none'; $err = 'mt-1.5 flex items-center gap-1.5 font-mono text-[11px] text-offline'; + $mail = $this->mailEnabled(); + // With SMTP the inline 2FA form is the secondary path (revealed via toggle); without it, it is the only path. + $showCode = $mail ? $showCodeReset : true; @endphp
{{ __('auth.security') }}
{{ __('auth.forgot_subtitle') }}
+{{ $mail ? __('auth.forgot_subtitle_mail') : __('auth.forgot_subtitle') }}