Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 41s
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.
284 lines
10 KiB
JavaScript
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>
|
|
`
|
|
})
|
|
}
|
|
|