'use client'; import { useEffect, useState } from 'react'; import { startRegistration } from '@simplewebauthn/browser'; interface Passkey { credentialId: string; label: string; createdAt: string; lastUsedAt: string | null; } function formatDate(iso: string | null): string { if (!iso) return '—'; // SQLite liefert "YYYY-MM-DD HH:MM:SS" (UTC). const d = new Date(iso.replace(' ', 'T') + 'Z'); if (isNaN(d.getTime())) return '—'; return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); } export default function Passkeys() { const [passkeys, setPasskeys] = useState([]); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [label, setLabel] = useState(''); function load() { return fetch('/api/passkey') .then((r) => (r.ok ? r.json() : Promise.reject(new Error()))) .then((data) => setPasskeys(data.passkeys ?? [])) .catch(() => setError('Passkeys konnten nicht geladen werden.')) .finally(() => setLoading(false)); } useEffect(() => { load(); }, []); async function handleRegister() { setError(null); setBusy(true); try { const optRes = await fetch('/api/passkey/register'); if (!optRes.ok) throw new Error('Optionen konnten nicht geladen werden.'); const optionsJSON = await optRes.json(); const response = await startRegistration({ optionsJSON }); const verifyRes = await fetch('/api/passkey/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ response, label }), }); if (!verifyRes.ok) { const data = await verifyRes.json().catch(() => null); throw new Error(data?.error ?? 'Passkey konnte nicht registriert werden.'); } setLabel(''); await load(); } catch (err) { if (err instanceof Error && err.name === 'NotAllowedError') { setError(null); // Abbruch durch Nutzer } else { setError(err instanceof Error ? err.message : 'Passkey konnte nicht registriert werden.'); } } finally { setBusy(false); } } async function handleDelete(credentialId: string) { if (!confirm('Diesen Passkey wirklich entfernen?')) return; setError(null); try { const res = await fetch(`/api/passkey?id=${encodeURIComponent(credentialId)}`, { method: 'DELETE', }); if (!res.ok) throw new Error(); await load(); } catch { setError('Passkey konnte nicht entfernt werden.'); } } return (

Passkeys

Melde dich künftig per Fingerabdruck, Gesichtserkennung oder Geräte-PIN an. Das Passwort bleibt als Alternative bestehen.

{loading ? (

Lädt…

) : passkeys.length === 0 ? (

Noch keine Passkeys registriert.

) : (
    {passkeys.map((pk) => (
  • {pk.label}
    Erstellt {formatDate(pk.createdAt)} · Zuletzt {formatDate(pk.lastUsedAt)}
  • ))}
)}
setLabel(e.target.value)} placeholder="z. B. iPhone von Birgit" maxLength={80} className="w-full px-3 py-2 border-2 border-gray-400 rounded-lg bg-white text-gray-900 text-sm focus:border-green-600 focus:outline-none" />
{error && (
{error}
)}
); }