Files
harheimertc/pages/passkey-wiederherstellen.vue
Torsten Schulz (local) badf91afef
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 49s
Update Passkey Registration to comply with @simplewebauthn/browser v13+ API
Refactor the Passkey registration logic in multiple components to utilize the new API structure requiring { optionsJSON: options }. Enhance debug logging to validate options, including checks for user ID format and challenge type. This update aims to improve compliance with the latest library requirements and provide better insights during the registration process.
2026-01-08 17:10:13 +01:00

192 lines
6.3 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">
Passkey wiederherstellen
</h2>
<p class="mt-2 text-sm text-gray-600">
Fügen Sie einen neuen Passkey hinzu, wenn Sie Ihr Gerät gewechselt haben.
</p>
</div>
<div class="bg-white rounded-xl shadow-lg p-8">
<!-- Token Flow -->
<div v-if="token" class="space-y-4">
<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>
<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>
<button
type="button"
class="w-full px-6 py-3 bg-gray-900 hover:bg-gray-800 disabled:bg-gray-400 text-white font-semibold rounded-lg transition-colors flex items-center justify-center"
:disabled="isLoading || !isPasskeySupported"
@click="addPasskeyViaToken"
>
<Loader2 v-if="isLoading" :size="20" class="mr-2 animate-spin" />
<span>
{{ isLoading ? 'Wird vorbereitet...' : (isPasskeySupported ? 'Neuen Passkey hinzufügen' : 'Passkeys nicht verfügbar') }}
</span>
</button>
<div class="text-center">
<NuxtLink to="/login" class="text-sm text-primary-600 hover:text-primary-700 font-medium">
Zurück zum Login
</NuxtLink>
</div>
</div>
<!-- Request Link Flow -->
<form v-else class="space-y-6" @submit.prevent="requestLink">
<div>
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">
E-Mail-Adresse
</label>
<input
id="email"
v-model="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>
<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>
<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>
<button
type="submit"
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"
:disabled="isLoading"
>
<Loader2 v-if="isLoading" :size="20" class="mr-2 animate-spin" />
<span>{{ isLoading ? 'Wird gesendet...' : 'Recovery-Link per E-Mail senden' }}</span>
</button>
<div class="text-center">
<NuxtLink to="/login" class="text-sm text-primary-600 hover:text-primary-700 font-medium">
Zurück zum Login
</NuxtLink>
</div>
</form>
</div>
<div class="bg-primary-50 border border-primary-100 rounded-lg p-4">
<p class="text-sm text-primary-800">
<Info :size="16" class="inline mr-1" />
Wir schicken immer die gleiche Rückmeldung, egal ob ein Konto existiert.
</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import { AlertCircle, Check, Loader2, Info } from 'lucide-vue-next'
const route = useRoute()
const token = ref(String(route.query.token || ''))
const email = ref('')
const isLoading = ref(false)
const errorMessage = ref('')
const successMessage = ref('')
const isPasskeySupported = ref(false)
if (process.client) {
isPasskeySupported.value = !!window.PublicKeyCredential
}
const requestLink = async () => {
errorMessage.value = ''
successMessage.value = ''
isLoading.value = true
try {
const res = await $fetch('/api/auth/passkeys/recovery/request', {
method: 'POST',
body: { email: email.value }
})
successMessage.value = res.message || 'Falls ein Konto existiert, wurde eine E-Mail gesendet.'
} catch (e) {
errorMessage.value = e?.data?.message || 'Fehler beim Senden der E-Mail.'
} finally {
isLoading.value = false
}
}
const addPasskeyViaToken = async () => {
errorMessage.value = ''
successMessage.value = ''
if (!isPasskeySupported.value) {
errorMessage.value = 'Passkeys sind in diesem Browser/unter dieser URL nicht verfügbar (HTTPS erforderlich).'
return
}
isLoading.value = true
try {
const opts = await $fetch('/api/auth/passkeys/recovery/options', {
method: 'GET',
query: { token: token.value }
})
const mod = await import('@simplewebauthn/browser')
// @simplewebauthn/browser v13+ erwartet { optionsJSON: options }
const credential = await mod.startRegistration({ optionsJSON: opts.options })
const res = await $fetch('/api/auth/passkeys/recovery/complete', {
method: 'POST',
body: {
recoveryId: opts.recoveryId,
credential
}
})
successMessage.value = res.message || 'Passkey hinzugefügt.'
} catch (e) {
errorMessage.value = e?.data?.message || e?.message || 'Passkey konnte nicht hinzugefügt werden.'
} finally {
isLoading.value = false
}
}
useHead({ title: 'Passkey wiederherstellen - Harheimer TC' })
</script>