import { readUsers, hashPassword, writeUsers, revokeRefreshSessionsForUser } from '../../utils/auth.js' import nodemailer from 'nodemailer' import crypto from 'crypto' import { assertRateLimit, getClientIp, registerRateLimitFailure, registerRateLimitSuccess } from '../../utils/rate-limit.js' import { writeAuditLog } from '../../utils/audit-log.js' import { maskResetEmail, normalizeResetEmail, writePasswordResetLog } from '../../utils/password-reset-log.js' export default defineEventHandler(async (event) => { const requestId = crypto.randomUUID() let emailKey = '' let ip = '' const logStep = async (step, status, detail = {}) => { try { await writePasswordResetLog({ requestId, email: emailKey, ip, step, status, ...detail }) } catch (logError) { console.error('Password-Reset-Diagnoselog-Fehler:', logError) } } try { const body = await readBody(event) const { email } = body emailKey = normalizeResetEmail(email) ip = getClientIp(event) await logStep('request_received', 'started') if (!emailKey) { await logStep('request_validation', 'failed', { reason: 'email_missing' }) throw createError({ statusCode: 400, message: 'E-Mail-Adresse ist erforderlich' }) } // Rate Limiting (IP + Account) await logStep('rate_limit', 'checking') try { assertRateLimit(event, { name: 'auth:reset:ip', keyParts: [ip], windowMs: 60 * 60 * 1000, maxAttempts: 20, lockoutMs: 30 * 60 * 1000 }) assertRateLimit(event, { name: 'auth:reset:account', keyParts: [emailKey], windowMs: 60 * 60 * 1000, maxAttempts: 5, lockoutMs: 60 * 60 * 1000 }) } catch (error) { await logStep('rate_limit', 'failed', { error }) throw error } await logStep('rate_limit', 'passed') // Find user let users try { users = await readUsers() } catch (error) { await logStep('user_lookup', 'failed', { error }) throw error } const user = users.find(u => normalizeResetEmail(u.email) === emailKey) // Always return success (security: don't reveal if email exists) if (!user) { await logStep('user_lookup', 'not_found') await registerRateLimitFailure(event, { name: 'auth:reset:ip', keyParts: [ip] }) await registerRateLimitFailure(event, { name: 'auth:reset:account', keyParts: [emailKey] }) await writeAuditLog('auth.reset.request', { ip, emailMasked: maskResetEmail(emailKey), userFound: false, requestId }) await logStep('request_completed', 'no_account') return { success: true, message: 'Falls ein Konto mit dieser E-Mail existiert, wurde eine E-Mail gesendet.' } } await logStep('user_lookup', 'found', { userId: user.id }) // Generate temporary password const tempPassword = crypto.randomBytes(8).toString('hex') const hashedPassword = await hashPassword(tempPassword) await logStep('temporary_password', 'generated', { userId: user.id }) // Send email with temporary password const smtpUser = process.env.SMTP_USER const smtpPass = process.env.SMTP_PASS if (!smtpUser || !smtpPass) { await logStep('mail_configuration', 'failed', { userId: user.id, reason: 'smtp_credentials_missing' }) console.warn('SMTP-Credentials fehlen! E-Mail-Versand wird übersprungen.') console.warn(`SMTP_USER=${smtpUser ? 'gesetzt' : 'FEHLT'}, SMTP_PASS=${smtpPass ? 'gesetzt' : 'FEHLT'}`) throw new Error('SMTP-Konfiguration fuer Passwort-Reset fehlt') } else { await logStep('mail_configuration', 'passed', { userId: user.id }) 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 mailOptions = { from: process.env.SMTP_FROM || 'noreply@harheimertc.de', to: user.email, subject: 'Passwort zurücksetzen - Harheimer TC', html: `

Passwort zurücksetzen

Hallo ${user.name},

Sie haben eine Anfrage zum Zurücksetzen Ihres Passworts gestellt.

Ihr temporäres Passwort lautet: ${tempPassword}

Bitte melden Sie sich damit an und ändern Sie Ihr Passwort im Mitgliederbereich.


Falls Sie diese Anfrage nicht gestellt haben, ignorieren Sie diese E-Mail.


Mit sportlichen Grüßen,
Ihr Harheimer TC

` } await logStep('mail_send', 'started', { userId: user.id }) try { await transporter.sendMail(mailOptions) } catch (error) { await logStep('mail_send', 'failed', { userId: user.id, error }) throw error } await logStep('mail_send', 'completed', { userId: user.id }) } // Erst nach erfolgreichem Versand das zugesandte Passwort aktivieren. user.password = hashedPassword user.passwordResetRequired = true const updatedUsers = users.map(u => u.id === user.id ? user : u) let passwordStored = false try { passwordStored = await writeUsers(updatedUsers) } catch (error) { await logStep('password_storage', 'failed', { userId: user.id, error }) throw error } if (!passwordStored) { await logStep('password_storage', 'failed', { userId: user.id, reason: 'write_failed' }) throw new Error('Passwort konnte nach E-Mail-Versand nicht gespeichert werden') } await logStep('password_storage', 'completed', { userId: user.id }) try { await revokeRefreshSessionsForUser(user.id, 'password_reset') } catch (error) { await logStep('session_revocation', 'failed', { userId: user.id, error }) throw error } await logStep('session_revocation', 'completed', { userId: user.id }) registerRateLimitSuccess(event, { name: 'auth:reset:account', keyParts: [emailKey] }) await writeAuditLog('auth.reset.request', { ip, emailMasked: maskResetEmail(emailKey), userFound: true, userId: user.id, requestId }) await logStep('request_completed', 'success', { userId: user.id }) return { success: true, message: 'Falls ein Konto mit dieser E-Mail existiert, wurde eine E-Mail gesendet.' } } catch (error) { await logStep('request_completed', 'failed', { error }) console.error('Password-Reset-Fehler:', { requestId, code: error?.code || error?.name || 'Error' }) // Don't reveal errors to prevent email enumeration return { success: true, message: 'Falls ein Konto mit dieser E-Mail existiert, wurde eine E-Mail gesendet.' } } })