diff --git a/backend/version.py b/backend/version.py index cb172ee..bfdad5c 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,12 +1,12 @@ # Shinkan Jinkendo Version Information -APP_VERSION = "0.8.240" +APP_VERSION = "0.8.241" BUILD_DATE = "2026-10-13" DB_SCHEMA_VERSION = "20261012096" MODULE_VERSIONS = { "legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste) - "auth": "1.3.1", # TOTP freiwillig; Wiederherstellungscodes bleiben bis zur Bestätigung stehen + "auth": "1.3.2", # TOTP-Einrichtung zeigt einen QR-Code zum Scannen "profiles": "1.8.1", # GET /profiles/me: account_state + club_roles "tenant_context": "1.1.0", # M3: account_state + email_verified im TenantContext "capabilities": "1.2.0", # Registry-first: module-Spalte; Admin nur registrierte Rechte @@ -53,6 +53,13 @@ MODULE_VERSIONS = { } CHANGELOG = [ + { + "version": "0.8.241", + "date": "2026-10-13", + "changes": [ + "Zwei-Faktor-Einrichtung zeigt einen QR-Code. Die Authenticator-App kann das Konto scannen, das Geheimnis bleibt als Ersatz sichtbar.", + ], + }, { "version": "0.8.240", "date": "2026-10-13", diff --git a/frontend/package.json b/frontend/package.json index 9a4af8e..491cf2d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "jspdf": "^4.2.1", "lucide-react": "^0.344.0", "marked": "^18.0.3", + "qrcode": "^1.5.4", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^10.1.0", diff --git a/frontend/src/components/TotpSetupCard.jsx b/frontend/src/components/TotpSetupCard.jsx index a8c1c6d..9e87d8a 100644 --- a/frontend/src/components/TotpSetupCard.jsx +++ b/frontend/src/components/TotpSetupCard.jsx @@ -1,17 +1,45 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import api from '../utils/api' /** - * TOTP einrichten: Geheimnis anzeigen, Code bestätigen, Wiederherstellungscodes einmal zeigen. + * TOTP einrichten: QR-Code und Geheimnis anzeigen, Code bestätigen, Wiederherstellungscodes einmal zeigen. */ export default function TotpSetupCard({ onEnabled }) { const [secret, setSecret] = useState('') const [otpauthUri, setOtpauthUri] = useState('') + const [qrDataUrl, setQrDataUrl] = useState('') const [code, setCode] = useState('') const [recoveryCodes, setRecoveryCodes] = useState([]) const [error, setError] = useState('') const [busy, setBusy] = useState(false) + useEffect(() => { + if (!otpauthUri) { + setQrDataUrl('') + return undefined + } + let cancelled = false + import('qrcode') + .then((mod) => { + const QRCode = typeof mod.toDataURL === 'function' ? mod : mod.default + return QRCode.toDataURL(otpauthUri, { + width: 240, + margin: 2, + errorCorrectionLevel: 'M', + color: { dark: '#000000', light: '#ffffff' }, + }) + }) + .then((url) => { + if (!cancelled) setQrDataUrl(url) + }) + .catch(() => { + if (!cancelled) setQrDataUrl('') + }) + return () => { + cancelled = true + } + }, [otpauthUri]) + const start = async () => { setError('') setBusy(true) @@ -36,6 +64,7 @@ export default function TotpSetupCard({ onEnabled }) { if (data?.token) localStorage.setItem('authToken', data.token) setRecoveryCodes(Array.isArray(data?.recovery_codes) ? data.recovery_codes : []) setSecret('') + setOtpauthUri('') setCode('') } catch (err) { setError(err.message || 'Code nicht akzeptiert') @@ -48,23 +77,39 @@ export default function TotpSetupCard({ onEnabled }) {

Zwei-Faktor-Anmeldung

- Authenticator-App (zum Beispiel Aegis, 1Password oder Microsoft Authenticator) öffnen und das - Geheimnis manuell eintragen. Den otpauth-Link nicht an Dritte geben. + Authenticator-App öffnen, ein Konto hinzufügen und den QR-Code scannen. Wenn das Scannen + nicht klappt, das Geheimnis darunter manuell eintragen.

{error ? (

{error}

) : null} {!secret && recoveryCodes.length === 0 ? ( ) : null} {secret ? (
-

{secret}

- {otpauthUri ? ( -

{otpauthUri}

+ {qrDataUrl ? ( +
+ QR-Code zum Einrichten in der Authenticator-App +
) : null} +

{secret}

diff --git a/frontend/src/pages/AccountSettingsPage.jsx b/frontend/src/pages/AccountSettingsPage.jsx index f55e78a..cb8b782 100644 --- a/frontend/src/pages/AccountSettingsPage.jsx +++ b/frontend/src/pages/AccountSettingsPage.jsx @@ -209,6 +209,90 @@ function AccountSettingsPage() {
)} + {user?.totp_enabled ? ( +
+

Zwei-Faktor ist aktiv

+

+ Beim Anmelden wird zusätzlich der Code aus der Authenticator-App verlangt. + Die ersten Codes lassen sich nicht erneut anzeigen. Hier entstehen neue, die alten gelten danach nicht mehr. +

+ {freshRecoveryCodes.length > 0 ? ( +
+