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

@@ -1,6 +1,5 @@
import { verifyRegistrationResponse } from '@simplewebauthn/server'
import crypto from 'crypto'
import nodemailer from 'nodemailer'
import { hashPassword, readUsers, writeUsers } from '../../utils/auth.js'
import { getWebAuthnConfig } from '../../utils/webauthn-config.js'
import { consumePreRegistration } from '../../utils/webauthn-challenges.js'
@@ -8,6 +7,7 @@ import { toBase64Url } from '../../utils/webauthn-encoding.js'
import { writeAuditLog } from '../../utils/audit-log.js'
import { assertPasswordNotPwned } from '../../utils/hibp.js'
import { getClientIp } from '../../utils/rate-limit.js'
import { sendRegistrationNotification } from '../../utils/email-service.js'
// Local fallback for Nitro globals when lint/run env doesn't provide them
const getMethod = globalThis.getMethod ?? ((e) => (e?.req?.method || e?.method || 'GET'))
@@ -260,50 +260,9 @@ export default defineEventHandler(async (event) => {
await writeAuditLog('auth.passkey.prereg.success', { email, userId: newUser.id })
// Send notification emails (same behavior as password registration)
// Send notification emails through the same central recipient logic as password registration.
try {
const smtpUser = process.env.SMTP_USER
const smtpPass = process.env.SMTP_PASS
if (smtpUser && smtpPass) {
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: process.env.SMTP_ADMIN || 'j.dichmann@gmx.de',
subject: 'Neue Registrierung (Passkey) - Harheimer TC',
html: `
<h2>Neue Registrierung (Passkey)</h2>
<p>Ein neuer Benutzer hat sich registriert und wartet auf Freigabe:</p>
<ul>
<li><strong>Name:</strong> ${name}</li>
<li><strong>E-Mail:</strong> ${email}</li>
<li><strong>Telefon:</strong> ${phone || 'Nicht angegeben'}</li>
<li><strong>Login:</strong> Passkey${password ? ' + Passwort (Fallback)' : ' (ohne Passwort)'}</li>
</ul>
<p>Bitte prüfen Sie die Registrierung im CMS.</p>
`
})
await transporter.sendMail({
from: process.env.SMTP_FROM || 'noreply@harheimertc.de',
to: email,
subject: 'Registrierung erhalten - Harheimer TC',
html: `
<h2>Registrierung erhalten</h2>
<p>Hallo ${name},</p>
<p>vielen Dank für Ihre Registrierung beim Harheimer TC!</p>
<p>Ihre Anfrage wird vom Vorstand geprüft. Sie erhalten eine E-Mail, sobald Ihr Zugang freigeschaltet wurde.</p>
<br>
<p>Mit sportlichen Grüßen,<br>Ihr Harheimer TC</p>
`
})
}
await sendRegistrationNotification({ name, email, phone })
} catch (emailError) {
console.error('E-Mail-Versand fehlgeschlagen:', emailError)
}

View File

