Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 44s
Enhance the registration process by adding error handling for the WebAuthn startRegistration method and ensuring the presence of required options. Include debug logging for received options and serialize the options correctly before returning them in the API response, improving robustness and clarity in the registration flow.
391 lines
13 KiB
Vue
391 lines
13 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">
|
|
<form
|
|
class="space-y-6"
|
|
@submit.prevent="usePasskey ? handleRegisterWithPasskey() : handleRegister()"
|
|
>
|
|
<!-- 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>
|
|
|
|
<!-- 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'
|
|
|
|
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)
|
|
|
|
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 () => {
|
|
errorMessage.value = ''
|
|
successMessage.value = ''
|
|
|
|
if (!isPasskeySupported.value) {
|
|
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
|
|
try {
|
|
const pre = await $fetch('/api/auth/register-passkey-options', {
|
|
method: 'POST',
|
|
body: {
|
|
name: formData.value.name,
|
|
email: formData.value.email,
|
|
phone: formData.value.phone
|
|
}
|
|
})
|
|
|
|
if (!pre.success || !pre.options) {
|
|
throw new Error('Ungültige Antwort vom Server')
|
|
}
|
|
|
|
// Debug: Prüfe Options-Struktur
|
|
console.log('Received options:', {
|
|
hasChallenge: !!pre.options?.challenge,
|
|
hasRp: !!pre.options?.rp,
|
|
hasUser: !!pre.options?.user,
|
|
timeout: pre.options?.timeout
|
|
})
|
|
|
|
if (!pre.options || !pre.options.challenge) {
|
|
console.error('Options fehlen challenge:', pre.options)
|
|
throw new Error('Ungültige WebAuthn-Options vom Server')
|
|
}
|
|
|
|
const mod = await import('@simplewebauthn/browser')
|
|
// startRegistration erwartet die Options direkt
|
|
// @simplewebauthn/browser v13+ erwartet die Options direkt
|
|
let credential
|
|
try {
|
|
credential = await mod.startRegistration(pre.options)
|
|
} catch (webauthnError) {
|
|
console.error('WebAuthn startRegistration error:', webauthnError)
|
|
throw new Error('Passkey-Registrierung fehlgeschlagen: ' + (webauthnError?.message || 'Unbekannter Fehler'))
|
|
}
|
|
|
|
const response = await $fetch('/api/auth/register-passkey', {
|
|
method: 'POST',
|
|
body: {
|
|
registrationId: pre.registrationId,
|
|
credential,
|
|
password: setPasswordForPasskey.value ? formData.value.password : undefined
|
|
}
|
|
})
|
|
|
|
if (response.success) {
|
|
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)
|
|
}
|
|
} catch (error) {
|
|
errorMessage.value = error.data?.message || error?.message || 'Registrierung mit Passkey fehlgeschlagen.'
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
useHead({
|
|
title: 'Registrierung - Harheimer TC',
|
|
})
|
|
</script>
|
|
|