Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 48s
Remove Passkey login and registration features from login.vue and registrieren.vue, including associated debug logs and UI elements. This change aims to streamline the user experience by focusing on standard login methods while Passkey support is under review. Additionally, disable Passkey management in profil.vue to ensure consistency across the application.
441 lines
13 KiB
Vue
441 lines
13 KiB
Vue
<template>
|
|
<div class="min-h-full py-16 bg-gray-50">
|
|
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<h1 class="text-4xl sm:text-5xl font-display font-bold text-gray-900 mb-6">
|
|
Mein Profil
|
|
</h1>
|
|
<div class="w-24 h-1 bg-primary-600 mb-8" />
|
|
|
|
<div class="bg-white rounded-xl shadow-lg p-8 border border-gray-100">
|
|
<!-- Loading State -->
|
|
<div
|
|
v-if="isLoading"
|
|
class="flex items-center justify-center py-12"
|
|
>
|
|
<Loader2
|
|
:size="40"
|
|
class="animate-spin text-primary-600"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Profile Form -->
|
|
<form
|
|
v-else
|
|
class="space-y-6"
|
|
@submit.prevent="handleSave"
|
|
>
|
|
<!-- Name -->
|
|
<div>
|
|
<label
|
|
for="name"
|
|
class="block text-sm font-medium text-gray-700 mb-2"
|
|
>
|
|
Name
|
|
</label>
|
|
<input
|
|
id="name"
|
|
v-model="formData.name"
|
|
type="text"
|
|
required
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|
:disabled="isSaving"
|
|
>
|
|
</div>
|
|
|
|
<!-- E-Mail -->
|
|
<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
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|
:disabled="isSaving"
|
|
>
|
|
</div>
|
|
|
|
<!-- Telefon -->
|
|
<div>
|
|
<label
|
|
for="phone"
|
|
class="block text-sm font-medium text-gray-700 mb-2"
|
|
>
|
|
Telefonnummer
|
|
</label>
|
|
<input
|
|
id="phone"
|
|
v-model="formData.phone"
|
|
type="tel"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|
:disabled="isSaving"
|
|
>
|
|
</div>
|
|
|
|
<!-- Passwort ändern -->
|
|
<div class="border-t border-gray-200 pt-6 mt-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">
|
|
Passwort ändern
|
|
</h3>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label
|
|
for="currentPassword"
|
|
class="block text-sm font-medium text-gray-700 mb-2"
|
|
>
|
|
Aktuelles Passwort
|
|
</label>
|
|
<input
|
|
id="currentPassword"
|
|
v-model="passwordData.current"
|
|
type="password"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|
:disabled="isSaving"
|
|
>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
for="newPassword"
|
|
class="block text-sm font-medium text-gray-700 mb-2"
|
|
>
|
|
Neues Passwort
|
|
</label>
|
|
<input
|
|
id="newPassword"
|
|
v-model="passwordData.new"
|
|
type="password"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|
:disabled="isSaving"
|
|
>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
for="confirmPassword"
|
|
class="block text-sm font-medium text-gray-700 mb-2"
|
|
>
|
|
Passwort bestätigen
|
|
</label>
|
|
<input
|
|
id="confirmPassword"
|
|
v-model="passwordData.confirm"
|
|
type="password"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|
:disabled="isSaving"
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Passkeys -->
|
|
<div class="border-t border-gray-200 pt-6 mt-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-2">
|
|
Passkeys
|
|
</h3>
|
|
<p class="text-sm text-gray-600 mb-4">
|
|
Passkeys erlauben eine Anmeldung ohne Passwort (z.B. per Fingerabdruck/FaceID/Windows Hello).
|
|
</p>
|
|
|
|
<div
|
|
v-if="passkeyError"
|
|
class="flex items-center p-3 rounded-md bg-red-50 text-red-700 text-sm mb-3"
|
|
>
|
|
<AlertCircle :size="20" class="mr-2" />
|
|
{{ passkeyError }}
|
|
</div>
|
|
|
|
<div class="flex flex-wrap gap-3 mb-4">
|
|
<button
|
|
type="button"
|
|
class="px-4 py-2 bg-gray-900 hover:bg-gray-800 text-white font-semibold rounded-lg transition-colors disabled:bg-gray-400"
|
|
:disabled="isSaving || passkeyLoading || !isPasskeySupported"
|
|
@click="addPasskey"
|
|
>
|
|
{{ passkeyLoading ? 'Passkey wird erstellt...' : (isPasskeySupported ? 'Passkey hinzufügen' : 'Passkeys nicht unterstützt') }}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors disabled:bg-gray-100 disabled:text-gray-400"
|
|
:disabled="isSaving || passkeyLoading"
|
|
@click="loadPasskeys"
|
|
>
|
|
Aktualisieren
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="passkeys.length === 0" class="text-sm text-gray-600">
|
|
Noch keine Passkeys hinterlegt.
|
|
</div>
|
|
|
|
<ul v-else class="space-y-2">
|
|
<li
|
|
v-for="pk in passkeys"
|
|
:key="pk.credentialId"
|
|
class="flex items-center justify-between p-3 border border-gray-200 rounded-lg"
|
|
>
|
|
<div class="min-w-0">
|
|
<div class="font-medium text-gray-900 truncate">
|
|
{{ pk.name || 'Passkey' }}
|
|
</div>
|
|
<div class="text-xs text-gray-600">
|
|
Erstellt: {{ formatDate(pk.createdAt) }}<span v-if="pk.lastUsedAt"> · Zuletzt genutzt: {{ formatDate(pk.lastUsedAt) }}</span>
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="ml-4 px-3 py-1.5 text-sm border border-red-300 text-red-700 rounded-lg hover:bg-red-50 disabled:bg-gray-100 disabled:text-gray-400"
|
|
:disabled="isSaving || passkeyLoading"
|
|
@click="removePasskey(pk.credentialId)"
|
|
>
|
|
Entfernen
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Error/Success Messages -->
|
|
<div
|
|
v-if="errorMessage"
|
|
class="flex items-center p-3 rounded-md bg-red-50 text-red-700 text-sm"
|
|
>
|
|
<AlertCircle
|
|
:size="20"
|
|
class="mr-2"
|
|
/>
|
|
{{ errorMessage }}
|
|
</div>
|
|
|
|
<div
|
|
v-if="successMessage"
|
|
class="flex items-center p-3 rounded-md bg-green-50 text-green-700 text-sm"
|
|
>
|
|
<Check
|
|
:size="20"
|
|
class="mr-2"
|
|
/>
|
|
{{ successMessage }}
|
|
</div>
|
|
|
|
<!-- Submit Button -->
|
|
<div class="flex justify-end space-x-4">
|
|
<button
|
|
type="button"
|
|
class="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
|
:disabled="isSaving"
|
|
@click="loadProfile"
|
|
>
|
|
Zurücksetzen
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
class="px-6 py-2 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors flex items-center"
|
|
:disabled="isSaving"
|
|
>
|
|
<Loader2
|
|
v-if="isSaving"
|
|
:size="20"
|
|
class="animate-spin mr-2"
|
|
/>
|
|
<span>{{ isSaving ? 'Speichert...' : 'Speichern' }}</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { AlertCircle, Check, Loader2 } from 'lucide-vue-next'
|
|
|
|
const authStore = useAuthStore()
|
|
|
|
const isLoading = ref(true)
|
|
const isSaving = ref(false)
|
|
const errorMessage = ref('')
|
|
const successMessage = ref('')
|
|
|
|
const passkeys = ref([])
|
|
const passkeyLoading = ref(false)
|
|
const passkeyError = ref('')
|
|
const isPasskeySupported = ref(false)
|
|
if (process.client) {
|
|
isPasskeySupported.value = !!window.PublicKeyCredential
|
|
}
|
|
|
|
const formData = ref({
|
|
name: '',
|
|
email: '',
|
|
phone: ''
|
|
})
|
|
|
|
const passwordData = ref({
|
|
current: '',
|
|
new: '',
|
|
confirm: ''
|
|
})
|
|
|
|
const loadProfile = async () => {
|
|
isLoading.value = true
|
|
errorMessage.value = ''
|
|
successMessage.value = ''
|
|
|
|
try {
|
|
const response = await $fetch('/api/profile')
|
|
formData.value = {
|
|
name: response.user.name,
|
|
email: response.user.email,
|
|
phone: response.user.phone || ''
|
|
}
|
|
} catch {
|
|
errorMessage.value = 'Fehler beim Laden des Profils.'
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
const loadPasskeys = async () => {
|
|
passkeyError.value = ''
|
|
try {
|
|
const res = await $fetch('/api/auth/passkeys/list')
|
|
passkeys.value = res.passkeys || []
|
|
} catch (e) {
|
|
passkeyError.value = e?.data?.message || 'Fehler beim Laden der Passkeys.'
|
|
}
|
|
}
|
|
|
|
const addPasskey = async () => {
|
|
passkeyError.value = ''
|
|
passkeyLoading.value = true
|
|
try {
|
|
const name = window.prompt('Name für den Passkey (z.B. "iPhone", "Laptop"):', 'Passkey') || 'Passkey'
|
|
const res = await $fetch('/api/auth/passkeys/registration-options', { method: 'POST' })
|
|
const mod = await import('@simplewebauthn/browser')
|
|
// @simplewebauthn/browser v13+ erwartet { optionsJSON: options }
|
|
const credential = await mod.startRegistration({ optionsJSON: res.options })
|
|
await $fetch('/api/auth/passkeys/register', {
|
|
method: 'POST',
|
|
body: { credential, name }
|
|
})
|
|
await loadPasskeys()
|
|
successMessage.value = 'Passkey hinzugefügt.'
|
|
} catch (e) {
|
|
passkeyError.value = e?.data?.message || e?.message || 'Passkey konnte nicht hinzugefügt werden.'
|
|
} finally {
|
|
passkeyLoading.value = false
|
|
}
|
|
}
|
|
|
|
const removePasskey = async (credentialId) => {
|
|
passkeyError.value = ''
|
|
passkeyLoading.value = true
|
|
try {
|
|
await $fetch('/api/auth/passkeys/remove', {
|
|
method: 'POST',
|
|
body: { credentialId }
|
|
})
|
|
await loadPasskeys()
|
|
successMessage.value = 'Passkey entfernt.'
|
|
} catch (e) {
|
|
passkeyError.value = e?.data?.message || 'Passkey konnte nicht entfernt werden.'
|
|
} finally {
|
|
passkeyLoading.value = false
|
|
}
|
|
}
|
|
|
|
const formatDate = (iso) => {
|
|
if (!iso) return '—'
|
|
try {
|
|
return new Date(iso).toLocaleString('de-DE')
|
|
} catch {
|
|
return iso
|
|
}
|
|
}
|
|
|
|
const handleSave = async () => {
|
|
isSaving.value = true
|
|
errorMessage.value = ''
|
|
successMessage.value = ''
|
|
|
|
// Validate password change if provided
|
|
if (passwordData.value.current || passwordData.value.new || passwordData.value.confirm) {
|
|
if (!passwordData.value.current) {
|
|
errorMessage.value = 'Bitte geben Sie Ihr aktuelles Passwort ein.'
|
|
isSaving.value = false
|
|
return
|
|
}
|
|
if (!passwordData.value.new) {
|
|
errorMessage.value = 'Bitte geben Sie ein neues Passwort ein.'
|
|
isSaving.value = false
|
|
return
|
|
}
|
|
if (passwordData.value.new !== passwordData.value.confirm) {
|
|
errorMessage.value = 'Die neuen Passwörter stimmen nicht überein.'
|
|
isSaving.value = false
|
|
return
|
|
}
|
|
if (passwordData.value.new.length < 6) {
|
|
errorMessage.value = 'Das neue Passwort muss mindestens 6 Zeichen lang sein.'
|
|
isSaving.value = false
|
|
return
|
|
}
|
|
}
|
|
|
|
try {
|
|
await $fetch('/api/profile', {
|
|
method: 'PUT',
|
|
body: {
|
|
name: formData.value.name,
|
|
email: formData.value.email,
|
|
phone: formData.value.phone,
|
|
currentPassword: passwordData.value.current || undefined,
|
|
newPassword: passwordData.value.new || undefined
|
|
}
|
|
})
|
|
|
|
successMessage.value = 'Profil erfolgreich aktualisiert!'
|
|
|
|
// Clear password fields
|
|
passwordData.value = {
|
|
current: '',
|
|
new: '',
|
|
confirm: ''
|
|
}
|
|
|
|
// Update auth store if email changed
|
|
await authStore.checkAuth()
|
|
|
|
// Scroll to top to show success message
|
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
} catch (error) {
|
|
errorMessage.value = error.data?.message || 'Fehler beim Speichern des Profils.'
|
|
} finally {
|
|
isSaving.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadProfile()
|
|
// Passkey-Verwaltung vorläufig deaktiviert
|
|
// loadPasskeys()
|
|
})
|
|
|
|
definePageMeta({
|
|
middleware: 'auth',
|
|
layout: 'default'
|
|
})
|
|
|
|
useHead({
|
|
title: 'Mein Profil - Harheimer TC',
|
|
})
|
|
</script>
|
|
|