Initialer Laravel Commit für BetiX
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled

This commit is contained in:
2026-04-04 18:01:50 +02:00
commit 0280278978
374 changed files with 65210 additions and 0 deletions

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