Files
portal/app/resources/js/views/auth/RecoveryCodesView.vue
T
Дмитрий 19096552b4 phase2(auth): закрыты 4 оставшихся auth-экрана из v8_login.html
- 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>
2026-05-08 17:09:56 +03:00

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>