WebAuthn biometric UI: login button + device management in settings

- Login page: "Face ID / Otisk prstu" button with full WebAuthn flow
  (auth options → navigator.credentials.get → verify → JWT)
  Remembers last biometric email in localStorage
- Settings page: Biometric device management section
  (list registered devices, add new via navigator.credentials.create, remove)
  Auto-detects device type (Face ID, Touch ID, Android fingerprint, Windows Hello)
- API: Added POST /webauthn/auth/verify endpoint returning JWT token
  Updated auth/options to accept email (no login required for biometric)
- API client: Added 6 WebAuthn helper functions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-30 11:41:38 +00:00
parent 4ace4d5f7d
commit 1fbbc84d24
4 changed files with 331 additions and 6 deletions

View File

@@ -53,6 +53,39 @@ export function getMe(token: string) {
return apiFetch<{ user: User }>("/api/v1/auth/me", { token });
}
// WebAuthn
export function webauthnRegisterOptions(token: string, userId: string) {
return apiFetch<{ data: Record<string, unknown> }>("/api/v1/webauthn/register/options", {
method: "POST", token, body: { user_id: userId },
});
}
export function webauthnRegisterVerify(token: string, data: { user_id: string; credential_id: string; public_key: string; device_name: string }) {
return apiFetch<{ data: Record<string, unknown>; status: string }>("/api/v1/webauthn/register/verify", {
method: "POST", token, body: data,
});
}
export function webauthnAuthOptions(email: string) {
return apiFetch<{ data: Record<string, unknown> }>("/api/v1/webauthn/auth/options", {
method: "POST", body: { email },
});
}
export function webauthnAuthVerify(credentialId: string) {
return apiFetch<{ data: { token: string; user: User } }>("/api/v1/webauthn/auth/verify", {
method: "POST", body: { credential_id: credentialId },
});
}
export function webauthnGetDevices(token: string, userId: string) {
return apiFetch<{ data: Array<{ id: string; device_name: string; created_at: string }> }>(`/api/v1/webauthn/devices/${userId}`, { token });
}
export function webauthnDeleteDevice(token: string, deviceId: string) {
return apiFetch<{ status: string }>(`/api/v1/webauthn/devices/${deviceId}`, { method: "DELETE", token });
}
// Tasks
export function getTasks(token: string, params?: Record<string, string>) {
const qs = params ? "?" + new URLSearchParams(params).toString() : "";