105 lines
3.9 KiB
Vue
105 lines
3.9 KiB
Vue
|
|
<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>
|