@@ -49,7 +49,7 @@ export default defineEventHandler(async (event) => {
phone: phone || '',
geburtsdatum,
visibility: {
showBirthday: visibility?.showBirthday !== undefined ? Boolean(visibility.showBirthday) : true
showBirthday: visibility?.showBirthday !== undefined ? Boolean(visibility.showBirthday) : false
},
role: 'mitglied',
active: false, // Requires admin approval
@@ -80,4 +80,3 @@ export default defineEventHandler(async (event) => {
throw error
}
})

View File

@@ -72,14 +72,14 @@ export default defineEventHandler(async (event) => {
: true
if (!isAccepted) continue
const vis = m.visibility || {}
const showBirthday = vis.showBirthday === undefined ? true : Boolean(vis.showBirthday)
const showBirthday = vis.showBirthday === true
candidates.push({ name: `${m.firstName || ''} ${m.lastName || ''}`.trim(), geburtsdatum: m.geburtsdatum, visibility: { showBirthday }, source: 'manual' })
}
for (const u of registeredUsers) {
if (!u.active || isHiddenUser(u)) continue
const vis = u.visibility || {}
const showBirthday = vis.showBirthday === undefined ? true : Boolean(vis.showBirthday)
const showBirthday = vis.showBirthday === true
candidates.push({ name: u.name, geburtsdatum: u.geburtsdatum, visibility: { showBirthday }, source: 'login' })
}

View File

@@ -1,6 +1,5 @@
import nodemailer from 'nodemailer'
import { promises as fs } from 'fs'
import path from 'path'
import { createContactRequest } from '../utils/contact-requests.js'
import { readUsers, migrateUserRoles, isHiddenUser } from '../utils/auth.js'
import { sendNewContactRequestPush } from '../utils/push-notifications.js'
@@ -24,17 +23,39 @@ async function loadConfig() {
}
}
async function collectRecipients(config) {
const isProduction = process.env.NODE_ENV === 'production' && process.env.APP_ENV !== 'test'
function envFlagEnabled(value) {
return ['1', 'true', 'yes', 'on'].includes(String(value || '').trim().toLowerCase())
}
if (!isProduction) {
function shouldUseDeveloperRecipients() {
if (process.env.DEBUG !== undefined) return envFlagEnabled(process.env.DEBUG)
return process.env.NODE_ENV !== 'production' || process.env.APP_ENV === 'test'
}
async function collectRecipients(config) {
if (shouldUseDeveloperRecipients()) {
return ['tsschulz@tsschulz.de']
}
const recipients = []
// Vorstand
if (config?.vorstand && typeof config.vorstand === 'object') {
// Vorstand: prefer active login users with the board role.
try {
const users = await readUsers()
for (const rawUser of users) {
if (!rawUser || rawUser.active === false || isHiddenUser(rawUser)) continue
const user = migrateUserRoles({ ...rawUser })
const roles = Array.isArray(user.roles) ? user.roles : []
if (roles.includes('vorstand') && user.email && String(user.email).trim()) {
recipients.push(String(user.email).trim())
}
}
} catch (error) {
console.error('Fehler beim Laden der Vorstand-Empfänger aus Benutzerdaten:', error)
}
// Fallback: legacy config.json Vorstand object.
if (recipients.length === 0 && 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())
@@ -73,10 +94,7 @@ async function collectRecipients(config) {
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']
throw new Error('Keine E-Mail-Empfänger in config.json konfiguriert.')
}
function createTransporter() {

View File

@@ -64,7 +64,8 @@ export default defineEventHandler(async (event) => {
showEmail: vis.showEmail === undefined ? true : Boolean(vis.showEmail),
showPhone: vis.showPhone === undefined ? true : Boolean(vis.showPhone),
// Address remains private by default
showAddress: vis.showAddress === undefined ? false : Boolean(vis.showAddress)
showAddress: vis.showAddress === undefined ? false : Boolean(vis.showAddress),
showBirthday: vis.showBirthday === true
}
mergedMembers.push({
@@ -163,7 +164,8 @@ export default defineEventHandler(async (event) => {
mergedMembers[matchedManualIndex].visibility = {
showEmail: user.visibility.showEmail === undefined ? Boolean(vis.showEmail) : Boolean(user.visibility.showEmail),
showPhone: user.visibility.showPhone === undefined ? Boolean(vis.showPhone) : Boolean(user.visibility.showPhone),
showAddress: user.visibility.showAddress === undefined ? Boolean(vis.showAddress) : Boolean(user.visibility.showAddress)
showAddress: user.visibility.showAddress === undefined ? Boolean(vis.showAddress) : Boolean(user.visibility.showAddress),
showBirthday: user.visibility.showBirthday === undefined ? vis.showBirthday === true : user.visibility.showBirthday === true
}
}
} else {
@@ -186,7 +188,8 @@ export default defineEventHandler(async (event) => {
visibility: {
showEmail: userVis.showEmail === undefined ? true : Boolean(userVis.showEmail),
showPhone: userVis.showPhone === undefined ? true : Boolean(userVis.showPhone),
showAddress: userVis.showAddress === undefined ? false : Boolean(userVis.showAddress)
showAddress: userVis.showAddress === undefined ? false : Boolean(userVis.showAddress),
showBirthday: userVis.showBirthday === true
},
notes: `Rolle(n): ${roles.join(', ')}`,
source: 'login',
@@ -226,7 +229,7 @@ export default defineEventHandler(async (event) => {
const emailVisible = (isPrivilegedViewer || (isViewerAuthenticated && showEmail))
const phoneVisible = (isPrivilegedViewer || (isViewerAuthenticated && showPhone))
const addressVisible = (isPrivilegedViewer || (isViewerAuthenticated && showAddress))
const birthdayVisible = (isPrivilegedViewer || (isViewerAuthenticated && (member.visibility && member.visibility.showBirthday !== undefined ? Boolean(member.visibility.showBirthday) : true)))
const birthdayVisible = (isPrivilegedViewer || (isViewerAuthenticated && member.visibility?.showBirthday === true))
return {
id: member.id,
@@ -246,7 +249,7 @@ export default defineEventHandler(async (event) => {
showEmail: visibility.showEmail === undefined ? true : Boolean(visibility.showEmail),
showPhone: visibility.showPhone === undefined ? true : Boolean(visibility.showPhone),
showAddress: visibility.showAddress === undefined ? false : Boolean(visibility.showAddress),
showBirthday: visibility.showBirthday === undefined ? true : Boolean(visibility.showBirthday),
showBirthday: visibility.showBirthday === true,
// Privileged viewers (vorstand) always see contact fields
email: emailVisible ? member.email : undefined,
phone: phoneVisible ? member.phone : undefined,

View File

@@ -1,5 +1,22 @@
import { getUserFromToken, hasAnyRole } from '../utils/auth.js'
import { saveMember } from '../utils/members.js'
import { getUserFromToken, hasAnyRole, readUsers, writeUsers, normalizeUserEmail } from '../utils/auth.js'
import { readMembers, saveMember } from '../utils/members.js'
function requestedBirthdayVisibility(body) {
return body?.visibility?.showBirthday ?? body?.showBirthday
}
function birthdayVisibilityIsTrue(value) {
return value === true || value === 'true'
}
function resolveAdminBirthdayVisibility({ requested, existingManualMember, existingUser }) {
if (requested === false || requested === 'false') return false
const existingValue = existingUser?.visibility?.showBirthday ?? existingManualMember?.visibility?.showBirthday
if (existingValue === true) return true
return false
}
export default defineEventHandler(async (event) => {
try {
@@ -39,7 +56,7 @@ export default defineEventHandler(async (event) => {
}
const body = await readBody(event)
const { id, firstName, lastName, geburtsdatum, email, phone, address, notes, isMannschaftsspieler, hasHallKey, hasHallenschluessel, active } = body
const { id, firstName, lastName, geburtsdatum, email, phone, address, notes, isMannschaftsspieler, hasHallKey, hasHallenschluessel, active, visibility } = body
if (!firstName || !lastName) {
throw createError({
@@ -56,6 +73,23 @@ export default defineEventHandler(async (event) => {
}
try {
const [members, users] = await Promise.all([readMembers(), readUsers()])
const normalizedEmail = normalizeUserEmail(email)
const existingManualMember = members.find(member => {
if (id && member.id === id) return true
return normalizedEmail && normalizeUserEmail(member.email) === normalizedEmail
})
const userIndex = users.findIndex(user => {
if (id && user.id === id) return true
return normalizedEmail && normalizeUserEmail(user.email) === normalizedEmail
})
const existingUser = userIndex !== -1 ? users[userIndex] : null
const nextShowBirthday = resolveAdminBirthdayVisibility({
requested: requestedBirthdayVisibility(body),
existingManualMember,
existingUser
})
await saveMember({
id: id || undefined,
firstName,
@@ -67,9 +101,21 @@ export default defineEventHandler(async (event) => {
notes: notes || '',
isMannschaftsspieler: isMannschaftsspieler === true || isMannschaftsspieler === 'true',
hasHallKey: hasHallKey === true || hasHallKey === 'true' || hasHallenschluessel === true || hasHallenschluessel === 'true',
visibility: {
...(visibility && typeof visibility === 'object' ? visibility : {}),
showBirthday: nextShowBirthday
},
active: typeof active === 'boolean' ? active : true
})
if (userIndex !== -1 && (!birthdayVisibilityIsTrue(requestedBirthdayVisibility(body)) || existingUser?.visibility?.showBirthday === true)) {
users[userIndex].visibility = {
...(users[userIndex].visibility || {}),
showBirthday: nextShowBirthday
}
await writeUsers(users)
}
return {
success: true,
message: 'Mitglied erfolgreich gespeichert.'
@@ -98,4 +144,3 @@ export default defineEventHandler(async (event) => {
})
}
})

View File

@@ -27,7 +27,7 @@ export default defineEventHandler(async (event) => {
email: user.email,
phone: user.phone || '',
geburtsdatum: user.geburtsdatum || '',
visibility: Object.assign({ showBirthday: true }, (user.visibility || {}))
visibility: Object.assign({ showBirthday: false }, (user.visibility || {}))
}
}
} catch (error) {

View File

@@ -1,44 +0,0 @@
// Script: set-all-birthday-visible.js
// Setzt für alle Mitglieder das Flag visibility.showBirthday auf true
const fs = require('fs')
const path = require('path')
const membersPath = path.join(__dirname, 'data', 'members.json')
let raw
try {
raw = fs.readFileSync(membersPath, 'utf8')
} catch (e) {
console.error('Fehler beim Lesen von members.json:', e)
process.exit(1)
}
let members
try {
members = JSON.parse(raw)
} catch (e) {
console.error('Fehler beim Parsen von members.json:', e)
process.exit(1)
}
if (!Array.isArray(members)) {
console.error('members.json ist kein Array!')
process.exit(1)
}
let changed = 0
for (const m of members) {
if (!m.visibility) m.visibility = {}
if (m.visibility.showBirthday !== true) {
m.visibility.showBirthday = true
changed++
}
}
if (changed > 0) {
fs.writeFileSync(membersPath, JSON.stringify(members, null, 2), 'utf8')
console.log(`Flag für ${changed} Mitglieder gesetzt.`)
} else {
console.log('Alle Mitglieder hatten das Flag bereits gesetzt.')
}

View File

@@ -1,33 +0,0 @@
// Script: set-all-birthday-visible.mjs
// Setzt für alle Mitglieder das Flag visibility.showBirthday auf true (mit Entschlüsselung)
import { readMembers, writeMembers } from './utils/members.js';
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
async function main() {
let members = await readMembers();
if (!Array.isArray(members)) {
console.error('members.json ist kein Array!')
process.exit(1)
}
let changed = 0;
for (const m of members) {
if (!m.visibility) m.visibility = {};
if (m.visibility.showBirthday !== true) {
m.visibility.showBirthday = true;
changed++;
}
}
if (changed > 0) {
await writeMembers(members);
console.log(`Flag für ${changed} Mitglieder gesetzt.`);
} else {
console.log('Alle Mitglieder hatten das Flag bereits gesetzt.');
}
}
main();

View File

@@ -1,72 +0,0 @@
// Script: set-all-visibility-flags.mjs
// Setzt für alle Mitglieder in allen relevanten Dateien alle visibility-Flags auf true (inkl. Entschlüsselung)
import { readMembers, writeMembers } from './utils/members.js';
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs/promises';
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
const usersPath = path.resolve(process.cwd(), 'server/data/users.json');
async function updateVisibility(obj) {
let changed = 0;
if (Array.isArray(obj)) {
for (const m of obj) {
if (!m.visibility) m.visibility = {};
if (m.visibility.showEmail !== true) { m.visibility.showEmail = true; changed++; }
if (m.visibility.showPhone !== true) { m.visibility.showPhone = true; changed++; }
if (m.visibility.showAddress !== true) { m.visibility.showAddress = true; changed++; }
if (m.visibility.showBirthday !== true) { m.visibility.showBirthday = true; changed++; }
}
}
return changed;
}
async function updateUsersFile() {
let changed = 0;
try {
let raw = await fs.readFile(usersPath, 'utf8');
let users;
if (raw.trim().startsWith('v2:')) {
// encrypted, try to use decryptObject from encryption.js
const { decryptObject } = await import('./utils/encryption.js');
const key = process.env.ENCRYPTION_KEY || 'local_development_encryption_key_change_in_production';
users = decryptObject(raw, key);
} else {
users = JSON.parse(raw);
}
changed = await updateVisibility(users);
// write back (encrypted if vorher encrypted)
if (raw.trim().startsWith('v2:')) {
const { encryptObject } = await import('./utils/encryption.js');
const key = process.env.ENCRYPTION_KEY || 'local_development_encryption_key_change_in_production';
const encrypted = encryptObject(users, key);
await fs.writeFile(usersPath, encrypted, 'utf8');
} else {
await fs.writeFile(usersPath, JSON.stringify(users, null, 2), 'utf8');
}
return changed;
} catch (e) {
console.error('Fehler beim Bearbeiten von users.json:', e);
return 0;
}
}
async function main() {
let changedMembers = 0;
let changedUsers = 0;
// members.json (manuelle Mitglieder)
let members = await readMembers();
changedMembers = await updateVisibility(members);
if (changedMembers > 0) {
await writeMembers(members);
}
// users.json (Login-System)
changedUsers = await updateUsersFile();
console.log(`members.json: ${changedMembers} Änderungen, users.json: ${changedUsers} Änderungen`);
}
main();

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)
}
}
}