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:
Torsten Schulz (local)
2025-12-19 09:51:28 +01:00
parent baf6c59c0d
commit 435e28fd55
69 changed files with 5034 additions and 276 deletions

View File

@@ -4,6 +4,27 @@ import { promises as fs } from 'fs'
import path from 'path'
import { encryptObject, decryptObject } from './encryption.js'
// Export migrateUserRoles für Verwendung in anderen Modulen
export function migrateUserRoles(user) {
if (!user) return user
// Wenn bereits roles Array vorhanden, nichts tun
if (Array.isArray(user.roles)) {
return user
}
// Wenn role vorhanden, zu roles Array konvertieren
if (user.role) {
user.roles = [user.role]
delete user.role
} else {
// Fallback: Standard-Rolle
user.roles = ['mitglied']
}
return user
}
const JWT_SECRET = process.env.JWT_SECRET || 'harheimertc-secret-key-change-in-production'
// Handle both dev and production paths
@@ -50,17 +71,17 @@ export async function readUsers() {
const data = await fs.readFile(USERS_FILE, 'utf-8')
const encrypted = isEncrypted(data)
let users = []
if (encrypted) {
const encryptionKey = getEncryptionKey()
try {
return decryptObject(data, encryptionKey)
users = decryptObject(data, encryptionKey)
} catch (decryptError) {
console.error('Fehler beim Entschlüsseln der Benutzerdaten:', decryptError)
try {
const plainData = JSON.parse(data)
users = JSON.parse(data)
console.warn('Entschlüsselung fehlgeschlagen, versuche als unverschlüsseltes Format zu lesen')
return plainData
} catch (parseError) {
console.error('Konnte Benutzerdaten weder entschlüsseln noch als JSON lesen')
return []
@@ -68,14 +89,30 @@ export async function readUsers() {
}
} else {
// Plain JSON - migrate to encrypted format
const users = JSON.parse(data)
users = JSON.parse(data)
console.log('Migriere unverschlüsselte Benutzerdaten zu verschlüsselter Speicherung...')
// Write back encrypted
await writeUsers(users)
return users
}
// Migriere Rollen von role zu roles Array
let needsMigration = false
users = users.map(user => {
const migrated = migrateUserRoles(user)
if (!Array.isArray(user.roles) && user.role) {
needsMigration = true
}
return migrated
})
// Wenn Migration nötig war, speichere zurück
if (needsMigration) {
console.log('Migriere Rollen von role zu roles Array...')
await writeUsers(users)
} else if (!encrypted) {
// Write back encrypted wenn noch nicht verschlüsselt
await writeUsers(users)
}
return users
} catch (error) {
if (error.code === 'ENOENT') {
return []
@@ -98,21 +135,65 @@ export async function writeUsers(users) {
}
}
// Read sessions from file
// Prüft ob Sessions-Daten verschlüsselt sind
function isSessionsEncrypted(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
}
}
// Read sessions from file (with encryption support)
export async function readSessions() {
try {
const data = await fs.readFile(SESSIONS_FILE, 'utf-8')
return JSON.parse(data)
const encrypted = isSessionsEncrypted(data)
if (encrypted) {
const encryptionKey = getEncryptionKey()
try {
return decryptObject(data, encryptionKey)
} catch (decryptError) {
console.error('Fehler beim Entschlüsseln der Sessions:', 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 Sessions weder entschlüsseln noch als JSON lesen')
return []
}
}
} else {
// Plain JSON - migriere zu verschlüsselter Speicherung
const sessions = JSON.parse(data)
console.log('Migriere unverschlüsselte Sessions zu verschlüsselter Speicherung...')
await writeSessions(sessions)
return sessions
}
} catch (error) {
if (error.code === 'ENOENT') {
return []
}
console.error('Fehler beim Lesen der Sessions:', error)
return []
}
}
// Write sessions to file
// Write sessions to file (always encrypted)
export async function writeSessions(sessions) {
try {
await fs.writeFile(SESSIONS_FILE, JSON.stringify(sessions, null, 2), 'utf-8')
const encryptionKey = getEncryptionKey()
const encryptedData = encryptObject(sessions, encryptionKey)
await fs.writeFile(SESSIONS_FILE, encryptedData, 'utf-8')
return true
} catch (error) {
console.error('Fehler beim Schreiben der Sessions:', error)
@@ -133,11 +214,15 @@ export async function verifyPassword(password, hash) {
// Generate JWT token
export function generateToken(user) {
// Stelle sicher, dass Rollen migriert sind
const migratedUser = migrateUserRoles({ ...user })
const roles = Array.isArray(migratedUser.roles) ? migratedUser.roles : (migratedUser.role ? [migratedUser.role] : ['mitglied'])
return jwt.sign(
{
id: user.id,
email: user.email,
role: user.role
roles: roles
},
JWT_SECRET,
{ expiresIn: '7d' }
@@ -156,13 +241,37 @@ export function verifyToken(token) {
// Get user by ID
export async function getUserById(id) {
const users = await readUsers()
return users.find(u => u.id === id)
const user = users.find(u => u.id === id)
return user ? migrateUserRoles(user) : null
}
// Get user by email
export async function getUserByEmail(email) {
const users = await readUsers()
return users.find(u => u.email === email)
const user = users.find(u => u.email === email)
return user ? migrateUserRoles(user) : null
}
// Prüft ob Benutzer eine bestimmte Rolle hat
export function hasRole(user, role) {
if (!user) return false
const roles = Array.isArray(user.roles) ? user.roles : (user.role ? [user.role] : [])
return roles.includes(role)
}
// Prüft ob Benutzer eine der angegebenen Rollen hat
export function hasAnyRole(user, ...roles) {
if (!user) return false
const userRoles = Array.isArray(user.roles) ? user.roles : (user.role ? [user.role] : [])
return roles.some(role => userRoles.includes(role))
}
// Prüft ob Benutzer alle angegebenen Rollen hat
export function hasAllRoles(user, ...roles) {
if (!user) return false
const userRoles = Array.isArray(user.roles) ? user.roles : (user.role ? [user.role] : [])
return roles.every(role => userRoles.includes(role))
}
// Get user from token
@@ -171,7 +280,14 @@ export async function getUserFromToken(token) {
if (!decoded) return null
const users = await readUsers()
return users.find(u => u.id === decoded.id)
const user = users.find(u => u.id === decoded.id)
// Migriere Rollen beim Laden
if (user) {
migrateUserRoles(user)
}
return user
}
// Create session

287
server/utils/newsletter.js Normal file
View 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
}