ec6434ee9a
UI-аудит раунд 2, вкладка «Безопасность» — убраны фейк-данные и мёртвые кнопки. Backend (schema-free, без смены SESSION_DRIVER): - AccountController + ChangePasswordRequest: POST /api/account/change-password — проверка текущего пароля (Hash::check против password_hash), новый >=10 симв + confirmed, лог password_changed/password_change_failed в auth_log (hash-chain). - GET /api/account/security: last_password_change_at (max по password-событиям auth_log) + recent_logins (реальные login_success: устройство/IP/время). - Роуты под auth:sanctum + throttle:auth-password. - Pest: 6 тестов. Регрессия Account+Auth — 23/23 GREEN. phpstan-baseline обновлён (Pest-$this false-positives нового теста, как у прочих тестов). Frontend: - api/account.ts. - ChangePasswordCard: реальная дата + диалог (текущий/новый/подтверждение, show/hide, обработка 422 неверного текущего пароля). - SessionsTable -> «Недавние входы»: реальный список из API, убраны 3 захардкоженных фейк-сессии + мёртвая кнопка «Завершить». NB: индивидуальный отзыв cookie-сессий требует database-драйвера сессий (инфра-решение владельца) — отдельный follow-up. Сейчас входы — честный read-only. Верификация: type-check, build, Playwright (диалог: неверный->ошибка, смена->дата 21.06, восстановление password123; недавние входы — реальные). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
162 lines
6.2 KiB
Vue
162 lines
6.2 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">
|
|
<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"
|
|
@click="openDialog"
|
|
>
|
|
Сменить пароль
|
|
</v-btn>
|
|
|
|
<v-dialog v-model="dialogOpen" max-width="440">
|
|
<v-card class="pa-2">
|
|
<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>
|