9ff19f3cc3
feat(bot): всеобъемлющая инструкция (36 статей) + 31 узкая экскурсия, которая открывает окна Живой урок владельца 12.07.2026: «дубли чистятся в 2-х местах, а бот выдумал третье», «стыдно такого бота показывать», «видео показываем полное, а я спрашиваю про одно — надо резать», «надо не только кнопку показывать, а и что за ней». База знаний: 17 обзорных статей → 36 узких по темам. Портал обойдён целиком (5 агентов): все экраны, окна, подсказки «(?)», тексты ошибок, внутренние правила. Новое, чего бот не знал: дубли в ДВУХ местах (Поле и Предложения) + окно «источник уже есть»; правило 18:00; почему приходит меньше лимита; оплата по счёту и акты; списания; безопасность и 2FA; отчёты; массовые действия; регистрация. Экскурсии: 15 → 31, каждая на свою тему (3–7 шагов). Раннер GuidedTour научен шагу `open` — сам открывает окно/вкладку и подсвечивает то, что ВНУТРИ (окно дублей, форму подбора, диалог пополнения, панель тарифов, карточку сделки). 99 новых якорей data-tour расставлено по всему порталу (4 агента). Поиск: веса в tsvector (заголовок A / синонимы B / текст C) — иначе на «чистка дублей» первой всплывала статья «Списания» из-за мимоходного упоминания «поля». Тесты: Pest бот 39/39, Vitest 1222/1222. Новые сторожа: у каждой статьи есть экскурсия; экскурсия не короче 3 и не длиннее 7 шагов; каждая цель экскурсии реально существует в разметке; экскурсии про действия обязаны открывать окно. Живая проверка в браузере: экскурсия про дубли сама открыла окно объединения и перешла на второе место чистки. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> @
105 lines
4.0 KiB
Vue
105 lines
4.0 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* RecoveryCodesCard — перегенерация 8 recovery-codes (доступна только при включённой 2FA).
|
||
* Sprint 4 Phase B/2 — split SecurityTab (audit O-refactor-04 хвост).
|
||
*
|
||
* Flow:
|
||
* - Кнопка «Перегенерировать резервные коды» → modal с password →
|
||
* POST /api/2fa/regenerate-recovery-codes → показ 8 новых кодов один раз.
|
||
*
|
||
* Видимость кнопки управляется через computed на auth.user?.totp_enabled —
|
||
* чтобы не показывать действие, недоступное без 2FA.
|
||
*/
|
||
import * as authApi from '../../../api/auth';
|
||
import { useAuthStore } from '../../../stores/auth';
|
||
import { computed, ref } from 'vue';
|
||
|
||
const auth = useAuthStore();
|
||
const has2fa = computed(() => auth.user?.totp_enabled ?? false);
|
||
|
||
const regenOpen = ref(false);
|
||
const regenPassword = ref('');
|
||
const regenError = ref('');
|
||
const regenCodes = ref<string[]>([]);
|
||
|
||
async function confirmRegen(): Promise<void> {
|
||
regenError.value = '';
|
||
try {
|
||
const r = await authApi.twoFactorRegenerateRecoveryCodes(regenPassword.value);
|
||
regenCodes.value = r.recovery_codes;
|
||
regenPassword.value = '';
|
||
} catch {
|
||
regenError.value = 'Неверный пароль.';
|
||
}
|
||
}
|
||
|
||
function closeRegen(): void {
|
||
regenOpen.value = false;
|
||
regenCodes.value = [];
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div v-if="has2fa" data-tour="settings-recovery-codes">
|
||
<v-btn
|
||
variant="outlined"
|
||
size="small"
|
||
prepend-icon="mdi-key"
|
||
class="mb-4"
|
||
data-testid="regen-codes-btn"
|
||
@click="regenOpen = true"
|
||
>
|
||
Перегенерировать резервные коды
|
||
</v-btn>
|
||
|
||
<v-dialog v-model="regenOpen" :max-width="480" data-testid="regen-dialog">
|
||
<v-card>
|
||
<v-card-title>Перегенерация резервных кодов</v-card-title>
|
||
<v-card-text>
|
||
<template v-if="!regenCodes.length">
|
||
<p class="mb-3">Старые коды будут аннулированы. Введите пароль для подтверждения.</p>
|
||
<v-text-field
|
||
v-model="regenPassword"
|
||
label="Пароль"
|
||
type="password"
|
||
autocomplete="current-password"
|
||
:error-messages="regenError ? [regenError] : []"
|
||
density="comfortable"
|
||
/>
|
||
</template>
|
||
<template v-else>
|
||
<v-alert type="warning" variant="tonal" class="mb-3">
|
||
Сохраните новые 8 кодов — старые больше не действуют.
|
||
</v-alert>
|
||
<div class="codes-grid">
|
||
<div v-for="(c, i) in regenCodes" :key="i" class="code-item font-mono">{{ c }}</div>
|
||
</div>
|
||
</template>
|
||
</v-card-text>
|
||
<v-card-actions>
|
||
<v-spacer />
|
||
<v-btn v-if="!regenCodes.length" color="primary" @click="confirmRegen">Перегенерировать</v-btn>
|
||
<v-btn v-else color="primary" @click="closeRegen">Готово</v-btn>
|
||
<v-btn v-if="!regenCodes.length" variant="text" @click="regenOpen = false">Отмена</v-btn>
|
||
</v-card-actions>
|
||
</v-card>
|
||
</v-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.codes-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 8px;
|
||
}
|
||
.code-item {
|
||
background: #f6f3ec;
|
||
padding: 8px 12px;
|
||
border-radius: 6px;
|
||
text-align: center;
|
||
font-size: 14px;
|
||
letter-spacing: 0.06em;
|
||
}
|
||
</style>
|