Files
harheimertc/server/api/newsletter/subscribe.post.js
Torsten Schulz (local) dee760d51a
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 41s
Enhance newsletter subscription functionality with user profile integration
This commit updates the newsletter subscription component to display the user's email when logged in, improving user experience. It also adds logic to load the user's profile data upon authentication, ensuring that the email field is pre-filled for logged-in users. Additionally, the server-side subscription handler is modified to check user authentication status, allowing only logged-in users to subscribe to certain groups. This change enhances the overall subscription process and aligns it with user authentication state.
2026-01-09 09:01:23 +01:00

284 lines
10 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'
import { assertRateLimit, getClientIp, registerRateLimitFailure, registerRateLimitSuccess } from '../../utils/rate-limit.js'
import { getUserFromToken } from '../../utils/auth.js'
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-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.path-join-resolve-traversal
return path.join(cwd, '../server/data', filename)
}
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-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'
})
}
const ip = getClientIp(event)
const emailKey = String(email || '').trim().toLowerCase()
assertRateLimit(event, {
name: 'newsletter:subscribe:ip',
keyParts: [ip],
windowMs: 10 * 60 * 1000,
maxAttempts: 30,
lockoutMs: 15 * 60 * 1000
})
assertRateLimit(event, {
name: 'newsletter:subscribe:email',
keyParts: [emailKey],
windowMs: 10 * 60 * 1000,
maxAttempts: 8,
lockoutMs: 30 * 60 * 1000
})
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'
})
}
// Prüfe ob Benutzer eingeloggt ist
let isLoggedIn = false
try {
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace('Bearer ', '')
if (token) {
const user = await getUserFromToken(token)
if (user && user.active) {
isLoggedIn = true
}
}
} catch (_e) {
// Nicht eingeloggt - kein Problem
}
// Prüfe ob Gruppe für Abonnements verfügbar ist
if (group.type !== 'subscription') {
throw createError({
statusCode: 403,
statusMessage: 'Diese Newsletter-Gruppe ist nicht für Abonnements verfügbar'
})
}
// Nicht eingeloggte Benutzer können sich nur für externe Newsletter anmelden
if (!isLoggedIn && group.sendToExternal !== true) {
throw createError({
statusCode: 403,
statusMessage: 'Diese Newsletter-Gruppe ist nur für Mitglieder verfügbar. Bitte melden Sie sich an.'
})
}
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) {
await registerRateLimitFailure(event, { name: 'newsletter:subscribe:ip', keyParts: [ip] })
await registerRateLimitFailure(event, { name: 'newsletter:subscribe:email', keyParts: [emailKey] })
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)
registerRateLimitSuccess(event, { name: 'newsletter:subscribe:email', keyParts: [emailKey] })
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) {
await registerRateLimitFailure(event, { name: 'newsletter:subscribe:ip', keyParts: [ip] })
await registerRateLimitFailure(event, { name: 'newsletter:subscribe:email', keyParts: [emailKey] })
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)
registerRateLimitSuccess(event, { name: 'newsletter:subscribe:email', keyParts: [emailKey] })
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
registerRateLimitSuccess(event, { name: 'newsletter:subscribe:email', keyParts: [emailKey] })
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)
registerRateLimitSuccess(event, { name: 'newsletter:subscribe:email', keyParts: [emailKey] })
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)
registerRateLimitSuccess(event, { name: 'newsletter:subscribe:email', keyParts: [emailKey] })
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>
`
})
}