Implement passkey recovery feature, including email link requests and registration options. Update login and registration pages to support passkey authentication, with UI enhancements for user experience. Add server-side handling for passkey registration and login, including account activation checks. Update environment configuration for passkey recovery TTL settings.
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 48s
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 48s
This commit is contained in:
147
server/api/auth/register-passkey.post.js
Normal file
147
server/api/auth/register-passkey.post.js
Normal file
@@ -0,0 +1,147 @@
|
||||
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 { 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'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event)
|
||||
const registrationId = String(body?.registrationId || '')
|
||||
const response = body?.credential
|
||||
|
||||
if (!registrationId || !response) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Ungültige Anfrage' })
|
||||
}
|
||||
|
||||
const pre = consumePreRegistration(registrationId)
|
||||
if (!pre) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Registrierungs-Session abgelaufen. Bitte erneut versuchen.' })
|
||||
}
|
||||
|
||||
const { challenge, userId, name, email, phone } = pre
|
||||
|
||||
const users = await readUsers()
|
||||
if (users.some(u => String(u.email || '').toLowerCase() === String(email).toLowerCase())) {
|
||||
throw createError({ statusCode: 409, message: 'Ein Benutzer mit dieser E-Mail-Adresse existiert bereits' })
|
||||
}
|
||||
|
||||
const { origin, rpId, requireUV } = getWebAuthnConfig()
|
||||
|
||||
const verification = await verifyRegistrationResponse({
|
||||
response,
|
||||
expectedChallenge: challenge,
|
||||
expectedOrigin: origin,
|
||||
expectedRPID: rpId,
|
||||
requireUserVerification: requireUV
|
||||
})
|
||||
|
||||
const { verified, registrationInfo } = verification
|
||||
if (!verified || !registrationInfo) {
|
||||
await writeAuditLog('auth.passkey.prereg.failed', { email })
|
||||
throw createError({ statusCode: 400, statusMessage: 'Passkey-Registrierung fehlgeschlagen' })
|
||||
}
|
||||
|
||||
const {
|
||||
credentialID,
|
||||
credentialPublicKey,
|
||||
counter,
|
||||
credentialDeviceType,
|
||||
credentialBackedUp
|
||||
} = registrationInfo
|
||||
|
||||
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)
|
||||
|
||||
const newUser = {
|
||||
id: String(userId),
|
||||
email: String(email).toLowerCase(),
|
||||
password: hashedPassword,
|
||||
name,
|
||||
phone: phone || '',
|
||||
role: 'mitglied',
|
||||
active: false,
|
||||
created: new Date().toISOString(),
|
||||
lastLogin: null,
|
||||
passkeys: [
|
||||
{
|
||||
id: `${Date.now()}`,
|
||||
credentialId,
|
||||
publicKey,
|
||||
counter: Number(counter) || 0,
|
||||
transports: Array.isArray(response.transports) ? response.transports : undefined,
|
||||
deviceType: credentialDeviceType,
|
||||
backedUp: !!credentialBackedUp,
|
||||
createdAt: new Date().toISOString(),
|
||||
lastUsedAt: null,
|
||||
name: 'Passkey'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
users.push(newUser)
|
||||
await writeUsers(users)
|
||||
|
||||
await writeAuditLog('auth.passkey.prereg.success', { email, userId: newUser.id })
|
||||
|
||||
// Send notification emails (same behavior as password registration)
|
||||
try {
|
||||
const smtpUser = process.env.SMTP_USER
|
||||
const smtpPass = process.env.SMTP_PASS
|
||||
|
||||
if (smtpUser && smtpPass) {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST || 'smtp.gmail.com',
|
||||
port: process.env.SMTP_PORT || 587,
|
||||
secure: false,
|
||||
auth: { user: smtpUser, pass: smtpPass }
|
||||
})
|
||||
|
||||
await transporter.sendMail({
|
||||
from: process.env.SMTP_FROM || 'noreply@harheimertc.de',
|
||||
to: process.env.SMTP_ADMIN || 'j.dichmann@gmx.de',
|
||||
subject: 'Neue Registrierung (Passkey) - Harheimer TC',
|
||||
html: `
|
||||
<h2>Neue Registrierung (Passkey)</h2>
|
||||
<p>Ein neuer Benutzer hat sich registriert und wartet auf Freigabe:</p>
|
||||
<ul>
|
||||
<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>
|
||||
</ul>
|
||||
<p>Bitte prüfen Sie die Registrierung im CMS.</p>
|
||||
`
|
||||
})
|
||||
|
||||
await transporter.sendMail({
|
||||
from: process.env.SMTP_FROM || 'noreply@harheimertc.de',
|
||||
to: email,
|
||||
subject: 'Registrierung erhalten - Harheimer TC',
|
||||
html: `
|
||||
<h2>Registrierung erhalten</h2>
|
||||
<p>Hallo ${name},</p>
|
||||
<p>vielen Dank für Ihre Registrierung beim Harheimer TC!</p>
|
||||
<p>Ihre Anfrage wird vom Vorstand geprüft. Sie erhalten eine E-Mail, sobald Ihr Zugang freigeschaltet wurde.</p>
|
||||
<br>
|
||||
<p>Mit sportlichen Grüßen,<br>Ihr Harheimer TC</p>
|
||||
`
|
||||
})
|
||||
}
|
||||
} catch (emailError) {
|
||||
console.error('E-Mail-Versand fehlgeschlagen:', emailError)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Registrierung erfolgreich. Sie erhalten eine E-Mail, sobald Ihr Zugang freigeschaltet wurde.'
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user