101 lines
3.8 KiB
JavaScript
101 lines
3.8 KiB
JavaScript
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'
|
|
const link = `${baseUrl}/passkey-wiederherstellen?token=${token}`
|
|
|
|
await transporter.sendMail({
|
|
from: process.env.SMTP_FROM || 'noreply@harheimertc.de',
|
|
to: user.email,
|
|
subject: 'Passkey wiederherstellen - Harheimer TC',
|
|
html: `
|
|
<h2>Passkey wiederherstellen</h2>
|
|
<p>Hallo ${user.name || ''},</p>
|
|
<p>Sie haben eine Anfrage gestellt, um einen neuen Passkey hinzuzufügen.</p>
|
|
<p>Bitte klicken Sie auf den folgenden Link (gültig für ${ttlMin} Minuten):</p>
|
|
<p><a href="${link}">Neuen Passkey hinzufügen</a></p>
|
|
<p>Wenn Sie das nicht waren, ignorieren Sie diese E-Mail.</p>
|
|
`
|
|
})
|
|
}
|
|
|
|
return { success: true, message: 'Falls ein Konto mit dieser E-Mail existiert, wurde eine E-Mail gesendet.' }
|
|
})
|
|
|