"use client"; import { useEffect, useState } from "react"; import { TextField, useToast } from "@nexarch/shl"; import { ApiError, Me, beginTotpSetup, changePassword, confirmTotpSetup, fetchMe, fetchTotpStatus, } from "../../lib/api"; export default function ProfilePage() { const { push } = useToast(); const [me, setMe] = useState(null); const [totpEnabled, setTotpEnabled] = useState(null); const [loadError, setLoadError] = useState(null); useEffect(() => { Promise.all([fetchMe(), fetchTotpStatus()]) .then(([meResult, statusResult]) => { setMe(meResult); setTotpEnabled(statusResult.enabled); }) .catch(() => setLoadError("Bitte melden Sie sich an, um Ihr Profil zu sehen.")); }, []); if (loadError) { return (

{loadError}

Zur Anmeldung
); } return (

Mein Profil

{me && (

Stammdaten

{me.name} — {me.email}

)} push("Passwort geändert.", "success")} /> setTotpEnabled(true)} onNotice={(text) => push(text, "info")} />
); } function ChangePasswordSection({ onSuccess }: { onSuccess: () => void }) { const [currentPassword, setCurrentPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [error, setError] = useState(null); const [submitting, setSubmitting] = useState(false); async function onSubmit(event: React.FormEvent) { event.preventDefault(); setError(null); setSubmitting(true); try { await changePassword(currentPassword, newPassword); setCurrentPassword(""); setNewPassword(""); onSuccess(); } catch (err) { setError(err instanceof ApiError ? err.message : "Passwort konnte nicht geändert werden."); } finally { setSubmitting(false); } } return (

Passwort ändern

setCurrentPassword(e.target.value)} /> setNewPassword(e.target.value)} /> {error && (

{error}

)}
); } function TotpSection({ enabled, onEnabled, onNotice, }: { enabled: boolean | null; onEnabled: () => void; onNotice: (text: string) => void; }) { const [setupData, setSetupData] = useState<{ secret: string; provisioningUri: string } | null>(null); const [confirmCode, setConfirmCode] = useState(""); const [recoveryCodes, setRecoveryCodes] = useState(null); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); async function onBeginSetup() { setError(null); setBusy(true); try { const result = await beginTotpSetup(); setSetupData({ secret: result.secret, provisioningUri: result.provisioning_uri }); } catch (err) { setError(err instanceof ApiError ? err.message : "Einrichtung konnte nicht gestartet werden."); } finally { setBusy(false); } } async function onConfirm(event: React.FormEvent) { event.preventDefault(); setError(null); setBusy(true); try { const result = await confirmTotpSetup(confirmCode); // Akzeptanzkriterium 2: Wiederherstellungscodes werden HIER, EINMALIG // im Klartext angezeigt — die Backend-API liefert sie an keiner // anderen Stelle je wieder aus (siehe internal/totp/store.go). setRecoveryCodes(result.recovery_codes); setSetupData(null); onEnabled(); onNotice("Zwei-Faktor-Authentifizierung aktiviert. Bewahren Sie die Wiederherstellungscodes sicher auf."); } catch (err) { setError(err instanceof ApiError ? err.message : "Code ungültig."); } finally { setBusy(false); } } return (

Zwei-Faktor-Authentifizierung

{recoveryCodes ? (

Wiederherstellungscodes — jetzt notieren, sie werden nicht erneut angezeigt:

    {recoveryCodes.map((code) => (
  • {code}
  • ))}
) : enabled ? (

Zwei-Faktor-Authentifizierung ist aktiv.

) : setupData ? (

Scannen Sie den folgenden Schlüssel mit Ihrer Authenticator-App oder geben Sie ihn manuell ein:

{setupData.secret}

{setupData.provisioningUri}

setConfirmCode(e.target.value)} /> {error && (

{error}

)} ) : ( <>

Zwei-Faktor-Authentifizierung ist derzeit nicht aktiv.

{error && (

{error}

)} )}
); }