import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { redirectSignedIn } from "~/lib/route-guards"; import { returnSearch, safeReturnPath } from "~/lib/return-to"; import { useState } from "react"; import { finishTwoFactorPasskey, getTwoFactorChallenge, startTwoFactorPasskey, verifyTwoFactorCode } from "~/fns/login"; import { useAction } from "~/hooks/use-action"; import { usePasskeyAssertion } from "~/hooks/use-passkey"; import { AuthCard } from "~/components/AuthCard"; import { Button } from "~/ui/Button"; import { TextField } from "~/ui/Field"; import { ErrorBanner } from "~/ui/Callout"; import { TextLink } from "~/ui/TextLink"; export const Route = createFileRoute("/login_/two-factor")({ validateSearch: returnSearch, beforeLoad: redirectSignedIn, loader: () => getTwoFactorChallenge(), head: () => ({ meta: [{ title: "Two-factor authentication ยท gitfor" }] }), component: TwoFactorPage, }); function TwoFactorPage() { const challenge = Route.useLoaderData(); // Validated again here: router search state can still carry the raw value during SSR. const next = safeReturnPath(Route.useSearch().next) ?? undefined; const navigate = useNavigate(); const [kind, setKind] = useState<"totp" | "recovery">(challenge.totp ? "totp" : "recovery"); const verify = useAction(verifyTwoFactorCode); const passkey = usePasskeyAssertion(startTwoFactorPasskey, finishTwoFactorPasskey); if (!challenge.pending) { return ( Back to sign in ); } async function submit(event: React.SubmitEvent) { event.preventDefault(); const code = String(new FormData(event.currentTarget).get("code") ?? ""); if (await verify.run({ data: { code, kind } })) await navigate({ href: next ?? "/" }); } return (
{challenge.passkey && ( )}
); }