V 2.2.0 Anmeldung per Passkey (WebAuthn)
Zusätzlich zu Benutzername/Passwort ist die Anmeldung per Passkey möglich (Fingerabdruck, Gesichtserkennung, Geräte-PIN). Das Passwort bleibt als Alternative bestehen. - lib/webauthn.ts kapselt @simplewebauthn/server, RP-Config über RP_ID/RP_ORIGIN/RP_NAME, Challenge in kurzlebigem httpOnly-Cookie - lib/passkeys.ts speichert Credentials je Benutzer in der MySQL-Tabelle passkeys und legt sie beim ersten Zugriff an - API unter app/api/passkey/: register (Session nötig), authenticate (öffentlich, in proxy.ts von der Session-Pflicht ausgenommen), GET/DELETE zum Verwalten - Neuer Tab "Einstellungen" zum Registrieren und Entfernen von Passkeys - Button "Mit Passkey anmelden" auf der Login-Seite Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import { useActionState, useState } from 'react';
|
||||
import { startAuthentication } from '@simplewebauthn/browser';
|
||||
import { login } from './actions';
|
||||
import packageJson from '@/package.json';
|
||||
|
||||
export default function LoginPage() {
|
||||
const [state, loginAction, isPending] = useActionState(login, undefined);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [passkeyBusy, setPasskeyBusy] = useState(false);
|
||||
const [passkeyError, setPasskeyError] = useState<string | null>(null);
|
||||
|
||||
async function handlePasskeyLogin() {
|
||||
setPasskeyError(null);
|
||||
setPasskeyBusy(true);
|
||||
try {
|
||||
const optRes = await fetch('/api/passkey/authenticate');
|
||||
if (!optRes.ok) throw new Error('Optionen konnten nicht geladen werden.');
|
||||
const optionsJSON = await optRes.json();
|
||||
|
||||
const response = await startAuthentication({ optionsJSON });
|
||||
|
||||
const verifyRes = await fetch('/api/passkey/authenticate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ response }),
|
||||
});
|
||||
if (!verifyRes.ok) {
|
||||
const data = await verifyRes.json().catch(() => null);
|
||||
throw new Error(data?.error ?? 'Anmeldung mit Passkey fehlgeschlagen.');
|
||||
}
|
||||
window.location.href = '/';
|
||||
} catch (err) {
|
||||
// Abbruch durch Nutzer (NotAllowedError) nicht als Fehler anzeigen.
|
||||
if (err instanceof Error && err.name === 'NotAllowedError') {
|
||||
setPasskeyError(null);
|
||||
} else {
|
||||
setPasskeyError(err instanceof Error ? err.message : 'Anmeldung mit Passkey fehlgeschlagen.');
|
||||
}
|
||||
} finally {
|
||||
setPasskeyBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
const version = packageJson.version;
|
||||
const buildDate = process.env.NEXT_PUBLIC_BUILD_DATE || new Date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
@@ -95,6 +130,30 @@ export default function LoginPage() {
|
||||
{isPending ? 'Anmeldung läuft...' : 'Anmelden'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="flex items-center gap-3 my-5">
|
||||
<div className="h-px flex-1 bg-gray-200" />
|
||||
<span className="text-xs text-gray-400">oder</span>
|
||||
<div className="h-px flex-1 bg-gray-200" />
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePasskeyLogin}
|
||||
disabled={passkeyBusy}
|
||||
className="w-full py-2 px-4 flex items-center justify-center gap-2 border-2 border-gray-400 hover:border-blue-500 text-gray-900 font-medium rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-sm"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
{passkeyBusy ? 'Anmeldung läuft...' : 'Mit Passkey anmelden'}
|
||||
</button>
|
||||
|
||||
{passkeyError && (
|
||||
<div className="mt-4 bg-red-50 border border-red-300 text-red-700 px-3 py-2 rounded-lg text-sm">
|
||||
{passkeyError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user