import fs from 'fs/promises' import path from 'path' import { readMembers } from './members.js' import { readUsers } from './auth.js' import { encryptObject, decryptObject } from './encryption.js' import crypto from 'crypto' // 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_SUBSCRIBERS_FILE = getDataPath('newsletter-subscribers.json') function getEncryptionKey() { return process.env.ENCRYPTION_KEY || 'local_development_encryption_key_change_in_production' } // Prüft ob Daten verschlüsselt sind function isEncrypted(data) { try { const parsed = JSON.parse(data.trim()) if (Array.isArray(parsed)) { return false } if (typeof parsed === 'object' && parsed !== null && !parsed.encryptedData) { return false } return false } catch (e) { return true } } export async function readSubscribers() { try { const data = await fs.readFile(NEWSLETTER_SUBSCRIBERS_FILE, 'utf-8') const encrypted = isEncrypted(data) if (encrypted) { const encryptionKey = getEncryptionKey() try { return decryptObject(data, encryptionKey) } catch (decryptError) { console.error('Fehler beim Entschlüsseln der Newsletter-Abonnenten:', decryptError) try { const plainData = JSON.parse(data) console.warn('Entschlüsselung fehlgeschlagen, versuche als unverschlüsseltes Format zu lesen') return plainData } catch (parseError) { console.error('Konnte Newsletter-Abonnenten weder entschlüsseln noch als JSON lesen') return [] } } } else { // Plain JSON - migriere zu verschlüsselter Speicherung const subscribers = JSON.parse(data) console.log('Migriere unverschlüsselte Newsletter-Abonnenten zu verschlüsselter Speicherung...') await writeSubscribers(subscribers) return subscribers } } catch (error) { if (error.code === 'ENOENT') { return [] } throw error } } export async function writeSubscribers(subscribers) { try { const encryptionKey = getEncryptionKey() const encryptedData = encryptObject(subscribers, encryptionKey) await fs.writeFile(NEWSLETTER_SUBSCRIBERS_FILE, encryptedData, 'utf-8') return true } catch (error) { console.error('Fehler beim Schreiben der Newsletter-Abonnenten:', error) return false } } // Berechnet Alter aus Geburtsdatum function calculateAge(geburtsdatum) { if (!geburtsdatum) return null try { const birthDate = new Date(geburtsdatum) const today = new Date() let age = today.getFullYear() - birthDate.getFullYear() const monthDiff = today.getMonth() - birthDate.getMonth() if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { age-- } return age } catch { return null } } // Filtert den Admin-User aus Empfängerliste heraus function filterAdminUser(recipients) { return recipients.filter(r => { const email = (r.email || '').toLowerCase().trim() return email !== 'admin@harheimertc.de' }) } // Filtert Mitglieder nach Zielgruppe export async function getRecipientsByGroup(targetGroup) { const members = await readMembers() const users = await readUsers() let recipients = [] switch (targetGroup) { case 'alle': // Alle Mitglieder mit E-Mail recipients = members .filter(m => m.email && m.email.trim() !== '') .map(m => ({ email: m.email, name: `${m.firstName || ''} ${m.lastName || ''}`.trim() || m.name || '' })) // Auch alle aktiven Benutzer hinzufügen users .filter(u => u.active && u.email) .forEach(u => { if (!recipients.find(r => r.email.toLowerCase() === u.email.toLowerCase())) { recipients.push({ email: u.email, name: u.name || '' }) } }) break case 'erwachsene': // Mitglieder über 18 Jahre recipients = members .filter(m => { if (!m.email || !m.email.trim()) return false const age = calculateAge(m.geburtsdatum) return age !== null && age >= 18 }) .map(m => ({ email: m.email.trim(), name: `${m.firstName || ''} ${m.lastName || ''}`.trim() || m.name || '' })) // Auch aktive Benutzer hinzufügen (als Erwachsene behandelt, wenn kein Geburtsdatum) users .filter(u => u.active && u.email && u.email.trim()) .forEach(u => { // Prüfe ob bereits vorhanden if (!recipients.find(r => r.email.toLowerCase() === u.email.toLowerCase().trim())) { recipients.push({ email: u.email.trim(), name: u.name || '' }) } }) break case 'nachwuchs': // Mitglieder unter 18 Jahre recipients = members .filter(m => { if (!m.email || !m.email.trim()) return false const age = calculateAge(m.geburtsdatum) return age !== null && age < 18 }) .map(m => ({ email: m.email, name: `${m.firstName || ''} ${m.lastName || ''}`.trim() || m.name || '' })) break case 'mannschaftsspieler': // Mitglieder die in einer Mannschaft spielen recipients = members .filter(m => { if (!m.email || !m.email.trim()) return false // Prüfe ob als Mannschaftsspieler markiert if (m.isMannschaftsspieler === true) { return true } // Fallback: Prüfe ob in notes etwas über Mannschaft steht (für Rückwärtskompatibilität) const notes = (m.notes || '').toLowerCase() return notes.includes('mannschaft') || notes.includes('spieler') }) .map(m => ({ email: m.email, name: `${m.firstName || ''} ${m.lastName || ''}`.trim() || m.name || '' })) break case 'vorstand': // Nur Vorstand (aus users.json) recipients = users .filter(u => { if (!u.active || !u.email) return false const roles = Array.isArray(u.roles) ? u.roles : (u.role ? [u.role] : []) return roles.includes('admin') || roles.includes('vorstand') }) .map(u => ({ email: u.email, name: u.name || '' })) break default: recipients = [] } // Admin-User herausfiltern return filterAdminUser(recipients) } // Holt Newsletter-Abonnenten (bestätigt und nicht abgemeldet) export async function getNewsletterSubscribers(internalOnly = false, groupId = null) { const subscribers = await readSubscribers() let confirmedSubscribers = subscribers.filter(s => { if (!s.confirmed || s.unsubscribedAt) { return false } // Wenn groupId angegeben ist, prüfe ob Subscriber für diese Gruppe angemeldet ist if (groupId) { // Stelle sicher, dass groupIds existiert (für Rückwärtskompatibilität) if (!s.groupIds || !Array.isArray(s.groupIds)) { return false } return s.groupIds.includes(groupId) } // Wenn keine groupId angegeben, prüfe ob Subscriber für mindestens eine Gruppe angemeldet ist // (für Rückwärtskompatibilität: wenn keine groupIds vorhanden, als abonniert behandeln) if (s.groupIds && Array.isArray(s.groupIds)) { return s.groupIds.length > 0 } // Rückwärtskompatibilität: alte Subscriber ohne groupIds werden als abonniert behandelt return true }) if (internalOnly) { // Nur interne Abonnenten (die auch Mitglieder sind) const members = await readMembers() const memberEmails = new Set( members .filter(m => m.email) .map(m => m.email.toLowerCase()) ) confirmedSubscribers = confirmedSubscribers.filter(s => memberEmails.has(s.email.toLowerCase()) ) } const result = confirmedSubscribers.map(s => ({ email: s.email, name: s.name || '' })) // Admin-User herausfiltern return filterAdminUser(result) } // Generiert Abmelde-Token für Abonnenten export async function generateUnsubscribeToken(email) { const subscribers = await readSubscribers() const subscriber = subscribers.find(s => s.email.toLowerCase() === email.toLowerCase()) if (!subscriber) { return null } if (!subscriber.unsubscribeToken) { subscriber.unsubscribeToken = crypto.randomBytes(32).toString('hex') await writeSubscribers(subscribers) } return subscriber.unsubscribeToken }