fix(pwa): код двухфакторки уходит сам, а не ждёт кнопки

Второй шаг входа сотрудника не отправлялся вовсе: у поля с кодом не было ни
обработки Enter, ни автоотправки, а «Готово» на клавиатуре телефона — это как
раз Enter. Со стороны выглядело как «код не принят», хотя запрос на /auth/2fa
не уходил ни разу (в логах бэкенда — только login с ответом «нужен второй шаг»).

Теперь шесть цифр отправляются сразу, Enter тоже работает, поле получает фокус и
autocomplete=one-time-code (подсказка кода из СМС/менеджера паролей).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 15:19:11 +10:00
co-authored by Claude Opus 5
parent a0ada7de67
commit 39ee2b7a49
2 changed files with 17 additions and 6 deletions
@@ -59,8 +59,9 @@ describe("DevLoginPage", () => {
await userEvent.click(screen.getByRole("button", { name: "Войти" })); await userEvent.click(screen.getByRole("button", { name: "Войти" }));
expect(useStaff.getState().token).toBeNull(); expect(useStaff.getState().token).toBeNull();
// Шестая цифра отправляет код сама: раньше форма ждала нажатия, а «Готово» на
// клавиатуре телефона ничего не делало — выглядело как «код не принят».
await userEvent.type(await screen.findByPlaceholderText("000000"), "123456"); await userEvent.type(await screen.findByPlaceholderText("000000"), "123456");
await userEvent.click(screen.getByRole("button", { name: "Подтвердить" }));
expect(staffApi.staffLogin2fa).toHaveBeenCalledWith("partial", "123456"); expect(staffApi.staffLogin2fa).toHaveBeenCalledWith("partial", "123456");
expect(useStaff.getState().token).toBe("staff-tok"); expect(useStaff.getState().token).toBe("staff-tok");
}); });
+15 -5
View File
@@ -42,11 +42,11 @@ export function DevLoginPage() {
} }
} }
async function submit2fa() { async function submit2fa(value = code) {
if (!partial || code.length < 6 || busy) return; if (!partial || value.length < 6 || busy) return;
setBusy(true); setBusy(true);
try { try {
const r = await staffLogin2fa(partial, code); const r = await staffLogin2fa(partial, value);
if (!r.access_token) throw new Error("no token"); if (!r.access_token) throw new Error("no token");
setStaff(r.access_token, username); setStaff(r.access_token, username);
setPartial(null); setPartial(null);
@@ -80,13 +80,23 @@ export function DevLoginPage() {
<> <>
<p className="text-sm mb-2">Код из приложения-аутентификатора.</p> <p className="text-sm mb-2">Код из приложения-аутентификатора.</p>
<input <input
autoFocus
inputMode="numeric" inputMode="numeric"
autoComplete="one-time-code"
value={code} value={code}
onChange={(e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6))} onChange={(e) => {
const next = e.target.value.replace(/\D/g, "").slice(0, 6);
setCode(next);
// Шесть цифр — код собран, ждать нажатия нечего: у TOTP всё равно
// остаётся секунд тридцать. Раньше отправки не было ни по Enter, ни
// по «Готово» на клавиатуре телефона, и это выглядело как «код не принят».
if (next.length === 6) void submit2fa(next);
}}
onKeyDown={(e) => { if (e.key === "Enter") void submit2fa(); }}
placeholder="000000" placeholder="000000"
className="card w-full px-4 py-3 mb-3 text-base tracking-widest bg-white" className="card w-full px-4 py-3 mb-3 text-base tracking-widest bg-white"
/> />
<button className="btn-primary" disabled={busy || code.length < 6} onClick={submit2fa}> <button className="btn-primary" disabled={busy || code.length < 6} onClick={() => submit2fa()}>
{busy ? <Spinner /> : "Подтвердить"} {busy ? <Spinner /> : "Подтвердить"}
</button> </button>
<button className="btn-ghost mt-2" onClick={() => { setPartial(null); setCode(""); }}> <button className="btn-ghost mt-2" onClick={() => { setPartial(null); setCode(""); }}>