89 lines · 3.5 KB
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 (
      <AuthCard title="Sign-in expired" subtitle="Enter your password again to continue.">
        <TextLink to="/login" className="text-sm">
          Back to sign in
        </TextLink>
      </AuthCard>
    );
  }