Update dependencies to include TinyMCE and Quill, enhance Navigation component with a new Newsletter submenu, and implement role-based access control for CMS features. Refactor user role handling to support multiple roles and improve user management functionality across various API endpoints.
This commit is contained in:
287
server/utils/newsletter.js
Normal file
287
server/utils/newsletter.js
Normal file
@@ -0,0 +1,287 @@
|
||||
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'
|
||||
|
||||
const getDataPath = (filename) => {
|
||||
const cwd = process.cwd()
|
||||
if (cwd.endsWith('.output')) {
|
||||
return path.join(cwd, '../server/data', filename)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user