From 3d9b6b57dcbc25c7ab4fa695ae88b5d8a0abf201 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 7 Jan 2026 20:16:17 +0100 Subject: [PATCH] Refactor authentication handling in Navigation and registration components to support lazy store access, improving resilience against Pinia initialization issues. Enhance registration logic to include optional password fallback for passkey users, with validation checks for password strength and confirmation. Update server-side registration to handle optional password securely, ensuring consistent user experience across different authentication methods. --- components/Navigation.vue | 33 ++++++++++++++---- pages/registrieren.vue | 43 ++++++++++++++++++++---- server/api/auth/register-passkey.post.js | 22 ++++++++---- 3 files changed, 79 insertions(+), 19 deletions(-) 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.

    `