Benachrichtigungen erweitert
Some checks failed
Code Analysis and Production Deploy / analyze (push) Failing after 7m53s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Has been skipped

Emails korrigiert
This commit is contained in:
Torsten Schulz (local)
2026-06-14 01:05:19 +02:00
parent 4b699de853
commit 77aabef4a9
32 changed files with 646 additions and 920 deletions

View File

@@ -7,6 +7,7 @@ import nodemailer from 'nodemailer'
import fs from 'fs/promises'
import path from 'path'
import { getServerDataPath } from './paths.js'
import { isHiddenUser, migrateUserRoles, readUsers } from './auth.js'
/**
* Gets the correct data path for config files
@@ -34,23 +35,45 @@ async function loadConfig() {
}
}
function envFlagEnabled(value) {
return ['1', 'true', 'yes', 'on'].includes(String(value || '').trim().toLowerCase())
}
function shouldUseDeveloperRecipients() {
if (process.env.DEBUG !== undefined) return envFlagEnabled(process.env.DEBUG)
return process.env.NODE_ENV !== 'production' || process.env.APP_ENV === 'test'
}
/**
* Gets email recipients based on membership type and environment
* @param {Object} data - Form data
* @param {Object} config - Configuration
* @returns {Array<string>} Email addresses
*/
function getEmailRecipients(data, config) {
const isProduction = process.env.NODE_ENV === 'production' && process.env.APP_ENV !== 'test'
if (!isProduction) {
async function collectBoardUserRecipients() {
try {
const users = await readUsers()
return users
.filter(user => user && user.active !== false && !isHiddenUser(user))
.map(user => migrateUserRoles({ ...user }))
.filter(user => Array.isArray(user.roles) && user.roles.includes('vorstand'))
.map(user => String(user.email || '').trim())
.filter(Boolean)
} catch (error) {
console.error('Could not load board recipients from users.json:', error.message || error)
return []
}
}
async function getEmailRecipients(data, config) {
if (shouldUseDeveloperRecipients()) {
return ['tsschulz@tsschulz.de']
}
const recipients = []
const recipients = await collectBoardUserRecipients()
// Config uses a 'vorstand' object with nested roles; collect all emails
if (config.vorstand && typeof config.vorstand === 'object') {
// Fallback for legacy installations where Vorstand members are only configured in config.json.
if (recipients.length === 0 && config.vorstand && typeof config.vorstand === 'object') {
Object.values(config.vorstand).forEach((member) => {
if (member && member.email && typeof member.email === 'string' && member.email.trim() !== '') {
recipients.push(member.email.trim())
@@ -59,7 +82,7 @@ function getEmailRecipients(data, config) {
}
// For minors, also add first trainer email if configured (trainer is an array)
if (!data.isVolljaehrig && Array.isArray(config.trainer) && config.trainer.length > 0 && config.trainer[0].email) {
if (data.isVolljaehrig === false && Array.isArray(config.trainer) && config.trainer.length > 0 && config.trainer[0].email) {
recipients.push(config.trainer[0].email)
}
@@ -69,11 +92,11 @@ function getEmailRecipients(data, config) {
if (config.website && config.website.verantwortlicher && config.website.verantwortlicher.email) {
recipients.push(config.website.verantwortlicher.email)
} else {
recipients.push('tsschulz@tsschulz.de')
throw new Error('Keine E-Mail-Empfänger in config.json konfiguriert.')
}
}
return recipients
return [...new Set(recipients)]
}
/**
@@ -111,7 +134,7 @@ function createTransporter() {
export async function sendMembershipEmail(data, pdfPath) {
try {
const config = await loadConfig()
const recipients = getEmailRecipients(data, config)
const recipients = await getEmailRecipients(data, config)
// Create transporter
const transporter = createTransporter()
@@ -167,7 +190,7 @@ Das ausgefüllte Formular ist als Anhang verfügbar.`
export async function sendRegistrationNotification(data) {
try {
const config = await loadConfig()
const recipients = getEmailRecipients(data, config)
const recipients = await getEmailRecipients(data, config)
// Create transporter
const transporter = createTransporter()

View File

@@ -276,12 +276,23 @@ function parseBirthday(value) {
return null
}
function hasBirthdayNotificationConsent(person) {
return person?.visibility?.showBirthday === true || person?.showBirthday === true
}
function formatBirthdaySummary(names) {
const visibleNames = names.map(name => String(name || '').trim()).filter(Boolean)
if (visibleNames.length === 1) return `${visibleNames[0]} hat heute Geburtstag.`
return `Geburtstage heute: ${visibleNames.slice(0, 5).join(', ')}${visibleNames.length > 5 ? ` und ${visibleNames.length - 5} weitere` : ''}.`
}
async function birthdaysOn(dateKey) {
const [, month, day] = dateKey.split('-').map(Number)
const [manualMembers, users] = await Promise.all([readMembers(), readUsers()])
const people = []
for (const member of manualMembers) {
if (member?.active === false) continue
if (!hasBirthdayNotificationConsent(member)) continue
const birthday = parseBirthday(member.geburtsdatum || member.birthday)
if (birthday?.month === month && birthday?.day === day) {
people.push(String(member.name || `${member.firstName || ''} ${member.lastName || ''}`.trim()).trim())
@@ -289,7 +300,7 @@ async function birthdaysOn(dateKey) {
}
for (const user of users) {
if (isHiddenUser(user) || user?.active === false) continue
if (user.visibility?.showBirthday === false) continue
if (!hasBirthdayNotificationConsent(user)) continue
const birthday = parseBirthday(user.geburtsdatum || user.birthday)
if (birthday?.month === month && birthday?.day === day) {
people.push(userDisplayName(user))
@@ -397,7 +408,7 @@ export async function runNotificationSchedulerTick(now = new Date()) {
results.birthdays = await sendIfDue(state, dateKey, time, 'birthdays', todaysBirthdays.length > 0, () => sendPushToUsers({
title: 'Geburtstage heute',
body: todaysBirthdays.length === 1 ? `${todaysBirthdays[0]} hat heute Geburtstag.` : `${todaysBirthdays.length} Mitglieder haben heute Geburtstag.`,
body: formatBirthdaySummary(todaysBirthdays),
data: { type: 'birthdays', date: dateKey },
predicate: (_user, settings) => settings.notificationTime === time && settings.birthdays,
failureLabel: 'FCM Geburtstags-Push'

View File

@@ -132,6 +132,10 @@ async function sendFcmMessage({ serviceAccount, accessToken, token, title, body,
}
}
function isStaleFcmTokenError(error) {
return /UNREGISTERED|NOT_FOUND|INVALID_ARGUMENT/.test(String(error?.message || error || ''))
}
function notificationIdFor(value) {
return String(value || Date.now()).split('').reduce((acc, char) => acc + char.charCodeAt(0), 0).toString()
}
@@ -185,13 +189,14 @@ export async function sendPushToUsers({ title, body, data = {}, predicate, bodyF
sent += 1
validTokens.push(entry)
} catch (error) {
failed += 1
console.error('FCM Push fehlgeschlagen:', { failureLabel, message: error.message })
if (/UNREGISTERED|NOT_FOUND|INVALID_ARGUMENT/.test(String(error.message)) === false) {
validTokens.push(entry)
} else {
if (isStaleFcmTokenError(error)) {
removed += 1
changed = true
console.warn('FCM Push-Token entfernt:', { failureLabel, reason: error.message })
} else {
failed += 1
console.error('FCM Push fehlgeschlagen:', { failureLabel, message: error.message })
validTokens.push(entry)
}
}
}