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 Logo` : ''}

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' }) } })