"use client"; import { authClient, resolveApiBaseUrl, signIn } from "@/lib/auth-client"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; const OTP_STEP_STORAGE_KEY = "hellthhub_login_step_otp"; const OTP_STEP_TTL_MS = 5 * 60 * 1000; function hasActiveOtpStep(): boolean { try { const raw = window.localStorage.getItem(OTP_STEP_STORAGE_KEY); if (!raw) return false; const parsed = JSON.parse(raw) as { expiresAt?: number }; if (!parsed?.expiresAt || Date.now() > parsed.expiresAt) { window.localStorage.removeItem(OTP_STEP_STORAGE_KEY); return false; } return true; } catch { return false; } } function setOtpStepActive() { try { window.localStorage.setItem( OTP_STEP_STORAGE_KEY, JSON.stringify({ expiresAt: Date.now() + OTP_STEP_TTL_MS }), ); } catch { // ignore storage access issues } } function clearOtpStep() { try { window.localStorage.removeItem(OTP_STEP_STORAGE_KEY); } catch { // ignore storage access issues } } async function assertAuthApiReachable() { const controller = new AbortController(); const timeout = window.setTimeout(() => controller.abort(), 3000); try { const response = await fetch(`${resolveApiBaseUrl().replace(/\/$/, "")}/api/health`, { cache: "no-store", signal: controller.signal, }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } } finally { window.clearTimeout(timeout); } } export default function LoginPage() { const router = useRouter(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const [otpStepChecked, setOtpStepChecked] = useState(false); const [passkeyAutofillTried, setPasskeyAutofillTried] = useState(false); useEffect(() => { if (hasActiveOtpStep()) { router.replace("/login/otp"); return; } setOtpStepChecked(true); }, [router]); useEffect(() => { if (!otpStepChecked || passkeyAutofillTried) return; async function tryPasskeyAutofill() { setPasskeyAutofillTried(true); if (typeof window === "undefined" || !("PublicKeyCredential" in window)) { return; } const pk = window.PublicKeyCredential as typeof PublicKeyCredential & { isConditionalMediationAvailable?: () => Promise; }; if (typeof pk.isConditionalMediationAvailable !== "function") { return; } try { const available = await pk.isConditionalMediationAvailable(); if (!available) return; const result = await authClient.signIn.passkey({ autoFill: true }); const hasTwoFactorChallenge = Boolean(result.data && "twoFactorRedirect" in result.data) || Boolean((result as { twoFactorRedirect?: boolean }).twoFactorRedirect); if (result.error) return; if (hasTwoFactorChallenge) { setOtpStepActive(); router.push("/login/otp"); return; } clearOtpStep(); router.push("/"); } catch { // Best-effort only; manual login remains available. } } void tryPasskeyAutofill(); }, [otpStepChecked, passkeyAutofillTried, router]); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setError(null); setLoading(true); try { await assertAuthApiReachable(); const result = await signIn.email({ email, password }); const hasTwoFactorChallenge = Boolean(result.data && "twoFactorRedirect" in result.data) || Boolean((result as { twoFactorRedirect?: boolean }).twoFactorRedirect); if (result.error) { setError("E-Mail oder Passwort ungültig."); return; } if (hasTwoFactorChallenge) { setOtpStepActive(); router.push("/login/otp"); return; } clearOtpStep(); router.push("/"); } catch { setError( "Login derzeit nicht möglich. Bitte prüfe, ob API/Auth auf Port 3002 läuft.", ); } finally { setLoading(false); } } async function handlePasskeyLogin() { setError(null); setLoading(true); try { const result = await authClient.signIn.passkey(); const hasTwoFactorChallenge = Boolean(result.data && "twoFactorRedirect" in result.data) || Boolean((result as { twoFactorRedirect?: boolean }).twoFactorRedirect); if (result.error) { const message = (result.error.message ?? "").toLowerCase(); if (message.includes("cancel")) { setError("Passkey-Vorgang abgebrochen."); return; } if ( window.location.hostname !== "localhost" && (message.includes("origin") || message.includes("rp") || message.includes("domain")) ) { setError( "Passkey auf lokaler Entwicklung bitte unter http://localhost:3001 nutzen (nicht 127.0.0.1 oder ::1).", ); return; } setError(result.error.message ?? "Passkey-Anmeldung fehlgeschlagen. Bitte erneut versuchen."); return; } if (hasTwoFactorChallenge) { setOtpStepActive(); router.push("/login/otp"); return; } clearOtpStep(); router.push("/"); } catch { setError("Passkey-Anmeldung derzeit nicht möglich."); } finally { setLoading(false); } } return (