import { useEffect, useState } from 'react' import QRCode from 'qrcode' import { api } from '../api/client' import { Layout } from '../components/Layout' interface UserOut { id: string; first_name: string; last_name: string; email: string; role: string; personnel_number: string | null totp_enabled?: boolean } const ROLE_LABELS: Record = { SUPER_ADMIN: 'Super Admin', COMPANY_ADMIN: 'Administrator', HR: 'HR', MANAGER: 'Manager', EMPLOYEE: 'Mitarbeiter', } // ── TOTP-Setup-Schritte ─────────────────────────────────────────────────────── type TotpStep = 'idle' | 'setup' | 'confirm' | 'done' function TotpSection({ enabled, onToggle }: { enabled: boolean; onToggle: () => void }) { const [step, setStep] = useState('idle') const [qrDataUrl, setQrDataUrl] = useState('') const [secret, setSecret] = useState('') const [code, setCode] = useState('') const [disablePw, setDisablePw] = useState('') const [disableCode, setDisableCode] = useState('') const [showDisable, setShowDisable] = useState(false) const [error, setError] = useState('') const [loading, setLoading] = useState(false) const inp = 'w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500' const startSetup = async () => { setLoading(true); setError('') try { const res = await api.post<{ secret: string; otpauth_uri: string }>('/auth/totp/setup', {}) setSecret(res.secret) // Secret in DB speichern (ohne Aktivierung) await api.post('/auth/totp/setup/save', {}) const dataUrl = await QRCode.toDataURL(res.otpauth_uri, { width: 200, margin: 2 }) setQrDataUrl(dataUrl) setStep('setup') } catch (e: unknown) { setError(e instanceof Error ? e.message : 'Fehler') } finally { setLoading(false) } } const confirmCode = async () => { if (code.length !== 6) { setError('Code muss 6 Ziffern haben'); return } setLoading(true); setError('') try { await api.post('/auth/totp/confirm', { code }) setStep('done') setCode('') onToggle() } catch (e: unknown) { setError(e instanceof Error ? e.message : 'Ungültiger Code') } finally { setLoading(false) } } const disableTotp = async () => { setLoading(true); setError('') try { await api.post('/auth/totp/disable', { password: disablePw, code: disableCode }) setShowDisable(false); setDisablePw(''); setDisableCode('') onToggle() } catch (e: unknown) { setError(e instanceof Error ? e.message : 'Fehler') } finally { setLoading(false) } } // Aktiv-Zustand if (enabled && step !== 'done') { return (
Aktiv
{showDisable && (

Zur Bestätigung Passwort und aktuellen TOTP-Code eingeben:

setDisablePw(e.target.value)} className={inp} /> setDisableCode(e.target.value.replace(/\D/g, ''))} className={inp} /> {error &&

{error}

}
)}
) } // Setup-Flow if (step === 'idle') { return (

Mit einem Einmalpasswort (TOTP) aus einer Authenticator-App wird dein Konto zusätzlich abgesichert. Kompatibel mit Google Authenticator, Authy, und anderen TOTP-Apps.

{error &&

{error}

}
) } if (step === 'setup') { return (

Schritt 1: Scanne den QR-Code mit deiner Authenticator-App.

{qrDataUrl && TOTP QR-Code}
Secret manuell eingeben {secret}

Schritt 2: Gib den 6-stelligen Code aus der App ein:

setCode(e.target.value.replace(/\D/g, ''))} className={`${inp} text-center text-xl tracking-widest font-mono`} autoFocus /> {error &&

{error}

}
) } // done return (
Zwei-Faktor-Authentifizierung erfolgreich aktiviert!
) } // ── Hauptseite ──────────────────────────────────────────────────────────────── export function ProfilePage() { const [me, setMe] = useState(null) const [totpEnabled, setTotpEnabled] = useState(false) const [currentPw, setCurrentPw] = useState('') const [newPw, setNewPw] = useState('') const [confirmPw, setConfirmPw] = useState('') const [saving, setSaving] = useState(false) const [success, setSuccess] = useState(false) const [error, setError] = useState(null) const loadMe = () => { api.get('/auth/me').then(u => { setMe(u) setTotpEnabled(u.totp_enabled ?? false) }).catch(() => {}) } useEffect(() => { loadMe() }, []) async function changePassword(e: React.FormEvent) { e.preventDefault() if (newPw !== confirmPw) { setError('Passwörter stimmen nicht überein'); return } if (newPw.length < 8) { setError('Mindestens 8 Zeichen'); return } setSaving(true); setError(null); setSuccess(false) try { await api.post('/auth/change-password', { current_password: currentPw, new_password: newPw }) setSuccess(true) setCurrentPw(''); setNewPw(''); setConfirmPw('') } catch (e: unknown) { setError(e instanceof Error ? e.message : 'Fehler beim Ändern des Passworts') } finally { setSaving(false) } } return (

Mein Profil

{/* Profil-Info */} {me && (
{me.first_name[0]}{me.last_name[0]}

{me.first_name} {me.last_name}

{me.email}

{ROLE_LABELS[me.role] ?? me.role}
{me.personnel_number && (
Personalnummer {me.personnel_number}
)}
)} {/* Zwei-Faktor-Authentifizierung */}

Zwei-Faktor-Authentifizierung

TOTP · Google Authenticator, Authy, etc.

{/* Passwort ändern */}

Passwort ändern

setCurrentPw(e.target.value)} required autoComplete='current-password' className='w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500' />
setNewPw(e.target.value)} required autoComplete='new-password' className='w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500' />

Mindestens 8 Zeichen, 1 Großbuchstabe, 1 Zahl

setConfirmPw(e.target.value)} required autoComplete='new-password' className='w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500' />
{error &&

{error}

} {success &&

Passwort erfolgreich geändert

}
) }