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.
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 49s

This commit is contained in:
Torsten Schulz (local)
2026-01-07 20:16:17 +01:00
parent 4c7ae87c70
commit 3d9b6b57dc
3 changed files with 79 additions and 19 deletions

View File

@@ -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)

View File

@@ -87,18 +87,18 @@
</div>
<!-- Password -->
<div v-if="!usePasskey">
<div v-if="!usePasskey || setPasswordForPasskey">
<label
for="password"
class="block text-sm font-medium text-gray-700 mb-2"
>
Passwort
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
: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="••••••••"
@@ -109,24 +109,35 @@
</div>
<!-- Confirm Password -->
<div v-if="!usePasskey">
<div v-if="!usePasskey || setPasswordForPasskey">
<label
for="confirmPassword"
class="block text-sm font-medium text-gray-700 mb-2"
>
Passwort bestätigen
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
: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"
@@ -214,6 +225,7 @@ const successMessage = ref('')
const usePasskey = ref(false)
const isPasskeySupported = ref(false)
const passkeySupportReason = ref('')
const setPasswordForPasskey = ref(true)
onMounted(() => {
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
}
})

View File

@@ -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) => {
<li><strong>Name:</strong> ${name}</li>
<li><strong>E-Mail:</strong> ${email}</li>
<li><strong>Telefon:</strong> ${phone || 'Nicht angegeben'}</li>
<li><strong>Login:</strong> Passkey (ohne Passwort)</li>
<li><strong>Login:</strong> Passkey${password ? ' + Passwort (Fallback)' : ' (ohne Passwort)'}</li>
</ul>
<p>Bitte prüfen Sie die Registrierung im CMS.</p>
`