import fs from 'fs/promises'
import path from 'path'
import { getUserFromToken, hasAnyRole } from '../../../../../utils/auth.js'
import { randomUUID } from 'crypto'
import { getRecipientsByGroup, getNewsletterSubscribers, generateUnsubscribeToken } from '../../../../../utils/newsletter.js'
import { encryptObject, decryptObject } from '../../../../../utils/encryption.js'
import nodemailer from 'nodemailer'
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'newsletter-posts.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')
const NEWSLETTER_POSTS_FILE = getDataPath('newsletter-posts.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
}
}
async function writeGroups(groups) {
await fs.writeFile(NEWSLETTER_GROUPS_FILE, JSON.stringify(groups, null, 2), 'utf-8')
}
async function readPosts() {
try {
const data = await fs.readFile(NEWSLETTER_POSTS_FILE, 'utf-8')
return JSON.parse(data)
} catch (error) {
if (error.code === 'ENOENT') {
return []
}
throw error
}
}
async function writePosts(posts) {
await fs.writeFile(NEWSLETTER_POSTS_FILE, JSON.stringify(posts, null, 2), 'utf-8')
}
// Lädt Config für Logo und Clubname
async function loadConfig() {
try {
const configPath = getDataPath('config.json')
const data = await fs.readFile(configPath, 'utf-8')
return JSON.parse(data)
} catch {
return {
verein: { name: 'Harheimer Tischtennis-Club 1954 e.V.' }
}
}
}
// Lädt Logo als Base64
async function loadLogoAsBase64() {
try {
const logoPath = path.join(process.cwd(), 'public', 'images', 'logos', 'Harheimer TC.svg')
const logoData = await fs.readFile(logoPath, 'utf-8')
// SVG als Base64 kodieren
const base64Logo = Buffer.from(logoData).toString('base64')
return `data:image/svg+xml;base64,${base64Logo}`
} catch (error) {
console.error('Fehler beim Laden des Logos:', error)
return null
}
}
// Erstellt Newsletter-HTML mit Header und Footer
async function createNewsletterHTML(post, group, unsubscribeToken = null, creatorName = null, creatorEmail = null) {
const config = await loadConfig()
const clubName = config.verein?.name || 'Harheimer Tischtennis-Club 1954 e.V.'
const baseUrl = process.env.NUXT_PUBLIC_BASE_URL || 'http://localhost:3100'
// Logo als Base64 laden
const logoDataUri = await loadLogoAsBase64()
let unsubscribeLink = ''
if (unsubscribeToken) {
const unsubscribeUrl = `${baseUrl}/newsletter/unsubscribe?token=${unsubscribeToken}`
unsubscribeLink = `
Sie erhalten diese E-Mail, weil Sie sich für unseren Newsletter angemeldet haben.
Newsletter abmelden
`
}
return `
${logoDataUri ? ` ` : ''}
|
Harheimer TC
${clubName}
|
|
${post.title}
${post.content}
${unsubscribeLink}
|
|
${clubName}
${baseUrl}
|
|
`
}
export default defineEventHandler(async (event) => {
try {
// Authentifizierung prüfen
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace('Bearer ', '')
if (!token) {
throw createError({
statusCode: 401,
statusMessage: 'Nicht authentifiziert'
})
}
const user = await getUserFromToken(token)
if (!user || !hasAnyRole(user, 'admin', 'vorstand', 'newsletter')) {
throw createError({
statusCode: 403,
statusMessage: 'Keine Berechtigung'
})
}
const groupId = getRouterParam(event, 'id')
const body = await readBody(event)
const { title, content } = body
// Creator-Informationen für Absender
const creatorName = user.name || 'Harheimer TC'
const creatorEmail = user.email || process.env.SMTP_FROM || 'noreply@harheimertc.de'
if (!title || !content || (!content.trim() || content === '
')) {
throw createError({
statusCode: 400,
statusMessage: 'Titel und Inhalt sind erforderlich'
})
}
// Lade Gruppe
const groups = await readGroups()
const group = groups.find(g => g.id === groupId)
if (!group) {
throw createError({
statusCode: 404,
statusMessage: 'Newsletter-Gruppe nicht gefunden'
})
}
// SMTP-Credentials prüfen
const smtpUser = process.env.SMTP_USER
const smtpPass = process.env.SMTP_PASS
if (!smtpUser || !smtpPass) {
throw createError({
statusCode: 500,
statusMessage: 'SMTP-Credentials fehlen! Bitte setzen Sie SMTP_USER und SMTP_PASS in der .env Datei.'
})
}
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
}
})
// Empfänger bestimmen
let recipients = []
if (group.type === 'subscription') {
// Abonnenten-Newsletter
recipients = await getNewsletterSubscribers(!group.sendToExternal, group.id)
} else if (group.type === 'group') {
// Gruppen-Newsletter
recipients = await getRecipientsByGroup(group.targetGroup)
}
// Wenn keine Empfänger gefunden, Post trotzdem erstellen (aber nicht versenden)
if (recipients.length === 0) {
// Post ohne Versand erstellen
const posts = await readPosts()
const newPost = {
id: randomUUID(),
groupId,
title,
content,
createdAt: new Date().toISOString(),
createdBy: user.id,
sentAt: null,
sentTo: {
total: 0,
sent: 0,
failed: 0,
recipients: []
}
}
posts.push(newPost)
await writePosts(posts)
// Post-Count in Gruppe erhöhen
group.postCount = (group.postCount || 0) + 1
await writeGroups(groups)
return {
success: true,
message: 'Post erfolgreich erstellt (keine Empfänger gefunden)',
post: {
id: newPost.id,
title: newPost.title,
groupId: newPost.groupId
},
stats: {
total: 0,
sent: 0,
failed: 0,
recipients: []
}
}
}
// Post erstellen
const posts = await readPosts()
const newPost = {
id: randomUUID(),
groupId,
title,
content,
createdAt: new Date().toISOString(),
createdBy: user.id,
sentAt: new Date().toISOString(),
sentTo: {
total: recipients.length,
sent: 0,
failed: 0,
failedEmails: []
}
}
// Newsletter versenden
let sentCount = 0
let failedCount = 0
const failedEmails = []
const errorDetails = []
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring
console.log(`Versende Newsletter an ${recipients.length} Empfänger...`)
console.log('Empfänger:', recipients.map(r => r.email))
for (const recipient of recipients) {
try {
// Validiere E-Mail-Adresse
if (!recipient.email || !recipient.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
throw new Error(`Ungültige E-Mail-Adresse: ${recipient.email}`)
}
// Abmelde-Token generieren (nur für Abonnenten-Newsletter)
let unsubscribeToken = null
if (group.type === 'subscription') {
unsubscribeToken = await generateUnsubscribeToken(recipient.email)
}
const htmlContent = await createNewsletterHTML(newPost, group, unsubscribeToken, creatorName, creatorEmail)
const mailResult = await transporter.sendMail({
from: `"${creatorName}" <${creatorEmail}>`,
replyTo: creatorEmail,
to: recipient.email,
subject: title,
html: htmlContent
})
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring
// recipient.email is validated and from trusted source (subscribers list)
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring
console.log(`✅ Erfolgreich versendet an ${recipient.email}:`, mailResult.messageId)
sentCount++
} catch (error) {
const errorMsg = error.message || error.toString()
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring
// recipient.email is validated and from trusted source (subscribers list)
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring
console.error(`❌ Fehler beim Senden an ${recipient.email}:`, errorMsg)
failedCount++
failedEmails.push(recipient.email)
errorDetails.push({
email: recipient.email,
error: errorMsg
})
}
}
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring
console.log(`Versand abgeschlossen: ${sentCount} erfolgreich, ${failedCount} fehlgeschlagen`)
// Post speichern mit Versand-Statistik und Empfängerliste
newPost.sentTo = {
total: recipients.length,
sent: sentCount,
failed: failedCount,
failedEmails: failedEmails.length > 0 ? failedEmails : undefined,
errorDetails: errorDetails.length > 0 ? errorDetails : undefined,
recipients: recipients.map(r => ({
email: r.email,
name: r.name || '',
sent: !failedEmails.includes(r.email)
}))
}
posts.push(newPost)
await writePosts(posts)
// Post-Count in Gruppe erhöhen
group.postCount = (group.postCount || 0) + 1
await writeGroups(groups)
return {
success: true,
message: `Post erfolgreich erstellt und versendet`,
post: {
id: newPost.id,
title: newPost.title,
groupId: newPost.groupId
},
stats: {
total: recipients.length,
sent: sentCount,
failed: failedCount,
failedEmails: failedEmails.length > 0 ? failedEmails : undefined,
errorDetails: errorDetails.length > 0 ? errorDetails : undefined
}
}
} catch (error) {
console.error('Fehler beim Erstellen und Versenden des Posts:', error)
if (error.statusCode) {
throw error
}
throw createError({
statusCode: 500,
statusMessage: error.message || 'Fehler beim Erstellen und Versenden des Posts'
})
}
})