import nodemailer from 'nodemailer' import { readUsers, writeUsers } from '../../../../utils/auth.js' import { assertRateLimit, getClientIp, registerRateLimitFailure, registerRateLimitSuccess } from '../../../../utils/rate-limit.js' import { generateRecoveryToken, hashRecoveryToken, pruneRecoveryTokens } from '../../../../utils/passkey-recovery.js' import { writeAuditLog } from '../../../../utils/audit-log.js' function isValidEmail(email) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(email || '')) } export default defineEventHandler(async (event) => { const body = await readBody(event) const email = String(body?.email || '').trim().toLowerCase() if (!email || !isValidEmail(email)) { // No enumeration; still 200 return { success: true, message: 'Falls ein Konto mit dieser E-Mail existiert, wurde eine E-Mail gesendet.' } } const ip = getClientIp(event) assertRateLimit(event, { name: 'auth:passkey-recovery:request:ip', keyParts: [ip], windowMs: 60 * 60 * 1000, maxAttempts: 30, lockoutMs: 30 * 60 * 1000 }) assertRateLimit(event, { name: 'auth:passkey-recovery:request:email', keyParts: [email], windowMs: 60 * 60 * 1000, maxAttempts: 5, lockoutMs: 60 * 60 * 1000 }) const users = await readUsers() const user = users.find(u => String(u.email || '').toLowerCase() === email) // Always respond success if (!user) { await registerRateLimitFailure(event, { name: 'auth:passkey-recovery:request:ip', keyParts: [ip] }) await registerRateLimitFailure(event, { name: 'auth:passkey-recovery:request:email', keyParts: [email] }) await writeAuditLog('auth.passkey.recovery.request', { ip, email, userFound: false }) return { success: true, message: 'Falls ein Konto mit dieser E-Mail existiert, wurde eine E-Mail gesendet.' } } // Token erzeugen und (gehasht) am User speichern const token = generateRecoveryToken() const tokenHash = hashRecoveryToken(token) const ttlMin = Number(process.env.PASSKEY_RECOVERY_TTL_MIN || 30) const expiresAt = new Date(Date.now() + ttlMin * 60 * 1000).toISOString() if (!Array.isArray(user.passkeyRecoveryTokens)) user.passkeyRecoveryTokens = [] user.passkeyRecoveryTokens.push({ tokenHash, createdAt: new Date().toISOString(), expiresAt, usedAt: null }) pruneRecoveryTokens(user) const updated = users.map(u => (u.id === user.id ? user : u)) await writeUsers(updated) registerRateLimitSuccess(event, { name: 'auth:passkey-recovery:request:email', keyParts: [email] }) await writeAuditLog('auth.passkey.recovery.request', { ip, email, userFound: true, userId: user.id }) // Mail senden (wenn SMTP konfiguriert) 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 } }) const baseUrl = process.env.NUXT_PUBLIC_BASE_URL || 'http://localhost:3100' // Passkey-Wiederherstellungsseite vorläufig deaktiviert // const link = `${baseUrl}/passkey-wiederherstellen?token=${token}` const link = `${baseUrl}/login` // Fallback auf Login-Seite await transporter.sendMail({ from: process.env.SMTP_FROM || 'noreply@harheimertc.de', to: user.email, subject: 'Passkey wiederherstellen - Harheimer TC', html: `

Passkey wiederherstellen

Hallo ${user.name || ''},

Sie haben eine Anfrage gestellt, um einen neuen Passkey hinzuzufügen.

Bitte klicken Sie auf den folgenden Link (gültig für ${ttlMin} Minuten):

Neuen Passkey hinzufügen

Wenn Sie das nicht waren, ignorieren Sie diese E-Mail.

` }) } return { success: true, message: 'Falls ein Konto mit dieser E-Mail existiert, wurde eine E-Mail gesendet.' } })