- {renderInline(block.lines.join('\n'), resolveMention, key)} + {renderInline(block.lines.join('\n'), resolveMention, channels, key)}); } return (
- {renderInline(block.lines.join('\n'), resolveMention, key)} + {renderInline(block.lines.join('\n'), resolveMention, channels, key)}
); })} diff --git a/web/src/pages/LoginPage.tsx b/web/src/pages/LoginPage.tsx index 8421e8d..2235620 100644 --- a/web/src/pages/LoginPage.tsx +++ b/web/src/pages/LoginPage.tsx @@ -15,7 +15,11 @@ interface LocationState { from?: string; } -/** Форма входа: поле кода 2FA появляется при ответе `auth.2fa_required`. */ +/** + * Форма входа. Поле кода 2FA показывается всегда: сервер принимает и код из + * приложения (6 цифр), и резервный код вида `a8eh-pshp-t8st`, а при попытке + * входа без кода отвечает `auth.2fa_required` — тогда подсказываем про код. + */ export default function LoginPage() { const { t } = useTranslation(); const navigate = useNavigate(); @@ -30,8 +34,11 @@ export default function LoginPage() { const from = (location.state as LocationState | null)?.from ?? '/app'; const submit = useMutation({ - mutationFn: () => - login(needsTotp ? { email, password, totp_code: totpCode } : { email, password }), + mutationFn: () => { + // Пустое поле — вход без кода: сервер сам скажет, нужен ли он. + const code = totpCode.trim(); + return login(code === '' ? { email, password } : { email, password, totp_code: code }); + }, onSuccess: async () => { // Профиль перечитываем заново: cookie уже выставлена сервером. await getQueryClient().invalidateQueries(); @@ -39,6 +46,7 @@ export default function LoginPage() { }, onError: (error: unknown) => { if (errorCode(error) === 'auth.2fa_required') { + // Логин, пароль и уже введённый код остаются в форме. setNeedsTotp(true); } }, @@ -46,6 +54,7 @@ export default function LoginPage() { const onSubmit = (event: FormEvent+ {t('auth.login.totpRequired')} +
) : null} - {submit.isError ?