Initialer Laravel Commit für BetiX
This commit is contained in:
153
resources/js/components/TwoFactorSetupModal.vue
Normal file
153
resources/js/components/TwoFactorSetupModal.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch, nextTick } from 'vue';
|
||||
import { useTwoFactorAuth } from '@/composables/useTwoFactorAuth';
|
||||
|
||||
// Props
|
||||
const props = withDefaults(defineProps<{
|
||||
isOpen: boolean;
|
||||
requiresConfirmation?: boolean;
|
||||
twoFactorEnabled?: boolean;
|
||||
}>(), {
|
||||
requiresConfirmation: false,
|
||||
twoFactorEnabled: false,
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
'update:isOpen': [boolean];
|
||||
}>();
|
||||
|
||||
const internalOpen = ref<boolean>(props.isOpen);
|
||||
watch(() => props.isOpen, (v) => internalOpen.value = v);
|
||||
watch(internalOpen, (v) => emit('update:isOpen', v));
|
||||
|
||||
// 2FA Data
|
||||
const {
|
||||
qrCodeSvg,
|
||||
manualSetupKey,
|
||||
errors,
|
||||
hasSetupData,
|
||||
clearTwoFactorAuthData,
|
||||
fetchSetupData,
|
||||
} = useTwoFactorAuth();
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
async function loadSetup() {
|
||||
loading.value = true;
|
||||
try {
|
||||
await fetchSetupData();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
nextTick(() => { if ((window as any).lucide) (window as any).lucide.createIcons(); });
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
internalOpen.value = false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (internalOpen.value) loadSetup();
|
||||
});
|
||||
|
||||
watch(internalOpen, (open) => {
|
||||
if (open) loadSetup();
|
||||
else clearTwoFactorAuthData();
|
||||
});
|
||||
|
||||
const canContinue = computed(() => hasSetupData.value);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<teleport to="body">
|
||||
<div v-if="internalOpen" class="modal-backdrop" @click.self="close">
|
||||
<div class="modal">
|
||||
<header class="modal-head">
|
||||
<div class="title">
|
||||
<i data-lucide="shield-check"></i>
|
||||
<span>Two-Factor Setup</span>
|
||||
</div>
|
||||
<button class="icon-btn" type="button" @click="close" aria-label="Close">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<section class="modal-body">
|
||||
<div v-if="errors.length" class="err-list">
|
||||
<div v-for="(e, idx) in errors" :key="idx" class="err">
|
||||
<i data-lucide="alert-triangle"></i>{{ e }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="qr-box">
|
||||
<div class="qr-wrap">
|
||||
<div v-if="loading" class="qr-loading">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
<div v-else-if="qrCodeSvg" class="qr" v-html="qrCodeSvg" />
|
||||
<div v-else class="empty">
|
||||
<i data-lucide="scan-line"></i>
|
||||
<div>QR code not available</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hint">Scan this code with your authenticator app</div>
|
||||
</div>
|
||||
|
||||
<div class="key-box">
|
||||
<div class="lbl">Manual Setup Key</div>
|
||||
<div class="key">
|
||||
<i data-lucide="key"></i>
|
||||
<span>{{ manualSetupKey || '—' }}</span>
|
||||
</div>
|
||||
<div class="hint">Enter this key in your authenticator app if you cannot scan the QR</div>
|
||||
|
||||
<div class="footer">
|
||||
<button class="btn" type="button" :disabled="loading" @click="loadSetup">
|
||||
<span v-if="loading" class="spinner" />
|
||||
<span>Refresh Data</span>
|
||||
</button>
|
||||
<button class="btn primary" type="button" :disabled="!canContinue || loading" @click="close">
|
||||
<i data-lucide="check"></i>
|
||||
<span>{{ props.requiresConfirmation ? 'Continue to Confirm' : 'Done' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:global(:root) { --bg:#0a0a0a; --overlay: rgba(0,0,0,.7); --border:#151515; --cyan:#00f2ff; }
|
||||
.modal-backdrop { position: fixed; inset: 0; background: var(--overlay); display: grid; place-items: center; z-index: 1000; padding: 16px; }
|
||||
.modal { width: min(900px, 100%); background: #050505; border: 1px solid var(--border); border-radius: 16px; overflow: hidden; box-shadow: 0 20px 60px rgba(0,0,0,.7); }
|
||||
.modal-head { display:flex; align-items:center; justify-content:space-between; padding:14px 16px; border-bottom:1px solid var(--border); }
|
||||
.title { display:flex; align-items:center; gap:10px; font-weight:900; color:#fff; text-transform:uppercase; letter-spacing:1px; font-size:12px; }
|
||||
.title i { width:14px; color:#666; }
|
||||
.icon-btn { background: transparent; border: 1px solid #151515; color:#888; border-radius: 8px; width:32px; height:32px; display:flex; align-items:center; justify-content:center; cursor:pointer; }
|
||||
.icon-btn:hover { color:#fff; border-color:#222; }
|
||||
.modal-body { padding: 16px; display:grid; gap: 16px; }
|
||||
.err-list { display:grid; gap:6px; }
|
||||
.err { display:flex; align-items:center; gap:8px; color:#ff5b5b; font-weight:800; }
|
||||
.err i { width:14px; }
|
||||
.grid { display:grid; grid-template-columns: 320px 1fr; gap: 16px; }
|
||||
.qr-wrap { border:1px solid var(--border); border-radius: 12px; padding: 10px; background:#0a0a0a; position: relative; min-height: 220px; display:flex; align-items:center; justify-content:center; }
|
||||
.qr { display:block; width: 100%; height: auto; }
|
||||
.qr-loading { display:flex; align-items:center; justify-content:center; width:100%; height:200px; }
|
||||
.spinner { width: 18px; height: 18px; border: 2px solid rgba(0,0,0,0.1); border-top-color:#000; border-radius: 50%; animation: spin .8s linear infinite; }
|
||||
.key-box { display:grid; gap: 10px; align-content:start; }
|
||||
.lbl { font-size:10px; font-weight:900; color:#555; text-transform:uppercase; letter-spacing:1px; }
|
||||
.key { display:flex; align-items:center; gap:10px; border:1px solid var(--border); background:#0a0a0a; border-radius:10px; padding:10px 12px; color:#eee; font-weight:800; }
|
||||
.key i { width:14px; color:#333; }
|
||||
.hint { color:#666; font-size:12px; }
|
||||
.footer { margin-top: 8px; display:flex; gap:10px; }
|
||||
.btn { background: #111; color:#ddd; border:1px solid #181818; border-radius: 10px; padding: 10px 14px; font-weight: 900; cursor: pointer; display:flex; align-items:center; gap:8px; }
|
||||
.btn.primary { background: var(--cyan); color:#000; border-color: transparent; box-shadow: 0 0 20px rgba(0,242,255,0.2); }
|
||||
.btn:disabled { opacity:.6; cursor:not-allowed; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@media (max-width: 800px) { .grid { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
Reference in New Issue
Block a user