import { verifyToken, getUserFromToken, hasRole } from '../utils/auth.js' import { readMembers } from '../utils/members.js' import { readUsers, migrateUserRoles } from '../utils/auth.js' export default defineEventHandler(async (event) => { try { const token = getCookie(event, 'auth_token') if (!token) { throw createError({ statusCode: 401, message: 'Nicht authentifiziert.' }) } const decoded = verifyToken(token) if (!decoded) { throw createError({ statusCode: 401, message: 'Ungültiges Token.' }) } const currentUser = await getUserFromToken(token) // Get manual members and registered users const manualMembers = await readMembers() const registeredUsers = await readUsers() // Merge members: combine manual + registered, detect duplicates const mergedMembers = [] // Create lookup maps for O(1) matching instead of O(n) findIndex const emailToIndexMap = new Map() // email -> index in mergedMembers const nameToIndexMap = new Map() // name -> index in mergedMembers // First, add manual members that are active/accepted (filter out pending applications) for (let i = 0; i < manualMembers.length; i++) { const member = manualMembers[i] // Normalize acceptance flags: accept if member.active===true or member.status==='accepted' or member.accepted===true const isAccepted = member.active === true || (member.status && String(member.status).toLowerCase() === 'accepted') || member.accepted === true if (!isAccepted) { // Skip applications that are not yet accepted continue } const normalizedEmail = member.email?.toLowerCase().trim() || '' const fullName = `${member.firstName || ''} ${member.lastName || ''}`.trim() const normalizedName = fullName.toLowerCase() const memberIndex = mergedMembers.length // Ensure visibility flags are booleans for manual entries const vis = member.visibility || {} member.visibility = { // Default: visible to all logged-in members unless explicitly hidden 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) } mergedMembers.push({ ...member, name: fullName, // Computed for display source: 'manual', editable: true, hasLogin: false }) // Build lookup maps (only for manual members) if (normalizedEmail) { // Only add if not already present (prefer first occurrence) if (!emailToIndexMap.has(normalizedEmail)) { emailToIndexMap.set(normalizedEmail, memberIndex) } } if (normalizedName) { // Only add if not already present (prefer first occurrence) if (!nameToIndexMap.has(normalizedName)) { nameToIndexMap.set(normalizedName, memberIndex) } } } // Then add registered users (only active ones) for (const user of registeredUsers) { if (!user.active) continue const normalizedEmail = user.email?.toLowerCase().trim() || '' const normalizedName = user.name?.toLowerCase().trim() || '' // Check if this user matches an existing manual member using O(1) lookup let matchedManualIndex = -1 // Try to match by email first (O(1) lookup) if (normalizedEmail && emailToIndexMap.has(normalizedEmail)) { matchedManualIndex = emailToIndexMap.get(normalizedEmail) // Verify it's still a manual member (safety check) if (mergedMembers[matchedManualIndex]?.source !== 'manual') { matchedManualIndex = -1 } } // If no email match, try name (O(1) lookup) if (matchedManualIndex === -1 && normalizedName && nameToIndexMap.has(normalizedName)) { matchedManualIndex = nameToIndexMap.get(normalizedName) // Verify it's still a manual member and email doesn't conflict (safety check) const candidate = mergedMembers[matchedManualIndex] if (candidate?.source === 'manual') { // Additional safety: if candidate has email, make sure it doesn't conflict const candidateEmail = candidate.email?.toLowerCase().trim() || '' if (!candidateEmail || candidateEmail === normalizedEmail) { // Safe to match by name } else { // Email mismatch - don't match by name alone matchedManualIndex = -1 } } else { matchedManualIndex = -1 } } if (matchedManualIndex !== -1) { // Merge with existing manual member const migratedUser = migrateUserRoles({ ...user }) const roles = Array.isArray(migratedUser.roles) ? migratedUser.roles : (migratedUser.role ? [migratedUser.role] : ['mitglied']) mergedMembers[matchedManualIndex] = { ...mergedMembers[matchedManualIndex], hasLogin: true, loginEmail: user.email, loginRoles: roles, loginRole: roles[0] || 'mitglied', // Rückwärtskompatibilität lastLogin: user.lastLogin, isMannschaftsspieler: user.isMannschaftsspieler === true || mergedMembers[matchedManualIndex].isMannschaftsspieler === true } // If the registered user has visibility preferences, apply them (coerce to booleans) if (user.visibility && typeof user.visibility === 'object') { const vis = mergedMembers[matchedManualIndex].visibility || {} 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) } } } else { // Add as new member (from login system) const migratedUser = migrateUserRoles({ ...user }) const roles = Array.isArray(migratedUser.roles) ? migratedUser.roles : (migratedUser.role ? [migratedUser.role] : ['mitglied']) // Registered-only user: default to privacy-preserving visibility (hidden) unless user explicitly set visibility elsewhere // Use stored visibility from user if present, otherwise default to false const userVis = user.visibility || {} mergedMembers.push({ id: user.id, name: user.name, email: user.email, phone: user.phone || '', address: '', 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) }, notes: `Rolle(n): ${roles.join(', ')}`, source: 'login', editable: false, hasLogin: true, loginEmail: user.email, loginRoles: roles, loginRole: roles[0] || 'mitglied', // Rückwärtskompatibilität lastLogin: user.lastLogin, isMannschaftsspieler: user.isMannschaftsspieler === true }) } } // Sort by name mergedMembers.sort((a, b) => a.name.localeCompare(b.name)) // Die Mitgliederliste ist nur für authentifizierte Nutzer sichtbar (siehe oben). // Respektiere individuelle Sichtbarkeitspräferenzen (user.visibility) const currentUserToken = token const isViewerAuthenticated = !!currentUser // Only 'vorstand' may override member visibility const isPrivilegedViewer = currentUser ? hasRole(currentUser, 'vorstand') : false const sanitizedMembers = mergedMembers.map(member => { // Default: show email/phone/address to other logged-in members unless member.visibility explicitly hides them const visibility = member.visibility || {} const showEmail = visibility.showEmail === undefined ? true : Boolean(visibility.showEmail) const showPhone = visibility.showPhone === undefined ? true : Boolean(visibility.showPhone) const showAddress = visibility.showAddress === undefined ? false : Boolean(visibility.showAddress) // Determine if contact info existed but was hidden to the viewer const hadEmail = !!member.email const hadPhone = !!member.phone const hadAddress = !!member.address const hadBirthday = !!member.geburtsdatum 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 contactHidden = (!emailVisible && hadEmail) || (!phoneVisible && hadPhone) || (!addressVisible && hadAddress) return { id: member.id, name: member.name, source: member.source, editable: member.editable, hasLogin: member.hasLogin, loginRoles: member.loginRoles, loginRole: member.loginRole, lastLogin: member.lastLogin, isMannschaftsspieler: member.isMannschaftsspieler, notes: member.notes || '', // Privileged viewers (vorstand) always see contact fields email: emailVisible ? member.email : undefined, phone: phoneVisible ? member.phone : undefined, address: addressVisible ? member.address : undefined, // Birthday: expose only day + month and only if allowed; do not expose year or age birthday: (birthdayVisible && hadBirthday) ? (function(){ try { const d = new Date(member.geburtsdatum) if (isNaN(d.getTime())) return undefined const day = `${d.getDate()}`.padStart(2, '0') const month = `${d.getMonth()+1}`.padStart(2, '0') return `${day}.${month}` } catch (_e) { return undefined } })() : undefined, // Flag for UI: data existed but is hidden to the current viewer contactHidden } }) return { success: true, members: sanitizedMembers } } catch (error) { console.error('Fehler beim Abrufen der Mitgliederliste:', error) throw error } })