/
evsedov
/
mpo-main
Обзор
Документация
Войти
/
evsedov
/
mpo-main
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
src/views/Auth/Code.vue
315 строк
9 KB
Evgeniy Sedov
fix(auth): show loading state for auth actions
18 июл 2026, 11:50
18 июл 2026, 11:50
479a166
Код
Авторство
О чём код?
<script setup lang="ts"> import { computed, onMounted, onUnmounted, ref } from "vue"; import router from "@/router"; import axiosAPI, { buildApiBody } from "@/api-service"; import { useAuthStore } from "@/stores/auth"; import { getStorageValue, setStorageValue, } from "@/utils/safeStorage"; import { AUTH_CODE_ATTEMPT_BLOCK_KEY, clearAuthCodeAttemptBlock, } from "@/utils/authCodeAttemptBlock"; import { useSingleFlight } from "@/utils/useSingleFlight"; import Notification from "@/components/Notification.vue"; import Logo from "@/components/Logo.vue"; import Button from "@/components/Button.vue"; import Delimiter from "@/components/Delimiter.vue"; const authStore = useAuthStore(); const notification = ref<{ show: (message: string, options?: object) => void; hide: () => void; }>(); const codePart1 = ref(""); const codePart2 = ref(""); const codePart3 = ref(""); const codePart4 = ref(""); const isLoading = ref(false); const { isPending: isRequestingCode, run: requestCode } = useSingleFlight(); const attemptCount = ref(0); const isBlocked = ref(false); const isRequestButtonEnabled = ref(false); const remainingTime = ref(120); const MAX_ATTEMPTS = 5; const BLOCK_DURATION_MS = 10 * 60 * 1000; let blockTimer: ReturnType<typeof setTimeout> | null = null; let cooldownTimer: ReturnType<typeof setInterval> | null = null; const code = computed( () => codePart1.value + codePart2.value + codePart3.value + codePart4.value ); const formattedRemainingTime = computed(() => { const minutes = Math.floor(remainingTime.value / 60); const seconds = remainingTime.value % 60; if (minutes === 0) return `${seconds} сек.`; if (seconds === 0) return `${minutes} мин.`; return `${minutes} мин. ${seconds} сек.`; }); const validateCode = (value: string) => /^\d{4}$/.test(value); const resetCode = () => { codePart1.value = ""; codePart2.value = ""; codePart3.value = ""; codePart4.value = ""; }; const startCooldown = () => { isRequestButtonEnabled.value = false; remainingTime.value = 120; if (cooldownTimer) { clearInterval(cooldownTimer); cooldownTimer = null; } cooldownTimer = setInterval(() => { remainingTime.value -= 1; if (remainingTime.value <= 0 && cooldownTimer) { clearInterval(cooldownTimer); cooldownTimer = null; isRequestButtonEnabled.value = true; } }, 1000); }; const handleRequestClick = () => requestCode(async () => { if (!isRequestButtonEnabled.value) return; try { if (!authStore.authPhone) { throw new Error("Не удалось восстановить телефон пользователя"); } const body = buildApiBody("MOB-ENTER-CHECK-PHONE", { phone: authStore.authPhone, }); const { data } = await axiosAPI.post("", body); if (data.res === "NO" || data.res === "ERROR") { throw new Error( data.title_head || data.title_page || "Не удалось отправить код" ); } notification.value?.show("Код отправлен", { type: "success", secondaryMessage: "Проверьте SMS или электронную почту.", duration: 3000, }); startCooldown(); } catch (error) { notification.value?.show("Не удалось отправить код", { type: "error", secondaryMessage: `${error}`, duration: 3000, }); } }); const submitCode = async () => { if (isLoading.value) { return; } if (isBlocked.value) { notification.value?.show("Слишком много попыток", { type: "error", secondaryMessage: "Попробуйте позже.", duration: 3000, }); return; } if (!validateCode(code.value)) { notification.value?.show("Неверный формат кода", { type: "info", secondaryMessage: "Код должен содержать 4 цифры.", duration: 3000, }); return; } try { isLoading.value = true; const body = buildApiBody("MOB-SMS-ENTER", { sms: code.value }); const { data } = await axiosAPI.post("", body); if (data.res === "NO") { attemptCount.value += 1; if (attemptCount.value >= MAX_ATTEMPTS) { const unlockTime = Date.now() + BLOCK_DURATION_MS; await setStorageValue( "local", AUTH_CODE_ATTEMPT_BLOCK_KEY, String(unlockTime) ); isBlocked.value = true; blockTimer = setTimeout(async () => { isBlocked.value = false; await clearAuthCodeAttemptBlock(); }, BLOCK_DURATION_MS); notification.value?.show("Слишком много попыток", { type: "error", secondaryMessage: "Попробуйте через 10 минут.", duration: 5000, }); } else { notification.value?.show(data.title_head || "Код неверен", { type: "info", secondaryMessage: data.title_page || "Попробуйте ещё раз.", duration: 3000, }); } resetCode(); return; } if (data.res === "OK") { attemptCount.value = 0; await clearAuthCodeAttemptBlock(); await authStore.setSmsLoginState("verified"); await authStore.setAuthStage("authenticated"); await authStore.touchSession(); router.replace("/mob-glavnaya"); return; } await authStore.clearAuthState({ keepWin: true }); notification.value?.show(data.title_head || "Ошибка", { type: "error", secondaryMessage: data.title_page || "Войдите заново.", duration: 3000, }); router.replace("/"); } catch (error) { notification.value?.show("Не удалось проверить код", { type: "error", secondaryMessage: `${error}`, duration: 3000, }); } finally { isLoading.value = false; } }; onMounted(async () => { startCooldown(); const storedUnlockTime = await getStorageValue( "local", AUTH_CODE_ATTEMPT_BLOCK_KEY ); if (!storedUnlockTime) { return; } const unlockTime = Number(storedUnlockTime); if (!Number.isFinite(unlockTime)) { await clearAuthCodeAttemptBlock(); return; } const now = Date.now(); if (now < unlockTime) { isBlocked.value = true; blockTimer = setTimeout(async () => { isBlocked.value = false; await clearAuthCodeAttemptBlock(); }, unlockTime - now); } else { await clearAuthCodeAttemptBlock(); } }); onUnmounted(() => { if (blockTimer) { clearTimeout(blockTimer); } if (cooldownTimer) { clearInterval(cooldownTimer); } resetCode(); attemptCount.value = 0; }); </script> <template> <section> <Notification ref="notification" /> <div class="background-secondary block-gap flex flex-column flex-x-center content__padding__big padding__both" > <Logo /> <h3 class="text-center font-bold text-uppercase font-s-small"> Мобильная помощь онлайн </h3> </div> <Delimiter /> <div class="background-secondary content__padding__big flex flex-column flex-x-center block-gap padding__both" > <h3 class="text-center font-bold font-s-big">Введите код</h3> <div class="code-inputs"> <input v-model="codePart1" maxlength="1" inputmode="numeric" /> <input v-model="codePart2" maxlength="1" inputmode="numeric" /> <input v-model="codePart3" maxlength="1" inputmode="numeric" /> <input v-model="codePart4" maxlength="1" inputmode="numeric" /> </div> <p class="typography-secondary font-s-little text-left"> Укажите код, который был отправлен вам по SMS/электронной почте. </p> <Button :text="isLoading ? 'Проверяем...' : 'Далее'" :disabled="isLoading || isBlocked || isRequestingCode" @click="submitCode" /> <Button :text=" isRequestingCode ? 'Отправляем...' : isRequestButtonEnabled ? 'Запросить код' : `Запросить код через ${formattedRemainingTime}` " :disabled="!isRequestButtonEnabled || isRequestingCode || isLoading" @click="handleRequestClick" /> </div> </section> </template> <style scoped> .code-inputs { display: flex; justify-content: center; gap: 10px; width: 100%; } .code-inputs input { width: 48px; height: 54px; border: 0; border-radius: var(--radius); background: var(--color-bg-field, #ffffff); text-align: center; font-size: 20px; font-weight: 700; } </style>