@@ -668,6 +676,7 @@ const addTrainer = () => {
name: '',
lizenz: '',
schwerpunkt: '',
+ email: '',
zusatz: ''
})
}
diff --git a/pages/cms/index.vue b/pages/cms/index.vue
index a9197c3..f0c3397 100644
--- a/pages/cms/index.vue
+++ b/pages/cms/index.vue
@@ -110,6 +110,27 @@
+
+
+
+
+
+
+
+ Kontaktanfragen
+
+
+
+ Kontaktformular-Anfragen einsehen und beantworten
+
+
+
diff --git a/pages/login.vue b/pages/login.vue
index 076abd4..2c9ec8c 100644
--- a/pages/login.vue
+++ b/pages/login.vue
@@ -154,7 +154,9 @@ const handleLogin = async () => {
// Redirect based on role
setTimeout(() => {
const roles = response.user.roles || (response.user.role ? [response.user.role] : [])
- if (roles.includes('admin') || roles.includes('vorstand') || roles.includes('newsletter')) {
+ if (roles.includes('trainer')) {
+ router.push('/cms/kontaktanfragen')
+ } else if (roles.includes('admin') || roles.includes('vorstand') || roles.includes('newsletter')) {
router.push('/cms')
} else {
router.push('/mitgliederbereich')
diff --git a/server/api/cms/contact-requests.get.js b/server/api/cms/contact-requests.get.js
new file mode 100644
index 0000000..e63947a
--- /dev/null
+++ b/server/api/cms/contact-requests.get.js
@@ -0,0 +1,17 @@
+import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
+import { readContactRequests } from '../../utils/contact-requests.js'
+
+export default defineEventHandler(async (event) => {
+ const token = getCookie(event, 'auth_token')
+ const currentUser = token ? await getUserFromToken(token) : null
+
+ if (!currentUser || !hasAnyRole(currentUser, 'admin', 'vorstand', 'trainer')) {
+ throw createError({
+ statusCode: 403,
+ statusMessage: 'Zugriff verweigert'
+ })
+ }
+
+ const requests = await readContactRequests()
+ return requests
+})
diff --git a/server/api/cms/contact-requests/[id]/reply.post.js b/server/api/cms/contact-requests/[id]/reply.post.js
new file mode 100644
index 0000000..339108c
--- /dev/null
+++ b/server/api/cms/contact-requests/[id]/reply.post.js
@@ -0,0 +1,75 @@
+import nodemailer from 'nodemailer'
+import { getUserFromToken, hasAnyRole } from '../../../../utils/auth.js'
+import { addContactReply, readContactRequests } from '../../../../utils/contact-requests.js'
+
+function createTransporter() {
+ const smtpUser = process.env.SMTP_USER
+ const smtpPass = process.env.SMTP_PASS || process.env.EMAIL_PASSWORD
+ if (!smtpUser || !smtpPass) return null
+
+ return nodemailer.createTransport({
+ host: process.env.SMTP_HOST || 'smtp.gmail.com',
+ port: Number(process.env.SMTP_PORT || 587),
+ secure: process.env.SMTP_SECURE === 'true',
+ auth: { user: smtpUser, pass: smtpPass }
+ })
+}
+
+export default defineEventHandler(async (event) => {
+ const token = getCookie(event, 'auth_token')
+ const currentUser = token ? await getUserFromToken(token) : null
+
+ if (!currentUser || !hasAnyRole(currentUser, 'admin', 'vorstand', 'trainer')) {
+ throw createError({
+ statusCode: 403,
+ statusMessage: 'Zugriff verweigert'
+ })
+ }
+
+ const body = await readBody(event)
+ const replyMessage = String(body?.message || '').trim()
+ if (!replyMessage) {
+ throw createError({ statusCode: 400, statusMessage: 'Antworttext fehlt' })
+ }
+
+ const requestId = getRouterParam(event, 'id')
+ if (!requestId) {
+ throw createError({ statusCode: 400, statusMessage: 'Anfrage-ID fehlt' })
+ }
+
+ const all = await readContactRequests()
+ const target = all.find((r) => r.id === requestId)
+ if (!target) {
+ throw createError({ statusCode: 404, statusMessage: 'Anfrage nicht gefunden' })
+ }
+
+ const transporter = createTransporter()
+ if (!transporter) {
+ throw createError({
+ statusCode: 500,
+ statusMessage: 'SMTP ist nicht konfiguriert'
+ })
+ }
+
+ const originalSubject = target.subject || 'Kontaktanfrage'
+ const responseSubject = `Aw: ${originalSubject}`
+
+ await transporter.sendMail({
+ from: `"Harheimer TC" <${process.env.SMTP_FROM || process.env.SMTP_USER}>`,
+ to: target.email,
+ subject: responseSubject,
+ text: replyMessage
+ })
+
+ const responderEmail = currentUser.email || ''
+ const updated = await addContactReply({
+ requestId,
+ replyText: replyMessage,
+ responderEmail
+ })
+
+ return {
+ success: true,
+ request: updated
+ }
+})
diff --git a/server/api/cms/users/update-role.post.js b/server/api/cms/users/update-role.post.js
index c6fe6d8..f221938 100644
--- a/server/api/cms/users/update-role.post.js
+++ b/server/api/cms/users/update-role.post.js
@@ -16,7 +16,7 @@ export default defineEventHandler(async (event) => {
const body = await readBody(event)
const { userId, roles } = body
- const validRoles = ['mitglied', 'vorstand', 'admin', 'newsletter']
+ const validRoles = ['mitglied', 'vorstand', 'admin', 'newsletter', 'trainer']
const rolesArray = Array.isArray(roles) ? roles : (roles ? [roles] : ['mitglied'])
if (!rolesArray.every(r => validRoles.includes(r))) {
diff --git a/server/api/contact.post.js b/server/api/contact.post.js
index a3bf226..2729412 100644
--- a/server/api/contact.post.js
+++ b/server/api/contact.post.js
@@ -1,10 +1,93 @@
import nodemailer from 'nodemailer'
+import { promises as fs } from 'fs'
+import path from 'path'
+import { createContactRequest } from '../utils/contact-requests.js'
+import { readUsers, migrateUserRoles } from '../utils/auth.js'
+
+// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
+// filename is always a hardcoded constant ('config.json'), never user input
+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)
+}
+
+async function loadConfig() {
+ try {
+ const configFile = getDataPath('config.json')
+ const raw = await fs.readFile(configFile, 'utf-8')
+ return JSON.parse(raw)
+ } catch (error) {
+ console.error('Fehler beim Laden der Konfiguration für Kontaktanfragen:', error)
+ return {}
+ }
+}
+
+async function collectRecipients(config) {
+ const recipients = []
+
+ // Vorstand
+ if (config?.vorstand && typeof config.vorstand === 'object') {
+ for (const member of Object.values(config.vorstand)) {
+ if (member?.email && typeof member.email === 'string' && member.email.trim()) {
+ recipients.push(member.email.trim())
+ }
+ }
+ }
+
+ // Trainer
+ if (Array.isArray(config?.trainer)) {
+ for (const trainer of config.trainer) {
+ if (trainer?.email && typeof trainer.email === 'string' && trainer.email.trim()) {
+ recipients.push(trainer.email.trim())
+ }
+ }
+ }
+
+ // Zusätzlich: Benutzer mit Trainer-Rolle aus dem Login-System
+ try {
+ const users = await readUsers()
+ for (const rawUser of users) {
+ const user = migrateUserRoles({ ...rawUser })
+ const roles = Array.isArray(user.roles) ? user.roles : []
+ if (roles.includes('trainer') && user.email && String(user.email).trim()) {
+ recipients.push(String(user.email).trim())
+ }
+ }
+ } catch (error) {
+ console.error('Fehler beim Laden der Trainer-Empfänger aus Benutzerdaten:', error)
+ }
+
+ const unique = [...new Set(recipients)]
+ if (unique.length > 0) return unique
+
+ // Fallback
+ if (config?.website?.verantwortlicher?.email) {
+ return [config.website.verantwortlicher.email]
+ }
+ if (process.env.SMTP_USER) {
+ return [process.env.SMTP_USER]
+ }
+ return ['j.dichmann@gmx.de']
+}
+
+function createTransporter() {
+ const smtpUser = process.env.SMTP_USER
+ const smtpPass = process.env.SMTP_PASS || process.env.EMAIL_PASSWORD
+ if (!smtpUser || !smtpPass) return null
+
+ return nodemailer.createTransport({
+ host: process.env.SMTP_HOST || 'smtp.gmail.com',
+ port: Number(process.env.SMTP_PORT || 587),
+ secure: process.env.SMTP_SECURE === 'true',
+ auth: { user: smtpUser, pass: smtpPass }
+ })
+}
export default defineEventHandler(async (event) => {
try {
const body = await readBody(event)
-
- // Validierung der Eingabedaten
+
if (!body.name || !body.email || !body.subject || !body.message) {
throw createError({
statusCode: 400,
@@ -12,7 +95,6 @@ export default defineEventHandler(async (event) => {
})
}
- // E-Mail-Validierung
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(body.email)) {
throw createError({
@@ -21,34 +103,32 @@ export default defineEventHandler(async (event) => {
})
}
- // SMTP-Konfiguration (hier können Sie Ihre SMTP-Daten eintragen)
- const smtpUser = process.env.SMTP_USER || 'j.dichmann@gmx.de'
- const smtpPass = process.env.SMTP_PASS || process.env.EMAIL_PASSWORD
-
- 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, // true für 465, false für andere Ports
- auth: {
- user: smtpUser,
- pass: smtpPass
- }
+ // Anfrage immer speichern, auch wenn E-Mail-Versand fehlschlägt.
+ await createContactRequest({
+ name: String(body.name).trim(),
+ email: String(body.email).trim(),
+ phone: body.phone ? String(body.phone).trim() : '',
+ subject: String(body.subject).trim(),
+ message: String(body.message).trim()
})
- // E-Mail-Template
+ const config = await loadConfig()
+ const recipients = await collectRecipients(config)
+ const transporter = createTransporter()
+
+ if (!transporter) {
+ return {
+ success: true,
+ message: 'Anfrage wurde gespeichert. E-Mail-Versand ist aktuell nicht konfiguriert.'
+ }
+ }
+
+ const nowLabel = new Date().toLocaleString('de-DE')
const emailHtml = `
Neue Kontaktanfrage - Harheimer TC
-
Kontaktdaten:
Name: ${body.name}
@@ -56,21 +136,18 @@ export default defineEventHandler(async (event) => {
Telefon: ${body.phone || 'Nicht angegeben'}
Betreff: ${body.subject}
-
Nachricht:
${body.message}
-
Diese Nachricht wurde über das Kontaktformular der Harheimer TC Website gesendet.
-
Zeitstempel: ${new Date().toLocaleString('de-DE')}
+
Zeitstempel: ${nowLabel}
`
- const emailText = `
-Neue Kontaktanfrage - Harheimer TC
+ const emailText = `Neue Kontaktanfrage - Harheimer TC
Kontaktdaten:
Name: ${body.name}
@@ -83,36 +160,29 @@ ${body.message}
---
Diese Nachricht wurde über das Kontaktformular der Harheimer TC Website gesendet.
-Zeitstempel: ${new Date().toLocaleString('de-DE')}
- `
+Zeitstempel: ${nowLabel}`
- // E-Mail senden
- const mailOptions = {
- from: `"Harheimer TC Website" <${process.env.SMTP_USER || 'j.dichmann@gmx.de'}>`,
- to: 'j.dichmann@gmx.de',
+ await transporter.sendMail({
+ from: `"Harheimer TC Website" <${process.env.SMTP_FROM || process.env.SMTP_USER}>`,
+ to: recipients.join(', '),
replyTo: body.email,
subject: `Kontaktanfrage: ${body.subject}`,
text: emailText,
html: emailHtml
- }
-
- await transporter.sendMail(mailOptions)
+ })
return {
success: true,
- message: 'E-Mail wurde erfolgreich gesendet!'
+ message: 'Anfrage wurde erfolgreich gesendet.'
}
-
} catch (error) {
- console.error('Fehler beim Senden der E-Mail:', error)
-
- if (error.statusCode) {
- throw error
- }
-
+ console.error('Fehler bei Kontaktanfrage:', error)
+
+ if (error.statusCode) throw error
+
throw createError({
statusCode: 500,
- statusMessage: 'Fehler beim Senden der E-Mail. Bitte versuchen Sie es später erneut.'
+ statusMessage: 'Fehler beim Senden der Anfrage. Bitte versuchen Sie es später erneut.'
})
}
})
diff --git a/server/utils/contact-requests.js b/server/utils/contact-requests.js
new file mode 100644
index 0000000..bd93fad
--- /dev/null
+++ b/server/utils/contact-requests.js
@@ -0,0 +1,79 @@
+import { promises as fs } from 'fs'
+import path from 'path'
+import { randomUUID } from 'crypto'
+
+// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
+// filename is always a hardcoded constant, never user input
+const getDataPath = (filename) => {
+ const cwd = process.cwd()
+ if (cwd.endsWith('.output')) {
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
+ return path.join(cwd, '../server/data', filename)
+ }
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
+ return path.join(cwd, 'server/data', filename)
+}
+
+const CONTACT_REQUESTS_FILE = getDataPath('contact-requests.json')
+
+export async function readContactRequests() {
+ try {
+ const raw = await fs.readFile(CONTACT_REQUESTS_FILE, 'utf-8')
+ const parsed = JSON.parse(raw)
+ return Array.isArray(parsed) ? parsed : []
+ } catch (error) {
+ if (error.code === 'ENOENT') return []
+ console.error('Fehler beim Lesen der Kontaktanfragen:', error)
+ return []
+ }
+}
+
+export async function writeContactRequests(items) {
+ await fs.writeFile(CONTACT_REQUESTS_FILE, JSON.stringify(items, null, 2), 'utf-8')
+}
+
+export async function createContactRequest(data) {
+ const current = await readContactRequests()
+ const now = new Date().toISOString()
+ const item = {
+ id: randomUUID(),
+ createdAt: now,
+ updatedAt: now,
+ status: 'offen',
+ name: data.name,
+ email: data.email,
+ phone: data.phone || '',
+ subject: data.subject,
+ message: data.message,
+ replies: []
+ }
+ current.unshift(item)
+ await writeContactRequests(current)
+ return item
+}
+
+export async function addContactReply({ requestId, replyText, responderEmail }) {
+ const current = await readContactRequests()
+ const index = current.findIndex((r) => r.id === requestId)
+ if (index === -1) return null
+
+ const now = new Date().toISOString()
+ const request = current[index]
+ const replies = Array.isArray(request.replies) ? request.replies : []
+ replies.push({
+ id: randomUUID(),
+ createdAt: now,
+ responderEmail: responderEmail || '',
+ message: replyText
+ })
+
+ current[index] = {
+ ...request,
+ status: 'beantwortet',
+ replies,
+ updatedAt: now
+ }
+
+ await writeContactRequests(current)
+ return current[index]
+}