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:
@@ -1,4 +1,4 @@
|
||||
import { readUsers, writeUsers, verifyPassword, generateToken, createSession } from '../../utils/auth.js'
|
||||
import { readUsers, writeUsers, verifyPassword, generateToken, createSession, migrateUserRoles } from '../../utils/auth.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
@@ -59,6 +59,10 @@ export default defineEventHandler(async (event) => {
|
||||
maxAge: 60 * 60 * 24 * 7 // 7 days
|
||||
})
|
||||
|
||||
// Migriere Rollen falls nötig
|
||||
const migratedUser = migrateUserRoles({ ...user })
|
||||
const roles = Array.isArray(migratedUser.roles) ? migratedUser.roles : (migratedUser.role ? [migratedUser.role] : ['mitglied'])
|
||||
|
||||
// Return user data (without password) and token for API usage
|
||||
return {
|
||||
success: true,
|
||||
@@ -67,8 +71,10 @@ export default defineEventHandler(async (event) => {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role
|
||||
}
|
||||
roles: roles
|
||||
},
|
||||
// Rückwärtskompatibilität: erste Rolle als role
|
||||
role: roles[0] || 'mitglied'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login-Fehler:', error)
|
||||
|
||||
@@ -23,15 +23,19 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
}
|
||||
|
||||
const roles = Array.isArray(user.roles) ? user.roles : (user.role ? [user.role] : ['mitglied'])
|
||||
|
||||
return {
|
||||
isLoggedIn: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role
|
||||
roles: roles
|
||||
},
|
||||
role: user.role
|
||||
roles: roles,
|
||||
// Rückwärtskompatibilität: erste Rolle als role
|
||||
role: roles[0] || 'mitglied'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth-Status-Fehler:', error)
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { exec } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import { getUserFromToken } from '../../utils/auth.js'
|
||||
import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
|
||||
|
||||
const execAsync = promisify(exec)
|
||||
|
||||
@@ -62,7 +62,7 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
}
|
||||
|
||||
if (currentUser.role !== 'admin' && currentUser.role !== 'vorstand') {
|
||||
if (!hasAnyRole(currentUser, 'admin', 'vorstand')) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'Keine Berechtigung'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { getUserFromToken } from '../../utils/auth.js'
|
||||
import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
@@ -14,7 +14,7 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
}
|
||||
|
||||
if (currentUser.role !== 'admin' && currentUser.role !== 'vorstand') {
|
||||
if (!hasAnyRole(currentUser, 'admin', 'vorstand')) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'Keine Berechtigung'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import multer from 'multer'
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { getUserFromToken } from '../../utils/auth.js'
|
||||
import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
|
||||
|
||||
// Multer-Konfiguration für PDF-Uploads
|
||||
const storage = multer.diskStorage({
|
||||
@@ -57,7 +57,7 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
}
|
||||
|
||||
if (currentUser.role !== 'admin' && currentUser.role !== 'vorstand') {
|
||||
if (!hasAnyRole(currentUser, 'admin', 'vorstand')) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'Keine Berechtigung'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getUserFromToken, readUsers, writeUsers } from '../../../utils/auth.js'
|
||||
import { getUserFromToken, readUsers, writeUsers, hasAnyRole, migrateUserRoles } from '../../../utils/auth.js'
|
||||
import nodemailer from 'nodemailer'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -6,7 +6,7 @@ export default defineEventHandler(async (event) => {
|
||||
const token = getCookie(event, 'auth_token')
|
||||
const currentUser = await getUserFromToken(token)
|
||||
|
||||
if (!currentUser || (currentUser.role !== 'admin' && currentUser.role !== 'vorstand')) {
|
||||
if (!currentUser || !hasAnyRole(currentUser, 'admin')) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Zugriff verweigert'
|
||||
@@ -14,7 +14,7 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
|
||||
const body = await readBody(event)
|
||||
const { userId, role } = body
|
||||
const { userId, roles } = body
|
||||
|
||||
const users = await readUsers()
|
||||
const user = users.find(u => u.id === userId)
|
||||
@@ -26,9 +26,17 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
}
|
||||
|
||||
// Activate user and set role
|
||||
// Migriere Benutzer falls nötig
|
||||
migrateUserRoles(user)
|
||||
|
||||
// Activate user and set roles
|
||||
user.active = true
|
||||
user.role = role || 'mitglied'
|
||||
if (Array.isArray(roles) && roles.length > 0) {
|
||||
user.roles = roles
|
||||
} else {
|
||||
// Fallback: einzelne Rolle als Array
|
||||
user.roles = roles ? [roles] : ['mitglied']
|
||||
}
|
||||
|
||||
const updatedUsers = users.map(u => u.id === userId ? user : u)
|
||||
await writeUsers(updatedUsers)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { getUserFromToken, readUsers, writeUsers } from '../../../utils/auth.js'
|
||||
import { getUserFromToken, readUsers, writeUsers, hasAnyRole } from '../../../utils/auth.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const token = getCookie(event, 'auth_token')
|
||||
const currentUser = await getUserFromToken(token)
|
||||
|
||||
if (!currentUser || (currentUser.role !== 'admin' && currentUser.role !== 'vorstand')) {
|
||||
if (!currentUser || !hasAnyRole(currentUser, 'admin')) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Zugriff verweigert'
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { getUserFromToken, readUsers } from '../../../utils/auth.js'
|
||||
import { getUserFromToken, readUsers, hasAnyRole, migrateUserRoles } from '../../../utils/auth.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const token = getCookie(event, 'auth_token')
|
||||
const currentUser = await getUserFromToken(token)
|
||||
|
||||
if (!currentUser || (currentUser.role !== 'admin' && currentUser.role !== 'vorstand')) {
|
||||
if (!currentUser || !hasAnyRole(currentUser, 'admin')) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Zugriff verweigert'
|
||||
@@ -15,16 +15,21 @@ export default defineEventHandler(async (event) => {
|
||||
const users = await readUsers()
|
||||
|
||||
// Return users without passwords
|
||||
const safeUsers = users.map(u => ({
|
||||
id: u.id,
|
||||
email: u.email,
|
||||
name: u.name,
|
||||
role: u.role,
|
||||
phone: u.phone || '',
|
||||
active: u.active,
|
||||
created: u.created,
|
||||
lastLogin: u.lastLogin
|
||||
}))
|
||||
const safeUsers = users.map(u => {
|
||||
const migrated = migrateUserRoles({ ...u })
|
||||
const roles = Array.isArray(migrated.roles) ? migrated.roles : (migrated.role ? [migrated.role] : ['mitglied'])
|
||||
return {
|
||||
id: u.id,
|
||||
email: u.email,
|
||||
name: u.name,
|
||||
roles: roles,
|
||||
role: roles[0] || 'mitglied', // Rückwärtskompatibilität
|
||||
phone: u.phone || '',
|
||||
active: u.active,
|
||||
created: u.created,
|
||||
lastLogin: u.lastLogin
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
users: safeUsers
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { getUserFromToken, readUsers, writeUsers } from '../../../utils/auth.js'
|
||||
import { getUserFromToken, readUsers, writeUsers, hasAnyRole } from '../../../utils/auth.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const token = getCookie(event, 'auth_token')
|
||||
const currentUser = await getUserFromToken(token)
|
||||
|
||||
if (!currentUser || (currentUser.role !== 'admin' && currentUser.role !== 'vorstand')) {
|
||||
if (!currentUser || !hasAnyRole(currentUser, 'admin')) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Zugriff verweigert'
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { getUserFromToken, readUsers, writeUsers } from '../../../utils/auth.js'
|
||||
import { getUserFromToken, readUsers, writeUsers, hasAnyRole, migrateUserRoles } from '../../../utils/auth.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const token = getCookie(event, 'auth_token')
|
||||
const currentUser = await getUserFromToken(token)
|
||||
|
||||
if (!currentUser || (currentUser.role !== 'admin' && currentUser.role !== 'vorstand')) {
|
||||
if (!currentUser || !hasAnyRole(currentUser, 'admin')) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Zugriff verweigert'
|
||||
@@ -13,12 +13,15 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
|
||||
const body = await readBody(event)
|
||||
const { userId, role } = body
|
||||
const { userId, roles } = body
|
||||
|
||||
if (!['mitglied', 'vorstand', 'admin'].includes(role)) {
|
||||
const validRoles = ['mitglied', 'vorstand', 'admin', 'newsletter']
|
||||
const rolesArray = Array.isArray(roles) ? roles : (roles ? [roles] : ['mitglied'])
|
||||
|
||||
if (!rolesArray.every(r => validRoles.includes(r))) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: 'Ungültige Rolle'
|
||||
message: 'Ungültige Rolle(n)'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -32,7 +35,11 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
}
|
||||
|
||||
user.role = role
|
||||
// Migriere Benutzer falls nötig
|
||||
migrateUserRoles(user)
|
||||
|
||||
// Setze Rollen
|
||||
user.roles = rolesArray
|
||||
const updatedUsers = users.map(u => u.id === userId ? user : u)
|
||||
await writeUsers(updatedUsers)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { verifyToken, getUserById } from '../utils/auth.js'
|
||||
import { verifyToken, getUserById, hasAnyRole } from '../utils/auth.js'
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
@@ -33,7 +33,7 @@ export default defineEventHandler(async (event) => {
|
||||
const user = await getUserById(decoded.id)
|
||||
|
||||
// Only admin and vorstand can edit config
|
||||
if (!user || (user.role !== 'admin' && user.role !== 'vorstand')) {
|
||||
if (!user || !hasAnyRole(user, 'admin', 'vorstand')) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Keine Berechtigung zum Bearbeiten der Konfiguration.'
|
||||
|
||||
@@ -51,7 +51,7 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
|
||||
const user = await getUserFromToken(token)
|
||||
if (!user || (user.role !== 'admin' && user.role !== 'vorstand')) {
|
||||
if (!user || !hasAnyRole(user, 'admin', 'vorstand')) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'Keine Berechtigung zum Löschen von Bildern'
|
||||
|
||||
@@ -2,7 +2,7 @@ import multer from 'multer'
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import sharp from 'sharp'
|
||||
import { getUserFromToken, verifyToken } from '../../utils/auth.js'
|
||||
import { getUserFromToken, verifyToken, hasAnyRole } from '../../utils/auth.js'
|
||||
import { randomUUID } from 'crypto'
|
||||
|
||||
// Handle both dev and production paths
|
||||
@@ -90,7 +90,7 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
|
||||
const user = await getUserFromToken(token)
|
||||
if (!user || (user.role !== 'admin' && user.role !== 'vorstand')) {
|
||||
if (!user || !hasAnyRole(user, 'admin', 'vorstand')) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'Keine Berechtigung zum Hochladen von Bildern'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { verifyToken, getUserById } from '../utils/auth.js'
|
||||
import { verifyToken, getUserById, hasAnyRole } from '../utils/auth.js'
|
||||
import { deleteMember } from '../utils/members.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -24,7 +24,7 @@ export default defineEventHandler(async (event) => {
|
||||
const user = await getUserById(decoded.id)
|
||||
|
||||
// Only admin and vorstand can delete members
|
||||
if (!user || (user.role !== 'admin' && user.role !== 'vorstand')) {
|
||||
if (!user || !hasAnyRole(user, 'admin', 'vorstand')) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Keine Berechtigung zum Löschen von Mitgliedern.'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { verifyToken } from '../utils/auth.js'
|
||||
import { readMembers } from '../utils/members.js'
|
||||
import { readUsers } from '../utils/auth.js'
|
||||
import { readUsers, migrateUserRoles } from '../utils/auth.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
@@ -75,27 +75,33 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
if (matchedManualIndex !== -1) {
|
||||
// Merge with existing manual member
|
||||
const migratedUser = migrateUserRoles({ ...user })
|
||||
const roles = Array.isArray(migratedUser.roles) ? migratedUser.roles : (migratedUser.role ? [migratedUser.role] : ['mitglied'])
|
||||
mergedMembers[matchedManualIndex] = {
|
||||
...mergedMembers[matchedManualIndex],
|
||||
hasLogin: true,
|
||||
loginEmail: user.email,
|
||||
loginRole: user.role,
|
||||
loginRoles: roles,
|
||||
loginRole: roles[0] || 'mitglied', // Rückwärtskompatibilität
|
||||
lastLogin: user.lastLogin
|
||||
}
|
||||
} else {
|
||||
// Add as new member (from login system)
|
||||
const migratedUser = migrateUserRoles({ ...user })
|
||||
const roles = Array.isArray(migratedUser.roles) ? migratedUser.roles : (migratedUser.role ? [migratedUser.role] : ['mitglied'])
|
||||
mergedMembers.push({
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
phone: user.phone || '',
|
||||
address: '',
|
||||
notes: `Rolle: ${user.role}`,
|
||||
notes: `Rolle(n): ${roles.join(', ')}`,
|
||||
source: 'login',
|
||||
editable: false,
|
||||
hasLogin: true,
|
||||
loginEmail: user.email,
|
||||
loginRole: user.role,
|
||||
loginRoles: roles,
|
||||
loginRole: roles[0] || 'mitglied', // Rückwärtskompatibilität
|
||||
lastLogin: user.lastLogin
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { verifyToken, getUserById } from '../utils/auth.js'
|
||||
import { verifyToken, getUserById, hasAnyRole } from '../utils/auth.js'
|
||||
import { saveMember } from '../utils/members.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -40,7 +40,7 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
|
||||
// Only admin and vorstand can add/edit members
|
||||
if (user.role !== 'admin' && user.role !== 'vorstand') {
|
||||
if (!hasAnyRole(user, 'admin', 'vorstand')) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Keine Berechtigung zum Hinzufügen/Bearbeiten von Mitgliedern. Erforderlich: admin oder vorstand Rolle.'
|
||||
@@ -48,7 +48,7 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
|
||||
const body = await readBody(event)
|
||||
const { id, firstName, lastName, geburtsdatum, email, phone, address, notes } = body
|
||||
const { id, firstName, lastName, geburtsdatum, email, phone, address, notes, isMannschaftsspieler } = body
|
||||
|
||||
if (!firstName || !lastName) {
|
||||
throw createError({
|
||||
@@ -73,7 +73,8 @@ export default defineEventHandler(async (event) => {
|
||||
email: email || '',
|
||||
phone: phone || '',
|
||||
address: address || '',
|
||||
notes: notes || ''
|
||||
notes: notes || '',
|
||||
isMannschaftsspieler: isMannschaftsspieler === true || isMannschaftsspieler === 'true'
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { verifyToken, getUserById } from '../../utils/auth.js'
|
||||
import { verifyToken, getUserById, hasAnyRole } from '../../utils/auth.js'
|
||||
import { readMembers, writeMembers, normalizeDate } from '../../utils/members.js'
|
||||
import { randomUUID } from 'crypto'
|
||||
|
||||
@@ -59,7 +59,7 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
|
||||
// Only admin and vorstand can add members in bulk
|
||||
if (user.role !== 'admin' && user.role !== 'vorstand') {
|
||||
if (!hasAnyRole(user, 'admin', 'vorstand')) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Keine Berechtigung zum Bulk-Import von Mitgliedern. Erforderlich: admin oder vorstand Rolle.'
|
||||
|
||||
@@ -39,7 +39,8 @@ export default defineEventHandler(async (event) => {
|
||||
if (token) {
|
||||
// Authentifizierte Benutzer prüfen
|
||||
const user = await getUserFromToken(token)
|
||||
if (user && ['admin', 'vorstand'].includes(user.role)) {
|
||||
const roles = Array.isArray(user.roles) ? user.roles : (user.role ? [user.role] : [])
|
||||
if (user && (roles.includes('admin') || roles.includes('vorstand'))) {
|
||||
// Admin/Vorstand kann alle Dateien herunterladen
|
||||
isAuthorized = true
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { verifyToken, getUserById } from '../utils/auth.js'
|
||||
import { verifyToken, getUserById, hasAnyRole } from '../utils/auth.js'
|
||||
import { deleteNews } from '../utils/news.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -24,7 +24,7 @@ export default defineEventHandler(async (event) => {
|
||||
const user = await getUserById(decoded.id)
|
||||
|
||||
// Only admin and vorstand can delete news
|
||||
if (!user || (user.role !== 'admin' && user.role !== 'vorstand')) {
|
||||
if (!user || !hasAnyRole(user, 'admin', 'vorstand')) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Keine Berechtigung zum Löschen von News.'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { verifyToken, getUserById } from '../utils/auth.js'
|
||||
import { verifyToken, getUserById, hasAnyRole } from '../utils/auth.js'
|
||||
import { saveNews } from '../utils/news.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -24,7 +24,7 @@ export default defineEventHandler(async (event) => {
|
||||
const user = await getUserById(decoded.id)
|
||||
|
||||
// Only admin and vorstand can create/edit news
|
||||
if (!user || (user.role !== 'admin' && user.role !== 'vorstand')) {
|
||||
if (!user || !hasAnyRole(user, 'admin', 'vorstand')) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Keine Berechtigung zum Erstellen/Bearbeiten von News.'
|
||||
|
||||
89
server/api/newsletter/[id].delete.js
Normal file
89
server/api/newsletter/[id].delete.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
|
||||
|
||||
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 NEWSLETTERS_FILE = getDataPath('newsletters.json')
|
||||
|
||||
async function readNewsletters() {
|
||||
try {
|
||||
const data = await fs.readFile(NEWSLETTERS_FILE, 'utf-8')
|
||||
return JSON.parse(data)
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return []
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function writeNewsletters(newsletters) {
|
||||
await fs.writeFile(NEWSLETTERS_FILE, JSON.stringify(newsletters, null, 2), 'utf-8')
|
||||
}
|
||||
|
||||
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 newsletterId = getRouterParam(event, 'id')
|
||||
|
||||
const newsletters = await readNewsletters()
|
||||
const newsletterIndex = newsletters.findIndex(n => n.id === newsletterId)
|
||||
|
||||
if (newsletterIndex === -1) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Newsletter nicht gefunden'
|
||||
})
|
||||
}
|
||||
|
||||
// Nur Entwürfe können gelöscht werden
|
||||
if (newsletters[newsletterIndex].status === 'sent') {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Versendete Newsletter können nicht gelöscht werden'
|
||||
})
|
||||
}
|
||||
|
||||
newsletters.splice(newsletterIndex, 1)
|
||||
await writeNewsletters(newsletters)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Newsletter erfolgreich gelöscht'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen des Newsletters:', error)
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Fehler beim Löschen des Newsletters'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
98
server/api/newsletter/[id].put.js
Normal file
98
server/api/newsletter/[id].put.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
|
||||
|
||||
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 NEWSLETTERS_FILE = getDataPath('newsletters.json')
|
||||
|
||||
async function readNewsletters() {
|
||||
try {
|
||||
const data = await fs.readFile(NEWSLETTERS_FILE, 'utf-8')
|
||||
return JSON.parse(data)
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return []
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function writeNewsletters(newsletters) {
|
||||
await fs.writeFile(NEWSLETTERS_FILE, JSON.stringify(newsletters, null, 2), 'utf-8')
|
||||
}
|
||||
|
||||
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 newsletterId = getRouterParam(event, 'id')
|
||||
const body = await readBody(event)
|
||||
|
||||
const newsletters = await readNewsletters()
|
||||
const newsletterIndex = newsletters.findIndex(n => n.id === newsletterId)
|
||||
|
||||
if (newsletterIndex === -1) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Newsletter nicht gefunden'
|
||||
})
|
||||
}
|
||||
|
||||
// Nur Entwürfe können bearbeitet werden
|
||||
if (newsletters[newsletterIndex].status === 'sent') {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Versendete Newsletter können nicht bearbeitet werden'
|
||||
})
|
||||
}
|
||||
|
||||
// Update Newsletter
|
||||
newsletters[newsletterIndex] = {
|
||||
...newsletters[newsletterIndex],
|
||||
...body,
|
||||
updatedAt: new Date().toISOString(),
|
||||
updatedBy: user.id
|
||||
}
|
||||
|
||||
await writeNewsletters(newsletters)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Newsletter erfolgreich aktualisiert',
|
||||
newsletter: newsletters[newsletterIndex]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren des Newsletters:', error)
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Fehler beim Aktualisieren des Newsletters'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
268
server/api/newsletter/[id]/send.post.js
Normal file
268
server/api/newsletter/[id]/send.post.js
Normal file
@@ -0,0 +1,268 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { getUserFromToken, hasAnyRole } from '../../../utils/auth.js'
|
||||
import { getRecipientsByGroup, getNewsletterSubscribers, generateUnsubscribeToken } from '../../../utils/newsletter.js'
|
||||
import nodemailer from 'nodemailer'
|
||||
|
||||
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 NEWSLETTERS_FILE = getDataPath('newsletters.json')
|
||||
|
||||
async function readNewsletters() {
|
||||
try {
|
||||
const data = await fs.readFile(NEWSLETTERS_FILE, 'utf-8')
|
||||
return JSON.parse(data)
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return []
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function writeNewsletters(newsletters) {
|
||||
await fs.writeFile(NEWSLETTERS_FILE, JSON.stringify(newsletters, 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.' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Erstellt Newsletter-HTML mit Header und Footer
|
||||
async function createNewsletterHTML(newsletter, unsubscribeToken = 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'
|
||||
|
||||
let unsubscribeLink = ''
|
||||
if (unsubscribeToken) {
|
||||
const unsubscribeUrl = `${baseUrl}/newsletter/unsubscribe?token=${unsubscribeToken}`
|
||||
unsubscribeLink = `
|
||||
<div style="margin-top: 40px; padding-top: 20px; border-top: 1px solid #e5e7eb; text-align: center; color: #6b7280; font-size: 12px;">
|
||||
<p>Sie erhalten diese E-Mail, weil Sie sich für unseren Newsletter angemeldet haben.</p>
|
||||
<p style="margin-top: 10px;">
|
||||
<a href="${unsubscribeUrl}" style="color: #dc2626; text-decoration: underline;">Newsletter abmelden</a>
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #f3f4f6;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f3f4f6; padding: 20px 0;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<!-- Header -->
|
||||
<tr>
|
||||
<td style="background-color: #dc2626; padding: 30px; text-align: center;">
|
||||
<h1 style="margin: 0; color: #ffffff; font-size: 24px; font-weight: bold;">
|
||||
${clubName}
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Content -->
|
||||
<tr>
|
||||
<td style="padding: 30px;">
|
||||
<h2 style="margin: 0 0 20px 0; color: #111827; font-size: 20px;">
|
||||
${newsletter.title}
|
||||
</h2>
|
||||
<div style="color: #374151; line-height: 1.6;">
|
||||
${newsletter.content}
|
||||
</div>
|
||||
${unsubscribeLink}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td style="background-color: #f9fafb; padding: 20px; text-align: center; color: #6b7280; font-size: 12px; border-top: 1px solid #e5e7eb;">
|
||||
<p style="margin: 0;">
|
||||
${clubName}<br>
|
||||
<a href="${baseUrl}" style="color: #dc2626; text-decoration: none;">${baseUrl}</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
}
|
||||
|
||||
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 newsletterId = getRouterParam(event, 'id')
|
||||
|
||||
const newsletters = await readNewsletters()
|
||||
const newsletterIndex = newsletters.findIndex(n => n.id === newsletterId)
|
||||
|
||||
if (newsletterIndex === -1) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Newsletter nicht gefunden'
|
||||
})
|
||||
}
|
||||
|
||||
const newsletter = newsletters[newsletterIndex]
|
||||
|
||||
if (newsletter.status === 'sent') {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Newsletter wurde bereits versendet'
|
||||
})
|
||||
}
|
||||
|
||||
// Prüfe ob Newsletter Inhalt hat
|
||||
if (!newsletter.content || newsletter.content.trim() === '' || newsletter.content === '<p><br></p>') {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Newsletter hat keinen Inhalt. Bitte fügen Sie Inhalte hinzu, bevor Sie den Newsletter versenden.'
|
||||
})
|
||||
}
|
||||
|
||||
// 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 (newsletter.type === 'subscription') {
|
||||
// Abonnenten-Newsletter
|
||||
recipients = await getNewsletterSubscribers(!newsletter.sendToExternal)
|
||||
} else if (newsletter.type === 'group') {
|
||||
// Gruppen-Newsletter
|
||||
recipients = await getRecipientsByGroup(newsletter.targetGroup)
|
||||
}
|
||||
|
||||
if (recipients.length === 0) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Keine Empfänger gefunden'
|
||||
})
|
||||
}
|
||||
|
||||
// Newsletter versenden
|
||||
let sentCount = 0
|
||||
let failedCount = 0
|
||||
const failedEmails = []
|
||||
|
||||
for (const recipient of recipients) {
|
||||
try {
|
||||
// Abmelde-Token generieren (nur für Abonnenten-Newsletter)
|
||||
let unsubscribeToken = null
|
||||
if (newsletter.type === 'subscription') {
|
||||
unsubscribeToken = await generateUnsubscribeToken(recipient.email)
|
||||
}
|
||||
|
||||
const htmlContent = await createNewsletterHTML(newsletter, unsubscribeToken)
|
||||
|
||||
await transporter.sendMail({
|
||||
from: process.env.SMTP_FROM || 'noreply@harheimertc.de',
|
||||
to: recipient.email,
|
||||
subject: newsletter.title,
|
||||
html: htmlContent
|
||||
})
|
||||
|
||||
sentCount++
|
||||
} catch (error) {
|
||||
console.error(`Fehler beim Senden an ${recipient.email}:`, error)
|
||||
failedCount++
|
||||
failedEmails.push(recipient.email)
|
||||
}
|
||||
}
|
||||
|
||||
// Newsletter als versendet markieren
|
||||
newsletters[newsletterIndex].status = 'sent'
|
||||
newsletters[newsletterIndex].sentAt = new Date().toISOString()
|
||||
newsletters[newsletterIndex].sentTo = {
|
||||
total: recipients.length,
|
||||
sent: sentCount,
|
||||
failed: failedCount,
|
||||
failedEmails: failedEmails.length > 0 ? failedEmails : undefined
|
||||
}
|
||||
|
||||
await writeNewsletters(newsletters)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Newsletter erfolgreich versendet`,
|
||||
stats: {
|
||||
total: recipients.length,
|
||||
sent: sentCount,
|
||||
failed: failedCount,
|
||||
failedEmails: failedEmails.length > 0 ? failedEmails : undefined
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Versenden des Newsletters:', error)
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: error.message || 'Fehler beim Versenden des Newsletters'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
38
server/api/newsletter/check-subscription.get.js
Normal file
38
server/api/newsletter/check-subscription.get.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { readSubscribers } from '../../utils/newsletter.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const query = getQuery(event)
|
||||
const { email, groupId } = query
|
||||
|
||||
if (!email || !groupId) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'E-Mail und Gruppen-ID sind erforderlich'
|
||||
})
|
||||
}
|
||||
|
||||
const subscribers = await readSubscribers()
|
||||
const emailLower = email.toLowerCase()
|
||||
|
||||
const subscriber = subscribers.find(s => {
|
||||
const sEmail = (s.email || '').toLowerCase()
|
||||
return sEmail === emailLower && s.groupIds && s.groupIds.includes(groupId) && s.confirmed && !s.unsubscribedAt
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
subscribed: !!subscriber
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Prüfen der Newsletter-Anmeldung:', error)
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Fehler beim Prüfen der Newsletter-Anmeldung'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
62
server/api/newsletter/confirm.get.js
Normal file
62
server/api/newsletter/confirm.get.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import { readSubscribers, writeSubscribers } from '../../utils/newsletter.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const query = getQuery(event)
|
||||
const token = query.token
|
||||
|
||||
if (!token) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Bestätigungstoken fehlt'
|
||||
})
|
||||
}
|
||||
|
||||
const subscribers = await readSubscribers()
|
||||
const subscriber = subscribers.find(s => s.confirmationToken === token)
|
||||
|
||||
if (!subscriber) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Ungültiger Bestätigungstoken'
|
||||
})
|
||||
}
|
||||
|
||||
if (subscriber.confirmed) {
|
||||
// Bereits bestätigt
|
||||
return {
|
||||
success: true,
|
||||
alreadyConfirmed: true,
|
||||
message: 'Newsletter-Anmeldung wurde bereits bestätigt'
|
||||
}
|
||||
}
|
||||
|
||||
// Bestätigung durchführen
|
||||
subscriber.confirmed = true
|
||||
subscriber.confirmedAt = new Date().toISOString()
|
||||
subscriber.confirmationToken = null
|
||||
|
||||
// Stelle sicher, dass groupIds existiert (für Rückwärtskompatibilität)
|
||||
if (!subscriber.groupIds) {
|
||||
subscriber.groupIds = []
|
||||
}
|
||||
|
||||
await writeSubscribers(subscribers)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
alreadyConfirmed: false,
|
||||
message: 'Newsletter-Anmeldung erfolgreich bestätigt'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler bei Newsletter-Bestätigung:', error)
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Fehler bei der Newsletter-Bestätigung'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
115
server/api/newsletter/create.post.js
Normal file
115
server/api/newsletter/create.post.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
|
||||
import { randomUUID } 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 NEWSLETTERS_FILE = getDataPath('newsletters.json')
|
||||
|
||||
async function readNewsletters() {
|
||||
try {
|
||||
const data = await fs.readFile(NEWSLETTERS_FILE, 'utf-8')
|
||||
return JSON.parse(data)
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return []
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function writeNewsletters(newsletters) {
|
||||
await fs.writeFile(NEWSLETTERS_FILE, JSON.stringify(newsletters, null, 2), 'utf-8')
|
||||
}
|
||||
|
||||
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 body = await readBody(event)
|
||||
const { title, content, type, targetGroup, sendToExternal } = body
|
||||
|
||||
if (!title || !type) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Titel und Typ sind erforderlich'
|
||||
})
|
||||
}
|
||||
|
||||
if (type === 'subscription' && sendToExternal === undefined) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'sendToExternal ist für Abonnenten-Newsletter erforderlich'
|
||||
})
|
||||
}
|
||||
|
||||
if (type === 'group' && !targetGroup) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Zielgruppe ist für Gruppen-Newsletter erforderlich'
|
||||
})
|
||||
}
|
||||
|
||||
const newsletters = await readNewsletters()
|
||||
|
||||
const newNewsletter = {
|
||||
id: randomUUID(),
|
||||
title,
|
||||
content,
|
||||
type, // 'subscription' oder 'group'
|
||||
targetGroup: type === 'group' ? targetGroup : null, // 'alle', 'erwachsene', 'nachwuchs', 'mannschaftsspieler', 'vorstand'
|
||||
sendToExternal: type === 'subscription' ? sendToExternal : false, // true = auch extern, false = nur intern
|
||||
status: 'draft', // 'draft', 'sent'
|
||||
createdAt: new Date().toISOString(),
|
||||
createdBy: user.id,
|
||||
sentAt: null,
|
||||
sentTo: null
|
||||
}
|
||||
|
||||
newsletters.push(newNewsletter)
|
||||
await writeNewsletters(newsletters)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Newsletter erfolgreich erstellt',
|
||||
newsletter: {
|
||||
id: newNewsletter.id,
|
||||
title: newNewsletter.title,
|
||||
type: newNewsletter.type
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen des Newsletters:', error)
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Fehler beim Erstellen des Newsletters'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
401
server/api/newsletter/groups/[id]/posts/create.post.js
Normal file
401
server/api/newsletter/groups/[id]/posts/create.post.js
Normal file
@@ -0,0 +1,401 @@
|
||||
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'
|
||||
|
||||
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_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 = `
|
||||
<div style="margin-top: 40px; padding-top: 20px; border-top: 1px solid #e5e7eb; text-align: center; color: #6b7280; font-size: 12px;">
|
||||
<p>Sie erhalten diese E-Mail, weil Sie sich für unseren Newsletter angemeldet haben.</p>
|
||||
<p style="margin-top: 10px;">
|
||||
<a href="${unsubscribeUrl}" style="color: #dc2626; text-decoration: underline;">Newsletter abmelden</a>
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #f3f4f6;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f3f4f6; padding: 20px 0;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<!-- Header -->
|
||||
<tr>
|
||||
<td style="background: linear-gradient(to right, #111827, #991b1b, #111827); padding: 30px;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td width="50" valign="middle" style="padding-right: 15px;">
|
||||
${logoDataUri ? `<img src="${logoDataUri}" alt="Harheimer TC Logo" style="width: 50px; height: 50px; display: block;" />` : ''}
|
||||
</td>
|
||||
<td valign="middle">
|
||||
<h1 style="margin: 0; color: #ffffff; font-size: 24px; font-weight: bold; font-family: 'Montserrat', Arial, sans-serif;">
|
||||
Harheimer <span style="color: #fca5a5;">TC</span>
|
||||
</h1>
|
||||
<p style="margin: 5px 0 0 0; color: #e5e7eb; font-size: 14px;">
|
||||
${clubName}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Content -->
|
||||
<tr>
|
||||
<td style="padding: 30px;">
|
||||
<h2 style="margin: 0 0 20px 0; color: #111827; font-size: 20px;">
|
||||
${post.title}
|
||||
</h2>
|
||||
<div style="color: #374151; line-height: 1.6;">
|
||||
${post.content}
|
||||
</div>
|
||||
${unsubscribeLink}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td style="background-color: #f9fafb; padding: 20px; text-align: center; color: #6b7280; font-size: 12px; border-top: 1px solid #e5e7eb;">
|
||||
<p style="margin: 0;">
|
||||
${clubName}<br>
|
||||
<a href="${baseUrl}" style="color: #dc2626; text-decoration: none;">${baseUrl}</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
}
|
||||
|
||||
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 === '<p><br></p>')) {
|
||||
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 = []
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
console.log(`✅ Erfolgreich versendet an ${recipient.email}:`, mailResult.messageId)
|
||||
sentCount++
|
||||
} catch (error) {
|
||||
const errorMsg = error.message || error.toString()
|
||||
console.error(`❌ Fehler beim Senden an ${recipient.email}:`, errorMsg)
|
||||
failedCount++
|
||||
failedEmails.push(recipient.email)
|
||||
errorDetails.push({
|
||||
email: recipient.email,
|
||||
error: errorMsg
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
113
server/api/newsletter/groups/[id]/posts/list.get.js
Normal file
113
server/api/newsletter/groups/[id]/posts/list.get.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { getUserFromToken, hasAnyRole } from '../../../../../utils/auth.js'
|
||||
import { encryptObject, decryptObject } from '../../../../../utils/encryption.js'
|
||||
|
||||
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_POSTS_FILE = getDataPath('newsletter-posts.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
|
||||
}
|
||||
}
|
||||
|
||||
async function readPosts() {
|
||||
try {
|
||||
const data = await fs.readFile(NEWSLETTER_POSTS_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-Posts:', 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-Posts weder entschlüsseln noch als JSON lesen')
|
||||
return []
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Plain JSON - migriere zu verschlüsselter Speicherung
|
||||
const posts = JSON.parse(data)
|
||||
console.log('Migriere unverschlüsselte Newsletter-Posts zu verschlüsselter Speicherung...')
|
||||
// Schreiben wird hier nicht gemacht, da wir nur lesen
|
||||
return posts
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return []
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
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 posts = await readPosts()
|
||||
|
||||
// Filtere Posts nach Gruppe und sortiere nach Datum (neueste zuerst)
|
||||
const groupPosts = posts
|
||||
.filter(p => p.groupId === groupId)
|
||||
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
posts: groupPosts
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Posts:', error)
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Fehler beim Laden der Posts'
|
||||
})
|
||||
}
|
||||
})
|
||||
254
server/api/newsletter/groups/[id]/subscribers/add.post.js
Normal file
254
server/api/newsletter/groups/[id]/subscribers/add.post.js
Normal file
@@ -0,0 +1,254 @@
|
||||
import { getUserFromToken, hasAnyRole } from '../../../../../utils/auth.js'
|
||||
import { readSubscribers, writeSubscribers } from '../../../../../utils/newsletter.js'
|
||||
import { randomUUID } from 'crypto'
|
||||
import nodemailer from 'nodemailer'
|
||||
import crypto from 'crypto'
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
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_GROUPS_FILE = getDataPath('newsletter-groups.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
|
||||
}
|
||||
}
|
||||
|
||||
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 { email, name, customMessage } = body
|
||||
|
||||
if (!email || !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Ungültige E-Mail-Adresse'
|
||||
})
|
||||
}
|
||||
|
||||
// Prüfe ob Gruppe existiert und vom Typ 'subscription' ist
|
||||
const groups = await readGroups()
|
||||
const group = groups.find(g => g.id === groupId)
|
||||
|
||||
if (!group) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Newsletter-Gruppe nicht gefunden'
|
||||
})
|
||||
}
|
||||
|
||||
if (group.type !== 'subscription') {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Diese Funktion ist nur für Abonnenten-Newsletter verfügbar'
|
||||
})
|
||||
}
|
||||
|
||||
const subscribers = await readSubscribers()
|
||||
const emailLower = email.toLowerCase()
|
||||
|
||||
// Prüfe ob bereits für diese Gruppe angemeldet
|
||||
const existing = subscribers.find(s => {
|
||||
const sEmail = (s.email || '').toLowerCase()
|
||||
return sEmail === emailLower && s.groupIds && s.groupIds.includes(groupId)
|
||||
})
|
||||
|
||||
if (existing) {
|
||||
if (existing.confirmed) {
|
||||
throw createError({
|
||||
statusCode: 409,
|
||||
statusMessage: 'Diese E-Mail-Adresse ist bereits für diesen Newsletter angemeldet'
|
||||
})
|
||||
} else {
|
||||
// Bestätigungsmail erneut senden mit individueller Nachricht
|
||||
await sendConfirmationEmail(existing.email, existing.name || name, existing.confirmationToken, group.name, customMessage, user.name)
|
||||
return {
|
||||
success: true,
|
||||
message: 'Eine Bestätigungsmail wurde an die E-Mail-Adresse gesendet'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfe ob E-Mail bereits existiert (für andere Gruppe oder ohne Gruppe)
|
||||
const existingEmail = subscribers.find(s => (s.email || '').toLowerCase() === emailLower)
|
||||
|
||||
if (existingEmail) {
|
||||
// Bestehender Subscriber - Gruppe hinzufügen
|
||||
if (!existingEmail.groupIds) {
|
||||
existingEmail.groupIds = []
|
||||
}
|
||||
|
||||
if (existingEmail.groupIds.includes(groupId)) {
|
||||
// Bereits für diese Gruppe angemeldet
|
||||
if (existingEmail.confirmed) {
|
||||
throw createError({
|
||||
statusCode: 409,
|
||||
statusMessage: 'Diese E-Mail-Adresse ist bereits für diesen Newsletter angemeldet'
|
||||
})
|
||||
} else {
|
||||
// Bestätigungsmail erneut senden mit individueller Nachricht
|
||||
await sendConfirmationEmail(existingEmail.email, existingEmail.name || name, existingEmail.confirmationToken, group.name, customMessage, user.name)
|
||||
return {
|
||||
success: true,
|
||||
message: 'Eine Bestätigungsmail wurde an die E-Mail-Adresse gesendet'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gruppe hinzufügen
|
||||
existingEmail.groupIds.push(groupId)
|
||||
if (!existingEmail.confirmed) {
|
||||
// Neuer Bestätigungstoken für alle Gruppen
|
||||
existingEmail.confirmationToken = crypto.randomBytes(32).toString('hex')
|
||||
}
|
||||
existingEmail.name = name || existingEmail.name || ''
|
||||
|
||||
await writeSubscribers(subscribers)
|
||||
|
||||
if (existingEmail.confirmed) {
|
||||
// Bereits bestätigt - sofort aktiviert
|
||||
return {
|
||||
success: true,
|
||||
message: `Empfänger wurde erfolgreich für den Newsletter "${group.name}" hinzugefügt`
|
||||
}
|
||||
} else {
|
||||
// Bestätigungsmail senden mit individueller Nachricht
|
||||
await sendConfirmationEmail(existingEmail.email, existingEmail.name, existingEmail.confirmationToken, group.name, customMessage, user.name)
|
||||
return {
|
||||
success: true,
|
||||
message: 'Eine Bestätigungsmail wurde an die E-Mail-Adresse gesendet'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Neuer Abonnent
|
||||
const confirmationToken = crypto.randomBytes(32).toString('hex')
|
||||
const unsubscribeToken = crypto.randomBytes(32).toString('hex')
|
||||
const newSubscriber = {
|
||||
id: randomUUID(),
|
||||
email: emailLower,
|
||||
name: name || '',
|
||||
groupIds: [groupId],
|
||||
confirmed: false,
|
||||
confirmationToken,
|
||||
unsubscribeToken,
|
||||
subscribedAt: new Date().toISOString(),
|
||||
confirmedAt: null,
|
||||
unsubscribedAt: null
|
||||
}
|
||||
|
||||
subscribers.push(newSubscriber)
|
||||
await writeSubscribers(subscribers)
|
||||
|
||||
// Bestätigungsmail senden mit individueller Nachricht
|
||||
await sendConfirmationEmail(email, name, confirmationToken, group.name, customMessage, user.name)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Eine Bestätigungsmail wurde an die E-Mail-Adresse gesendet'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Hinzufügen des Empfängers:', error)
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Fehler beim Hinzufügen des Empfängers'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
async function sendConfirmationEmail(email, name, token, groupName, customMessage = null, inviterName = null) {
|
||||
const smtpUser = process.env.SMTP_USER
|
||||
const smtpPass = process.env.SMTP_PASS
|
||||
|
||||
if (!smtpUser || !smtpPass) {
|
||||
console.warn('SMTP-Credentials fehlen! Bestätigungsmail kann nicht gesendet werden.')
|
||||
return
|
||||
}
|
||||
|
||||
const baseUrl = process.env.NUXT_PUBLIC_BASE_URL || 'http://localhost:3100'
|
||||
const confirmationUrl = `${baseUrl}/newsletter/confirm?token=${token}`
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
// Individuelle Nachricht einbauen, falls vorhanden
|
||||
const customMessageHtml = customMessage
|
||||
? `<div style="background-color: #f3f4f6; padding: 15px; border-left: 4px solid #dc2626; margin: 20px 0;">
|
||||
<p style="margin: 0; color: #374151; font-style: italic;">${customMessage.replace(/\n/g, '<br>')}</p>
|
||||
</div>`
|
||||
: ''
|
||||
|
||||
const inviterText = inviterName
|
||||
? `<p style="margin-top: 20px; color: #666; font-size: 14px;">Sie wurden von ${inviterName} zum Newsletter eingeladen.</p>`
|
||||
: ''
|
||||
|
||||
await transporter.sendMail({
|
||||
from: process.env.SMTP_FROM || 'noreply@harheimertc.de',
|
||||
to: email,
|
||||
subject: `Newsletter-Anmeldung bestätigen - ${groupName} - Harheimer TC`,
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: #dc2626;">Newsletter-Anmeldung bestätigen</h2>
|
||||
<p>Hallo ${name || 'Liebe/r Abonnent/in'},</p>
|
||||
${inviterText}
|
||||
<p>vielen Dank für Ihre Anmeldung zum Newsletter "${groupName}" des Harheimer TC!</p>
|
||||
${customMessageHtml}
|
||||
<p>Bitte bestätigen Sie Ihre Anmeldung, indem Sie auf den folgenden Link klicken:</p>
|
||||
<p style="margin: 30px 0;">
|
||||
<a href="${confirmationUrl}" style="display: inline-block; padding: 12px 24px; background-color: #dc2626; color: white; text-decoration: none; border-radius: 5px;">
|
||||
Newsletter-Anmeldung bestätigen
|
||||
</a>
|
||||
</p>
|
||||
<p>Falls Sie sich nicht angemeldet haben, können Sie diese E-Mail ignorieren.</p>
|
||||
<p style="margin-top: 30px; color: #666; font-size: 12px;">
|
||||
Mit sportlichen Grüßen,<br>
|
||||
Ihr Harheimer Tischtennis-Club 1954 e.V.
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
115
server/api/newsletter/groups/[id]/subscribers/list.get.js
Normal file
115
server/api/newsletter/groups/[id]/subscribers/list.get.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { getUserFromToken, hasAnyRole } from '../../../../../utils/auth.js'
|
||||
import { readSubscribers } from '../../../../../utils/newsletter.js'
|
||||
|
||||
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_GROUPS_FILE = getDataPath('newsletter-groups.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
|
||||
}
|
||||
}
|
||||
|
||||
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')
|
||||
|
||||
// Prüfe ob Gruppe existiert und vom Typ 'subscription' ist
|
||||
const groups = await readGroups()
|
||||
const group = groups.find(g => g.id === groupId)
|
||||
|
||||
if (!group) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Newsletter-Gruppe nicht gefunden'
|
||||
})
|
||||
}
|
||||
|
||||
if (group.type !== 'subscription') {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Diese Funktion ist nur für Abonnenten-Newsletter verfügbar'
|
||||
})
|
||||
}
|
||||
|
||||
// Lade alle Abonnenten
|
||||
const subscribers = await readSubscribers()
|
||||
|
||||
// Filtere Abonnenten für diese Gruppe
|
||||
const groupSubscribers = subscribers
|
||||
.filter(s => {
|
||||
// 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)
|
||||
})
|
||||
.map(s => ({
|
||||
id: s.id,
|
||||
email: s.email,
|
||||
name: s.name || '',
|
||||
confirmed: s.confirmed || false,
|
||||
subscribedAt: s.subscribedAt || null,
|
||||
confirmedAt: s.confirmedAt || null,
|
||||
unsubscribedAt: s.unsubscribedAt || null
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
// Sortiere nach bestätigt, dann nach Datum
|
||||
if (a.confirmed !== b.confirmed) {
|
||||
return a.confirmed ? -1 : 1
|
||||
}
|
||||
if (a.subscribedAt && b.subscribedAt) {
|
||||
return new Date(b.subscribedAt) - new Date(a.subscribedAt)
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
subscribers: groupSubscribers
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Abonnenten:', error)
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Fehler beim Laden der Abonnenten'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
84
server/api/newsletter/groups/[id]/subscribers/remove.post.js
Normal file
84
server/api/newsletter/groups/[id]/subscribers/remove.post.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import { getUserFromToken, hasAnyRole } from '../../../../../utils/auth.js'
|
||||
import { readSubscribers, writeSubscribers } from '../../../../../utils/newsletter.js'
|
||||
|
||||
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 { subscriberId } = body
|
||||
|
||||
if (!subscriberId) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Abonnenten-ID ist erforderlich'
|
||||
})
|
||||
}
|
||||
|
||||
const subscribers = await readSubscribers()
|
||||
const subscriber = subscribers.find(s => s.id === subscriberId)
|
||||
|
||||
if (!subscriber) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Abonnent nicht gefunden'
|
||||
})
|
||||
}
|
||||
|
||||
// Stelle sicher, dass groupIds existiert
|
||||
if (!subscriber.groupIds || !Array.isArray(subscriber.groupIds)) {
|
||||
subscriber.groupIds = []
|
||||
}
|
||||
|
||||
// Entferne Gruppe aus groupIds
|
||||
const index = subscriber.groupIds.indexOf(groupId)
|
||||
if (index === -1) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Abonnent ist nicht für diese Gruppe angemeldet'
|
||||
})
|
||||
}
|
||||
|
||||
subscriber.groupIds.splice(index, 1)
|
||||
|
||||
// Wenn keine Gruppen mehr vorhanden, als abgemeldet markieren
|
||||
if (subscriber.groupIds.length === 0) {
|
||||
subscriber.unsubscribedAt = new Date().toISOString()
|
||||
subscriber.confirmed = false
|
||||
}
|
||||
|
||||
await writeSubscribers(subscribers)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Abonnent erfolgreich entfernt'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Entfernen des Abonnenten:', error)
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Fehler beim Entfernen des Abonnenten'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
117
server/api/newsletter/groups/create.post.js
Normal file
117
server/api/newsletter/groups/create.post.js
Normal file
@@ -0,0 +1,117 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { getUserFromToken, hasAnyRole } from '../../../utils/auth.js'
|
||||
import { randomUUID } 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_GROUPS_FILE = getDataPath('newsletter-groups.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')
|
||||
}
|
||||
|
||||
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 body = await readBody(event)
|
||||
const { name, type, targetGroup, sendToExternal, description } = body
|
||||
|
||||
if (!name || !type) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Name und Typ sind erforderlich'
|
||||
})
|
||||
}
|
||||
|
||||
if (type === 'subscription' && sendToExternal === undefined) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'sendToExternal ist für Abonnenten-Newsletter erforderlich'
|
||||
})
|
||||
}
|
||||
|
||||
if (type === 'group' && !targetGroup) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Zielgruppe ist für Gruppen-Newsletter erforderlich'
|
||||
})
|
||||
}
|
||||
|
||||
const groups = await readGroups()
|
||||
|
||||
// Prüfe ob Name bereits existiert
|
||||
if (groups.find(g => g.name.toLowerCase() === name.toLowerCase())) {
|
||||
throw createError({
|
||||
statusCode: 409,
|
||||
statusMessage: 'Eine Newsletter-Gruppe mit diesem Namen existiert bereits'
|
||||
})
|
||||
}
|
||||
|
||||
const newGroup = {
|
||||
id: randomUUID(),
|
||||
name,
|
||||
description: description || '',
|
||||
type, // 'subscription' oder 'group'
|
||||
targetGroup: type === 'group' ? targetGroup : null,
|
||||
sendToExternal: type === 'subscription' ? sendToExternal : false,
|
||||
createdAt: new Date().toISOString(),
|
||||
createdBy: user.id,
|
||||
postCount: 0
|
||||
}
|
||||
|
||||
groups.push(newGroup)
|
||||
await writeGroups(groups)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Newsletter-Gruppe erfolgreich erstellt',
|
||||
group: newGroup
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen der Newsletter-Gruppe:', error)
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Fehler beim Erstellen der Newsletter-Gruppe'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
64
server/api/newsletter/groups/list.get.js
Normal file
64
server/api/newsletter/groups/list.get.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { getUserFromToken, hasAnyRole } from '../../../utils/auth.js'
|
||||
|
||||
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_GROUPS_FILE = getDataPath('newsletter-groups.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
|
||||
}
|
||||
}
|
||||
|
||||
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 groups = await readGroups()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
groups
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Newsletter-Gruppen:', error)
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Fehler beim Laden der Newsletter-Gruppen'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
73
server/api/newsletter/groups/public-list.get.js
Normal file
73
server/api/newsletter/groups/public-list.get.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { getUserFromToken } from '../../../utils/auth.js'
|
||||
|
||||
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_GROUPS_FILE = getDataPath('newsletter-groups.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
|
||||
}
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
// Prüfe ob Benutzer eingeloggt ist
|
||||
let isLoggedIn = false
|
||||
try {
|
||||
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace('Bearer ', '')
|
||||
if (token) {
|
||||
const user = await getUserFromToken(token)
|
||||
if (user && user.active) {
|
||||
isLoggedIn = true
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Nicht eingeloggt - kein Problem
|
||||
}
|
||||
|
||||
const groups = await readGroups()
|
||||
|
||||
// Filtere Newsletter-Gruppen basierend auf Login-Status
|
||||
let publicGroups
|
||||
if (isLoggedIn) {
|
||||
// Eingeloggte Benutzer sehen alle Abonnenten-Newsletter (intern und extern)
|
||||
publicGroups = groups.filter(g => g.type === 'subscription')
|
||||
} else {
|
||||
// Nicht eingeloggte Benutzer sehen nur externe Newsletter
|
||||
publicGroups = groups.filter(g =>
|
||||
g.type === 'subscription' && g.sendToExternal === true
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
groups: publicGroups.map(g => ({
|
||||
id: g.id,
|
||||
name: g.name,
|
||||
description: g.description || ''
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der öffentlichen Newsletter-Gruppen:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Fehler beim Laden der Newsletter-Gruppen'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
67
server/api/newsletter/list.get.js
Normal file
67
server/api/newsletter/list.get.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
|
||||
|
||||
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 NEWSLETTERS_FILE = getDataPath('newsletters.json')
|
||||
|
||||
async function readNewsletters() {
|
||||
try {
|
||||
const data = await fs.readFile(NEWSLETTERS_FILE, 'utf-8')
|
||||
return JSON.parse(data)
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return []
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
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 newsletters = await readNewsletters()
|
||||
|
||||
// Sortiere nach Erstellungsdatum (neueste zuerst)
|
||||
newsletters.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
newsletters
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Newsletter:', error)
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Fehler beim Laden der Newsletter'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
228
server/api/newsletter/subscribe.post.js
Normal file
228
server/api/newsletter/subscribe.post.js
Normal file
@@ -0,0 +1,228 @@
|
||||
import { readSubscribers, writeSubscribers } from '../../utils/newsletter.js'
|
||||
import { randomUUID } from 'crypto'
|
||||
import nodemailer from 'nodemailer'
|
||||
import crypto from 'crypto'
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
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_GROUPS_FILE = getDataPath('newsletter-groups.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
|
||||
}
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const body = await readBody(event)
|
||||
const { email, name, groupId } = body
|
||||
|
||||
if (!email || !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Ungültige E-Mail-Adresse'
|
||||
})
|
||||
}
|
||||
|
||||
if (!groupId) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Newsletter-Gruppe muss ausgewählt werden'
|
||||
})
|
||||
}
|
||||
|
||||
// Prüfe ob Gruppe existiert und für externe Abonnements verfügbar ist
|
||||
const groups = await readGroups()
|
||||
const group = groups.find(g => g.id === groupId)
|
||||
|
||||
if (!group) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Newsletter-Gruppe nicht gefunden'
|
||||
})
|
||||
}
|
||||
|
||||
if (group.type !== 'subscription' || group.sendToExternal !== true) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'Diese Newsletter-Gruppe ist nicht für externe Abonnements verfügbar'
|
||||
})
|
||||
}
|
||||
|
||||
const subscribers = await readSubscribers()
|
||||
const emailLower = email.toLowerCase()
|
||||
|
||||
// Prüfe ob bereits für diese Gruppe angemeldet
|
||||
const existing = subscribers.find(s => {
|
||||
const sEmail = (s.email || '').toLowerCase()
|
||||
return sEmail === emailLower && s.groupIds && s.groupIds.includes(groupId)
|
||||
})
|
||||
|
||||
if (existing) {
|
||||
if (existing.confirmed) {
|
||||
throw createError({
|
||||
statusCode: 409,
|
||||
statusMessage: 'Sie sind bereits für diesen Newsletter angemeldet'
|
||||
})
|
||||
} else {
|
||||
// Bestätigungsmail erneut senden
|
||||
await sendConfirmationEmail(existing.email, existing.name || name, existing.confirmationToken, group.name)
|
||||
return {
|
||||
success: true,
|
||||
message: 'Eine Bestätigungsmail wurde an Ihre E-Mail-Adresse gesendet'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfe ob E-Mail bereits existiert (für andere Gruppe oder ohne Gruppe)
|
||||
const existingEmail = subscribers.find(s => (s.email || '').toLowerCase() === emailLower)
|
||||
|
||||
if (existingEmail) {
|
||||
// Bestehender Subscriber - Gruppe hinzufügen
|
||||
if (!existingEmail.groupIds) {
|
||||
existingEmail.groupIds = []
|
||||
}
|
||||
|
||||
if (existingEmail.groupIds.includes(groupId)) {
|
||||
// Bereits für diese Gruppe angemeldet
|
||||
if (existingEmail.confirmed) {
|
||||
throw createError({
|
||||
statusCode: 409,
|
||||
statusMessage: 'Sie sind bereits für diesen Newsletter angemeldet'
|
||||
})
|
||||
} else {
|
||||
// Bestätigungsmail erneut senden
|
||||
await sendConfirmationEmail(existingEmail.email, existingEmail.name || name, existingEmail.confirmationToken, group.name)
|
||||
return {
|
||||
success: true,
|
||||
message: 'Eine Bestätigungsmail wurde an Ihre E-Mail-Adresse gesendet'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gruppe hinzufügen
|
||||
existingEmail.groupIds.push(groupId)
|
||||
if (!existingEmail.confirmed) {
|
||||
// Neuer Bestätigungstoken für alle Gruppen
|
||||
existingEmail.confirmationToken = crypto.randomBytes(32).toString('hex')
|
||||
}
|
||||
existingEmail.name = name || existingEmail.name || ''
|
||||
|
||||
await writeSubscribers(subscribers)
|
||||
|
||||
if (existingEmail.confirmed) {
|
||||
// Bereits bestätigt - sofort aktiviert
|
||||
return {
|
||||
success: true,
|
||||
message: `Sie wurden erfolgreich für den Newsletter "${group.name}" angemeldet`
|
||||
}
|
||||
} else {
|
||||
// Bestätigungsmail senden
|
||||
await sendConfirmationEmail(existingEmail.email, existingEmail.name, existingEmail.confirmationToken, group.name)
|
||||
return {
|
||||
success: true,
|
||||
message: 'Eine Bestätigungsmail wurde an Ihre E-Mail-Adresse gesendet. Bitte bestätigen Sie Ihre Anmeldung.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Neuer Abonnent
|
||||
const confirmationToken = crypto.randomBytes(32).toString('hex')
|
||||
const unsubscribeToken = crypto.randomBytes(32).toString('hex')
|
||||
const newSubscriber = {
|
||||
id: randomUUID(),
|
||||
email: emailLower,
|
||||
name: name || '',
|
||||
groupIds: [groupId],
|
||||
confirmed: false,
|
||||
confirmationToken,
|
||||
unsubscribeToken,
|
||||
subscribedAt: new Date().toISOString(),
|
||||
confirmedAt: null,
|
||||
unsubscribedAt: null
|
||||
}
|
||||
|
||||
subscribers.push(newSubscriber)
|
||||
await writeSubscribers(subscribers)
|
||||
|
||||
// Bestätigungsmail senden
|
||||
await sendConfirmationEmail(email, name, confirmationToken, group.name)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Eine Bestätigungsmail wurde an Ihre E-Mail-Adresse gesendet. Bitte bestätigen Sie Ihre Anmeldung.'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler bei Newsletter-Anmeldung:', error)
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Fehler bei der Newsletter-Anmeldung'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
async function sendConfirmationEmail(email, name, token, groupName) {
|
||||
const smtpUser = process.env.SMTP_USER
|
||||
const smtpPass = process.env.SMTP_PASS
|
||||
|
||||
if (!smtpUser || !smtpPass) {
|
||||
console.warn('SMTP-Credentials fehlen! Bestätigungsmail kann nicht gesendet werden.')
|
||||
return
|
||||
}
|
||||
|
||||
const baseUrl = process.env.NUXT_PUBLIC_BASE_URL || 'http://localhost:3100'
|
||||
const confirmationUrl = `${baseUrl}/newsletter/confirm?token=${token}`
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
await transporter.sendMail({
|
||||
from: process.env.SMTP_FROM || 'noreply@harheimertc.de',
|
||||
to: email,
|
||||
subject: `Newsletter-Anmeldung bestätigen - ${groupName} - Harheimer TC`,
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: #dc2626;">Newsletter-Anmeldung bestätigen</h2>
|
||||
<p>Hallo ${name || 'Liebe/r Abonnent/in'},</p>
|
||||
<p>vielen Dank für Ihre Anmeldung zum Newsletter "${groupName}" des Harheimer TC!</p>
|
||||
<p>Bitte bestätigen Sie Ihre Anmeldung, indem Sie auf den folgenden Link klicken:</p>
|
||||
<p style="margin: 30px 0;">
|
||||
<a href="${confirmationUrl}" style="display: inline-block; padding: 12px 24px; background-color: #dc2626; color: white; text-decoration: none; border-radius: 5px;">
|
||||
Newsletter-Anmeldung bestätigen
|
||||
</a>
|
||||
</p>
|
||||
<p>Falls Sie sich nicht angemeldet haben, können Sie diese E-Mail ignorieren.</p>
|
||||
<p style="margin-top: 30px; color: #666; font-size: 12px;">
|
||||
Mit sportlichen Grüßen,<br>
|
||||
Ihr Harheimer Tischtennis-Club 1954 e.V.
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
121
server/api/newsletter/unsubscribe-by-email.post.js
Normal file
121
server/api/newsletter/unsubscribe-by-email.post.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import { readSubscribers, writeSubscribers } from '../../utils/newsletter.js'
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
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_GROUPS_FILE = getDataPath('newsletter-groups.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
|
||||
}
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const body = await readBody(event)
|
||||
const { email, groupId } = body
|
||||
|
||||
if (!email || !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Ungültige E-Mail-Adresse'
|
||||
})
|
||||
}
|
||||
|
||||
if (!groupId) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Newsletter-Gruppe muss angegeben werden'
|
||||
})
|
||||
}
|
||||
|
||||
// Prüfe ob Gruppe existiert
|
||||
const groups = await readGroups()
|
||||
const group = groups.find(g => g.id === groupId)
|
||||
|
||||
if (!group) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Newsletter-Gruppe nicht gefunden'
|
||||
})
|
||||
}
|
||||
|
||||
if (group.type !== 'subscription') {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Diese Funktion ist nur für Abonnenten-Newsletter verfügbar'
|
||||
})
|
||||
}
|
||||
|
||||
const subscribers = await readSubscribers()
|
||||
const emailLower = email.toLowerCase()
|
||||
|
||||
const subscriber = subscribers.find(s => {
|
||||
const sEmail = (s.email || '').toLowerCase()
|
||||
return sEmail === emailLower
|
||||
})
|
||||
|
||||
if (!subscriber) {
|
||||
// Nicht gefunden - aber trotzdem Erfolg zurückgeben (keine Information preisgeben)
|
||||
return {
|
||||
success: true,
|
||||
message: 'Sie wurden erfolgreich vom Newsletter abgemeldet'
|
||||
}
|
||||
}
|
||||
|
||||
// Stelle sicher, dass groupIds existiert
|
||||
if (!subscriber.groupIds || !Array.isArray(subscriber.groupIds)) {
|
||||
subscriber.groupIds = []
|
||||
}
|
||||
|
||||
// Prüfe ob für diese Gruppe angemeldet
|
||||
if (!subscriber.groupIds.includes(groupId)) {
|
||||
// Nicht für diese Gruppe angemeldet - aber trotzdem Erfolg zurückgeben
|
||||
return {
|
||||
success: true,
|
||||
message: 'Sie wurden erfolgreich vom Newsletter abgemeldet'
|
||||
}
|
||||
}
|
||||
|
||||
// Entferne Gruppe aus groupIds
|
||||
const index = subscriber.groupIds.indexOf(groupId)
|
||||
subscriber.groupIds.splice(index, 1)
|
||||
|
||||
// Wenn keine Gruppen mehr vorhanden, als abgemeldet markieren
|
||||
if (subscriber.groupIds.length === 0) {
|
||||
subscriber.unsubscribedAt = new Date().toISOString()
|
||||
subscriber.confirmed = false
|
||||
}
|
||||
|
||||
await writeSubscribers(subscribers)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Sie wurden erfolgreich vom Newsletter abgemeldet'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler bei Newsletter-Abmeldung:', error)
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Fehler bei der Newsletter-Abmeldung'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
56
server/api/newsletter/unsubscribe.get.js
Normal file
56
server/api/newsletter/unsubscribe.get.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import { readSubscribers, writeSubscribers } from '../../utils/newsletter.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const query = getQuery(event)
|
||||
const token = query.token
|
||||
|
||||
if (!token) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Abmeldetoken fehlt'
|
||||
})
|
||||
}
|
||||
|
||||
const subscribers = await readSubscribers()
|
||||
const subscriber = subscribers.find(s => s.unsubscribeToken === token)
|
||||
|
||||
if (!subscriber) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Ungültiger Abmeldetoken'
|
||||
})
|
||||
}
|
||||
|
||||
if (subscriber.unsubscribedAt) {
|
||||
// Bereits abgemeldet
|
||||
return sendRedirect(event, '/newsletter/unsubscribed?already=true')
|
||||
}
|
||||
|
||||
// Abmeldung durchführen
|
||||
subscriber.unsubscribedAt = new Date().toISOString()
|
||||
subscriber.confirmed = false
|
||||
|
||||
// Stelle sicher, dass groupIds existiert (für Rückwärtskompatibilität)
|
||||
if (!subscriber.groupIds) {
|
||||
subscriber.groupIds = []
|
||||
}
|
||||
// Leere groupIds, um von allen Gruppen abzumelden
|
||||
subscriber.groupIds = []
|
||||
|
||||
await writeSubscribers(subscribers)
|
||||
|
||||
// Weiterleitung zur Abmelde-Bestätigungsseite
|
||||
return sendRedirect(event, '/newsletter/unsubscribed')
|
||||
} catch (error) {
|
||||
console.error('Fehler bei Newsletter-Abmeldung:', error)
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Fehler bei der Newsletter-Abmeldung'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import multer from 'multer'
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import sharp from 'sharp'
|
||||
import { getUserFromToken, verifyToken } from '../../utils/auth.js'
|
||||
import { getUserFromToken, verifyToken, hasAnyRole } from '../../utils/auth.js'
|
||||
import { randomUUID } from 'crypto'
|
||||
|
||||
// Handle both dev and production paths
|
||||
@@ -69,7 +69,7 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
|
||||
const user = await getUserFromToken(token)
|
||||
if (!user || (user.role !== 'admin' && user.role !== 'vorstand')) {
|
||||
if (!user || !hasAnyRole(user, 'admin', 'vorstand')) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'Keine Berechtigung zum Hochladen von Bildern'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { verifyToken, getUserById } from '../utils/auth.js'
|
||||
import { verifyToken, getUserById, migrateUserRoles } from '../utils/auth.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
@@ -29,6 +29,9 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
}
|
||||
|
||||
const migratedUser = migrateUserRoles({ ...user })
|
||||
const roles = Array.isArray(migratedUser.roles) ? migratedUser.roles : (migratedUser.role ? [migratedUser.role] : ['mitglied'])
|
||||
|
||||
// Return user data (without password)
|
||||
return {
|
||||
success: true,
|
||||
@@ -37,7 +40,8 @@ export default defineEventHandler(async (event) => {
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
phone: user.phone || '',
|
||||
role: user.role
|
||||
roles: roles,
|
||||
role: roles[0] || 'mitglied' // Rückwärtskompatibilität
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { verifyToken, getUserById, readUsers, writeUsers, verifyPassword, hashPassword } from '../utils/auth.js'
|
||||
import { verifyToken, getUserById, readUsers, writeUsers, verifyPassword, hashPassword, migrateUserRoles } from '../utils/auth.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
@@ -80,6 +80,9 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
await writeUsers(users)
|
||||
|
||||
const migratedUser = migrateUserRoles({ ...user })
|
||||
const roles = Array.isArray(migratedUser.roles) ? migratedUser.roles : (migratedUser.role ? [migratedUser.role] : ['mitglied'])
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Profil erfolgreich aktualisiert.',
|
||||
@@ -88,7 +91,8 @@ export default defineEventHandler(async (event) => {
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
phone: user.phone,
|
||||
role: user.role
|
||||
roles: roles,
|
||||
role: roles[0] || 'mitglied' // Rückwärtskompatibilität
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { verifyToken, getUserById } from '../utils/auth.js'
|
||||
import { verifyToken, getUserById, hasAnyRole } from '../utils/auth.js'
|
||||
import { deleteTermin } from '../utils/termine.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -24,7 +24,7 @@ export default defineEventHandler(async (event) => {
|
||||
const user = await getUserById(decoded.id)
|
||||
|
||||
// Only admin and vorstand can delete termine
|
||||
if (!user || (user.role !== 'admin' && user.role !== 'vorstand')) {
|
||||
if (!user || !hasAnyRole(user, 'admin', 'vorstand')) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Keine Berechtigung zum Löschen von Terminen.'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { verifyToken, getUserById } from '../utils/auth.js'
|
||||
import { verifyToken, getUserById, hasAnyRole } from '../utils/auth.js'
|
||||
import { readTermine } from '../utils/termine.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -24,7 +24,7 @@ export default defineEventHandler(async (event) => {
|
||||
const user = await getUserById(decoded.id)
|
||||
|
||||
// Only admin and vorstand can manage termine
|
||||
if (!user || (user.role !== 'admin' && user.role !== 'vorstand')) {
|
||||
if (!user || !hasAnyRole(user, 'admin', 'vorstand')) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Keine Berechtigung zum Verwalten von Terminen.'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { verifyToken, getUserById } from '../utils/auth.js'
|
||||
import { verifyToken, getUserById, hasAnyRole } from '../utils/auth.js'
|
||||
import { saveTermin } from '../utils/termine.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -24,7 +24,7 @@ export default defineEventHandler(async (event) => {
|
||||
const user = await getUserById(decoded.id)
|
||||
|
||||
// Only admin and vorstand can create termine
|
||||
if (!user || (user.role !== 'admin' && user.role !== 'vorstand')) {
|
||||
if (!user || !hasAnyRole(user, 'admin', 'vorstand')) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Keine Berechtigung zum Erstellen von Terminen.'
|
||||
|
||||
@@ -1 +1 @@
|
||||
j1MPucV7uLGcNrRns92uU3f+fTt35Vpw7ImrahWaPQOAxGPJP0zZq6VOYYjuhvGlmE708rxZsPog/7PvuKc5YOwM5H9Bzhwf4HZFj98JrVU7gCkS5bm39NB1MrDS1yPblAurGFRrL28mi3d3Py+02cbV+YccQEw678jHqt6tazRfz1z005S5pYNAGf8GfJqAhtR4IA9ZTolszBiGA71gb33/RlwyZqUpnA7IEr1tlG7t21ueXcRNH+N2REgPYBrwmOkACGn6efdJpWoyglFLUOzt/uheXlrrprzJaUta3CZSPLC4JIHDHGEWgjwvAs14eDsfJbuaDegUAIpUrkGEsicPXIwj5gXrEc8FnEZSQISnrmj+jkYv86VZ8fXf8rmgSTjW5F8+tA5lSlJompb7wRQNmFLzLehdiatJtwHh1zhjfHBVG3VKKYgLppG8n0/LMc8BGKtb7xvIFshjnuTnbhbe5C7ocnefcOUVkVXhqXnbLAcmLQPn8ZjkJC0Vk9I5bTbRQr/1X0gsTPlkbtnLwtWF8puRPlx2eFimt3ZvzjTh+BxGagGM0wmTSqNh51WvbC10oPUyjCrL/tQJ2essSkufZ8KSVrnC3Tum/xATaL4fei/DFiYxoS9HqXaf1GvreiScbIPP61wgrjBSpuQmiDQfsVprdT9l+A7diF6LJXlcEpeWvSWq5E0h39QgoqYrg6uwd6Jilg6RSMcpnNWozRzqTLhTJ9ZCdaH3TLcX3qX5M9zLV/gVmy8m4gXaaiMo2WSjuryXNapT/lGIhZlFojhz0BmnId/SYUTvh2ds0WdYHfXEP1HGVcFhgLibI5tz4B9zJma/nReDle/23lhTc5coh3Gi2bvxC2CrkXUwiPK+SYW+yaBiZ5c7rGAhXtZebRJjGGZD6IA5dT6fYeQF7tVbYs3KcKWEVHVB4XomcsTd1neHPtnCVMj3d3aJ94ihJ5C5CJy3qHd60ovxcbsbqL7sK+jY84muHB2AKkSm5TdM95aGxovGpaPvXiHYMbSf+pbPKdKzS16NJ/8RnNweUYYC2Naabw+GYxrlxqS31j5ZXnLVsms1GQ9yMcJyrNmCbzu6aqM0j9HBq5Wi5Lo5y04EdIqqIzBCrNlmRllWK2Sa78j6nLLiP1W4zzF/51W/Oqp++zd2ns/TS1+JUDnHYqRUaBFHLB1lz854ODv8T9Shu85ExY/RWkCZan+P0bi24x+6hYVR0sPpgvyBjka4cY7NXnpU9nSnJpK1ve4nAA2o0vdv9OpO3CIgv37vucFIXOCJRfcWBL5rrFYE99Kaf+tTSWEwBoIm71txBLhy9yVElefY4G9C1S3Wl/cyCmgz
|
||||
WDoadIosSFzqf+ASCWH+PIYZmZ+aFg+GkJYAEReZOaZ8FrVlC+e2kx69+XYTMf3vRUzem5xrzc4RxX6s434EyJV2vW+LqPzVFTtyFZ9Zc26kQ7svj024kAgF89Pc3E/Bq+XRvH1oHX0puAY3/3aaiI/uQmIZ/sk1E9PHvern/zyZvwx53lThl3Ac6Z6k6qb5ogYIPNSPUn2U+bkgdW/jxxwRff7UuenVW+xdeR2YsrHBIWa+9UPan2xLNo+zHAv7/WI5XPmo4esooOTOU12MY4hD6+ELmzcgbc/5F6V4TQLR+X8WDdHJ2OfH/8J/nQ2ba9x8NuKuB/Obbd9ChDDNLlrP9P3vHZV99VdMAC9WsgAVRnM5hT1ZneXe1iuOq0Zblnk7f0MwXJGpwt1h8BKAI1MAcy7GGZSC24Ca2mOUWZ7UaQp3RmUkoqRGNANN+wGqBtOnYO5BS1qbn/sedApGfTX1PYi5WyzLr7RB0ZGBtcz567rS0E1O1fApe9QKTuWvAXdFsLz8Ssh9BerWsPTgifgbHPQJJqYQy/CiG82i9tOYA5N1udTuTFWgb23tuJATUpY75WW7kehNKGRpBkKfUo8C9RPcNCRqzg6H3iMkZNKEFYx+y1fACkUw0lUES5f6rrj8QNgCSZieQmBh8ETarAyg8jMreQI8hz1cJZcoNeBI2b3tjBjTMhaVwx45M8/r
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
3+uWOe4pSXnAFtrdeCqRG+HvbRIsI2HUcMkzrEBlqEmf/9rasPUIv5xhfS+3vh3BJh89fjff0N9l7C8SZbe/ABq75ffwHa1rT72fExEAQ0B/TntBBARNeACYRtx7j3OTJs0+DPiJvraXshqqVjJQjFVMRk1PdmNs3wbZQ9JkXazyne+Gvb6NJWBAeBv4s5pOe3y06GnUO2ZMsGPX3nKdumbRjFXoNzOWtzMQy9m8GYTAQGtC+dMzRTAjKuPxMLLYT1e8hMhJkhbGDOB0+VgOG2o1zGa+eD2ayoHqzUBf5/RZs09rRspXJZ7HKjvgdnkJuO2lstjQeOFtzoljGE9EC2ueRGUuOsyi0AQrbBhVTj3wWIb5V+mNxNccKv9KDs4/EPwyu8l32Ql6kepNuXofZMbJHuwwYvXIvpIj31HdJP0=
|
||||
CyRDzIKS5Ou7WW0Ri1r4G7yGBFv1MwyJUsdJYNUI2gx7KJ5Mr4d8JBe4YQ+vQlpFw/ZEhrBLXjsKwbEIMlmO/xZWln9TsE/1s9rwCd9WoCWrXOlSqQc6kWP6xJuoy0tXRBCGfEPqoIg/x/G/QsN0kIdnWPETOqOd9p9nc/OsmbhXHTGIUa2KKDNsk5JMJVRI1IUt8CzdpXQUQpbSBA8AgBV7sUiePWXlbqxfoWC7leV8oRWcgTz1Y0hKVB/yczjPUQP7hEI7GZ9O/2fysrTRPa5JtmwQ4CbfXe1wWANmxrIsUf1n/+yogcVfkG+Ld6YjhCnh1hmDQFEh7RkSB8J9uknvlrk/uXsnwRP55jBeum0ujsOaxisagJ1oniCVg27r2+fx0qiAIQDv5pVDp+EWkDMo4Wkw4qis6HwA46hy4ex22O4As550xhnomHq/Rtk6mO20Srlt+7dbUcopvVZn/ekXzL8ovzYFHA978B63m2Vt6m7wYdGduSjUChzXXcRUJwF2JKnOiSym2/zQ9EJi8UFBMgSaXAku9PakLUWI13VInKItLCX/Ib9ADWMLiViDmzW3dYHKxENdBeo8tD4vGExEY7+5x+Ari6zIGhcoYt8MRyGMGdrqSYTLCnlRnzgeHqN2JTyiYns8fCNUuV7aa31x5GgzD/Bpc1JJG+o6DYAva1GBLaaCTLTpuuDNC6V32cJECjzQaQKm8hhIg9OWjpApxhvx/0aiVs2Yne63Ot8183YAdfpX6QCD2F89hqQi6LjBxzC8vYi+2MWTdw4ZdkIRhrROe0/gxOWvecmrpyM=
|
||||
@@ -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
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