Files
portal/app/resources/js/components/settings/security/RecoveryCodesCard.vue
T
Дмитрий 849bc73290 refactor(frontend): Sprint 4 Phase B/2 — split 3 user views (audit O-refactor-04 хвост)
BillingView 416→114 (+ BalanceCard 155 + TransactionsTable 113 + InvoicesTable 90
+ billingFormatters 51 composable: formatPlain/formatCost/statusChipColor/
statusLabel/formatLabel/formatIcon/txAmountClass).
SecurityTab 354→39 (+ ChangePasswordCard 17 + TwoFactorCard 218 + RecoveryCodesCard 104
+ SessionsTable 66; auth-store читается напрямую в каждом sub-component).
RemindersView 345→183 (+ RemindersFilters 51 + RemindersList 173;
ReminderDialog уже отдельный с прошлой фазы — служит как ReminderForm).

State (`activeTab`, `editingReminder`, `deletingReminderId` в RemindersView)
остаётся в parent ради единого reload-flow + confirm-dialog'ов. Auth-store
читается напрямую в TwoFactorCard/RecoveryCodesCard через useAuthStore() —
без prop-drilling. Reminders-store читается напрямую в RemindersFilters/
RemindersList.

Все sub-components <250 строк (acceptance threshold). 3 view-shells: 114/39/183.

Регрессия: ESLint 0 + vue-tsc 0 + Vitest 416/416 + build OK 968 ms.

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

105 lines
3.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
/**
* RecoveryCodesCard — перегенерация 8 recovery-codes (доступна только при включённой 2FA).
* Sprint 4 Phase B/2 — split SecurityTab (audit O-refactor-04 хвост).
*
* Flow:
* - Кнопка «Перегенерировать резервные коды» → modal с password →
* POST /api/2fa/regenerate-recovery-codes → показ 8 новых кодов один раз.
*
* Видимость кнопки управляется через computed на auth.user?.totp_enabled —
* чтобы не показывать действие, недоступное без 2FA.
*/
import * as authApi from '../../../api/auth';
import { useAuthStore } from '../../../stores/auth';
import { computed, ref } from 'vue';
const auth = useAuthStore();
const has2fa = computed(() => auth.user?.totp_enabled ?? false);
const regenOpen = ref(false);
const regenPassword = ref('');
const regenError = ref('');
const regenCodes = ref<string[]>([]);
async function confirmRegen(): Promise<void> {
regenError.value = '';
try {
const r = await authApi.twoFactorRegenerateRecoveryCodes(regenPassword.value);
regenCodes.value = r.recovery_codes;
regenPassword.value = '';
} catch {
regenError.value = 'Неверный пароль.';
}
}
function closeRegen(): void {
regenOpen.value = false;
regenCodes.value = [];
}
</script>
<template>
<div v-if="has2fa">
<v-btn
variant="outlined"
size="small"
prepend-icon="mdi-key"
class="mb-4"
data-testid="regen-codes-btn"
@click="regenOpen = true"
>
Перегенерировать резервные коды
</v-btn>
<v-dialog v-model="regenOpen" :max-width="480" data-testid="regen-dialog">
<v-card>
<v-card-title>Перегенерация резервных кодов</v-card-title>
<v-card-text>
<template v-if="!regenCodes.length">
<p class="mb-3">Старые коды будут аннулированы. Введите пароль для подтверждения.</p>
<v-text-field
v-model="regenPassword"
label="Пароль"
type="password"
autocomplete="current-password"
:error-messages="regenError ? [regenError] : []"
density="comfortable"
/>
</template>
<template v-else>
<v-alert type="warning" variant="tonal" class="mb-3">
Сохраните новые 8 кодов старые больше не действуют.
</v-alert>
<div class="codes-grid">
<div v-for="(c, i) in regenCodes" :key="i" class="code-item font-mono">{{ c }}</div>
</div>
</template>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn v-if="!regenCodes.length" color="primary" @click="confirmRegen">Перегенерировать</v-btn>
<v-btn v-else color="primary" @click="closeRegen">Готово</v-btn>
<v-btn v-if="!regenCodes.length" variant="text" @click="regenOpen = false">Отмена</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<style scoped>
.codes-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.code-item {
background: #f6f3ec;
padding: 8px 12px;
border-radius: 6px;
text-align: center;
font-size: 14px;
letter-spacing: 0.06em;
}
</style>