0c36b7a28d
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>
114 lines
3.9 KiB
Vue
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>
|