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> @
163 lines
6.3 KiB
Vue
163 lines
6.3 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* ChangePasswordCard — смена пароля (UI-аудит 21.06.2026, Security).
|
|
*
|
|
* Реальные данные: дата последней смены — GET /api/account/security;
|
|
* смена — POST /api/account/change-password (текущий + новый + подтверждение).
|
|
* Заменяет прежнюю статичную заглушку (захардкоженная дата + мёртвая кнопка).
|
|
*/
|
|
import { onMounted, ref } from 'vue';
|
|
import { changePassword, getAccountSecurity } from '../../../api/account';
|
|
import axios from 'axios';
|
|
|
|
const lastChangeAt = ref<string | null>(null);
|
|
const dialogOpen = ref(false);
|
|
const currentPassword = ref('');
|
|
const newPassword = ref('');
|
|
const confirmPassword = ref('');
|
|
const saving = ref(false);
|
|
const errorMessage = ref<string | null>(null);
|
|
const toastOpen = ref(false);
|
|
const showCurrent = ref(false);
|
|
const showNew = ref(false);
|
|
|
|
function formatDate(iso: string | null): string {
|
|
if (!iso) return 'не менялся';
|
|
const d = new Date(iso);
|
|
return d.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
|
}
|
|
|
|
async function loadSecurity(): Promise<void> {
|
|
try {
|
|
const data = await getAccountSecurity();
|
|
lastChangeAt.value = data.last_password_change_at;
|
|
} catch {
|
|
lastChangeAt.value = null;
|
|
}
|
|
}
|
|
|
|
function openDialog(): void {
|
|
currentPassword.value = '';
|
|
newPassword.value = '';
|
|
confirmPassword.value = '';
|
|
errorMessage.value = null;
|
|
dialogOpen.value = true;
|
|
}
|
|
|
|
async function submit(): Promise<void> {
|
|
if (saving.value) return;
|
|
saving.value = true;
|
|
errorMessage.value = null;
|
|
try {
|
|
lastChangeAt.value = await changePassword({
|
|
current_password: currentPassword.value,
|
|
password: newPassword.value,
|
|
password_confirmation: confirmPassword.value,
|
|
});
|
|
dialogOpen.value = false;
|
|
toastOpen.value = true;
|
|
} catch (err) {
|
|
if (axios.isAxiosError(err) && err.response?.status === 422) {
|
|
const errors = (err.response.data as { errors?: Record<string, string[]> })?.errors ?? {};
|
|
errorMessage.value =
|
|
errors.current_password?.[0] ?? errors.password?.[0] ?? 'Проверьте введённые данные.';
|
|
} else {
|
|
errorMessage.value = 'Не удалось сменить пароль. Попробуйте позже.';
|
|
}
|
|
} finally {
|
|
saving.value = false;
|
|
}
|
|
}
|
|
|
|
onMounted(loadSecurity);
|
|
</script>
|
|
|
|
<template>
|
|
<v-card variant="outlined" class="pa-4 mb-4" data-tour="settings-password">
|
|
<h3 class="text-subtitle-2 mb-3">Пароль</h3>
|
|
<p class="text-body-2 text-medium-emphasis mb-3">
|
|
Последняя смена: {{ formatDate(lastChangeAt) }}
|
|
</p>
|
|
<v-btn
|
|
variant="outlined"
|
|
size="small"
|
|
prepend-icon="mdi-lock-reset"
|
|
data-testid="change-password-open"
|
|
data-tour="settings-password-btn"
|
|
@click="openDialog"
|
|
>
|
|
Сменить пароль
|
|
</v-btn>
|
|
|
|
<v-dialog v-model="dialogOpen" max-width="440">
|
|
<v-card class="pa-2" data-tour="password-dialog">
|
|
<v-card-title class="text-subtitle-1">Смена пароля</v-card-title>
|
|
<v-card-text>
|
|
<v-alert
|
|
v-if="errorMessage"
|
|
type="error"
|
|
variant="tonal"
|
|
density="compact"
|
|
class="mb-3"
|
|
data-testid="change-password-error"
|
|
>
|
|
{{ errorMessage }}
|
|
</v-alert>
|
|
<v-text-field
|
|
v-model="currentPassword"
|
|
:type="showCurrent ? 'text' : 'password'"
|
|
label="Текущий пароль"
|
|
variant="outlined"
|
|
density="comfortable"
|
|
autocomplete="current-password"
|
|
:append-inner-icon="showCurrent ? 'mdi-eye-off' : 'mdi-eye'"
|
|
data-testid="change-password-current"
|
|
class="mb-2"
|
|
@click:append-inner="showCurrent = !showCurrent"
|
|
/>
|
|
<v-text-field
|
|
v-model="newPassword"
|
|
:type="showNew ? 'text' : 'password'"
|
|
label="Новый пароль"
|
|
variant="outlined"
|
|
density="comfortable"
|
|
autocomplete="new-password"
|
|
persistent-hint
|
|
hint="Не короче 10 символов"
|
|
:append-inner-icon="showNew ? 'mdi-eye-off' : 'mdi-eye'"
|
|
data-testid="change-password-new"
|
|
class="mb-2"
|
|
@click:append-inner="showNew = !showNew"
|
|
/>
|
|
<v-text-field
|
|
v-model="confirmPassword"
|
|
:type="showNew ? 'text' : 'password'"
|
|
label="Повторите новый пароль"
|
|
variant="outlined"
|
|
density="comfortable"
|
|
autocomplete="new-password"
|
|
data-testid="change-password-confirm"
|
|
/>
|
|
</v-card-text>
|
|
<v-card-actions class="px-4 pb-3">
|
|
<v-spacer />
|
|
<v-btn variant="text" :disabled="saving" @click="dialogOpen = false">Отмена</v-btn>
|
|
<v-btn
|
|
color="primary"
|
|
variant="flat"
|
|
:loading="saving"
|
|
data-testid="change-password-submit"
|
|
@click="submit"
|
|
>
|
|
Сменить
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
|
|
<v-snackbar v-model="toastOpen" :timeout="3000" location="bottom right" data-testid="change-password-toast">
|
|
Пароль изменён.
|
|
</v-snackbar>
|
|
</v-card>
|
|
</template>
|