199 lines
6.1 KiB
JavaScript
199 lines
6.1 KiB
JavaScript
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 || 'local_development_encryption_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
|
|
}
|
|
|