import { promises as fs } from 'fs' import path from 'path' import { randomUUID } from 'crypto' import { encrypt, decrypt, encryptObject, decryptObject } from './encryption.js' // Handle both dev and production paths const getDataPath = (filename) => { const cwd = process.cwd() // In production (.output/server), working dir is .output if (cwd.endsWith('.output')) { return path.join(cwd, '../server/data', filename) } // In development, working dir is project root return path.join(cwd, 'server/data', filename) } const MEMBERS_FILE = getDataPath('members.json') // Get encryption key from environment or config function getEncryptionKey() { return process.env.ENCRYPTION_KEY || 'default-key-change-in-production' } // Check if data is encrypted by trying to parse as JSON first function isEncrypted(data) { try { // Try to parse as JSON - if successful and looks like member data, it's unencrypted const parsed = JSON.parse(data.trim()) // If it's an array (members list) or object with member-like structure, it's unencrypted if (Array.isArray(parsed)) { return false // Unencrypted array } // If it's an object but not encrypted format, it's unencrypted if (typeof parsed === 'object' && parsed !== null && !parsed.encryptedData) { return false } return false } catch (e) { // JSON parsing failed - likely encrypted base64 return true } } // Read manual members from file (with encryption support and migration) export async function readMembers() { try { const data = await fs.readFile(MEMBERS_FILE, 'utf-8') // Check if data is encrypted or plain JSON const encrypted = isEncrypted(data) if (encrypted) { // Decrypt and parse const encryptionKey = getEncryptionKey() try { return decryptObject(data, encryptionKey) } catch (decryptError) { console.error('Fehler beim Entschlüsseln der Mitgliederdaten:', decryptError) // Fallback: try to read as plain JSON (migration scenario) try { const plainData = JSON.parse(data) console.warn('Entschlüsselung fehlgeschlagen, versuche als unverschlüsseltes Format zu lesen') return plainData } catch (parseError) { console.error('Konnte Mitgliederdaten weder entschlüsseln noch als JSON lesen') return [] } } } else { // Plain JSON - migrate to encrypted format const members = JSON.parse(data) console.log('Migriere unverschlüsselte Mitgliederdaten zu verschlüsselter Speicherung...') // Write back encrypted await writeMembers(members) return members } } catch (error) { if (error.code === 'ENOENT') { return [] } console.error('Fehler beim Lesen der Mitgliederdaten:', error) return [] } } // Write manual members to file (always encrypted) export async function writeMembers(members) { try { const encryptionKey = getEncryptionKey() const encryptedData = encryptObject(members, encryptionKey) await fs.writeFile(MEMBERS_FILE, encryptedData, 'utf-8') return true } catch (error) { console.error('Fehler beim Schreiben der Mitgliederdaten:', error) return false } } // Get member by ID export async function getMemberById(id) { const members = await readMembers() return members.find(m => m.id === id) } // Normalize date string for comparison (handles different date formats) export function normalizeDate(dateString) { if (!dateString) return '' // Try to parse and normalize to ISO format (YYYY-MM-DD) try { const date = new Date(dateString) if (isNaN(date.getTime())) return dateString.trim() return date.toISOString().split('T')[0] } catch (e) { return dateString.trim() } } // Check for duplicate member based on firstName, lastName, and geburtsdatum function findDuplicateMember(members, firstName, lastName, geburtsdatum) { const normalizedFirstName = (firstName || '').trim().toLowerCase() const normalizedLastName = (lastName || '').trim().toLowerCase() const normalizedDate = normalizeDate(geburtsdatum) return members.find(m => { const mFirstName = (m.firstName || '').trim().toLowerCase() const mLastName = (m.lastName || '').trim().toLowerCase() const mDate = normalizeDate(m.geburtsdatum) return mFirstName === normalizedFirstName && mLastName === normalizedLastName && mDate === normalizedDate && mDate !== '' // Only match if date is provided for both }) } // Add or update manual member export async function saveMember(memberData) { const members = await readMembers() if (memberData.id) { // Update existing const index = members.findIndex(m => m.id === memberData.id) if (index !== -1) { // Check for duplicate (excluding current member) const duplicate = findDuplicateMember( members.filter(m => m.id !== memberData.id), memberData.firstName, memberData.lastName, memberData.geburtsdatum ) if (duplicate) { throw new Error('Ein Mitglied mit diesem Namen und Geburtsdatum existiert bereits.') } members[index] = { ...members[index], ...memberData } } else { throw new Error('Mitglied nicht gefunden') } } else { // Add new - check for duplicate first if (memberData.firstName && memberData.lastName && memberData.geburtsdatum) { const duplicate = findDuplicateMember( members, memberData.firstName, memberData.lastName, memberData.geburtsdatum ) if (duplicate) { throw new Error('Ein Mitglied mit diesem Namen und Geburtsdatum existiert bereits.') } } // Add new - use UUID for guaranteed uniqueness const newMember = { ...memberData, id: randomUUID() // Cryptographically secure unique ID } members.push(newMember) } await writeMembers(members) return true } // Delete manual member export async function deleteMember(id) { const members = await readMembers() const filtered = members.filter(m => m.id !== id) await writeMembers(filtered) return true }