import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Divider, Input, Modal, QRCode, message } from 'antd'; import * as OTPAuth from 'otpauth'; import { ClipboardManager } from '@/utils'; import { activateOnKey } from '@/utils/a11y'; import { TotpCodeSchema } from '@/schemas/login'; import './TwoFactorModal.css'; type Type = 'set' | 'confirm'; interface TwoFactorModalProps { open: boolean; title?: string; description?: string; token?: string; type?: Type; onConfirm: (success: boolean, code?: string) => void; onOpenChange: (open: boolean) => void; } export default function TwoFactorModal({ open, title = '', description = '', token = '', type = 'set', onConfirm, onOpenChange, }: TwoFactorModalProps) { const { t } = useTranslation(); const [messageApi, messageContextHolder] = message.useMessage(); const [enteredCode, setEnteredCode] = useState(''); const totp = useMemo(() => { if (!open || !token) return null; return new OTPAuth.TOTP({ issuer: '3x-ui', label: 'Administrator', algorithm: 'SHA1', digits: 6, period: 30, secret: token, }); }, [open, token]); const qrValue = totp ? totp.toString() : ''; const [wasOpen, setWasOpen] = useState(false); if (open !== wasOpen) { setWasOpen(open); if (open) setEnteredCode(''); } function close(success: boolean, code = '') { onConfirm(success, code); onOpenChange(false); setEnteredCode(''); } function onOk() { const codeOk = TotpCodeSchema.safeParse(enteredCode); if (!codeOk.success) { messageApi.error( t(codeOk.error.issues[0]?.message ?? 'pages.settings.security.twoFactorModalError'), ); return; } if (type === 'confirm' && !token) { close(true, codeOk.data); return; } if (!totp) return; if (totp.generate() === codeOk.data) { close(true); } else { messageApi.error(t('pages.settings.security.twoFactorModalError')); } } function onCancel() { close(false); } async function copyToken() { const ok = await ClipboardManager.copyText(token); if (ok) messageApi.success(t('copied')); } return ( <> {messageContextHolder} {t('cancel')} , , ]} > {type === 'set' ? ( <>

{t('pages.settings.security.twoFactorModalSteps')}

{t('pages.settings.security.twoFactorModalFirstStep')}

{token}

{t('pages.settings.security.twoFactorModalSecondStep')}

setEnteredCode(e.target.value)} style={{ width: '100%' }} aria-label={t('twoFactorCode')} /> ) : ( <>

{description}

setEnteredCode(e.target.value)} style={{ width: '100%' }} aria-label={t('twoFactorCode')} /> )}
); }