Files
harheimertc/server/api/newsletter/subscribe.post.js
2025-12-20 11:15:31 +01:00

233 lines
7.7 KiB
JavaScript

import { readSubscribers, writeSubscribers } from '../../utils/newsletter.js'
import { randomUUID } from 'crypto'
import nodemailer from 'nodemailer'
import crypto from 'crypto'
import fs from 'fs/promises'
import path from 'path'
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'newsletter-subscribers.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
return path.join(cwd, '../server/data', filename)
}
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
return path.join(cwd, 'server/data', filename)
}
const NEWSLETTER_GROUPS_FILE = getDataPath('newsletter-groups.json')
async function readGroups() {
try {
const data = await fs.readFile(NEWSLETTER_GROUPS_FILE, 'utf-8')
return JSON.parse(data)
} catch (error) {
if (error.code === 'ENOENT') {
return []
}
throw error
}
}
export default defineEventHandler(async (event) => {
try {
const body = await readBody(event)
const { email, name, groupId } = body
if (!email || !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
throw createError({
statusCode: 400,
statusMessage: 'Ungültige E-Mail-Adresse'
})
}
if (!groupId) {
throw createError({
statusCode: 400,
statusMessage: 'Newsletter-Gruppe muss ausgewählt werden'
})
}
// Prüfe ob Gruppe existiert und für externe Abonnements verfügbar ist
const groups = await readGroups()
const group = groups.find(g => g.id === groupId)
if (!group) {
throw createError({
statusCode: 404,
statusMessage: 'Newsletter-Gruppe nicht gefunden'
})
}
if (group.type !== 'subscription' || group.sendToExternal !== true) {
throw createError({
statusCode: 403,
statusMessage: 'Diese Newsletter-Gruppe ist nicht für externe Abonnements verfügbar'
})
}
const subscribers = await readSubscribers()
const emailLower = email.toLowerCase()
// Prüfe ob bereits für diese Gruppe angemeldet
const existing = subscribers.find(s => {
const sEmail = (s.email || '').toLowerCase()
return sEmail === emailLower && s.groupIds && s.groupIds.includes(groupId)
})
if (existing) {
if (existing.confirmed) {
throw createError({
statusCode: 409,
statusMessage: 'Sie sind bereits für diesen Newsletter angemeldet'
})
} else {
// Bestätigungsmail erneut senden
await sendConfirmationEmail(existing.email, existing.name || name, existing.confirmationToken, group.name)
return {
success: true,
message: 'Eine Bestätigungsmail wurde an Ihre E-Mail-Adresse gesendet'
}
}
}
// Prüfe ob E-Mail bereits existiert (für andere Gruppe oder ohne Gruppe)
const existingEmail = subscribers.find(s => (s.email || '').toLowerCase() === emailLower)
if (existingEmail) {
// Bestehender Subscriber - Gruppe hinzufügen
if (!existingEmail.groupIds) {
existingEmail.groupIds = []
}
if (existingEmail.groupIds.includes(groupId)) {
// Bereits für diese Gruppe angemeldet
if (existingEmail.confirmed) {
throw createError({
statusCode: 409,
statusMessage: 'Sie sind bereits für diesen Newsletter angemeldet'
})
} else {
// Bestätigungsmail erneut senden
await sendConfirmationEmail(existingEmail.email, existingEmail.name || name, existingEmail.confirmationToken, group.name)
return {
success: true,
message: 'Eine Bestätigungsmail wurde an Ihre E-Mail-Adresse gesendet'
}
}
}
// Gruppe hinzufügen
existingEmail.groupIds.push(groupId)
if (!existingEmail.confirmed) {
// Neuer Bestätigungstoken für alle Gruppen
existingEmail.confirmationToken = crypto.randomBytes(32).toString('hex')
}
existingEmail.name = name || existingEmail.name || ''
await writeSubscribers(subscribers)
if (existingEmail.confirmed) {
// Bereits bestätigt - sofort aktiviert
return {
success: true,
message: `Sie wurden erfolgreich für den Newsletter "${group.name}" angemeldet`
}
} else {
// Bestätigungsmail senden
await sendConfirmationEmail(existingEmail.email, existingEmail.name, existingEmail.confirmationToken, group.name)
return {
success: true,
message: 'Eine Bestätigungsmail wurde an Ihre E-Mail-Adresse gesendet. Bitte bestätigen Sie Ihre Anmeldung.'
}
}
}
// Neuer Abonnent
const confirmationToken = crypto.randomBytes(32).toString('hex')
const unsubscribeToken = crypto.randomBytes(32).toString('hex')
const newSubscriber = {
id: randomUUID(),
email: emailLower,
name: name || '',
groupIds: [groupId],
confirmed: false,
confirmationToken,
unsubscribeToken,
subscribedAt: new Date().toISOString(),
confirmedAt: null,
unsubscribedAt: null
}
subscribers.push(newSubscriber)
await writeSubscribers(subscribers)
// Bestätigungsmail senden
await sendConfirmationEmail(email, name, confirmationToken, group.name)
return {
success: true,
message: 'Eine Bestätigungsmail wurde an Ihre E-Mail-Adresse gesendet. Bitte bestätigen Sie Ihre Anmeldung.'
}
} catch (error) {
console.error('Fehler bei Newsletter-Anmeldung:', error)
if (error.statusCode) {
throw error
}
throw createError({
statusCode: 500,
statusMessage: 'Fehler bei der Newsletter-Anmeldung'
})
}
})
async function sendConfirmationEmail(email, name, token, groupName) {
const smtpUser = process.env.SMTP_USER
const smtpPass = process.env.SMTP_PASS
if (!smtpUser || !smtpPass) {
console.warn('SMTP-Credentials fehlen! Bestätigungsmail kann nicht gesendet werden.')
return
}
const baseUrl = process.env.NUXT_PUBLIC_BASE_URL || 'http://localhost:3100'
const confirmationUrl = `${baseUrl}/newsletter/confirm?token=${token}`
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: email,
subject: `Newsletter-Anmeldung bestätigen - ${groupName} - Harheimer TC`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #dc2626;">Newsletter-Anmeldung bestätigen</h2>
<p>Hallo ${name || 'Liebe/r Abonnent/in'},</p>
<p>vielen Dank für Ihre Anmeldung zum Newsletter "${groupName}" des Harheimer TC!</p>
<p>Bitte bestätigen Sie Ihre Anmeldung, indem Sie auf den folgenden Link klicken:</p>
<p style="margin: 30px 0;">
<a href="${confirmationUrl}" style="display: inline-block; padding: 12px 24px; background-color: #dc2626; color: white; text-decoration: none; border-radius: 5px;">
Newsletter-Anmeldung bestätigen
</a>
</p>
<p>Falls Sie sich nicht angemeldet haben, können Sie diese E-Mail ignorieren.</p>
<p style="margin-top: 30px; color: #666; font-size: 12px;">
Mit sportlichen Grüßen,<br>
Ihr Harheimer Tischtennis-Club 1954 e.V.
</p>
</div>
`
})
}