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
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 49s
This commit is contained in:
@@ -851,17 +851,35 @@ import { useRoute } from 'vue-router'
|
|||||||
import { Menu, X, ChevronDown } from 'lucide-vue-next'
|
import { Menu, X, ChevronDown } from 'lucide-vue-next'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const authStore = useAuthStore()
|
|
||||||
const isMobileMenuOpen = ref(false)
|
const isMobileMenuOpen = ref(false)
|
||||||
const mobileSubmenu = ref(null)
|
const mobileSubmenu = ref(null)
|
||||||
const mannschaften = ref([])
|
const mannschaften = ref([])
|
||||||
const hasGalleryImages = ref(false)
|
const hasGalleryImages = ref(false)
|
||||||
const showCmsDropdown = ref(false)
|
const showCmsDropdown = ref(false)
|
||||||
|
|
||||||
// Reactive auth state from store
|
// Lazy store access to avoid Pinia initialization issues
|
||||||
const isLoggedIn = computed(() => authStore.isLoggedIn)
|
const getAuthStore = () => {
|
||||||
const isAdmin = computed(() => authStore.isAdmin)
|
try {
|
||||||
const canAccessNewsletter = computed(() => authStore.hasAnyRole('admin', 'vorstand', 'newsletter'))
|
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
|
// Automatisches Setzen des Submenus basierend auf der Route
|
||||||
const currentSubmenu = computed(() => {
|
const currentSubmenu = computed(() => {
|
||||||
@@ -954,7 +972,10 @@ const handleDocumentClick = (e) => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadMannschaften()
|
loadMannschaften()
|
||||||
checkGalleryImages()
|
checkGalleryImages()
|
||||||
authStore.checkAuth()
|
const store = getAuthStore()
|
||||||
|
if (store) {
|
||||||
|
store.checkAuth()
|
||||||
|
}
|
||||||
|
|
||||||
// Close CMS dropdown when clicking outside
|
// Close CMS dropdown when clicking outside
|
||||||
document.addEventListener('click', handleDocumentClick)
|
document.addEventListener('click', handleDocumentClick)
|
||||||
|
|||||||
@@ -87,18 +87,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Password -->
|
<!-- Password -->
|
||||||
<div v-if="!usePasskey">
|
<div v-if="!usePasskey || setPasswordForPasskey">
|
||||||
<label
|
<label
|
||||||
for="password"
|
for="password"
|
||||||
class="block text-sm font-medium text-gray-700 mb-2"
|
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>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="password"
|
id="password"
|
||||||
v-model="formData.password"
|
v-model="formData.password"
|
||||||
type="password"
|
type="password"
|
||||||
required
|
:required="!usePasskey"
|
||||||
autocomplete="new-password"
|
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"
|
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="••••••••"
|
placeholder="••••••••"
|
||||||
@@ -109,24 +109,35 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Confirm Password -->
|
<!-- Confirm Password -->
|
||||||
<div v-if="!usePasskey">
|
<div v-if="!usePasskey || setPasswordForPasskey">
|
||||||
<label
|
<label
|
||||||
for="confirmPassword"
|
for="confirmPassword"
|
||||||
class="block text-sm font-medium text-gray-700 mb-2"
|
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>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="confirmPassword"
|
id="confirmPassword"
|
||||||
v-model="formData.confirmPassword"
|
v-model="formData.confirmPassword"
|
||||||
type="password"
|
type="password"
|
||||||
required
|
:required="!usePasskey"
|
||||||
autocomplete="new-password"
|
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"
|
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="••••••••"
|
placeholder="••••••••"
|
||||||
>
|
>
|
||||||
</div>
|
</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 -->
|
<!-- Error Message -->
|
||||||
<div
|
<div
|
||||||
v-if="errorMessage"
|
v-if="errorMessage"
|
||||||
@@ -214,6 +225,7 @@ const successMessage = ref('')
|
|||||||
const usePasskey = ref(false)
|
const usePasskey = ref(false)
|
||||||
const isPasskeySupported = ref(false)
|
const isPasskeySupported = ref(false)
|
||||||
const passkeySupportReason = ref('')
|
const passkeySupportReason = ref('')
|
||||||
|
const setPasswordForPasskey = ref(true)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
try {
|
try {
|
||||||
@@ -295,6 +307,22 @@ const handleRegisterWithPasskey = async () => {
|
|||||||
return
|
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
|
isLoading.value = true
|
||||||
try {
|
try {
|
||||||
const pre = await $fetch('/api/auth/register-passkey-options', {
|
const pre = await $fetch('/api/auth/register-passkey-options', {
|
||||||
@@ -313,7 +341,8 @@ const handleRegisterWithPasskey = async () => {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
registrationId: pre.registrationId,
|
registrationId: pre.registrationId,
|
||||||
credential
|
credential,
|
||||||
|
password: setPasswordForPasskey.value ? formData.value.password : undefined
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import { verifyRegistrationResponse } from '@simplewebauthn/server'
|
import { verifyRegistrationResponse } from '@simplewebauthn/server'
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
import bcrypt from 'bcryptjs'
|
|
||||||
import nodemailer from 'nodemailer'
|
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 { getWebAuthnConfig } from '../../utils/webauthn-config.js'
|
||||||
import { consumePreRegistration } from '../../utils/webauthn-challenges.js'
|
import { consumePreRegistration } from '../../utils/webauthn-challenges.js'
|
||||||
import { toBase64Url } from '../../utils/webauthn-encoding.js'
|
import { toBase64Url } from '../../utils/webauthn-encoding.js'
|
||||||
import { writeAuditLog } from '../../utils/audit-log.js'
|
import { writeAuditLog } from '../../utils/audit-log.js'
|
||||||
|
import { assertPasswordNotPwned } from '../../utils/hibp.js'
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const body = await readBody(event)
|
const body = await readBody(event)
|
||||||
const registrationId = String(body?.registrationId || '')
|
const registrationId = String(body?.registrationId || '')
|
||||||
const response = body?.credential
|
const response = body?.credential
|
||||||
|
const password = body?.password ? String(body.password) : ''
|
||||||
|
|
||||||
if (!registrationId || !response) {
|
if (!registrationId || !response) {
|
||||||
throw createError({ statusCode: 400, statusMessage: 'Ungültige Anfrage' })
|
throw createError({ statusCode: 400, statusMessage: 'Ungültige Anfrage' })
|
||||||
@@ -56,9 +57,18 @@ export default defineEventHandler(async (event) => {
|
|||||||
const credentialId = toBase64Url(credentialID)
|
const credentialId = toBase64Url(credentialID)
|
||||||
const publicKey = toBase64Url(credentialPublicKey)
|
const publicKey = toBase64Url(credentialPublicKey)
|
||||||
|
|
||||||
// Dummy password hash (login via password isn't intended)
|
// Optional: Passwort als Fallback (z.B. Firefox/Linux) erlauben
|
||||||
const salt = await bcrypt.genSalt(10)
|
let hashedPassword
|
||||||
const hashedPassword = await bcrypt.hash(crypto.randomBytes(32).toString('hex'), salt)
|
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 = {
|
const newUser = {
|
||||||
id: String(userId),
|
id: String(userId),
|
||||||
@@ -115,7 +125,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
<li><strong>Name:</strong> ${name}</li>
|
<li><strong>Name:</strong> ${name}</li>
|
||||||
<li><strong>E-Mail:</strong> ${email}</li>
|
<li><strong>E-Mail:</strong> ${email}</li>
|
||||||
<li><strong>Telefon:</strong> ${phone || 'Nicht angegeben'}</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>
|
</ul>
|
||||||
<p>Bitte prüfen Sie die Registrierung im CMS.</p>
|
<p>Bitte prüfen Sie die Registrierung im CMS.</p>
|
||||||
`
|
`
|
||||||
|
|||||||
Reference in New Issue
Block a user