Files
portal/app/resources/js/components/settings/security/RecoveryCodesCard.vue
T

105 lines
3.9 KiB
Vue
Raw Normal View History

<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>