19096552b4
- RegisterView: email + password (strength-meter 0..4) + 2 click-wrap'а (оферта + ПДн). 3-й «маркетинг» из handoff НЕ реализован (расхождение #2 реестра v1.13 - handoff противоречит ТЗ §1.5/§4.1). - TwoFactorView: 6 input-cell с auto-focus вперёд при вводе цифры, Backspace назад при empty, paste 6 цифр заполняет все. - ForgotPasswordView: email + alert «5 попыток / 15 минут» по ТЗ §1.7. - RecoveryCodesView: 8 кодов в 2-column grid + Скачать .txt (Blob/URL.createObjectURL) + Копировать (navigator.clipboard) + warning о невозможности повторного просмотра. Router: 4 новых маршрута (/register, /2fa, /forgot, /recovery), все meta.layout='auth', lazy-imports. Vitest +14 тестов (всего 24/24 за 3.29s): - RegisterView 4 (вкл. assertion на отсутствие маркетингового click-wrap) - TwoFactorView 3, ForgotPasswordView 3, RecoveryCodesView 4 Stories +4 (Histoire 6/6 за 29.17s). Регресс: lint+type-check+format OK; vitest 24/24; vite build 5 lazy-chunks для views + Vuetify в отдельные chunks (app chunk 198KB→78KB); Pest 48/48 за 4.85s. CLAUDE.md v1.19→v1.20, реестр Открытых_вопросов v1.28→v1.29. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
107 lines
3.6 KiB
Vue
107 lines
3.6 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* Экран резервных кодов (RecoveryCodesView).
|
|
*
|
|
* Источник дизайна: liderra_v8_handoff/concepts/v8_login.html секция #form-recovery.
|
|
* Источник логики: ТЗ v8.5 §1.6 / Прил. Г.4.2 — 8 одноразовых 8-символьных кодов;
|
|
* после использования код в БД удаляется (recovery_codes table); генерируются один
|
|
* раз при включении 2FA, посмотреть повторно нельзя — только перегенерация.
|
|
*
|
|
* MVP: коды-заглушки. Реальные коды будут с backend через POST /2fa/recovery-codes.
|
|
*/
|
|
import { ref } from 'vue';
|
|
|
|
// TODO(phase2): получать из API после step1 настройки 2FA.
|
|
const codes = ref([
|
|
'A4FX-91KZ',
|
|
'9MRT-2P3D',
|
|
'QH7B-XK4N',
|
|
'5VLW-T8RY',
|
|
'B2ZJ-N6FP',
|
|
'D3WK-Q9MX',
|
|
'7YHC-8GVB',
|
|
'RP4S-K1NA',
|
|
]);
|
|
|
|
function downloadTxt() {
|
|
const blob = new Blob([codes.value.join('\n') + '\n'], { type: 'text/plain;charset=utf-8' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'liderra-recovery-codes.txt';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
async function copyAll() {
|
|
try {
|
|
await navigator.clipboard.writeText(codes.value.join('\n'));
|
|
} catch {
|
|
// Fallback на legacy execCommand добавим только если будут жалобы — у нас HTTPS-only.
|
|
}
|
|
}
|
|
|
|
function handleContinue() {
|
|
// TODO(phase2): редирект на /dashboard.
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<v-card variant="flat" :max-width="420" width="100%" color="transparent" class="recovery-card">
|
|
<header class="recovery-header">
|
|
<h1 class="text-h5 mb-1">Резервные коды</h1>
|
|
<p class="text-body-2 text-medium-emphasis ma-0">
|
|
Сохраните эти 8 одноразовых кодов в безопасном месте. Каждый можно использовать только раз вместо 2FA.
|
|
</p>
|
|
</header>
|
|
|
|
<div class="codes-grid">
|
|
<span v-for="code in codes" :key="code" class="code-item">{{ code }}</span>
|
|
</div>
|
|
|
|
<v-alert type="warning" variant="tonal" density="compact">
|
|
<strong>После закрытия страницы коды нельзя посмотреть снова</strong>. Скачайте файл или сделайте скриншот.
|
|
</v-alert>
|
|
|
|
<div class="d-flex ga-2">
|
|
<v-btn variant="outlined" prepend-icon="mdi-download" @click="downloadTxt"> Скачать .txt </v-btn>
|
|
<v-btn variant="outlined" prepend-icon="mdi-content-copy" @click="copyAll"> Копировать </v-btn>
|
|
</div>
|
|
|
|
<v-btn color="primary" block size="large" variant="flat" @click="handleContinue"> Понятно — продолжить </v-btn>
|
|
</v-card>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.recovery-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
.recovery-header h1 {
|
|
font-variation-settings: 'opsz' 26;
|
|
letter-spacing: -0.018em;
|
|
}
|
|
|
|
.codes-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 8px;
|
|
}
|
|
|
|
.code-item {
|
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
padding: 10px 12px;
|
|
background: #fff;
|
|
border: 1px solid #d9d5cd;
|
|
border-radius: 6px;
|
|
text-align: center;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
</style>
|