"use client"; import { useState, Suspense } from "react"; import { signIn } from "next-auth/react"; import { useRouter, useSearchParams } from "next/navigation"; import Link from "next/link"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Loader2, Fingerprint } from "lucide-react"; import { toast } from "sonner"; const ENCRYPTID_SERVER = process.env.NEXT_PUBLIC_ENCRYPTID_SERVER_URL || "https://encryptid.jeffemmett.com"; function SignInForm() { const router = useRouter(); const searchParams = useSearchParams(); const callbackUrl = searchParams.get("callbackUrl") || "/"; const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [isLoading, setIsLoading] = useState(false); const [isPasskeyLoading, setIsPasskeyLoading] = useState(false); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setIsLoading(true); try { const result = await signIn("credentials", { email, password, redirect: false, }); if (result?.error) { toast.error("Invalid email or password"); } else { toast.success("Signed in successfully!"); router.push(callbackUrl); router.refresh(); } } catch (error) { toast.error("An error occurred. Please try again."); } finally { setIsLoading(false); } } async function handlePasskeySignIn() { setIsPasskeyLoading(true); try { // Step 1: Get authentication options from EncryptID server const startRes = await fetch(`${ENCRYPTID_SERVER}/api/auth/start`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}), }); const { options } = await startRes.json(); // Step 2: Trigger WebAuthn ceremony in the browser const challenge = Uint8Array.from(atob(options.challenge.replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0)); const publicKeyOptions: PublicKeyCredentialRequestOptions = { challenge, rpId: options.rpId, userVerification: options.userVerification as UserVerificationRequirement, timeout: options.timeout, allowCredentials: options.allowCredentials?.map((c: { type: string; id: string; transports?: string[] }) => ({ type: c.type as PublicKeyCredentialType, id: Uint8Array.from(atob(c.id.replace(/-/g, "+").replace(/_/g, "/")), ch => ch.charCodeAt(0)), transports: c.transports as AuthenticatorTransport[], })), }; const assertion = await navigator.credentials.get({ publicKey: publicKeyOptions }) as PublicKeyCredential; if (!assertion) throw new Error("Passkey authentication cancelled"); const response = assertion.response as AuthenticatorAssertionResponse; // Helper to convert ArrayBuffer to base64url function toBase64url(buffer: ArrayBuffer): string { return btoa(String.fromCharCode(...new Uint8Array(buffer))) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/, ""); } // Step 3: Complete authentication with EncryptID server const completeRes = await fetch(`${ENCRYPTID_SERVER}/api/auth/complete`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ challenge: options.challenge, credential: { credentialId: assertion.id, authenticatorData: toBase64url(response.authenticatorData), clientDataJSON: toBase64url(response.clientDataJSON), signature: toBase64url(response.signature), userHandle: response.userHandle ? toBase64url(response.userHandle) : null, }, }), }); const authResult = await completeRes.json(); if (!authResult.success) { throw new Error(authResult.error || "Authentication failed"); } // Step 4: Exchange EncryptID token for NextAuth session const result = await signIn("encryptid", { token: authResult.token, redirect: false, }); if (result?.error) { toast.error("Passkey verified but session creation failed"); } else { toast.success("Signed in with passkey!"); router.push(callbackUrl); router.refresh(); } } catch (error: any) { if (error.name === "NotAllowedError") { toast.error("Passkey authentication was cancelled"); } else { toast.error(error.message || "Passkey authentication failed"); } } finally { setIsPasskeyLoading(false); } } const anyLoading = isLoading || isPasskeyLoading; return ( Sign in Sign in with a passkey or your email and password {/* Passkey sign-in button */}
Or continue with email
{/* Email/password form */}
setEmail(e.target.value)} required disabled={anyLoading} />
setPassword(e.target.value)} required disabled={anyLoading} />

Don't have an account?{" "} Sign up

); } export default function SignInPage() { return (
Loading...
}> ); }