118 lines · 4.8 KB
import { useState } from "react";
import { beginTotpSetup, confirmTotpSetup, disableTwoFactor, newRecoveryCodes } from "~/fns/account-security";
import { useAction } from "~/hooks/use-action";
import { Button } from "~/components/ui/Button";
import { ErrorBanner, TextField } from "~/components/ui/Field";
import { Badge, Panel } from "~/components/ui/Primitives";
import { RecoveryCodes } from "./RecoveryCodes";

type Props = { enabled: boolean; recoveryCodesLeft: number; hasPassword: boolean };

function field(form: HTMLFormElement, name: string): string {
  return String(new FormData(form).get(name) ?? "");
}

export function TwoFactorPanel({ enabled, recoveryCodesLeft, hasPassword }: Props) {
  const [setup, setSetup] = useState<{ secret: string; uri: string } | null>(null);
  const [codes, setCodes] = useState<string[] | null>(null);
  const begin = useAction(beginTotpSetup, { invalidate: false });
  const confirm = useAction(confirmTotpSetup);
  const disable = useAction(disableTwoFactor);
  const regenerate = useAction(newRecoveryCodes);
  const error = begin.error ?? confirm.error ?? disable.error ?? regenerate.error;
  const passwordField = hasPassword ? (
    <TextField label="Current password" name="password" type="password" autoComplete="current-password" required />
  ) : null;

  return (
    <Panel
      title={
        <span className="flex items-center gap-2">
          Authenticator app {enabled && <Badge tone="accent">Enabled</Badge>}
        </span>
      }
    >
      <div className="space-y-4 p-4 text-sm">
        <ErrorBanner message={error} />
        {codes && <RecoveryCodes codes={codes} />}
        {enabled ? (
          <>
            <p className="text-gray-400">