shinkan-jinkendo/frontend/src/components/TotpSetupCard.jsx

164 lines
5.2 KiB
React
Raw Normal View History

import { useEffect, useState } from 'react'
import api from '../utils/api'
/**
* 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)
try {
const data = await api.setupTotp()
setSecret(data.secret || '')
setOtpauthUri(data.otpauth_uri || '')
setRecoveryCodes([])
} catch (err) {
setError(err.message || 'Einrichtung fehlgeschlagen')
} finally {
setBusy(false)
}
}
const confirm = async (e) => {
e.preventDefault()
setError('')
setBusy(true)
try {
const data = await api.confirmTotp(code.trim())
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')
} finally {
setBusy(false)
}
}
return (
<div className="card" style={{ marginBottom: '1rem' }}>
<h2 style={{ margin: '0 0 0.75rem', fontSize: '1.1rem' }}>Zwei-Faktor-Anmeldung</h2>
<p style={{ color: 'var(--text2)', fontSize: '0.875rem', lineHeight: 1.5 }}>
Authenticator-App öffnen, ein Konto hinzufügen und den QR-Code scannen. Wenn das Scannen
nicht klappt, das Geheimnis darunter manuell eintragen.
</p>
{error ? (
<p style={{ color: 'var(--danger)', fontSize: '0.875rem' }}>{error}</p>
) : null}
{!secret && recoveryCodes.length === 0 ? (
<button type="button" className="btn btn-primary" onClick={start} disabled={busy}>
{busy ? 'Wird erzeugt…' : 'QR-Code erzeugen'}
</button>
) : null}
{secret ? (
<form onSubmit={confirm}>
{qrDataUrl ? (
<div
style={{
display: 'inline-block',
background: '#ffffff',
padding: 8,
borderRadius: 8,
margin: '0.25rem 0 0.85rem',
}}
>
<img
src={qrDataUrl}
alt="QR-Code zum Einrichten in der Authenticator-App"
width={240}
height={240}
style={{ display: 'block', width: 240, height: 240 }}
/>
</div>
) : null}
<p style={{ fontFamily: 'ui-monospace, monospace', letterSpacing: '0.04em', wordBreak: 'break-all' }}>{secret}</p>
<label className="form-label" htmlFor="totp-confirm-code">
Code aus der App
</label>
<input
id="totp-confirm-code"
className="form-input"
inputMode="numeric"
autoComplete="one-time-code"
value={code}
onChange={(e) => setCode(e.target.value)}
/>
<button type="submit" className="btn btn-primary" disabled={busy} style={{ marginTop: '0.75rem' }}>
{busy ? 'Prüft…' : 'Aktivieren'}
</button>
</form>
) : null}
{recoveryCodes.length > 0 ? (
<div>
<p style={{ fontSize: '0.875rem' }}>
Diese acht Codes jetzt sichern. Die Seite bleibt stehen, bis du unten bestätigst.
Ein Neuladen löscht die Anzeige. Neue Codes kannst du danach in den Einstellungen erzeugen.
</p>
<textarea
readOnly
rows={8}
className="form-input"
value={recoveryCodes.join('\n')}
style={{ fontFamily: 'ui-monospace, monospace', marginBottom: '0.75rem' }}
/>
<button
type="button"
className="btn btn-secondary"
style={{ marginRight: '0.5rem' }}
onClick={() => navigator.clipboard?.writeText(recoveryCodes.join('\n'))}
>
Kopieren
</button>
<button
type="button"
className="btn btn-primary"
onClick={() => {
if (onEnabled) onEnabled()
}}
>
Ich habe die Codes gespeichert
</button>
</div>
) : null}
</div>
)
}