Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 43s
Update the registrieren.vue component to enhance debug logging for local authenticator usage, providing clearer messages about the expected behavior during registration. Modify the register-passkey-options API to specify the use of local authenticators, ensuring better clarity on the authenticator selection process. This update aims to improve user understanding and troubleshooting during Passkey registration without the need for Cross-Device functionality.
899 lines
39 KiB
Vue
899 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.log('[DEBUG] ✅ Platform Authenticator ist verfügbar!')
|
||
console.log('[DEBUG] Der Browser wird wahrscheinlich einen lokalen Authenticator verwenden:')
|
||
console.log('[DEBUG] - Windows Hello (biometrisch oder PIN)')
|
||
console.log('[DEBUG] - TouchID auf Mac')
|
||
console.log('[DEBUG] - USB-Sicherheitsschlüssel')
|
||
console.log('[DEBUG] - Lokale Passkey-Speicherung im Browser')
|
||
console.log('[DEBUG] Das ist NORMAL und FUNKTIONIERT ohne Smartphone!')
|
||
console.log('[DEBUG] Cross-Device ist nur eine Option, nicht zwingend erforderlich.')
|
||
} else {
|
||
console.log('[DEBUG] ⚠️ Platform Authenticator ist NICHT verfügbar.')
|
||
console.log('[DEBUG] Der Browser könnte Cross-Device verwenden (Smartphone via QR-Code).')
|
||
console.log('[DEBUG] Oder einen USB-Sicherheitsschlüssel.')
|
||
}
|
||
|
||
// Versuche startRegistration aufzurufen
|
||
// Mit authenticatorAttachment: 'platform' wird ein lokaler Authenticator verwendet:
|
||
// - Browser-eigene Passkey-Speicherung (Chrome Password Manager, Firefox Password Manager, etc.)
|
||
// - Windows Hello (biometrisch oder PIN)
|
||
// - TouchID auf Mac
|
||
// - KEIN Cross-Device (kein Smartphone nötig)
|
||
console.log('[DEBUG] Calling startRegistration...')
|
||
console.log('[DEBUG] Konfiguriert für lokale Passkey-Speicherung im Browser')
|
||
console.log('[DEBUG] Erwartetes Verhalten:')
|
||
console.log('[DEBUG] - Browser zeigt Dialog für Passkey-Erstellung')
|
||
console.log('[DEBUG] - Passkey wird lokal im Browser gespeichert')
|
||
console.log('[DEBUG] - KEIN QR-Code (Cross-Device wird nicht verwendet)')
|
||
console.log('[DEBUG] - KEIN Smartphone nötig')
|
||
|
||
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>
|
||
|