Files
harheimertc/pages/registrieren.vue
Torsten Schulz (local) 34968742f0
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 55s
Add CORS testing documentation and HTML test page for Passkey Cross-Device Authentication
Introduce a comprehensive CORS testing guide in CORS_TEST_ANLEITUNG.md, detailing steps for testing OPTIONS and POST requests, along with expected responses. Additionally, add a new HTML test page (test-cors.html) to facilitate interactive testing of CORS headers and responses for the Passkey registration API. Update the server API to ensure proper CORS headers are set for Cross-Device Authentication, enhancing the overall testing and debugging process.
2026-01-08 11:14:22 +01:00

600 lines
22 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"
>
<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">{{ debugChallenge.substring(0, 40) }}...</code></div>
<div><strong>RP-ID:</strong> <code class="bg-blue-100 px-1 rounded">{{ debugRpId }}</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 class="mt-2 text-blue-700">
<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>
</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('')
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')
}
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 || ''
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()
const mod = await import('@simplewebauthn/browser')
// startRegistration erwartet die Options direkt (wie in anderen Dateien auch)
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)')
// Direkt die Options übergeben (wie in profil.vue und passkey-wiederherstellen.vue)
credential = await mod.startRegistration(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)
})
} 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
}
}
useHead({
title: 'Registrierung - Harheimer TC',
})
</script>