diff --git a/components/Navigation.vue b/components/Navigation.vue index 7d77631..094a5ed 100644 --- a/components/Navigation.vue +++ b/components/Navigation.vue @@ -851,17 +851,35 @@ import { useRoute } from 'vue-router' import { Menu, X, ChevronDown } from 'lucide-vue-next' const route = useRoute() -const authStore = useAuthStore() const isMobileMenuOpen = ref(false) const mobileSubmenu = ref(null) const mannschaften = ref([]) const hasGalleryImages = ref(false) const showCmsDropdown = ref(false) -// Reactive auth state from store -const isLoggedIn = computed(() => authStore.isLoggedIn) -const isAdmin = computed(() => authStore.isAdmin) -const canAccessNewsletter = computed(() => authStore.hasAnyRole('admin', 'vorstand', 'newsletter')) +// Lazy store access to avoid Pinia initialization issues +const getAuthStore = () => { + try { + return useAuthStore() + } catch (e) { + // Fallback if Pinia is not yet initialized + return null + } +} + +// Reactive auth state from store (lazy) +const isLoggedIn = computed(() => { + const store = getAuthStore() + return store?.isLoggedIn ?? false +}) +const isAdmin = computed(() => { + const store = getAuthStore() + return store?.isAdmin ?? false +}) +const canAccessNewsletter = computed(() => { + const store = getAuthStore() + return store?.hasAnyRole('admin', 'vorstand', 'newsletter') ?? false +}) // Automatisches Setzen des Submenus basierend auf der Route const currentSubmenu = computed(() => { @@ -954,7 +972,10 @@ const handleDocumentClick = (e) => { onMounted(() => { loadMannschaften() checkGalleryImages() - authStore.checkAuth() + const store = getAuthStore() + if (store) { + store.checkAuth() + } // Close CMS dropdown when clicking outside document.addEventListener('click', handleDocumentClick) diff --git a/pages/registrieren.vue b/pages/registrieren.vue index c3c8ecc..a4abb72 100644 --- a/pages/registrieren.vue +++ b/pages/registrieren.vue @@ -87,18 +87,18 @@ -
+
-
+
+ +
+ + Zusätzlich ein Passwort als Fallback setzen (z.B. für Firefox/Linux) +
+
{ try { @@ -295,6 +307,22 @@ const handleRegisterWithPasskey = async () => { 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', { @@ -313,7 +341,8 @@ const handleRegisterWithPasskey = async () => { method: 'POST', body: { registrationId: pre.registrationId, - credential + credential, + password: setPasswordForPasskey.value ? formData.value.password : undefined } }) diff --git a/server/api/auth/register-passkey.post.js b/server/api/auth/register-passkey.post.js index ff3ec78..94d5cb0 100644 --- a/server/api/auth/register-passkey.post.js +++ b/server/api/auth/register-passkey.post.js @@ -1,17 +1,18 @@ import { verifyRegistrationResponse } from '@simplewebauthn/server' import crypto from 'crypto' -import bcrypt from 'bcryptjs' import nodemailer from 'nodemailer' -import { readUsers, writeUsers } from '../../utils/auth.js' +import { hashPassword, readUsers, writeUsers } from '../../utils/auth.js' import { getWebAuthnConfig } from '../../utils/webauthn-config.js' import { consumePreRegistration } from '../../utils/webauthn-challenges.js' import { toBase64Url } from '../../utils/webauthn-encoding.js' import { writeAuditLog } from '../../utils/audit-log.js' +import { assertPasswordNotPwned } from '../../utils/hibp.js' export default defineEventHandler(async (event) => { const body = await readBody(event) const registrationId = String(body?.registrationId || '') const response = body?.credential + const password = body?.password ? String(body.password) : '' if (!registrationId || !response) { throw createError({ statusCode: 400, statusMessage: 'Ungültige Anfrage' }) @@ -56,9 +57,18 @@ export default defineEventHandler(async (event) => { const credentialId = toBase64Url(credentialID) const publicKey = toBase64Url(credentialPublicKey) - // Dummy password hash (login via password isn't intended) - const salt = await bcrypt.genSalt(10) - const hashedPassword = await bcrypt.hash(crypto.randomBytes(32).toString('hex'), salt) + // Optional: Passwort als Fallback (z.B. Firefox/Linux) erlauben + let hashedPassword + if (password && password.trim().length > 0) { + if (password.length < 8) { + throw createError({ statusCode: 400, message: 'Das Passwort muss mindestens 8 Zeichen lang sein' }) + } + await assertPasswordNotPwned(password) + hashedPassword = await hashPassword(password) + } else { + // Kein Passwort gesetzt: random Hash, damit bestehende Code-Pfade (verifyPassword) konsistent bleiben. + hashedPassword = await hashPassword(crypto.randomBytes(32).toString('hex')) + } const newUser = { id: String(userId), @@ -115,7 +125,7 @@ export default defineEventHandler(async (event) => {
  • Name: ${name}
  • E-Mail: ${email}
  • Telefon: ${phone || 'Nicht angegeben'}
  • -
  • Login: Passkey (ohne Passwort)
  • +
  • Login: Passkey${password ? ' + Passwort (Fallback)' : ' (ohne Passwort)'}
  • Bitte prüfen Sie die Registrierung im CMS.

    `