Implemented the possibility ofa hidden user for playstore tests
Some checks failed
Code Analysis and Production Deploy / analyze (push) Failing after 5m40s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Has been skipped

This commit is contained in:
Torsten Schulz (local)
2026-06-09 11:32:00 +02:00
parent 300dce9835
commit 530e544542
11 changed files with 184 additions and 42 deletions

View File

@@ -1,5 +1,5 @@
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)
function getUpcomingBirthdays(entries, daysAhead = 28) {
@@ -53,10 +53,14 @@ export default defineEventHandler(async (event) => {
const manualMembers = await readMembers()
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
const candidates = []
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 hasExplicitAcceptanceFlag = m.active !== undefined || m.accepted !== undefined || normalizedStatus !== ''
const isAccepted = hasExplicitAcceptanceFlag
@@ -73,7 +77,7 @@ export default defineEventHandler(async (event) => {
}
for (const u of registeredUsers) {
if (!u.active) continue
if (!u.active || isHiddenUser(u)) continue
const vis = u.visibility || {}
const showBirthday = vis.showBirthday === undefined ? true : Boolean(vis.showBirthday)
candidates.push({ name: u.name, geburtsdatum: u.geburtsdatum, visibility: { showBirthday }, source: 'login' })

View File

@@ -1,4 +1,4 @@
import { getUserFromToken, hasRole, readUsers } from '../../utils/auth.js'
import { getUserFromToken, hasRole, readUsers, isHiddenUser } from '../../utils/auth.js'
import {
fingerprintResetEmail,
normalizeResetEmail,
@@ -59,17 +59,20 @@ export default defineEventHandler(async (event) => {
const email = normalizeResetEmail(query.email)
const failedOnly = query.failedOnly !== 'false'
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 filteredLogs = email
const filteredLogs = (email
? logs.filter(entry => entry.emailFingerprint === fingerprintResetEmail(email))
: logs
: logs)
.filter(entry => !hiddenEmailFingerprints.has(entry.emailFingerprint))
const attempts = summarizeAttempts(filteredLogs)
.filter(attempt => !failedOnly || attempt.failed)
let matchingUsers = []
if (email) {
const term = email.toLowerCase()
matchingUsers = users
matchingUsers = visibleUsers
.filter(user => {
const userEmail = normalizeResetEmail(user.email)
const name = String(user.name || '').toLowerCase()

View File

@@ -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) => {
try {
@@ -18,7 +18,7 @@ export default defineEventHandler(async (event) => {
// Nur Admin oder Vorstand duerfen vollen Benutzer-Contact und Rollen sehen.
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 roles = Array.isArray(migrated.roles) ? migrated.roles : (migrated.role ? [migrated.role] : ['mitglied'])

View File

@@ -2,7 +2,7 @@ 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'
import { readUsers, migrateUserRoles, isHiddenUser } 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
@@ -54,6 +54,7 @@ async function collectRecipients(config) {
try {
const users = await readUsers()
for (const rawUser of users) {
if (isHiddenUser(rawUser)) continue
const user = migrateUserRoles({ ...rawUser })
const roles = Array.isArray(user.roles) ? user.roles : []
if (roles.includes('trainer') && user.email && String(user.email).trim()) {

View File

@@ -1,6 +1,6 @@
import { verifyToken, getUserFromToken, hasRole } from '../utils/auth.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) => {
try {
@@ -52,7 +52,7 @@ export default defineEventHandler(async (event) => {
// Skip applications that are not yet accepted
continue
}
const normalizedEmail = member.email?.toLowerCase().trim() || ''
const normalizedEmail = normalizeUserEmail(member.email)
const fullName = `${member.firstName || ''} ${member.lastName || ''}`.trim()
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)
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() || ''
// Hilfsfunktion: Extrahiere Vorname/Nachname aus user.name
@@ -208,7 +210,10 @@ export default defineEventHandler(async (event) => {
const isPrivilegedViewer = currentUser ? hasRole(currentUser, 'vorstand') : false
// 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 => {
// Default: show email/phone/address to other logged-in members unless member.visibility explicitly hides them
const visibility = member.visibility || {}

View File

@@ -1,8 +1,7 @@
import { readFile } from 'fs/promises'
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 { readUsers } from '../../utils/auth.js'
const QTTR_FILE = getServerDataPath('qttr-values.json')
@@ -62,15 +61,27 @@ export default defineEventHandler(async (event) => {
readMembers(),
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 {
...payload,
rows: Array.isArray(payload.rows)
? payload.rows.map((row) => ({
...row,
birthdate: birthdateLookup.get(normalizeName(row.playerName)) || row.birthdate || ''
}))
? payload.rows
.filter(row => !hiddenNames.has(normalizeName(row.playerName)))
.map((row) => ({
...row,
birthdate: birthdateLookup.get(normalizeName(row.playerName)) || row.birthdate || ''
}))
: []
}
} catch (error) {

View File

@@ -27,6 +27,28 @@ export function migrateUserRoles(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'
// Handle both dev and production paths

View File

@@ -1,7 +1,7 @@
import fs from 'fs/promises'
import path from 'path'
import { readMembers } from './members.js'
import { readUsers } from './auth.js'
import { readUsers, isHiddenUser, normalizeUserEmail } from './auth.js'
import { encryptObject, decryptObject } from './encryption.js'
import crypto from 'crypto'
import { writeDataFileWithRotation } from './data-file-rotation.js'
@@ -162,11 +162,11 @@ function calculateAge(geburtsdatum) {
}
}
// Filtert den Admin-User aus Empfängerliste heraus
function filterAdminUser(recipients) {
// Filtert interne System-/Hidden-Accounts aus Empfängerliste heraus
function filterInternalUsers(recipients, hiddenEmails = new Set()) {
return recipients.filter(r => {
const email = (r.email || '').toLowerCase().trim()
return email !== 'admin@harheimertc.de'
const email = normalizeUserEmail(r.email)
return email && email !== 'admin@harheimertc.de' && !hiddenEmails.has(email)
})
}
@@ -174,20 +174,26 @@ function filterAdminUser(recipients) {
export async function getRecipientsByGroup(targetGroup) {
const members = await readMembers()
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 = []
switch (targetGroup) {
case 'alle':
// Alle Mitglieder mit E-Mail
recipients = members
recipients = visibleMembers
.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
visibleUsers
.filter(u => u.active && u.email)
.forEach(u => {
if (!recipients.find(r => r.email.toLowerCase() === u.email.toLowerCase())) {
@@ -201,7 +207,7 @@ export async function getRecipientsByGroup(targetGroup) {
case 'erwachsene':
// Mitglieder über 18 Jahre
recipients = members
recipients = visibleMembers
.filter(m => {
if (!m.email || !m.email.trim()) return false
const age = calculateAge(m.geburtsdatum)
@@ -212,7 +218,7 @@ export async function getRecipientsByGroup(targetGroup) {
name: `${m.firstName || ''} ${m.lastName || ''}`.trim() || m.name || ''
}))
// Auch aktive Benutzer hinzufügen (als Erwachsene behandelt, wenn kein Geburtsdatum)
users
visibleUsers
.filter(u => u.active && u.email && u.email.trim())
.forEach(u => {
// Prüfe ob bereits vorhanden
@@ -227,7 +233,7 @@ export async function getRecipientsByGroup(targetGroup) {
case 'nachwuchs':
// Mitglieder unter 18 Jahre
recipients = members
recipients = visibleMembers
.filter(m => {
if (!m.email || !m.email.trim()) return false
const age = calculateAge(m.geburtsdatum)
@@ -239,7 +245,7 @@ export async function getRecipientsByGroup(targetGroup) {
}))
// Zusätzlich aktive Trainer aus users.json anschreiben
users
visibleUsers
.filter(u => {
if (!u.active || !u.email || !u.email.trim()) return false
const roles = Array.isArray(u.roles) ? u.roles : (u.role ? [u.role] : [])
@@ -257,7 +263,7 @@ export async function getRecipientsByGroup(targetGroup) {
case 'mannschaftsspieler':
// Mitglieder die in einer Mannschaft spielen
recipients = members
recipients = visibleMembers
.filter(m => {
if (!m.email || !m.email.trim()) return false
// Prüfe ob als Mannschaftsspieler markiert
@@ -276,7 +282,7 @@ export async function getRecipientsByGroup(targetGroup) {
case 'vorstand':
// Nur Vorstand (aus users.json)
recipients = users
recipients = visibleUsers
.filter(u => {
if (!u.active || !u.email) return false
const roles = Array.isArray(u.roles) ? u.roles : (u.role ? [u.role] : [])
@@ -293,12 +299,13 @@ export async function getRecipientsByGroup(targetGroup) {
}
// Admin-User herausfiltern
return filterAdminUser(recipients)
return filterInternalUsers(recipients, hiddenUserEmails)
}
// Holt Newsletter-Abonnenten (bestätigt und nicht abgemeldet)
export async function getNewsletterSubscribers(internalOnly = false, groupId = null) {
const subscribers = await readSubscribers()
const hiddenUserEmails = new Set((await readUsers()).filter(isHiddenUser).map(user => normalizeUserEmail(user.email)).filter(Boolean))
let confirmedSubscribers = subscribers.filter(s => {
if (!s.confirmed || s.unsubscribedAt) {
@@ -329,12 +336,12 @@ export async function getNewsletterSubscribers(internalOnly = false, groupId = n
const members = await readMembers()
const memberEmails = new Set(
members
.filter(m => m.email)
.map(m => m.email.toLowerCase())
.filter(m => m.email && m.hidden !== true && m.invisible !== true && m.isHidden !== true && !hiddenUserEmails.has(normalizeUserEmail(m.email)))
.map(m => normalizeUserEmail(m.email))
)
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
return filterAdminUser(result)
return filterInternalUsers(result, hiddenUserEmails)
}
// Generiert Abmelde-Token für Abonnenten