import { verifyRegistrationResponse } from '@simplewebauthn/server' import crypto from 'crypto' import nodemailer from 'nodemailer' 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 requestStart = Date.now() const requestOrigin = getHeader(event, 'origin') console.log('[DEBUG] register-passkey request received', { origin: requestOrigin, timestamp: new Date().toISOString() }) const body = await readBody(event) const registrationId = String(body?.registrationId || '') const response = body?.credential const password = body?.password ? String(body.password) : '' console.log('[DEBUG] Request body parsed', { hasRegistrationId: !!registrationId, registrationId: registrationId.substring(0, 10) + '...', hasCredential: !!response, credentialId: response?.id, hasPassword: !!password }) if (!registrationId || !response) { console.error('[DEBUG] Validation failed: missing registrationId or credential') throw createError({ statusCode: 400, statusMessage: 'Ungültige Anfrage' }) } const pre = consumePreRegistration(registrationId) if (!pre) { console.error('[DEBUG] Pre-registration not found or expired', { registrationId }) throw createError({ statusCode: 400, statusMessage: 'Registrierungs-Session abgelaufen. Bitte erneut versuchen.' }) } const { challenge, userId, name, email, phone } = pre console.log('[DEBUG] Pre-registration found', { userId, email: email.substring(0, 10) + '...', hasChallenge: !!challenge }) const users = await readUsers() if (users.some(u => String(u.email || '').toLowerCase() === String(email).toLowerCase())) { console.error('[DEBUG] User already exists', { email }) throw createError({ statusCode: 409, message: 'Ein Benutzer mit dieser E-Mail-Adresse existiert bereits' }) } const { origin, rpId, requireUV } = getWebAuthnConfig() // Debug: Prüfe die tatsächliche Origin aus der Response const clientData = response?.response?.clientDataJSON let actualOrigin = null if (clientData) { try { const decoded = Buffer.from(clientData, 'base64').toString('utf-8') const parsed = JSON.parse(decoded) actualOrigin = parsed.origin } catch (e) { console.warn('[DEBUG] Could not parse clientDataJSON:', e) } } console.log('[DEBUG] WebAuthn config for verification', { expectedOrigin: origin, actualOriginFromResponse: actualOrigin, rpId, requireUV, originMatch: origin === actualOrigin }) console.log('[DEBUG] Verifying registration response...') const verifyStart = Date.now() const verification = await verifyRegistrationResponse({ response, expectedChallenge: challenge, expectedOrigin: origin, expectedRPID: rpId, requireUserVerification: requireUV }) const verifyDuration = Date.now() - verifyStart const { verified, registrationInfo } = verification console.log(`[DEBUG] Verification completed (${verifyDuration}ms)`, { verified, hasRegistrationInfo: !!registrationInfo, credentialId: registrationInfo?.credentialID ? 'present' : 'missing', deviceType: registrationInfo?.credentialDeviceType, backedUp: registrationInfo?.credentialBackedUp }) if (!verified || !registrationInfo) { console.error('[DEBUG] Verification failed', { verified, hasRegistrationInfo: !!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) // 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), 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) const totalDuration = Date.now() - requestStart console.log(`[DEBUG] User created successfully (total: ${totalDuration}ms)`, { userId: newUser.id, email: newUser.email.substring(0, 10) + '...', hasPasskey: newUser.passkeys?.length > 0, hasPassword: !!newUser.password }) 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: `
Ein neuer Benutzer hat sich registriert und wartet auf Freigabe:
Bitte prüfen Sie die Registrierung im CMS.
` }) await transporter.sendMail({ from: process.env.SMTP_FROM || 'noreply@harheimertc.de', to: email, subject: 'Registrierung erhalten - Harheimer TC', html: `Hallo ${name},
vielen Dank für Ihre Registrierung beim Harheimer TC!
Ihre Anfrage wird vom Vorstand geprüft. Sie erhalten eine E-Mail, sobald Ihr Zugang freigeschaltet wurde.
Mit sportlichen Grüßen,
Ihr Harheimer TC