Files
portal/app/resources/js/views/auth/RecoveryCodesView.vue
T
Дмитрий 0c36b7a28d feat(a11y): migrate Pa11y scope from handoff prototypes to live Vue app
Closes Audit #3 sole P1 (F-A11Y-PA11Y-SCOPE-01).

Pa11y was scanning handoff HTML prototypes from liderra_v8_handoff/concepts/
(3 URLs, ~10 contrast violations), NOT the live Vue app. Audit #2 baseline
"0 errors" was inaccurate — real portal was never covered.

Changes:
- pa11y.config.json: now targets http://localhost:8000/<route> for 7 guest
  pages (login, register, forgot, 2fa, recovery, 403, 500)
- pa11y-handoff.config.json: preserves historical handoff baseline as
  opt-in (`npm run a11y:handoff`)
- package.json: new `a11y:handoff` script; `a11y` repointed to live target
- RecoveryCodesView.vue: scoped CSS override fixes Vuetify warning-tonal
  alert content contrast (2.03:1 → ≥4.5:1, color #0a0700 per Pa11y rec)
- .github/workflows/a11y.yml: new CI job with dev-server lifecycle
  (php artisan serve + curl wait-on + Pa11y + screenshot artifact upload)
- docs/audit-baseline-pa11y.md: first live baseline document with per-URL
  status, ignore selectors rationale, re-run instructions

Local verification:
- npm run a11y: 7/7 URLs passed (0 violations)
- vue-tsc: 0 errors
- ESLint: 0 errors
- Vitest: 88 files / 683 passed / 3 skipped / 0 failed (no regressions)

Plan: docs/superpowers/plans/2026-05-14-audit3-deferred-fixes.md Task 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 08:25:14 +03:00

114 lines
3.9 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;
}
/* WCAG2AA contrast fix for Vuetify warning tonal variant (2.03:1 → ≥4.5:1).
Pa11y baseline 2026-05-14 — see docs/audit-baseline-pa11y.md. */
.recovery-card :deep(.v-alert--variant-tonal.bg-warning .v-alert__content),
.recovery-card :deep(.v-alert--variant-tonal.text-warning .v-alert__content) {
color: #0a0700;
}
</style>