Implemented the possibility ofa hidden user for playstore tests
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { readMembers } from '../utils/members.js'
|
import { readMembers } from '../utils/members.js'
|
||||||
import { readUsers, getUserFromToken, verifyToken } from '../utils/auth.js'
|
import { readUsers, getUserFromToken, verifyToken, isHiddenUser, normalizeUserEmail } from '../utils/auth.js'
|
||||||
|
|
||||||
// Helper: returns array of upcoming birthdays within daysAhead (inclusive)
|
// Helper: returns array of upcoming birthdays within daysAhead (inclusive)
|
||||||
function getUpcomingBirthdays(entries, daysAhead = 28) {
|
function getUpcomingBirthdays(entries, daysAhead = 28) {
|
||||||
@@ -53,10 +53,14 @@ export default defineEventHandler(async (event) => {
|
|||||||
const manualMembers = await readMembers()
|
const manualMembers = await readMembers()
|
||||||
const registeredUsers = await readUsers()
|
const registeredUsers = await readUsers()
|
||||||
|
|
||||||
|
const hiddenUserEmails = new Set(registeredUsers.filter(isHiddenUser).map(user => normalizeUserEmail(user.email)).filter(Boolean))
|
||||||
|
|
||||||
// Build unified list of candidates with geburtsdatum and visibility
|
// Build unified list of candidates with geburtsdatum and visibility
|
||||||
const candidates = []
|
const candidates = []
|
||||||
|
|
||||||
for (const m of manualMembers) {
|
for (const m of manualMembers) {
|
||||||
|
const memberEmail = normalizeUserEmail(m.email)
|
||||||
|
if (m.hidden === true || m.invisible === true || m.isHidden === true || hiddenUserEmails.has(memberEmail)) continue
|
||||||
const normalizedStatus = m.status ? String(m.status).toLowerCase() : ''
|
const normalizedStatus = m.status ? String(m.status).toLowerCase() : ''
|
||||||
const hasExplicitAcceptanceFlag = m.active !== undefined || m.accepted !== undefined || normalizedStatus !== ''
|
const hasExplicitAcceptanceFlag = m.active !== undefined || m.accepted !== undefined || normalizedStatus !== ''
|
||||||
const isAccepted = hasExplicitAcceptanceFlag
|
const isAccepted = hasExplicitAcceptanceFlag
|
||||||
@@ -73,7 +77,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const u of registeredUsers) {
|
for (const u of registeredUsers) {
|
||||||
if (!u.active) continue
|
if (!u.active || isHiddenUser(u)) continue
|
||||||
const vis = u.visibility || {}
|
const vis = u.visibility || {}
|
||||||
const showBirthday = vis.showBirthday === undefined ? true : Boolean(vis.showBirthday)
|
const showBirthday = vis.showBirthday === undefined ? true : Boolean(vis.showBirthday)
|
||||||
candidates.push({ name: u.name, geburtsdatum: u.geburtsdatum, visibility: { showBirthday }, source: 'login' })
|
candidates.push({ name: u.name, geburtsdatum: u.geburtsdatum, visibility: { showBirthday }, source: 'login' })
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getUserFromToken, hasRole, readUsers } from '../../utils/auth.js'
|
import { getUserFromToken, hasRole, readUsers, isHiddenUser } from '../../utils/auth.js'
|
||||||
import {
|
import {
|
||||||
fingerprintResetEmail,
|
fingerprintResetEmail,
|
||||||
normalizeResetEmail,
|
normalizeResetEmail,
|
||||||
@@ -59,17 +59,20 @@ export default defineEventHandler(async (event) => {
|
|||||||
const email = normalizeResetEmail(query.email)
|
const email = normalizeResetEmail(query.email)
|
||||||
const failedOnly = query.failedOnly !== 'false'
|
const failedOnly = query.failedOnly !== 'false'
|
||||||
const users = await readUsers()
|
const users = await readUsers()
|
||||||
|
const visibleUsers = users.filter(user => !isHiddenUser(user))
|
||||||
|
const hiddenEmailFingerprints = new Set(users.filter(isHiddenUser).map(user => fingerprintResetEmail(user.email)).filter(Boolean))
|
||||||
const logs = await readPasswordResetLogs()
|
const logs = await readPasswordResetLogs()
|
||||||
const filteredLogs = email
|
const filteredLogs = (email
|
||||||
? logs.filter(entry => entry.emailFingerprint === fingerprintResetEmail(email))
|
? logs.filter(entry => entry.emailFingerprint === fingerprintResetEmail(email))
|
||||||
: logs
|
: logs)
|
||||||
|
.filter(entry => !hiddenEmailFingerprints.has(entry.emailFingerprint))
|
||||||
const attempts = summarizeAttempts(filteredLogs)
|
const attempts = summarizeAttempts(filteredLogs)
|
||||||
.filter(attempt => !failedOnly || attempt.failed)
|
.filter(attempt => !failedOnly || attempt.failed)
|
||||||
|
|
||||||
let matchingUsers = []
|
let matchingUsers = []
|
||||||
if (email) {
|
if (email) {
|
||||||
const term = email.toLowerCase()
|
const term = email.toLowerCase()
|
||||||
matchingUsers = users
|
matchingUsers = visibleUsers
|
||||||
.filter(user => {
|
.filter(user => {
|
||||||
const userEmail = normalizeResetEmail(user.email)
|
const userEmail = normalizeResetEmail(user.email)
|
||||||
const name = String(user.name || '').toLowerCase()
|
const name = String(user.name || '').toLowerCase()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getUserFromToken, readUsers, hasAnyRole, migrateUserRoles } from '../../../utils/auth.js'
|
import { getUserFromToken, readUsers, hasAnyRole, migrateUserRoles, isHiddenUser } from '../../../utils/auth.js'
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
try {
|
try {
|
||||||
@@ -18,7 +18,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
// Nur Admin oder Vorstand duerfen vollen Benutzer-Contact und Rollen sehen.
|
// Nur Admin oder Vorstand duerfen vollen Benutzer-Contact und Rollen sehen.
|
||||||
const canSeePrivate = hasAnyRole(currentUser, 'admin', 'vorstand')
|
const canSeePrivate = hasAnyRole(currentUser, 'admin', 'vorstand')
|
||||||
|
|
||||||
const safeUsers = users.map(u => {
|
const safeUsers = users.filter(u => !isHiddenUser(u)).map(u => {
|
||||||
const migrated = migrateUserRoles({ ...u })
|
const migrated = migrateUserRoles({ ...u })
|
||||||
const roles = Array.isArray(migrated.roles) ? migrated.roles : (migrated.role ? [migrated.role] : ['mitglied'])
|
const roles = Array.isArray(migrated.roles) ? migrated.roles : (migrated.role ? [migrated.role] : ['mitglied'])
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import nodemailer from 'nodemailer'
|
|||||||
import { promises as fs } from 'fs'
|
import { promises as fs } from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { createContactRequest } from '../utils/contact-requests.js'
|
import { createContactRequest } from '../utils/contact-requests.js'
|
||||||
import { readUsers, migrateUserRoles } from '../utils/auth.js'
|
import { readUsers, migrateUserRoles, isHiddenUser } from '../utils/auth.js'
|
||||||
|
|
||||||
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
// 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
|
// filename is always a hardcoded constant ('config.json'), never user input
|
||||||
@@ -54,6 +54,7 @@ async function collectRecipients(config) {
|
|||||||
try {
|
try {
|
||||||
const users = await readUsers()
|
const users = await readUsers()
|
||||||
for (const rawUser of users) {
|
for (const rawUser of users) {
|
||||||
|
if (isHiddenUser(rawUser)) continue
|
||||||
const user = migrateUserRoles({ ...rawUser })
|
const user = migrateUserRoles({ ...rawUser })
|
||||||
const roles = Array.isArray(user.roles) ? user.roles : []
|
const roles = Array.isArray(user.roles) ? user.roles : []
|
||||||
if (roles.includes('trainer') && user.email && String(user.email).trim()) {
|
if (roles.includes('trainer') && user.email && String(user.email).trim()) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { verifyToken, getUserFromToken, hasRole } from '../utils/auth.js'
|
import { verifyToken, getUserFromToken, hasRole } from '../utils/auth.js'
|
||||||
import { readMembers } from '../utils/members.js'
|
import { readMembers } from '../utils/members.js'
|
||||||
import { readUsers, migrateUserRoles } from '../utils/auth.js'
|
import { readUsers, migrateUserRoles, isHiddenUser, normalizeUserEmail } from '../utils/auth.js'
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
try {
|
try {
|
||||||
@@ -52,7 +52,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
// Skip applications that are not yet accepted
|
// Skip applications that are not yet accepted
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const normalizedEmail = member.email?.toLowerCase().trim() || ''
|
const normalizedEmail = normalizeUserEmail(member.email)
|
||||||
const fullName = `${member.firstName || ''} ${member.lastName || ''}`.trim()
|
const fullName = `${member.firstName || ''} ${member.lastName || ''}`.trim()
|
||||||
const normalizedName = fullName.toLowerCase()
|
const normalizedName = fullName.toLowerCase()
|
||||||
|
|
||||||
@@ -90,11 +90,13 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hiddenUserEmails = new Set(registeredUsers.filter(isHiddenUser).map(user => normalizeUserEmail(user.email)).filter(Boolean))
|
||||||
|
|
||||||
// Then add registered users (only active ones)
|
// Then add registered users (only active ones)
|
||||||
for (const user of registeredUsers) {
|
for (const user of registeredUsers) {
|
||||||
if (!user.active) continue
|
if (!user.active || isHiddenUser(user)) continue
|
||||||
|
|
||||||
const normalizedEmail = user.email?.toLowerCase().trim() || ''
|
const normalizedEmail = normalizeUserEmail(user.email)
|
||||||
const normalizedName = user.name?.toLowerCase().trim() || ''
|
const normalizedName = user.name?.toLowerCase().trim() || ''
|
||||||
|
|
||||||
// Hilfsfunktion: Extrahiere Vorname/Nachname aus user.name
|
// Hilfsfunktion: Extrahiere Vorname/Nachname aus user.name
|
||||||
@@ -208,7 +210,10 @@ export default defineEventHandler(async (event) => {
|
|||||||
const isPrivilegedViewer = currentUser ? hasRole(currentUser, 'vorstand') : false
|
const isPrivilegedViewer = currentUser ? hasRole(currentUser, 'vorstand') : false
|
||||||
|
|
||||||
// Filtere den Admin-Account heraus
|
// Filtere den Admin-Account heraus
|
||||||
const filteredMembers = mergedMembers.filter(m => m.email?.toLowerCase() !== 'admin@harheimertc.de')
|
const filteredMembers = mergedMembers.filter(m => {
|
||||||
|
const email = normalizeUserEmail(m.email || m.loginEmail)
|
||||||
|
return email !== 'admin@harheimertc.de' && !hiddenUserEmails.has(email) && !m.hidden && !m.invisible && !m.isHidden
|
||||||
|
})
|
||||||
const sanitizedMembers = filteredMembers.map(member => {
|
const sanitizedMembers = filteredMembers.map(member => {
|
||||||
// Default: show email/phone/address to other logged-in members unless member.visibility explicitly hides them
|
// Default: show email/phone/address to other logged-in members unless member.visibility explicitly hides them
|
||||||
const visibility = member.visibility || {}
|
const visibility = member.visibility || {}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { readFile } from 'fs/promises'
|
import { readFile } from 'fs/promises'
|
||||||
import { getServerDataPath } from '../../utils/paths.js'
|
import { getServerDataPath } from '../../utils/paths.js'
|
||||||
import { getUserFromToken, verifyToken } from '../../utils/auth.js'
|
import { getUserFromToken, verifyToken, readUsers, isHiddenUser, normalizeUserEmail } from '../../utils/auth.js'
|
||||||
import { readMembers } from '../../utils/members.js'
|
import { readMembers } from '../../utils/members.js'
|
||||||
import { readUsers } from '../../utils/auth.js'
|
|
||||||
|
|
||||||
const QTTR_FILE = getServerDataPath('qttr-values.json')
|
const QTTR_FILE = getServerDataPath('qttr-values.json')
|
||||||
|
|
||||||
@@ -62,12 +61,24 @@ export default defineEventHandler(async (event) => {
|
|||||||
readMembers(),
|
readMembers(),
|
||||||
readUsers()
|
readUsers()
|
||||||
])
|
])
|
||||||
const birthdateLookup = buildBirthdateLookup([...manualMembers, ...registeredUsers])
|
const hiddenUserEmails = new Set(registeredUsers.filter(isHiddenUser).map(user => normalizeUserEmail(user.email)).filter(Boolean))
|
||||||
|
const visibleManualMembers = manualMembers.filter(member => {
|
||||||
|
const email = normalizeUserEmail(member.email)
|
||||||
|
return member.hidden !== true && member.invisible !== true && member.isHidden !== true && !hiddenUserEmails.has(email)
|
||||||
|
})
|
||||||
|
const visibleUsers = registeredUsers.filter(user => !isHiddenUser(user))
|
||||||
|
const hiddenNames = new Set([
|
||||||
|
...manualMembers.filter(member => member.hidden === true || member.invisible === true || member.isHidden === true || hiddenUserEmails.has(normalizeUserEmail(member.email))),
|
||||||
|
...registeredUsers.filter(isHiddenUser)
|
||||||
|
].flatMap(entry => [entry?.name, `${entry?.firstName || ''} ${entry?.lastName || ''}`.trim()]).map(normalizeName).filter(Boolean))
|
||||||
|
const birthdateLookup = buildBirthdateLookup([...visibleManualMembers, ...visibleUsers])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...payload,
|
...payload,
|
||||||
rows: Array.isArray(payload.rows)
|
rows: Array.isArray(payload.rows)
|
||||||
? payload.rows.map((row) => ({
|
? payload.rows
|
||||||
|
.filter(row => !hiddenNames.has(normalizeName(row.playerName)))
|
||||||
|
.map((row) => ({
|
||||||
...row,
|
...row,
|
||||||
birthdate: birthdateLookup.get(normalizeName(row.playerName)) || row.birthdate || ''
|
birthdate: birthdateLookup.get(normalizeName(row.playerName)) || row.birthdate || ''
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -27,6 +27,28 @@ export function migrateUserRoles(user) {
|
|||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function normalizeUserEmail(email) {
|
||||||
|
return String(email || '').trim().toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function configuredHiddenUserEmails() {
|
||||||
|
return [process.env.PLAYSTORE_REVIEW_EMAIL, process.env.HIDDEN_USER_EMAILS]
|
||||||
|
.filter(Boolean)
|
||||||
|
.flatMap(value => String(value).split(','))
|
||||||
|
.map(normalizeUserEmail)
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isHiddenUser(user) {
|
||||||
|
if (!user) return false
|
||||||
|
if (user.hidden === true || user.invisible === true || user.isHidden === true || user.systemAccount === true) return true
|
||||||
|
if (String(user.accountType || '').toLowerCase() === 'playstore_review') return true
|
||||||
|
|
||||||
|
const email = normalizeUserEmail(user.email)
|
||||||
|
return email ? configuredHiddenUserEmails().includes(email) : false
|
||||||
|
}
|
||||||
|
|
||||||
const JWT_SECRET = process.env.JWT_SECRET || 'harheimertc-secret-key-change-in-production'
|
const JWT_SECRET = process.env.JWT_SECRET || 'harheimertc-secret-key-change-in-production'
|
||||||
|
|
||||||
// Handle both dev and production paths
|
// Handle both dev and production paths
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import fs from 'fs/promises'
|
import fs from 'fs/promises'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { readMembers } from './members.js'
|
import { readMembers } from './members.js'
|
||||||
import { readUsers } from './auth.js'
|
import { readUsers, isHiddenUser, normalizeUserEmail } from './auth.js'
|
||||||
import { encryptObject, decryptObject } from './encryption.js'
|
import { encryptObject, decryptObject } from './encryption.js'
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
import { writeDataFileWithRotation } from './data-file-rotation.js'
|
import { writeDataFileWithRotation } from './data-file-rotation.js'
|
||||||
@@ -162,11 +162,11 @@ function calculateAge(geburtsdatum) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtert den Admin-User aus Empfängerliste heraus
|
// Filtert interne System-/Hidden-Accounts aus Empfängerliste heraus
|
||||||
function filterAdminUser(recipients) {
|
function filterInternalUsers(recipients, hiddenEmails = new Set()) {
|
||||||
return recipients.filter(r => {
|
return recipients.filter(r => {
|
||||||
const email = (r.email || '').toLowerCase().trim()
|
const email = normalizeUserEmail(r.email)
|
||||||
return email !== 'admin@harheimertc.de'
|
return email && email !== 'admin@harheimertc.de' && !hiddenEmails.has(email)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,20 +174,26 @@ function filterAdminUser(recipients) {
|
|||||||
export async function getRecipientsByGroup(targetGroup) {
|
export async function getRecipientsByGroup(targetGroup) {
|
||||||
const members = await readMembers()
|
const members = await readMembers()
|
||||||
const users = await readUsers()
|
const users = await readUsers()
|
||||||
|
const hiddenUserEmails = new Set(users.filter(isHiddenUser).map(user => normalizeUserEmail(user.email)).filter(Boolean))
|
||||||
|
const visibleUsers = users.filter(user => !isHiddenUser(user))
|
||||||
|
const visibleMembers = members.filter(member => {
|
||||||
|
const email = normalizeUserEmail(member.email)
|
||||||
|
return member.hidden !== true && member.invisible !== true && member.isHidden !== true && !hiddenUserEmails.has(email)
|
||||||
|
})
|
||||||
|
|
||||||
let recipients = []
|
let recipients = []
|
||||||
|
|
||||||
switch (targetGroup) {
|
switch (targetGroup) {
|
||||||
case 'alle':
|
case 'alle':
|
||||||
// Alle Mitglieder mit E-Mail
|
// Alle Mitglieder mit E-Mail
|
||||||
recipients = members
|
recipients = visibleMembers
|
||||||
.filter(m => m.email && m.email.trim() !== '')
|
.filter(m => m.email && m.email.trim() !== '')
|
||||||
.map(m => ({
|
.map(m => ({
|
||||||
email: m.email,
|
email: m.email,
|
||||||
name: `${m.firstName || ''} ${m.lastName || ''}`.trim() || m.name || ''
|
name: `${m.firstName || ''} ${m.lastName || ''}`.trim() || m.name || ''
|
||||||
}))
|
}))
|
||||||
// Auch alle aktiven Benutzer hinzufügen
|
// Auch alle aktiven Benutzer hinzufügen
|
||||||
users
|
visibleUsers
|
||||||
.filter(u => u.active && u.email)
|
.filter(u => u.active && u.email)
|
||||||
.forEach(u => {
|
.forEach(u => {
|
||||||
if (!recipients.find(r => r.email.toLowerCase() === u.email.toLowerCase())) {
|
if (!recipients.find(r => r.email.toLowerCase() === u.email.toLowerCase())) {
|
||||||
@@ -201,7 +207,7 @@ export async function getRecipientsByGroup(targetGroup) {
|
|||||||
|
|
||||||
case 'erwachsene':
|
case 'erwachsene':
|
||||||
// Mitglieder über 18 Jahre
|
// Mitglieder über 18 Jahre
|
||||||
recipients = members
|
recipients = visibleMembers
|
||||||
.filter(m => {
|
.filter(m => {
|
||||||
if (!m.email || !m.email.trim()) return false
|
if (!m.email || !m.email.trim()) return false
|
||||||
const age = calculateAge(m.geburtsdatum)
|
const age = calculateAge(m.geburtsdatum)
|
||||||
@@ -212,7 +218,7 @@ export async function getRecipientsByGroup(targetGroup) {
|
|||||||
name: `${m.firstName || ''} ${m.lastName || ''}`.trim() || m.name || ''
|
name: `${m.firstName || ''} ${m.lastName || ''}`.trim() || m.name || ''
|
||||||
}))
|
}))
|
||||||
// Auch aktive Benutzer hinzufügen (als Erwachsene behandelt, wenn kein Geburtsdatum)
|
// Auch aktive Benutzer hinzufügen (als Erwachsene behandelt, wenn kein Geburtsdatum)
|
||||||
users
|
visibleUsers
|
||||||
.filter(u => u.active && u.email && u.email.trim())
|
.filter(u => u.active && u.email && u.email.trim())
|
||||||
.forEach(u => {
|
.forEach(u => {
|
||||||
// Prüfe ob bereits vorhanden
|
// Prüfe ob bereits vorhanden
|
||||||
@@ -227,7 +233,7 @@ export async function getRecipientsByGroup(targetGroup) {
|
|||||||
|
|
||||||
case 'nachwuchs':
|
case 'nachwuchs':
|
||||||
// Mitglieder unter 18 Jahre
|
// Mitglieder unter 18 Jahre
|
||||||
recipients = members
|
recipients = visibleMembers
|
||||||
.filter(m => {
|
.filter(m => {
|
||||||
if (!m.email || !m.email.trim()) return false
|
if (!m.email || !m.email.trim()) return false
|
||||||
const age = calculateAge(m.geburtsdatum)
|
const age = calculateAge(m.geburtsdatum)
|
||||||
@@ -239,7 +245,7 @@ export async function getRecipientsByGroup(targetGroup) {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
// Zusätzlich aktive Trainer aus users.json anschreiben
|
// Zusätzlich aktive Trainer aus users.json anschreiben
|
||||||
users
|
visibleUsers
|
||||||
.filter(u => {
|
.filter(u => {
|
||||||
if (!u.active || !u.email || !u.email.trim()) return false
|
if (!u.active || !u.email || !u.email.trim()) return false
|
||||||
const roles = Array.isArray(u.roles) ? u.roles : (u.role ? [u.role] : [])
|
const roles = Array.isArray(u.roles) ? u.roles : (u.role ? [u.role] : [])
|
||||||
@@ -257,7 +263,7 @@ export async function getRecipientsByGroup(targetGroup) {
|
|||||||
|
|
||||||
case 'mannschaftsspieler':
|
case 'mannschaftsspieler':
|
||||||
// Mitglieder die in einer Mannschaft spielen
|
// Mitglieder die in einer Mannschaft spielen
|
||||||
recipients = members
|
recipients = visibleMembers
|
||||||
.filter(m => {
|
.filter(m => {
|
||||||
if (!m.email || !m.email.trim()) return false
|
if (!m.email || !m.email.trim()) return false
|
||||||
// Prüfe ob als Mannschaftsspieler markiert
|
// Prüfe ob als Mannschaftsspieler markiert
|
||||||
@@ -276,7 +282,7 @@ export async function getRecipientsByGroup(targetGroup) {
|
|||||||
|
|
||||||
case 'vorstand':
|
case 'vorstand':
|
||||||
// Nur Vorstand (aus users.json)
|
// Nur Vorstand (aus users.json)
|
||||||
recipients = users
|
recipients = visibleUsers
|
||||||
.filter(u => {
|
.filter(u => {
|
||||||
if (!u.active || !u.email) return false
|
if (!u.active || !u.email) return false
|
||||||
const roles = Array.isArray(u.roles) ? u.roles : (u.role ? [u.role] : [])
|
const roles = Array.isArray(u.roles) ? u.roles : (u.role ? [u.role] : [])
|
||||||
@@ -293,12 +299,13 @@ export async function getRecipientsByGroup(targetGroup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Admin-User herausfiltern
|
// Admin-User herausfiltern
|
||||||
return filterAdminUser(recipients)
|
return filterInternalUsers(recipients, hiddenUserEmails)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Holt Newsletter-Abonnenten (bestätigt und nicht abgemeldet)
|
// Holt Newsletter-Abonnenten (bestätigt und nicht abgemeldet)
|
||||||
export async function getNewsletterSubscribers(internalOnly = false, groupId = null) {
|
export async function getNewsletterSubscribers(internalOnly = false, groupId = null) {
|
||||||
const subscribers = await readSubscribers()
|
const subscribers = await readSubscribers()
|
||||||
|
const hiddenUserEmails = new Set((await readUsers()).filter(isHiddenUser).map(user => normalizeUserEmail(user.email)).filter(Boolean))
|
||||||
|
|
||||||
let confirmedSubscribers = subscribers.filter(s => {
|
let confirmedSubscribers = subscribers.filter(s => {
|
||||||
if (!s.confirmed || s.unsubscribedAt) {
|
if (!s.confirmed || s.unsubscribedAt) {
|
||||||
@@ -329,12 +336,12 @@ export async function getNewsletterSubscribers(internalOnly = false, groupId = n
|
|||||||
const members = await readMembers()
|
const members = await readMembers()
|
||||||
const memberEmails = new Set(
|
const memberEmails = new Set(
|
||||||
members
|
members
|
||||||
.filter(m => m.email)
|
.filter(m => m.email && m.hidden !== true && m.invisible !== true && m.isHidden !== true && !hiddenUserEmails.has(normalizeUserEmail(m.email)))
|
||||||
.map(m => m.email.toLowerCase())
|
.map(m => normalizeUserEmail(m.email))
|
||||||
)
|
)
|
||||||
|
|
||||||
confirmedSubscribers = confirmedSubscribers.filter(s =>
|
confirmedSubscribers = confirmedSubscribers.filter(s =>
|
||||||
memberEmails.has(s.email.toLowerCase())
|
memberEmails.has(normalizeUserEmail(s.email))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,7 +351,7 @@ export async function getNewsletterSubscribers(internalOnly = false, groupId = n
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
// Admin-User herausfiltern
|
// Admin-User herausfiltern
|
||||||
return filterAdminUser(result)
|
return filterInternalUsers(result, hiddenUserEmails)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generiert Abmelde-Token für Abonnenten
|
// Generiert Abmelde-Token für Abonnenten
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ vi.mock('../server/utils/auth.js', () => ({
|
|||||||
user.roles = ['mitglied']
|
user.roles = ['mitglied']
|
||||||
}
|
}
|
||||||
return user
|
return user
|
||||||
})
|
}),
|
||||||
|
isHiddenUser: vi.fn(user => user?.hidden === true || user?.invisible === true || user?.isHidden === true || user?.systemAccount === true || user?.accountType === 'playstore_review')
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('nodemailer', () => {
|
vi.mock('nodemailer', () => {
|
||||||
@@ -96,6 +97,20 @@ describe('CMS User Management Endpoints', () => {
|
|||||||
expect(response.users[0]).not.toHaveProperty('password')
|
expect(response.users[0]).not.toHaveProperty('password')
|
||||||
expect(response.users).toHaveLength(1)
|
expect(response.users).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
it('blendet unsichtbare Playstore-Benutzer auch für Admins aus', async () => {
|
||||||
|
const event = adminEvent()
|
||||||
|
authUtils.readUsers.mockResolvedValue([
|
||||||
|
{ id: '1', email: 'a@b.de', name: 'Anna', roles: ['mitglied'], active: true },
|
||||||
|
{ id: '2', email: 'review@club.de', name: 'Playstore Review', roles: ['mitglied'], active: true, accountType: 'playstore_review' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const response = await usersListHandler(event)
|
||||||
|
|
||||||
|
expect(response.users).toHaveLength(1)
|
||||||
|
expect(response.users[0].email).toBe('a@b.de')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('POST /api/cms/users/approve', () => {
|
describe('POST /api/cms/users/approve', () => {
|
||||||
@@ -239,5 +254,31 @@ describe('CMS User Management Endpoints', () => {
|
|||||||
expect(response.attempts).toHaveLength(1)
|
expect(response.attempts).toHaveLength(1)
|
||||||
expect(response.attempts[0]).toMatchObject({ requestId: 'r1', failed: true })
|
expect(response.attempts[0]).toMatchObject({ requestId: 'r1', failed: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
it('blendet unsichtbare Benutzer und ihre Reset-Logs in der Diagnose aus', async () => {
|
||||||
|
const event = adminEvent()
|
||||||
|
event.__query = { email: 'review@club.de', failedOnly: 'false' }
|
||||||
|
authUtils.hasRole.mockReturnValue(true)
|
||||||
|
authUtils.readUsers.mockResolvedValue([
|
||||||
|
{ id: '2', email: 'review@club.de', name: 'Playstore Review', active: true, accountType: 'playstore_review' }
|
||||||
|
])
|
||||||
|
passwordResetLog.readPasswordResetLogs.mockResolvedValue([
|
||||||
|
{
|
||||||
|
requestId: 'r-hidden',
|
||||||
|
ts: '2026-05-27T10:00:01.000Z',
|
||||||
|
emailMasked: 're***@cl***.de',
|
||||||
|
emailFingerprint: 'fingerprint:review@club.de',
|
||||||
|
ip: '127.0.0.1',
|
||||||
|
step: 'request_completed',
|
||||||
|
status: 'failed'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const response = await passwordResetDiagnosticsHandler(event)
|
||||||
|
|
||||||
|
expect(response.matchingUsers).toHaveLength(0)
|
||||||
|
expect(response.attempts).toHaveLength(0)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ vi.mock('../server/utils/auth.js', () => ({
|
|||||||
if (!user) return false
|
if (!user) return false
|
||||||
const userRoles = Array.isArray(user.roles) ? user.roles : (user.role ? [user.role] : [])
|
const userRoles = Array.isArray(user.roles) ? user.roles : (user.role ? [user.role] : [])
|
||||||
return userRoles.includes(role)
|
return userRoles.includes(role)
|
||||||
})
|
}),
|
||||||
|
normalizeUserEmail: vi.fn(email => String(email || '').trim().toLowerCase()),
|
||||||
|
isHiddenUser: vi.fn(user => user?.hidden === true || user?.invisible === true || user?.isHidden === true || user?.systemAccount === true || user?.accountType === 'playstore_review')
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../server/utils/members.js', () => ({
|
vi.mock('../server/utils/members.js', () => ({
|
||||||
@@ -97,6 +99,28 @@ describe('Members API Endpoints', () => {
|
|||||||
expect(response.members).toHaveLength(1)
|
expect(response.members).toHaveLength(1)
|
||||||
expect(response.members[0].name).toBe('Anna Muster')
|
expect(response.members[0].name).toBe('Anna Muster')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
it('blendet unsichtbare Playstore-Benutzer und passende manuelle Einträge aus', async () => {
|
||||||
|
const event = createEvent({ cookies: { auth_token: 'token' } })
|
||||||
|
authUtils.verifyToken.mockReturnValue({ id: '1' })
|
||||||
|
memberUtils.readMembers.mockResolvedValue([
|
||||||
|
{ id: 'm1', firstName: 'Anna', lastName: 'Muster', email: 'anna@club.de' },
|
||||||
|
{ id: 'm2', firstName: 'Play', lastName: 'Store', email: 'review@club.de' }
|
||||||
|
])
|
||||||
|
authUtils.readUsers.mockResolvedValue([
|
||||||
|
{ id: 'u1', name: 'Ben Nutzer', email: 'ben@club.de', role: 'mitglied', active: true },
|
||||||
|
{ id: 'u2', name: 'Playstore Review', email: 'review@club.de', roles: ['mitglied'], active: true, accountType: 'playstore_review' }
|
||||||
|
])
|
||||||
|
authUtils.getUserFromToken.mockResolvedValue({ id: '1', role: 'mitglied' })
|
||||||
|
|
||||||
|
const response = await membersGetHandler(event)
|
||||||
|
|
||||||
|
expect(response.members.map(member => member.email || member.name)).not.toContain('review@club.de')
|
||||||
|
expect(response.members.map(member => member.name)).not.toContain('Play Store')
|
||||||
|
expect(response.members.map(member => member.name)).not.toContain('Playstore Review')
|
||||||
|
expect(response.members).toHaveLength(2)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('POST /api/members', () => {
|
describe('POST /api/members', () => {
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ vi.mock('../server/utils/auth.js', () => ({
|
|||||||
verifyToken: vi.fn(),
|
verifyToken: vi.fn(),
|
||||||
getUserFromToken: vi.fn(),
|
getUserFromToken: vi.fn(),
|
||||||
readUsers: vi.fn().mockResolvedValue([]),
|
readUsers: vi.fn().mockResolvedValue([]),
|
||||||
migrateUserRoles: vi.fn(user => user)
|
migrateUserRoles: vi.fn(user => user),
|
||||||
|
normalizeUserEmail: vi.fn(email => String(email || '').trim().toLowerCase()),
|
||||||
|
isHiddenUser: vi.fn(user => user?.hidden === true || user?.invisible === true || user?.isHidden === true || user?.systemAccount === true || user?.accountType === 'playstore_review')
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../server/utils/members.js', () => ({
|
vi.mock('../server/utils/members.js', () => ({
|
||||||
@@ -264,5 +266,27 @@ describe('Spielplan, Mannschaften & öffentliche Endpoints', () => {
|
|||||||
|
|
||||||
expect(result.birthdays).toHaveLength(0)
|
expect(result.birthdays).toHaveLength(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
it('blendet unsichtbare Playstore-Benutzer auch für Vorstand aus', async () => {
|
||||||
|
const event = createEvent({ cookies: { auth_token: 'token' } })
|
||||||
|
const inDays = 7
|
||||||
|
const targetDate = new Date()
|
||||||
|
targetDate.setDate(targetDate.getDate() + inDays)
|
||||||
|
const geburtsdatum = `${targetDate.getFullYear() - 30}-${String(targetDate.getMonth() + 1).padStart(2, '0')}-${String(targetDate.getDate()).padStart(2, '0')}`
|
||||||
|
|
||||||
|
authUtils.verifyToken.mockReturnValue({ id: 'v1' })
|
||||||
|
authUtils.getUserFromToken.mockResolvedValue({ id: 'v1', roles: ['vorstand'], active: true })
|
||||||
|
authUtils.readUsers.mockResolvedValue([
|
||||||
|
{ id: 'u2', name: 'Playstore Review', email: 'review@club.de', active: true, geburtsdatum, accountType: 'playstore_review' }
|
||||||
|
])
|
||||||
|
memberUtils.readMembers.mockResolvedValue([
|
||||||
|
{ firstName: 'Play', lastName: 'Store', email: 'review@club.de', geburtsdatum, visibility: { showBirthday: true } }
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await birthdaysHandler(event)
|
||||||
|
|
||||||
|
expect(result.birthdays).toHaveLength(0)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user