Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 47s
Update the registrieren.vue component to improve debug logging for Cross-Device registration, including checks for local authenticators and tunnel server connections. Add warnings for potential issues when Cross-Device is not used. Modify the register-passkey-options API to ensure compatibility with Cross-Device requirements by allowing both platform and cross-platform authenticators. This update aims to provide clearer insights and troubleshooting guidance during the Passkey registration process.
890 lines
39 KiB
Vue
890 lines
39 KiB
Vue
<template>
|
||
<div class="min-h-full flex items-center justify-center py-16 px-4 sm:px-6 lg:px-8 bg-gray-50">
|
||
<div class="max-w-md w-full space-y-8">
|
||
<div class="text-center">
|
||
<h2 class="text-3xl font-display font-bold text-gray-900">
|
||
Registrierung
|
||
</h2>
|
||
<p class="mt-2 text-sm text-gray-600">
|
||
Beantragen Sie Zugang zum Mitgliederbereich
|
||
</p>
|
||
</div>
|
||
|
||
<div class="bg-white rounded-xl shadow-lg p-8">
|
||
<!-- Debug: Sichtbarer Test -->
|
||
<div class="mb-4 p-3 bg-yellow-200 border-2 border-yellow-500 rounded-lg text-sm font-mono">
|
||
<div class="font-bold text-yellow-900">🔍 DEBUG MODE AKTIV</div>
|
||
<div class="mt-1 text-yellow-800">Komponente: registrieren.vue geladen</div>
|
||
<div class="mt-1 text-yellow-800">Passkey-Support: {{ isPasskeySupported ? 'JA' : 'NEIN' }}</div>
|
||
<div class="mt-1 text-yellow-800">Use Passkey: {{ usePasskey ? 'JA' : 'NEIN' }}</div>
|
||
</div>
|
||
|
||
<form
|
||
class="space-y-6"
|
||
@submit.prevent="handleFormSubmit"
|
||
>
|
||
<!-- Registration Mode -->
|
||
<div class="flex items-center justify-between bg-gray-50 border border-gray-200 rounded-lg p-3">
|
||
<div class="text-sm text-gray-700">
|
||
<div class="font-medium">Registrierungsmethode</div>
|
||
<div class="text-xs text-gray-600">Passkey = Anmeldung ohne Passwort (z.B. FaceID/TouchID/Windows Hello)</div>
|
||
<div v-if="!isPasskeySupported" class="text-xs text-red-700 mt-1">
|
||
Passkeys aktuell nicht verfügbar: {{ passkeySupportReason || 'Bitte HTTPS/aktuellen Browser verwenden.' }}
|
||
</div>
|
||
</div>
|
||
<label class="flex items-center gap-2 text-sm font-medium text-gray-800">
|
||
<input v-model="usePasskey" type="checkbox" class="h-4 w-4" :disabled="isLoading || !isPasskeySupported">
|
||
Mit Passkey
|
||
</label>
|
||
</div>
|
||
|
||
<!-- Name -->
|
||
<div>
|
||
<label
|
||
for="name"
|
||
class="block text-sm font-medium text-gray-700 mb-2"
|
||
>
|
||
Vollständiger Name
|
||
</label>
|
||
<input
|
||
id="name"
|
||
v-model="formData.name"
|
||
type="text"
|
||
required
|
||
autocomplete="name"
|
||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-600 focus:border-transparent transition-all"
|
||
placeholder="Max Mustermann"
|
||
>
|
||
</div>
|
||
|
||
<!-- Email -->
|
||
<div>
|
||
<label
|
||
for="email"
|
||
class="block text-sm font-medium text-gray-700 mb-2"
|
||
>
|
||
E-Mail-Adresse
|
||
</label>
|
||
<input
|
||
id="email"
|
||
v-model="formData.email"
|
||
type="email"
|
||
required
|
||
autocomplete="email"
|
||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-600 focus:border-transparent transition-all"
|
||
placeholder="ihre-email@example.com"
|
||
>
|
||
</div>
|
||
|
||
<!-- Phone -->
|
||
<div>
|
||
<label
|
||
for="phone"
|
||
class="block text-sm font-medium text-gray-700 mb-2"
|
||
>
|
||
Telefonnummer (optional)
|
||
</label>
|
||
<input
|
||
id="phone"
|
||
v-model="formData.phone"
|
||
type="tel"
|
||
autocomplete="tel"
|
||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-600 focus:border-transparent transition-all"
|
||
placeholder="069-12345678"
|
||
>
|
||
</div>
|
||
|
||
<!-- Password -->
|
||
<div v-if="!usePasskey || setPasswordForPasskey">
|
||
<label
|
||
for="password"
|
||
class="block text-sm font-medium text-gray-700 mb-2"
|
||
>
|
||
Passwort <span v-if="usePasskey" class="text-xs text-gray-500">(Fallback, optional)</span>
|
||
</label>
|
||
<input
|
||
id="password"
|
||
v-model="formData.password"
|
||
type="password"
|
||
:required="!usePasskey"
|
||
autocomplete="new-password"
|
||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-600 focus:border-transparent transition-all"
|
||
placeholder="••••••••"
|
||
>
|
||
<p class="mt-1 text-xs text-gray-500">
|
||
Mindestens 8 Zeichen
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Confirm Password -->
|
||
<div v-if="!usePasskey || setPasswordForPasskey">
|
||
<label
|
||
for="confirmPassword"
|
||
class="block text-sm font-medium text-gray-700 mb-2"
|
||
>
|
||
Passwort bestätigen <span v-if="usePasskey" class="text-xs text-gray-500">(Fallback)</span>
|
||
</label>
|
||
<input
|
||
id="confirmPassword"
|
||
v-model="formData.confirmPassword"
|
||
type="password"
|
||
:required="!usePasskey"
|
||
autocomplete="new-password"
|
||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-600 focus:border-transparent transition-all"
|
||
placeholder="••••••••"
|
||
>
|
||
</div>
|
||
|
||
<!-- Optional password toggle for passkey users -->
|
||
<div v-if="usePasskey" class="flex items-center gap-2 text-sm text-gray-700">
|
||
<input
|
||
v-model="setPasswordForPasskey"
|
||
type="checkbox"
|
||
class="h-4 w-4"
|
||
:disabled="isLoading"
|
||
>
|
||
<span>Zusätzlich ein Passwort als Fallback setzen (z.B. für Firefox/Linux)</span>
|
||
</div>
|
||
|
||
<!-- Error Message -->
|
||
<div
|
||
v-if="errorMessage"
|
||
class="bg-red-50 border border-red-200 rounded-lg p-4"
|
||
>
|
||
<p class="text-sm text-red-800 flex items-center">
|
||
<AlertCircle
|
||
:size="18"
|
||
class="mr-2"
|
||
/>
|
||
{{ errorMessage }}
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Success Message -->
|
||
<div
|
||
v-if="successMessage"
|
||
class="bg-green-50 border border-green-200 rounded-lg p-4"
|
||
>
|
||
<p class="text-sm text-green-800 flex items-center">
|
||
<Check
|
||
:size="18"
|
||
class="mr-2"
|
||
/>
|
||
{{ successMessage }}
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Debug Info (nur bei Passkey-Registrierung) -->
|
||
<div
|
||
v-if="usePasskey && showDebugInfo && debugChallenge"
|
||
class="bg-blue-50 border border-blue-200 rounded-lg p-4 text-xs space-y-3"
|
||
>
|
||
<div class="font-semibold text-blue-900 mb-2">🔍 Debug-Informationen (QR-Code):</div>
|
||
<div class="space-y-1 text-blue-800">
|
||
<div><strong>Challenge:</strong> <code class="bg-blue-100 px-1 rounded break-all">{{ debugChallenge }}</code></div>
|
||
<div><strong>RP-ID:</strong> <code class="bg-blue-100 px-1 rounded">{{ debugRpId }}</code></div>
|
||
<div v-if="debugRegistrationId"><strong>Registration-ID:</strong> <code class="bg-blue-100 px-1 rounded break-all">{{ debugRegistrationId }}</code></div>
|
||
<div><strong>Origin:</strong> <code class="bg-blue-100 px-1 rounded">{{ typeof window !== 'undefined' ? window.location.origin : 'N/A (SSR)' }}</code></div>
|
||
</div>
|
||
|
||
<!-- FIDO QR-Code Info -->
|
||
<div class="mt-3 p-3 bg-purple-50 border border-purple-300 rounded">
|
||
<div class="font-semibold text-purple-900 mb-2">🔐 FIDO Cross-Device Info:</div>
|
||
<div class="text-xs text-purple-800 space-y-2">
|
||
<div><strong>QR-Code-Format:</strong> FIDO-URI (enthält öffentlichen Schlüssel + Secret)</div>
|
||
<div><strong>Hinweis:</strong> Der QR-Code enthält einen FIDO-URI, der vom Smartphone gescannt werden muss.</div>
|
||
|
||
<div class="mt-2 p-2 bg-purple-100 rounded">
|
||
<strong class="text-purple-900">Wie funktioniert Cross-Device?</strong>
|
||
<ol class="list-decimal list-inside mt-1 space-y-1 text-purple-700">
|
||
<li>Desktop-Browser generiert QR-Code mit öffentlichem Schlüssel (FIDO-URI)</li>
|
||
<li>Desktop-Browser registriert sich beim <strong>Tunnel-Server</strong> (z.B. cable.ua5v.com)</li>
|
||
<li>Smartphone scannt QR-Code</li>
|
||
<li>Smartphone verbindet sich über <strong>Tunnel-Server</strong> mit Desktop-Browser</li>
|
||
<li>Desktop-Browser leitet Credential-Response an den Server weiter</li>
|
||
</ol>
|
||
<div class="mt-2 text-xs text-purple-600">
|
||
<strong>Hinweis:</strong> Die Verbindung läuft über den Tunnel-Server, nicht direkt zwischen den Geräten.
|
||
Bluetooth wird optional für physische Nähe-Bestätigung verwendet (abhängig vom Browser/Gerät).
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-2 p-2 bg-yellow-50 border border-yellow-300 rounded">
|
||
<strong class="text-yellow-900">Voraussetzungen:</strong>
|
||
<ul class="list-disc list-inside mt-1 space-y-1 text-yellow-800">
|
||
<li>✅ Beide Geräte müssen <strong>Internetverbindung</strong> haben</li>
|
||
<li>✅ Tunnel-Server müssen erreichbar sein (cable.ua5v.com, cable.auth.com)</li>
|
||
<li>⚠️ Bluetooth kann für physische Nähe-Bestätigung verwendet werden (abhängig vom Browser/Gerät)</li>
|
||
<li>⚠️ Einige Browser/Implementierungen erfordern Bluetooth für Cross-Device</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="mt-2 text-purple-700">
|
||
<strong>Problem:</strong> Wenn das Smartphone den QR-Code scannt, aber keine Verbindung herstellt:
|
||
<ul class="list-disc list-inside mt-1 space-y-1">
|
||
<li>Prüfen Sie, ob beide Geräte Internetverbindung haben</li>
|
||
<li>Prüfen Sie, ob Tunnel-Server erreichbar sind (Firewall/Netzwerk: cable.ua5v.com, cable.auth.com)</li>
|
||
<li>Prüfen Sie, ob Bluetooth aktiviert ist (kann je nach Browser/Gerät erforderlich sein)</li>
|
||
<li>Versuchen Sie die alternative Smartphone-URL unten (umgeht Tunnel-Server)</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Smartphone URL -->
|
||
<div v-if="debugSmartphoneUrl" class="mt-3 p-3 bg-green-50 border border-green-300 rounded">
|
||
<div class="font-semibold text-green-900 mb-2">📱 Alternative: Smartphone-URL (manuell öffnen):</div>
|
||
<div class="break-all text-xs mb-2 p-2 bg-white rounded border">
|
||
<a :href="debugSmartphoneUrl" target="_blank" class="text-blue-600 hover:underline">
|
||
{{ debugSmartphoneUrl }}
|
||
</a>
|
||
</div>
|
||
<div class="text-xs text-gray-700 mb-2">
|
||
<strong>Anleitung:</strong> Falls der QR-Code nicht funktioniert, öffnen Sie diese URL manuell auf Ihrem Smartphone.
|
||
</div>
|
||
<button
|
||
@click="copyToClipboard(debugSmartphoneUrl)"
|
||
class="px-3 py-1 bg-green-600 text-white text-xs rounded hover:bg-green-700"
|
||
>
|
||
📋 URL kopieren
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Full Options JSON -->
|
||
<div v-if="debugOptions" class="mt-3">
|
||
<details class="text-xs">
|
||
<summary class="cursor-pointer font-semibold text-blue-900 hover:text-blue-700 mb-2">
|
||
📄 Vollständige Options (JSON) - Klicken zum Anzeigen
|
||
</summary>
|
||
<pre class="mt-2 p-2 bg-gray-100 rounded overflow-auto text-xs max-h-60 border">{{ JSON.stringify(debugOptions, null, 2) }}</pre>
|
||
<button
|
||
@click="copyToClipboard(JSON.stringify(debugOptions, null, 2))"
|
||
class="mt-2 px-3 py-1 bg-gray-600 text-white text-xs rounded hover:bg-gray-700"
|
||
>
|
||
📋 JSON kopieren
|
||
</button>
|
||
</details>
|
||
</div>
|
||
|
||
<div class="mt-2 text-blue-700 text-xs">
|
||
<strong>Hinweis:</strong> Der QR-Code wird vom Browser generiert.
|
||
Prüfe in der Browser-Konsole (F12) für vollständige Debug-Ausgaben.
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Submit Button -->
|
||
<button
|
||
type="submit"
|
||
:disabled="isLoading"
|
||
class="w-full px-6 py-3 bg-primary-600 hover:bg-primary-700 disabled:bg-gray-400 text-white font-semibold rounded-lg transition-colors flex items-center justify-center"
|
||
>
|
||
<Loader2
|
||
v-if="isLoading"
|
||
:size="20"
|
||
class="mr-2 animate-spin"
|
||
/>
|
||
<span>{{ isLoading ? 'Wird gesendet...' : (usePasskey ? 'Mit Passkey registrieren' : 'Registrierung beantragen') }}</span>
|
||
</button>
|
||
|
||
<!-- Back to Login -->
|
||
<div class="text-center">
|
||
<NuxtLink
|
||
to="/login"
|
||
class="text-sm text-primary-600 hover:text-primary-700 font-medium"
|
||
>
|
||
Bereits registriert? Zum Login
|
||
</NuxtLink>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Info Box -->
|
||
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||
<p class="text-sm text-yellow-800">
|
||
<Info
|
||
:size="16"
|
||
class="inline mr-1"
|
||
/>
|
||
<strong>Hinweis:</strong> Ihre Registrierung muss vom Vorstand freigegeben werden.
|
||
Sie erhalten eine E-Mail, sobald Ihr Zugang aktiviert wurde.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { onMounted, ref } from 'vue'
|
||
import { AlertCircle, Check, Loader2, Info } from 'lucide-vue-next'
|
||
|
||
// Debug: Test-Ausgabe beim Laden der Komponente
|
||
console.log('[DEBUG] ===== registrieren.vue component loaded =====')
|
||
console.log('[DEBUG] Component setup started')
|
||
|
||
const formData = ref({
|
||
name: '',
|
||
email: '',
|
||
phone: '',
|
||
password: '',
|
||
confirmPassword: ''
|
||
})
|
||
|
||
const isLoading = ref(false)
|
||
const errorMessage = ref('')
|
||
const successMessage = ref('')
|
||
const usePasskey = ref(false)
|
||
const isPasskeySupported = ref(false)
|
||
const passkeySupportReason = ref('')
|
||
const setPasswordForPasskey = ref(true)
|
||
|
||
console.log('[DEBUG] Component refs initialized')
|
||
|
||
// Debug: Log beim Form-Submit
|
||
const handleFormSubmit = (event) => {
|
||
console.log('[DEBUG] ===== FORM SUBMIT EVENT =====')
|
||
console.log('[DEBUG] Form submitted', {
|
||
usePasskey: usePasskey.value,
|
||
name: formData.value.name,
|
||
email: formData.value.email
|
||
})
|
||
|
||
if (usePasskey.value) {
|
||
console.log('[DEBUG] Calling handleRegisterWithPasskey...')
|
||
handleRegisterWithPasskey()
|
||
} else {
|
||
console.log('[DEBUG] Calling handleRegister...')
|
||
handleRegister()
|
||
}
|
||
}
|
||
const showDebugInfo = ref(false)
|
||
const debugChallenge = ref('')
|
||
const debugRpId = ref('')
|
||
const debugRegistrationId = ref('')
|
||
const debugOptions = ref(null)
|
||
const debugSmartphoneUrl = ref('')
|
||
|
||
onMounted(() => {
|
||
try {
|
||
const hasPKC = typeof window !== 'undefined' && typeof window.PublicKeyCredential !== 'undefined'
|
||
const secure = typeof window !== 'undefined' && !!window.isSecureContext
|
||
isPasskeySupported.value = !!(hasPKC && secure)
|
||
if (!secure) passkeySupportReason.value = 'Kein Secure Context (HTTPS oder localhost erforderlich).'
|
||
else if (!hasPKC) passkeySupportReason.value = 'Browser unterstützt WebAuthn/Passkeys nicht.'
|
||
else passkeySupportReason.value = ''
|
||
} catch {
|
||
isPasskeySupported.value = false
|
||
passkeySupportReason.value = 'Passkey-Check fehlgeschlagen.'
|
||
}
|
||
})
|
||
|
||
const handleRegister = async () => {
|
||
errorMessage.value = ''
|
||
successMessage.value = ''
|
||
|
||
// Validate
|
||
if (formData.value.password.length < 8) {
|
||
errorMessage.value = 'Das Passwort muss mindestens 8 Zeichen lang sein.'
|
||
return
|
||
}
|
||
|
||
if (formData.value.password !== formData.value.confirmPassword) {
|
||
errorMessage.value = 'Die Passwörter stimmen nicht überein.'
|
||
return
|
||
}
|
||
|
||
isLoading.value = true
|
||
|
||
try {
|
||
const response = await $fetch('/api/auth/register', {
|
||
method: 'POST',
|
||
body: {
|
||
name: formData.value.name,
|
||
email: formData.value.email,
|
||
phone: formData.value.phone,
|
||
password: formData.value.password
|
||
}
|
||
})
|
||
|
||
if (response.success) {
|
||
successMessage.value = 'Registrierung erfolgreich! Sie erhalten eine E-Mail, sobald Ihr Zugang freigeschaltet wurde.'
|
||
|
||
// Reset form
|
||
formData.value = {
|
||
name: '',
|
||
email: '',
|
||
phone: '',
|
||
password: '',
|
||
confirmPassword: ''
|
||
}
|
||
|
||
// Redirect after 3 seconds
|
||
setTimeout(() => {
|
||
navigateTo('/login')
|
||
}, 3000)
|
||
}
|
||
} catch (error) {
|
||
errorMessage.value = error.data?.message || 'Registrierung fehlgeschlagen. Bitte versuchen Sie es später erneut.'
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
|
||
const handleRegisterWithPasskey = async () => {
|
||
console.log('[DEBUG] ===== handleRegisterWithPasskey CALLED =====')
|
||
console.log('[DEBUG] Function entry point reached')
|
||
|
||
errorMessage.value = ''
|
||
successMessage.value = ''
|
||
|
||
console.log('[DEBUG] Checking passkey support:', {
|
||
isPasskeySupported: isPasskeySupported.value,
|
||
passkeySupportReason: passkeySupportReason.value
|
||
})
|
||
|
||
if (!isPasskeySupported.value) {
|
||
console.warn('[DEBUG] Passkey not supported, returning early')
|
||
errorMessage.value = passkeySupportReason.value || 'Passkeys sind in diesem Browser/unter dieser URL nicht verfügbar (HTTPS erforderlich).'
|
||
return
|
||
}
|
||
|
||
if (!formData.value.name || !formData.value.email) {
|
||
errorMessage.value = 'Bitte Name und E-Mail ausfüllen.'
|
||
return
|
||
}
|
||
|
||
// Passwort-Fallback optional validieren
|
||
if (setPasswordForPasskey.value) {
|
||
if (formData.value.password.length < 8) {
|
||
errorMessage.value = 'Das Passwort muss mindestens 8 Zeichen lang sein.'
|
||
return
|
||
}
|
||
if (formData.value.password !== formData.value.confirmPassword) {
|
||
errorMessage.value = 'Die Passwörter stimmen nicht überein.'
|
||
return
|
||
}
|
||
} else {
|
||
// Nicht mitschicken
|
||
formData.value.password = ''
|
||
formData.value.confirmPassword = ''
|
||
}
|
||
|
||
isLoading.value = true
|
||
|
||
console.log('[DEBUG] Passkey-Registrierung gestartet', {
|
||
name: formData.value.name,
|
||
email: formData.value.email,
|
||
hasPassword: !!formData.value.password,
|
||
isPasskeySupported: isPasskeySupported.value,
|
||
userAgent: navigator.userAgent,
|
||
origin: window.location.origin,
|
||
isSecureContext: window.isSecureContext
|
||
})
|
||
|
||
try {
|
||
console.log('[DEBUG] Requesting registration options from server...')
|
||
const requestStart = Date.now()
|
||
|
||
const pre = await $fetch('/api/auth/register-passkey-options', {
|
||
method: 'POST',
|
||
body: {
|
||
name: formData.value.name,
|
||
email: formData.value.email,
|
||
phone: formData.value.phone
|
||
}
|
||
})
|
||
|
||
const requestDuration = Date.now() - requestStart
|
||
console.log(`[DEBUG] Options received (${requestDuration}ms)`, {
|
||
success: pre.success,
|
||
hasOptions: !!pre.options,
|
||
hasRegistrationId: !!pre.registrationId,
|
||
registrationId: pre.registrationId
|
||
})
|
||
|
||
if (!pre.success || !pre.options) {
|
||
console.error('[DEBUG] Invalid server response:', pre)
|
||
throw new Error('Ungültige Antwort vom Server')
|
||
}
|
||
|
||
// Debug: Prüfe Options-Struktur
|
||
console.log('[DEBUG] Options structure:', {
|
||
hasChallenge: !!pre.options?.challenge,
|
||
challengeType: typeof pre.options?.challenge,
|
||
challengeLength: pre.options?.challenge?.length,
|
||
hasRp: !!pre.options?.rp,
|
||
rpId: pre.options?.rp?.id,
|
||
rpName: pre.options?.rp?.name,
|
||
hasUser: !!pre.options?.user,
|
||
userId: pre.options?.user?.id ? 'present' : 'missing',
|
||
userName: pre.options?.user?.name,
|
||
timeout: pre.options?.timeout,
|
||
pubKeyCredParams: pre.options?.pubKeyCredParams?.length,
|
||
authenticatorSelection: pre.options?.authenticatorSelection,
|
||
excludeCredentials: pre.options?.excludeCredentials?.length || 0
|
||
})
|
||
|
||
if (!pre.options || !pre.options.challenge) {
|
||
console.error('[DEBUG] Options missing challenge:', JSON.stringify(pre.options, null, 2))
|
||
throw new Error('Ungültige WebAuthn-Options vom Server')
|
||
}
|
||
|
||
// Debug: Prüfe die vollständige Options-Struktur
|
||
// WICHTIG: @simplewebauthn/browser erwartet:
|
||
// - challenge: Base64URL-String
|
||
// - user.id: Base64URL-String (wird automatisch zu Uint8Array konvertiert)
|
||
// - excludeCredentials[].id: Base64URL-String (wird automatisch zu Uint8Array konvertiert)
|
||
console.log('[DEBUG] Full options structure check:', {
|
||
hasChallenge: !!pre.options.challenge,
|
||
challengeValue: pre.options.challenge?.substring(0, 20) + '...',
|
||
challengeType: typeof pre.options.challenge,
|
||
challengeLength: pre.options.challenge?.length,
|
||
hasRp: !!pre.options.rp,
|
||
rpId: pre.options.rp?.id,
|
||
rpName: pre.options.rp?.name,
|
||
hasUser: !!pre.options.user,
|
||
userId: pre.options.user?.id ? 'present' : 'missing',
|
||
userIdType: typeof pre.options.user?.id,
|
||
userIdValue: typeof pre.options.user?.id === 'string' ? pre.options.user.id.substring(0, 20) + '...' : 'not a string',
|
||
userName: pre.options.user?.name,
|
||
userDisplayName: pre.options.user?.displayName,
|
||
hasPubKeyCredParams: !!pre.options.pubKeyCredParams,
|
||
pubKeyCredParamsCount: pre.options.pubKeyCredParams?.length,
|
||
hasAuthenticatorSelection: !!pre.options.authenticatorSelection,
|
||
authenticatorSelection: pre.options.authenticatorSelection,
|
||
timeout: pre.options.timeout,
|
||
timeoutType: typeof pre.options.timeout,
|
||
excludeCredentialsCount: pre.options.excludeCredentials?.length || 0,
|
||
hasExtensions: !!pre.options.extensions,
|
||
hasHints: !!pre.options.hints,
|
||
allKeys: Object.keys(pre.options),
|
||
userKeys: pre.options.user ? Object.keys(pre.options.user) : []
|
||
})
|
||
|
||
// Prüfe, ob user.id ein String ist (Base64URL)
|
||
// @simplewebauthn/browser erwartet user.id als Base64URL-String
|
||
if (pre.options.user?.id && typeof pre.options.user.id !== 'string') {
|
||
console.error('[DEBUG] ERROR: user.id is not a string!', typeof pre.options.user.id, pre.options.user.id)
|
||
console.error('[DEBUG] @simplewebauthn/browser erwartet user.id als Base64URL-String')
|
||
throw new Error('Invalid user.id format - must be Base64URL string')
|
||
}
|
||
|
||
console.log('[DEBUG] Calling startRegistration...')
|
||
console.log('[DEBUG] Full options object (will be encoded in QR code for Cross-Device):', JSON.stringify(pre.options, null, 2))
|
||
console.log('[DEBUG] Options summary for startRegistration:', {
|
||
challenge: pre.options?.challenge ? 'present' : 'missing',
|
||
challengeValue: pre.options?.challenge ? pre.options.challenge.substring(0, 20) + '...' : 'missing',
|
||
rp: pre.options?.rp,
|
||
rpId: pre.options?.rp?.id,
|
||
rpName: pre.options?.rp?.name,
|
||
user: pre.options?.user,
|
||
userName: pre.options?.user?.name,
|
||
userDisplayName: pre.options?.user?.displayName,
|
||
timeout: pre.options?.timeout,
|
||
timeoutSeconds: pre.options?.timeout ? Math.round(pre.options.timeout / 1000) : 'default',
|
||
pubKeyCredParams: pre.options?.pubKeyCredParams?.length,
|
||
authenticatorSelection: pre.options?.authenticatorSelection,
|
||
excludeCredentials: pre.options?.excludeCredentials?.length || 0
|
||
})
|
||
|
||
// Vollständige Options für QR-Code-Debug (wird im QR-Code kodiert)
|
||
console.log('[DEBUG] Full options object (encoded in QR code for Cross-Device):', JSON.stringify(pre.options, null, 2))
|
||
|
||
// Prüfe, ob Cross-Device-Authentifizierung verwendet wird
|
||
console.log('[DEBUG] Cross-Device Info (QR-Code sollte zu dieser URL führen):', {
|
||
isSecureContext: window.isSecureContext,
|
||
origin: window.location.origin,
|
||
protocol: window.location.protocol,
|
||
hostname: window.location.hostname,
|
||
port: window.location.port || 'default (443 for HTTPS)',
|
||
fullUrl: window.location.href,
|
||
// Der QR-Code sollte zur aktuellen Origin führen
|
||
qrCodeShouldPointTo: window.location.origin,
|
||
// Prüfe, ob die Options die richtige Origin enthalten
|
||
optionsRpId: pre.options?.rp?.id,
|
||
optionsMatchesOrigin: pre.options?.rp?.id === window.location.hostname
|
||
})
|
||
|
||
// QR-Code-Debug: Die Challenge ist Teil der WebAuthn-Request
|
||
// Der Browser generiert automatisch einen QR-Code für Cross-Device
|
||
debugChallenge.value = pre.options?.challenge || ''
|
||
debugRpId.value = pre.options?.rp?.id || ''
|
||
debugRegistrationId.value = pre.registrationId || ''
|
||
debugOptions.value = pre.options || null
|
||
|
||
// Generiere Smartphone-URL für Cross-Device
|
||
// Das Smartphone kann diese URL öffnen, um die Passkey-Registrierung durchzuführen
|
||
if (pre.registrationId) {
|
||
debugSmartphoneUrl.value = `${window.location.origin}/passkey-register-cross-device?registrationId=${pre.registrationId}`
|
||
}
|
||
|
||
showDebugInfo.value = true
|
||
|
||
console.log('[DEBUG] QR-Code Info (for Cross-Device):', {
|
||
challenge: pre.options?.challenge,
|
||
challengeLength: pre.options?.challenge?.length,
|
||
rpId: pre.options?.rp?.id,
|
||
expectedOrigin: window.location.origin,
|
||
currentURL: window.location.href,
|
||
isHTTPS: window.location.protocol === 'https:',
|
||
note: 'Der QR-Code wird vom Browser generiert und enthält die Challenge + Server-Info'
|
||
})
|
||
|
||
// Prüfe, ob die Origin korrekt ist
|
||
if (window.location.origin.includes(':3100')) {
|
||
console.error('[DEBUG] WARNING: Current origin contains port 3100!', window.location.origin)
|
||
console.error('[DEBUG] This might cause Cross-Device authentication to fail.')
|
||
}
|
||
|
||
const webauthnStart = Date.now()
|
||
|
||
// Importiere @simplewebauthn/browser
|
||
const mod = await import('@simplewebauthn/browser')
|
||
|
||
// Prüfe, ob startRegistration verfügbar ist
|
||
if (!mod.startRegistration) {
|
||
console.error('[DEBUG] ERROR: mod.startRegistration is not available!')
|
||
console.error('[DEBUG] Available exports:', Object.keys(mod))
|
||
throw new Error('startRegistration ist nicht verfügbar. Bitte prüfen Sie, ob @simplewebauthn/browser korrekt installiert ist.')
|
||
}
|
||
|
||
console.log('[DEBUG] @simplewebauthn/browser imported successfully')
|
||
console.log('[DEBUG] Available exports:', Object.keys(mod))
|
||
console.log('[DEBUG] startRegistration type:', typeof mod.startRegistration)
|
||
|
||
let credential
|
||
try {
|
||
// Timeout-Warnung nach 2 Minuten
|
||
const timeoutWarning = setTimeout(() => {
|
||
console.warn('[DEBUG] startRegistration still waiting after 2 minutes. This might be a Cross-Device timeout.')
|
||
console.warn('[DEBUG] Make sure your smartphone can reach the server and CORS is configured correctly.')
|
||
console.warn('[DEBUG] Current origin:', typeof window !== 'undefined' ? window.location.origin : 'N/A')
|
||
console.warn('[DEBUG] Challenge:', pre.options?.challenge)
|
||
}, 120000)
|
||
|
||
console.log('[DEBUG] startRegistration called - QR-Code should appear now (if Cross-Device)')
|
||
console.log('[DEBUG] Passing options directly to startRegistration (same as in profil.vue)')
|
||
console.log('[DEBUG] Options for QR-Code:', {
|
||
rpId: pre.options.rp?.id,
|
||
rpName: pre.options.rp?.name,
|
||
challenge: pre.options.challenge?.substring(0, 20) + '...',
|
||
timeout: pre.options.timeout,
|
||
timeoutSeconds: Math.round(pre.options.timeout / 1000),
|
||
note: 'Der Browser generiert automatisch einen QR-Code. Das Smartphone muss diese Origin erreichen können: ' + window.location.origin
|
||
})
|
||
|
||
// WICHTIG: Für Cross-Device muss das Smartphone die gleiche Origin erreichen können
|
||
// Der QR-Code enthält die Challenge und die Server-Info
|
||
// Das Smartphone öffnet dann die Website und sendet die Credential-Response zurück
|
||
console.log('[DEBUG] Cross-Device Flow:')
|
||
console.log('[DEBUG] 1. Browser generiert QR-Code mit Challenge:', pre.options.challenge?.substring(0, 20) + '...')
|
||
console.log('[DEBUG] 2. Smartphone scannt QR-Code')
|
||
console.log('[DEBUG] 3. Smartphone muss diese URL erreichen können:', window.location.origin)
|
||
console.log('[DEBUG] 4. Smartphone sendet Credential-Response an:', window.location.origin + '/api/auth/register-passkey')
|
||
console.log('[DEBUG] 5. Server verifiziert die Response')
|
||
|
||
// Prüfe Options-Struktur vor dem Aufruf
|
||
console.log('[DEBUG] Options validation before startRegistration:', {
|
||
hasChallenge: !!pre.options.challenge,
|
||
challengeType: typeof pre.options.challenge,
|
||
challengeValue: pre.options.challenge?.substring(0, 20) + '...',
|
||
hasRp: !!pre.options.rp,
|
||
hasRpId: !!pre.options.rp?.id,
|
||
hasUser: !!pre.options.user,
|
||
hasUserID: !!pre.options.user?.id,
|
||
userIdType: typeof pre.options.user?.id,
|
||
userIdValue: typeof pre.options.user?.id === 'string' ? pre.options.user.id.substring(0, 20) + '...' : 'not a string',
|
||
timeout: pre.options.timeout,
|
||
timeoutType: typeof pre.options.timeout,
|
||
allKeys: Object.keys(pre.options),
|
||
userKeys: pre.options.user ? Object.keys(pre.options.user) : []
|
||
})
|
||
|
||
// Stelle sicher, dass challenge ein String ist (Base64URL)
|
||
if (typeof pre.options.challenge !== 'string') {
|
||
console.error('[DEBUG] ERROR: Challenge is not a string!', typeof pre.options.challenge, pre.options.challenge)
|
||
throw new Error('Invalid challenge format')
|
||
}
|
||
|
||
// Prüfe user.id - sollte ein String (Base64URL) sein
|
||
if (pre.options.user?.id && typeof pre.options.user.id !== 'string') {
|
||
console.warn('[DEBUG] WARNING: user.id is not a string!', typeof pre.options.user.id, pre.options.user.id)
|
||
console.warn('[DEBUG] @simplewebauthn/browser erwartet user.id als Base64URL-String')
|
||
}
|
||
|
||
// @simplewebauthn/browser v13+ erwartet { optionsJSON: options }
|
||
// Die Bibliothek unterstützt auch die alte API (direkt options), gibt aber eine Warnung
|
||
// Wir verwenden die neue API-Struktur, um die Warnung zu vermeiden
|
||
console.log('[DEBUG] Calling mod.startRegistration with new API structure...')
|
||
console.log('[DEBUG] Options structure:', {
|
||
challenge: pre.options.challenge?.substring(0, 20) + '...',
|
||
rpId: pre.options.rp?.id,
|
||
userId: typeof pre.options.user?.id === 'string' ? pre.options.user.id.substring(0, 20) + '...' : pre.options.user?.id,
|
||
timeout: pre.options.timeout
|
||
})
|
||
|
||
// Neue API-Struktur: { optionsJSON: options }
|
||
// WICHTIG: Bei Cross-Device wird der QR-Code automatisch generiert
|
||
// Das Smartphone scannt den QR-Code und öffnet die Website
|
||
// Dann ruft das Smartphone startRegistration auf, um die Credential zu erstellen
|
||
// Die Credential-Response wird dann an den Desktop-Browser zurückgesendet
|
||
console.log('[DEBUG] About to call startRegistration - this will trigger QR code generation for Cross-Device')
|
||
console.log('[DEBUG] If using Cross-Device:')
|
||
console.log('[DEBUG] 1. QR-Code wird angezeigt')
|
||
console.log('[DEBUG] 2. Smartphone scannt QR-Code')
|
||
console.log('[DEBUG] 3. Smartphone öffnet:', window.location.origin)
|
||
console.log('[DEBUG] 4. Smartphone muss startRegistration aufrufen können')
|
||
console.log('[DEBUG] 5. Smartphone sendet Credential-Response an:', window.location.origin + '/api/auth/register-passkey')
|
||
|
||
// WICHTIG: Prüfe, ob Cross-Device überhaupt unterstützt wird
|
||
console.log('[DEBUG] Checking Cross-Device support...')
|
||
const platformAuthAvailable = await mod.platformAuthenticatorIsAvailable()
|
||
const browserSupportsWebAuthn = mod.browserSupportsWebAuthn()
|
||
console.log('[DEBUG] Platform Authenticator Available:', platformAuthAvailable)
|
||
console.log('[DEBUG] Browser Supports WebAuthn:', browserSupportsWebAuthn)
|
||
|
||
// Prüfe, ob ein lokaler Authenticator verfügbar ist
|
||
// Wenn ja, wird möglicherweise kein Cross-Device verwendet
|
||
if (platformAuthAvailable) {
|
||
console.warn('[DEBUG] ⚠️ WARNUNG: Platform Authenticator ist verfügbar!')
|
||
console.warn('[DEBUG] Der Browser könnte einen lokalen Authenticator verwenden (Windows Hello, TouchID)')
|
||
console.warn('[DEBUG] statt Cross-Device. Prüfe, ob ein QR-Code erscheint oder ein lokaler Dialog.')
|
||
}
|
||
|
||
// Versuche startRegistration aufzurufen
|
||
// Bei Cross-Device wird automatisch ein QR-Code generiert
|
||
// Der Browser sollte eine Verbindung zu Tunnel-Servern aufbauen (cable.ua5v.com, cable.auth.com)
|
||
console.log('[DEBUG] Calling startRegistration...')
|
||
console.log('[DEBUG] ⚠️ WICHTIG: Prüfe Browser Network-Tab während startRegistration läuft!')
|
||
console.log('[DEBUG] Erwartete Tunnel-Server-Requests:')
|
||
console.log('[DEBUG] - cable.ua5v.com (Google Chrome/Edge)')
|
||
console.log('[DEBUG] - cable.auth.com (Apple Safari)')
|
||
console.log('[DEBUG] - Andere Tunnel-Server (abhängig vom Browser)')
|
||
console.log('[DEBUG] Wenn KEINE Tunnel-Requests sichtbar sind:')
|
||
console.log('[DEBUG] → Cross-Device wird NICHT verwendet')
|
||
console.log('[DEBUG] → Möglicherweise wird ein lokaler Authenticator verwendet')
|
||
console.log('[DEBUG] → Oder der Browser unterstützt Cross-Device nicht')
|
||
|
||
credential = await mod.startRegistration({ optionsJSON: pre.options })
|
||
|
||
clearTimeout(timeoutWarning)
|
||
|
||
const webauthnDuration = Date.now() - webauthnStart
|
||
console.log(`[DEBUG] startRegistration completed (${webauthnDuration}ms)`, {
|
||
hasCredential: !!credential,
|
||
credentialId: credential?.id,
|
||
responseType: credential?.response?.constructor?.name,
|
||
transports: credential?.transports,
|
||
durationSeconds: Math.round(webauthnDuration / 1000),
|
||
note: 'Wenn Cross-Device verwendet wurde, sollte die Credential-Response vom Smartphone kommen'
|
||
})
|
||
|
||
// Prüfe, ob Cross-Device verwendet wurde
|
||
if (credential?.transports && credential.transports.length > 0) {
|
||
console.log('[DEBUG] Credential transports:', credential.transports)
|
||
const isCrossDevice = credential.transports.includes('hybrid') || credential.transports.includes('cable')
|
||
console.log('[DEBUG] Cross-Device verwendet:', isCrossDevice ? '✅ JA' : '❌ NEIN')
|
||
if (!isCrossDevice) {
|
||
console.warn('[DEBUG] ⚠️ WARNUNG: Cross-Device wurde NICHT verwendet!')
|
||
console.warn('[DEBUG] Mögliche Ursachen:')
|
||
console.warn('[DEBUG] 1. Lokaler Authenticator wurde verwendet (z.B. Windows Hello, TouchID)')
|
||
console.warn('[DEBUG] 2. Tunnel-Server-Verbindung fehlgeschlagen')
|
||
console.warn('[DEBUG] 3. Browser unterstützt Cross-Device nicht')
|
||
console.warn('[DEBUG] 4. Keine Tunnel-Requests im Network-Tab sichtbar')
|
||
}
|
||
} else {
|
||
console.warn('[DEBUG] ⚠️ WARNUNG: Keine transports-Information verfügbar!')
|
||
console.warn('[DEBUG] Kann nicht bestimmen, ob Cross-Device verwendet wurde.')
|
||
console.warn('[DEBUG] Prüfe Browser Network-Tab: Wurden Requests zu Tunnel-Servern gemacht?')
|
||
console.warn('[DEBUG] Erwartete Tunnel-Server: cable.ua5v.com, cable.auth.com')
|
||
}
|
||
} catch (webauthnError) {
|
||
const webauthnDuration = Date.now() - webauthnStart
|
||
console.error(`[DEBUG] WebAuthn startRegistration failed (${webauthnDuration}ms / ${Math.round(webauthnDuration / 1000)}s):`, {
|
||
error: webauthnError,
|
||
message: webauthnError?.message,
|
||
name: webauthnError?.name,
|
||
code: webauthnError?.code,
|
||
stack: webauthnError?.stack
|
||
})
|
||
|
||
// Spezifische Fehlermeldungen für häufige Probleme
|
||
if (webauthnError?.message?.includes('timeout') || webauthnDuration > 290000) {
|
||
throw new Error('Passkey-Registrierung: Timeout. Bitte stellen Sie sicher, dass Ihr Smartphone eine Internetverbindung hat und die Website über HTTPS erreichbar ist.')
|
||
} else if (webauthnError?.message?.includes('NotAllowedError') || webauthnError?.name === 'NotAllowedError') {
|
||
throw new Error('Passkey-Registrierung abgebrochen oder nicht erlaubt.')
|
||
} else {
|
||
throw new Error('Passkey-Registrierung fehlgeschlagen: ' + (webauthnError?.message || 'Unbekannter Fehler'))
|
||
}
|
||
}
|
||
|
||
console.log('[DEBUG] Sending credential to server...')
|
||
const verifyStart = Date.now()
|
||
|
||
const response = await $fetch('/api/auth/register-passkey', {
|
||
method: 'POST',
|
||
body: {
|
||
registrationId: pre.registrationId,
|
||
credential,
|
||
password: setPasswordForPasskey.value ? formData.value.password : undefined
|
||
}
|
||
})
|
||
|
||
const verifyDuration = Date.now() - verifyStart
|
||
console.log(`[DEBUG] Server verification completed (${verifyDuration}ms)`, {
|
||
success: response.success,
|
||
message: response.message
|
||
})
|
||
|
||
if (response.success) {
|
||
console.log('[DEBUG] Registration successful!')
|
||
successMessage.value = 'Registrierung erfolgreich! Sie erhalten eine E-Mail, sobald Ihr Zugang freigeschaltet wurde.'
|
||
formData.value = { name: '', email: '', phone: '', password: '', confirmPassword: '' }
|
||
setTimeout(() => navigateTo('/login'), 3000)
|
||
} else {
|
||
console.warn('[DEBUG] Registration response indicates failure:', response)
|
||
}
|
||
} catch (error) {
|
||
console.error('[DEBUG] Registration error:', {
|
||
error,
|
||
message: error?.message,
|
||
data: error?.data,
|
||
statusCode: error?.statusCode,
|
||
statusMessage: error?.statusMessage
|
||
})
|
||
errorMessage.value = error.data?.message || error?.message || 'Registrierung mit Passkey fehlgeschlagen.'
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
|
||
// Funktion zum Kopieren in die Zwischenablage
|
||
async function copyToClipboard(text) {
|
||
try {
|
||
await navigator.clipboard.writeText(text)
|
||
alert('In Zwischenablage kopiert!')
|
||
} catch (err) {
|
||
console.error('Fehler beim Kopieren:', err)
|
||
// Fallback für ältere Browser
|
||
const textArea = document.createElement('textarea')
|
||
textArea.value = text
|
||
textArea.style.position = 'fixed'
|
||
textArea.style.opacity = '0'
|
||
document.body.appendChild(textArea)
|
||
textArea.select()
|
||
try {
|
||
document.execCommand('copy')
|
||
alert('In Zwischenablage kopiert!')
|
||
} catch (err) {
|
||
console.error('Fallback-Kopieren fehlgeschlagen:', err)
|
||
alert('Kopieren fehlgeschlagen. Bitte manuell kopieren.')
|
||
}
|
||
document.body.removeChild(textArea)
|
||
}
|
||
}
|
||
}
|
||
|
||
useHead({
|
||
title: 'Registrierung - Harheimer TC',
|
||
})
|
||
</script>
|
||
|