108 lines
4.1 KiB
Vue
108 lines
4.1 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted, nextTick } from 'vue';
|
|
import { useTwoFactorAuth } from '@/composables/useTwoFactorAuth';
|
|
import { regenerateRecoveryCodes } from '@/routes/two-factor';
|
|
|
|
const loading = ref(false);
|
|
const regenerating = ref(false);
|
|
const { recoveryCodesList, fetchRecoveryCodes, errors, clearErrors } = useTwoFactorAuth();
|
|
|
|
async function loadCodes() {
|
|
loading.value = true;
|
|
clearErrors();
|
|
try {
|
|
await fetchRecoveryCodes();
|
|
} finally {
|
|
loading.value = false;
|
|
nextTick(() => { if ((window as any).lucide) (window as any).lucide.createIcons(); });
|
|
}
|
|
}
|
|
|
|
async function regenerate() {
|
|
regenerating.value = true;
|
|
clearErrors();
|
|
try {
|
|
const res = await fetch(regenerateRecoveryCodes.url(), {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'X-CSRF-TOKEN': (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement)?.content || '',
|
|
'Accept': 'application/json',
|
|
},
|
|
});
|
|
if (!res.ok) {
|
|
throw new Error('Failed to regenerate recovery codes');
|
|
}
|
|
await loadCodes();
|
|
} catch (e: any) {
|
|
errors.value.push(e?.message || 'Failed to regenerate recovery codes');
|
|
} finally {
|
|
regenerating.value = false;
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
// Lazy-load by default; uncomment to auto-load
|
|
loadCodes();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="rc-panel">
|
|
<div class="rc-head">
|
|
<div class="rc-title"><i data-lucide="key-round"></i> Recovery Codes</div>
|
|
<div class="rc-actions">
|
|
<button class="btn ghost" type="button" @click="loadCodes" :disabled="loading">
|
|
<span v-if="loading" class="spinner" />
|
|
<span>Show Codes</span>
|
|
</button>
|
|
<button class="btn danger" type="button" @click="regenerate" :disabled="regenerating">
|
|
<span v-if="regenerating" class="spinner" />
|
|
<span>Regenerate</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="errors.length" class="rc-errors">
|
|
<div v-for="(err, i) in errors" :key="i" class="err-item">
|
|
<i data-lucide="alert-triangle"></i>{{ err }}
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="!recoveryCodesList.length && !loading" class="empty">
|
|
<i data-lucide="folder-open"></i>
|
|
<div>No recovery codes yet</div>
|
|
</div>
|
|
|
|
<ul v-else class="codes">
|
|
<li v-for="(code, idx) in recoveryCodesList" :key="idx" class="code">
|
|
<i data-lucide="shield"></i>
|
|
<span>{{ code }}</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
:global(:root) { --bg-card:#0a0a0a; --border:#151515; --cyan:#00f2ff; --red:#ff5b5b; }
|
|
.rc-panel { border:1px solid var(--border); background:#0a0a0a; border-radius:14px; padding:16px; display:grid; gap:12px; }
|
|
.rc-head { display:flex; align-items:center; justify-content:space-between; gap:12px; }
|
|
.rc-title { display:flex; align-items:center; gap:8px; font-weight:900; color:#fff; }
|
|
.rc-title i { width:14px; color:#666; }
|
|
.rc-actions { display:flex; gap:8px; }
|
|
.btn { background: var(--cyan); color:#000; border:none; border-radius:10px; padding:10px 14px; font-weight:900; cursor:pointer; display:flex; align-items:center; gap:8px; }
|
|
.btn.ghost { background: #111; color:#ddd; border:1px solid #181818; }
|
|
.btn.danger { background: var(--red); color:#000; }
|
|
.btn:disabled { opacity:.6; cursor:not-allowed; }
|
|
.spinner { width: 14px; height: 14px; border: 2px solid rgba(0,0,0,0.1); border-top-color:#000; border-radius: 50%; animation: spin .8s linear infinite; }
|
|
.codes { list-style:none; padding:0; margin:0; display:grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap:10px; }
|
|
.code { display:flex; align-items:center; gap:10px; border:1px solid #151515; background:#050505; border-radius:10px; padding:10px 12px; font-weight:800; color:#eee; }
|
|
.code i { width:14px; color:#333; }
|
|
.empty { color:#666; display:flex; align-items:center; gap:10px; }
|
|
.empty i { width:18px; }
|
|
.rc-errors { display:grid; gap:6px; }
|
|
.err-item { display:flex; align-items:center; gap:8px; color:#ff5b5b; font-weight:800; }
|
|
.err-item i { width:14px; }
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
</style>
|