feat(admin-ui): TOTP MFA enrollment + two-step login; remove stub 2FA toggle

Frontend for #738.
- mfa.service.ts + MfaSettingsCard (Settings → General → Admin Account):
  per-user setup (QR + manual secret + verify), recovery codes shown once
  (copy/download/confirm), status, regenerate, disable. Renders for
  super_admin (closes #735).
- Two-step login in AdminLoginPage: on {mfaRequired,mfaToken} swap to a
  code step (TOTP or recovery), call /auth/admin/login/mfa; handle
  MFA_INVALID / MFA_SESSION_EXPIRED / 423 lockout.
- Removed the non-functional global enable_2fa checkbox from SecurityTab
  (and its persistence) — replaced with a note pointing to per-user setup.
- en + de i18n.

Verified live in-browser: enroll (QR→code→recovery codes), logout, and
the two-step challenge into the dashboard as super_admin.
This commit is contained in:
Paul Nothaft
2026-07-03 11:44:07 +02:00
parent cdbfb514bd
commit 96e3c68b9d
10 changed files with 679 additions and 24 deletions
+58 -3
View File
@@ -1422,13 +1422,14 @@
"attemptWindowMinutesHelp": "Zeitraum, in dem fehlgeschlagene Anmeldeversuche gezählt werden",
"lockoutDurationMinutes": "Sperrdauer (Minuten)",
"lockoutDurationMinutesHelp": "Wie lange Galerie oder Konto nach zu vielen Fehlern gesperrt bleiben",
"enable2FA": "Zwei-Faktor-Authentifizierung für Admins aktivieren",
"recaptchaSettings": "reCAPTCHA-Einstellungen",
"enableRecaptcha": "reCAPTCHA für Anmeldeformulare aktivieren",
"siteKey": "Site-Schlüssel",
"secretKey": "Geheimer Schlüssel",
"recaptchaHelp": "Holen Sie sich Ihre reCAPTCHA-Schlüssel von",
"saveSecuritySettings": "Sicherheitseinstellungen speichern"
"saveSecuritySettings": "Sicherheitseinstellungen speichern",
"twoFactorTitle": "Zwei-Faktor-Authentifizierung",
"twoFactorNote": "Die Zwei-Faktor-Authentifizierung wird jetzt pro Admin unter Einstellungen → Allgemein → Admin-Konto verwaltet. Jeder Admin aktiviert sie für seine eigene Anmeldung."
},
"events": {
"title": "Veranstaltungserstellung",
@@ -2040,6 +2041,42 @@
"testSend": "Test senden",
"testSending": "Senden…",
"testSentToast": "Testnachricht gesendet (ID: {{id}})."
},
"mfa": {
"title": "Zwei-Faktor-Authentifizierung",
"description": "Sichere deine Admin-Anmeldung mit einem zweiten Schritt über eine Authenticator-App (TOTP).",
"notEnrolled": "Die Zwei-Faktor-Authentifizierung ist für dein Konto nicht aktiviert.",
"setUp": "Einrichten",
"setupScanInstruction": "Scanne diesen QR-Code mit deiner Authenticator-App (z. B. Google Authenticator, 1Password, Authy).",
"manualEntry": "Oder gib diesen Schlüssel manuell ein:",
"qrAlt": "QR-Code zur Zwei-Faktor-Einrichtung",
"enterCodeLabel": "Gib den 6-stelligen Code aus deiner App ein",
"codePlaceholder": "123456",
"enable": "Aktivieren",
"enabledBadge": "Die Zwei-Faktor-Authentifizierung ist aktiviert.",
"recoveryCodesRemaining": "Noch {{count}} Wiederherstellungscode übrig.",
"recoveryCodesRemaining_other": "Noch {{count}} Wiederherstellungscodes übrig.",
"regenerate": "Wiederherstellungscodes neu erzeugen",
"regenerateHelp": "Gib einen aktuellen Authentifizierungscode ein, um neue Wiederherstellungscodes zu erzeugen. Deine alten Codes werden ungültig.",
"regenerateConfirm": "Neu erzeugen",
"disable": "Deaktivieren",
"disablePrompt": "Gib einen aktuellen Authentifizierungs- oder Wiederherstellungscode ein, um die Zwei-Faktor-Authentifizierung zu deaktivieren:",
"disableConfirmTitle": "Zwei-Faktor-Authentifizierung deaktivieren?",
"disableConfirmMessage": "Für dein Konto ist bei der Anmeldung dann kein zweiter Schritt mehr erforderlich. Du kannst sie jederzeit wieder aktivieren.",
"disableConfirmButton": "Deaktivieren",
"disabledToast": "Zwei-Faktor-Authentifizierung deaktiviert.",
"codeRequired": "Ein Code ist erforderlich.",
"recoveryCodesWarning": "Speichere diese Wiederherstellungscodes jetzt. Jeder kann einmal verwendet werden, falls du den Zugriff auf deine Authenticator-App verlierst. Sie werden nicht erneut angezeigt.",
"recoveryCodesAck": "Ich habe meine Wiederherstellungscodes an einem sicheren Ort gespeichert.",
"copy": "Kopieren",
"copied": "Kopiert",
"copyFailed": "Kopieren in die Zwischenablage fehlgeschlagen.",
"download": ".txt herunterladen",
"done": "Fertig",
"setupFailed": "Zwei-Faktor-Einrichtung konnte nicht gestartet werden. Bitte versuche es erneut.",
"enableFailed": "Zwei-Faktor-Authentifizierung konnte nicht aktiviert werden. Prüfe den Code und versuche es erneut.",
"disableFailed": "Zwei-Faktor-Authentifizierung konnte nicht deaktiviert werden. Prüfe den Code und versuche es erneut.",
"regenerateFailed": "Wiederherstellungscodes konnten nicht neu erzeugt werden. Prüfe den Code und versuche es erneut."
}
},
"branding": {
@@ -3585,7 +3622,25 @@
"generalError": "Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.",
"needHelp": "Hilfe benötigt? Kontakt",
"poweredBy": "Bereitgestellt von PicPeak",
"devModeHint": "Entwicklungsmodus: E-Mail: [email protected], Passwort: admin123"
"devModeHint": "Entwicklungsmodus: E-Mail: [email protected], Passwort: admin123",
"mfa": {
"title": "Zwei-Faktor-Authentifizierung",
"subtitle": "Gib den 6-stelligen Code aus deiner Authenticator-App ein.",
"recoverySubtitle": "Gib einen deiner Wiederherstellungscodes ein.",
"codeLabel": "Authentifizierungscode",
"codePlaceholder": "123456",
"recoveryCodeLabel": "Wiederherstellungscode",
"recoveryCodePlaceholder": "awzq-jca3-va",
"verify": "Bestätigen",
"back": "Zurück",
"useRecoveryCode": "Stattdessen Wiederherstellungscode verwenden",
"useAuthenticator": "Stattdessen Authenticator-App verwenden",
"codeRequired": "Gib deinen Authentifizierungscode ein",
"invalidCode": "Ungültiger Code. Bitte versuche es erneut.",
"sessionExpired": "Deine Bestätigungssitzung ist abgelaufen. Bitte melde dich erneut an.",
"locked": "Konto wegen zu vieler Versuche vorübergehend gesperrt. Versuche es später erneut.",
"lockedRetry": "Konto vorübergehend gesperrt. Versuche es in {{seconds}} Sekunden erneut."
}
},
"cssTemplates": {
"title": "Benutzerdefinierte CSS-Vorlagen",
+58 -3
View File
@@ -969,13 +969,14 @@
"attemptWindowMinutesHelp": "How long to look back when counting failed login attempts",
"lockoutDurationMinutes": "Lockout Duration (minutes)",
"lockoutDurationMinutesHelp": "How long the gallery or account stays locked after too many failures",
"enable2FA": "Enable two-factor authentication for admins",
"recaptchaSettings": "reCAPTCHA Settings",
"enableRecaptcha": "Enable reCAPTCHA for login forms",
"siteKey": "Site Key",
"secretKey": "Secret Key",
"recaptchaHelp": "Get your reCAPTCHA keys from",
"saveSecuritySettings": "Save Security Settings"
"saveSecuritySettings": "Save Security Settings",
"twoFactorTitle": "Two-factor authentication",
"twoFactorNote": "Two-factor authentication is now managed per admin from Settings → General → Admin Account. Each admin enables it for their own login."
},
"categories": {
"title": "Categories",
@@ -1587,6 +1588,42 @@
"testSend": "Send test",
"testSending": "Sending…",
"testSentToast": "Test message sent (id: {{id}})."
},
"mfa": {
"title": "Two-factor authentication",
"description": "Add a second step to your admin sign-in using an authenticator app (TOTP).",
"notEnrolled": "Two-factor authentication is not enabled for your account.",
"setUp": "Set up",
"setupScanInstruction": "Scan this QR code with your authenticator app (e.g. Google Authenticator, 1Password, Authy).",
"manualEntry": "Or enter this secret manually:",
"qrAlt": "Two-factor setup QR code",
"enterCodeLabel": "Enter the 6-digit code from your app",
"codePlaceholder": "123456",
"enable": "Enable",
"enabledBadge": "Two-factor authentication is enabled.",
"recoveryCodesRemaining": "{{count}} recovery code remaining.",
"recoveryCodesRemaining_other": "{{count}} recovery codes remaining.",
"regenerate": "Regenerate recovery codes",
"regenerateHelp": "Enter a current authentication code to generate a new set of recovery codes. Your old codes will stop working.",
"regenerateConfirm": "Regenerate",
"disable": "Disable",
"disablePrompt": "Enter a current authentication or recovery code to disable two-factor authentication:",
"disableConfirmTitle": "Disable two-factor authentication?",
"disableConfirmMessage": "Your account will no longer require a second step at sign-in. You can re-enable it at any time.",
"disableConfirmButton": "Disable",
"disabledToast": "Two-factor authentication disabled.",
"codeRequired": "A code is required.",
"recoveryCodesWarning": "Save these recovery codes now. Each can be used once if you lose access to your authenticator app. They will not be shown again.",
"recoveryCodesAck": "I have saved my recovery codes in a safe place.",
"copy": "Copy",
"copied": "Copied",
"copyFailed": "Failed to copy to clipboard.",
"download": "Download .txt",
"done": "Done",
"setupFailed": "Could not start two-factor setup. Please try again.",
"enableFailed": "Could not enable two-factor authentication. Check the code and try again.",
"disableFailed": "Could not disable two-factor authentication. Check the code and try again.",
"regenerateFailed": "Could not regenerate recovery codes. Check the code and try again."
}
},
"analytics": {
@@ -3481,7 +3518,25 @@
"generalError": "An error occurred. Please try again.",
"needHelp": "Need help? Contact",
"poweredBy": "Powered by PicPeak",
"devModeHint": "Development Mode: Use email: [email protected], password: admin123"
"devModeHint": "Development Mode: Use email: [email protected], password: admin123",
"mfa": {
"title": "Two-factor authentication",
"subtitle": "Enter the 6-digit code from your authenticator app.",
"recoverySubtitle": "Enter one of your recovery codes.",
"codeLabel": "Authentication code",
"codePlaceholder": "123456",
"recoveryCodeLabel": "Recovery code",
"recoveryCodePlaceholder": "awzq-jca3-va",
"verify": "Verify",
"back": "Back",
"useRecoveryCode": "Use a recovery code instead",
"useAuthenticator": "Use your authenticator app instead",
"codeRequired": "Enter your authentication code",
"invalidCode": "Invalid code. Please try again.",
"sessionExpired": "Your verification session expired. Please sign in again.",
"locked": "Account temporarily locked due to too many attempts. Try again later.",
"lockedRetry": "Account temporarily locked. Try again in {{seconds}} seconds."
}
},
"slideshow": {
"adminTitle": "Live Slideshow",