import { useState } from 'react'; import { useMutation } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { enableTotp, setupTotp } from '@/api/auth'; import { ErrorNotice } from '@/components/ui/ErrorNotice'; import { Field } from '@/components/ui/Field'; import { Button, Card } from '@/components/ui/primitives'; /** * Включение двухфакторной аутентификации: setup → секрет (и QR) → код → * одноразовые коды восстановления, которые показываются ровно один раз. */ export function TwoFactorSection({ enabled, onEnabled, }: { enabled: boolean; onEnabled: () => void; }) { const { t } = useTranslation(); const [secret, setSecret] = useState(null); const [otpauthUrl, setOtpauthUrl] = useState(''); const [qrPng, setQrPng] = useState(''); const [code, setCode] = useState(''); const [recoveryCodes, setRecoveryCodes] = useState(null); const [qrFailed, setQrFailed] = useState(false); const [copied, setCopied] = useState(false); const setup = useMutation({ mutationFn: () => setupTotp(), onSuccess: (payload) => { setSecret(payload.secret); setOtpauthUrl(payload.otpauth_url); setQrPng(payload.qr_png ?? ''); setRecoveryCodes(null); setQrFailed(false); }, }); const enable = useMutation({ mutationFn: () => enableTotp(code.trim()), onSuccess: (codes) => { setRecoveryCodes(codes); setSecret(null); setCode(''); onEnabled(); }, }); const copyRecovery = (): void => { if (recoveryCodes === null) { return; } void navigator.clipboard?.writeText(recoveryCodes.join('\n')); setCopied(true); }; return (

{t('settings.security.twoFactorTitle')}

{t( enabled ? 'settings.security.twoFactorEnabled' : 'settings.security.twoFactorDisabled', )}

{enabled || secret !== null ? null : ( )}
{setup.isError ? : null} {secret !== null ? (

{t('settings.security.twoFactorSetupHint')}

{qrFailed || qrPng === '' ? (

{t('settings.security.twoFactorQrUnavailable')}

) : ( {t('settings.security.twoFactorQrAlt')} setQrFailed(true)} /> )}
{t('settings.security.twoFactorSecret')}
{secret}
{t('settings.security.twoFactorUri')}
{otpauthUrl}
{ event.preventDefault(); if (code.trim() !== '') { enable.mutate(); } }} > setCode(event.target.value)} required /> {enable.isError ? : null}
) : null} {recoveryCodes !== null ? (

{t('settings.security.twoFactorEnabledDone')}

{t('settings.security.recoveryTitle')}

{t('settings.security.recoveryHint')}

    {/* Коды вида `a8eh-pshp-t8st` не обрезаем: переносим по символам. */} {recoveryCodes.map((recoveryCode) => (
  • {recoveryCode}
  • ))}
) : null}
); }