jagd-apps/nachsuche/backend/controllers/userController.js

708 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const User = require('../models/User');
const { geocodeAddress } = require('../utils/geocode');
const logger = require('../utils/logger');
const getAllUsers = async (req, res) => {
try {
// Pagination parameters
const page = Math.max(1, parseInt(req.query.page) || 1);
const limit = Math.min(100, Math.max(1, parseInt(req.query.limit) || 50)); // Max 100 items per page
const skip = (page - 1) * limit;
// Optional filters
const filter = {};
if (req.query.available !== undefined) {
filter.available = req.query.available === 'true';
}
if (req.query.type) {
filter.type = req.query.type;
}
if (req.query.search) {
// Text search using the text index
filter.$text = { $search: req.query.search };
}
// Execute query with pagination
const [users, total] = await Promise.all([
User.find(filter)
.sort(req.query.search ? { score: { $meta: 'textScore' } } : { name: 1 })
.skip(skip)
.limit(limit)
.select('-__v'), // Exclude version key
User.countDocuments(filter)
]);
res.json({
success: true,
data: users,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
hasMore: page < Math.ceil(total / limit)
}
});
} catch (error) {
logger.error('Fehler beim Abrufen der Benutzer:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Abrufen der Benutzer'
});
}
};
const getUserById = async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({
success: false,
message: 'Benutzer nicht gefunden'
});
}
res.json({
success: true,
data: user
});
} catch (error) {
logger.error('Fehler beim Abrufen des Benutzers:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Abrufen des Benutzers'
});
}
};
const createUser = async (req, res) => {
try {
const payload = { ...req.body };
let geoWarning = null;
if (!payload.gps && payload.address) {
const coords = await geocodeAddress(payload.address);
if (coords) {
payload.gps = coords;
} else {
geoWarning = 'Adresse konnte nicht geokodiert werden. GPS kann manuell über die Karte gesetzt werden.';
logger.warn(`Geocoding fehlgeschlagen für Adresse: ${payload.address}`);
}
}
const user = new User(payload);
await user.save();
res.status(201).json({
success: true,
data: user,
...(geoWarning && { warning: geoWarning })
});
} catch (error) {
logger.error('Fehler beim Erstellen des Benutzers:', error);
res.status(400).json({
success: false,
message: error.message || 'Fehler beim Erstellen des Benutzers'
});
}
};
const updateUser = async (req, res) => {
try {
const existing = await User.findById(req.params.id);
if (!existing) {
return res.status(404).json({
success: false,
message: 'Benutzer nicht gefunden'
});
}
const updateData = { ...req.body };
const addressChanged = typeof updateData.address === 'string'
&& updateData.address.trim() !== existing.address;
const hasGpsInRequest = updateData.gps
&& updateData.gps.lat !== undefined
&& updateData.gps.lng !== undefined;
let geoWarning = null;
if (addressChanged && !hasGpsInRequest) {
const coords = await geocodeAddress(updateData.address);
if (coords) {
updateData.gps = coords;
} else {
geoWarning = 'Adresse konnte nicht geokodiert werden. GPS kann manuell über die Karte gesetzt werden.';
logger.warn(`Geocoding fehlgeschlagen für Adresse: ${updateData.address}`);
}
}
const user = await User.findByIdAndUpdate(
req.params.id,
updateData,
{ new: true, runValidators: true }
);
if (!user) {
return res.status(404).json({
success: false,
message: 'Benutzer nicht gefunden'
});
}
res.json({
success: true,
data: user,
...(geoWarning && { warning: geoWarning })
});
} catch (error) {
logger.error('Fehler beim Aktualisieren des Benutzers:', error);
res.status(400).json({
success: false,
message: error.message || 'Fehler beim Aktualisieren des Benutzers'
});
}
};
const deleteUser = async (req, res) => {
try {
// Soft delete - mark as deleted instead of removing from database
const user = await User.findByIdAndUpdate(
req.params.id,
{
deleted: true,
deletedAt: new Date(),
deletedBy: req.user?.id || null
},
{ new: true }
);
if (!user) {
return res.status(404).json({
success: false,
message: 'Benutzer nicht gefunden'
});
}
logger.info(`User soft-deleted: ${user.name} (ID: ${user._id}) by admin ${req.user?.username || 'unknown'}`);
res.json({
success: true,
message: 'Benutzer erfolgreich gelöscht',
data: user
});
} catch (error) {
logger.error('Fehler beim Löschen des Benutzers:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Löschen des Benutzers'
});
}
};
const restoreUser = async (req, res) => {
try {
// Restore soft-deleted user
const user = await User.findOneAndUpdate(
{ _id: req.params.id, deleted: true },
{
deleted: false,
deletedAt: null,
deletedBy: null
},
{ new: true }
).setOptions({ includeDeleted: true });
if (!user) {
return res.status(404).json({
success: false,
message: 'Gelöschter Benutzer nicht gefunden'
});
}
logger.info(`User restored: ${user.name} (ID: ${user._id}) by admin ${req.user?.username || 'unknown'}`);
res.json({
success: true,
message: 'Benutzer erfolgreich wiederhergestellt',
data: user
});
} catch (error) {
logger.error('Fehler beim Wiederherstellen des Benutzers:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Wiederherstellen des Benutzers'
});
}
};
const getDeletedUsers = async (req, res) => {
try {
// Get only deleted users
const users = await User.find({ deleted: true })
.sort({ deletedAt: -1 })
.populate('deletedBy', 'username')
.setOptions({ includeDeleted: true });
res.json({
success: true,
data: users,
count: users.length
});
} catch (error) {
logger.error('Fehler beim Abrufen gelöschter Benutzer:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Abrufen gelöschter Benutzer'
});
}
};
const getPublicUsers = async (req, res) => {
try {
// Pagination parameters
const page = Math.max(1, parseInt(req.query.page) || 1);
const limit = Math.min(100, Math.max(1, parseInt(req.query.limit) || 50));
const skip = (page - 1) * limit;
// Filter for available users only + optional type filter
const filter = { available: true };
if (req.query.type) {
filter.type = req.query.type;
}
if (req.query.search) {
filter.$text = { $search: req.query.search };
}
// Execute query with pagination
const [users, total] = await Promise.all([
User.find(filter)
.sort(req.query.search ? { score: { $meta: 'textScore' } } : { name: 1 })
.skip(skip)
.limit(limit)
.select('name type available gps'),
User.countDocuments(filter)
]);
res.json({
success: true,
data: users,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
hasMore: page < Math.ceil(total / limit)
}
});
} catch (error) {
logger.error('Fehler beim Abrufen der öffentlichen Benutzer:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Abrufen der öffentlichen Benutzer'
});
}
};
const updateAvailability = async (req, res) => {
try {
const { id } = req.params;
const { available } = req.body;
if (typeof available !== 'boolean') {
return res.status(400).json({
success: false,
message: 'Verfügbarkeit muss ein Boolean-Wert sein'
});
}
const user = await User.findByIdAndUpdate(
id,
{ available },
{ new: true, runValidators: true }
);
if (!user) {
return res.status(404).json({
success: false,
message: 'Benutzer nicht gefunden'
});
}
res.json({
success: true,
data: user
});
} catch (error) {
logger.error('Fehler beim Aktualisieren der Verfügbarkeit:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Aktualisieren der Verfügbarkeit'
});
}
};
const updateGPS = async (req, res) => {
try {
const { id } = req.params;
const { lat, lng } = req.body;
// Validate GPS coordinates
if (lat === undefined || lng === undefined) {
return res.status(400).json({
success: false,
message: 'Latitude und Longitude sind erforderlich'
});
}
const latNum = parseFloat(lat);
const lngNum = parseFloat(lng);
if (isNaN(latNum) || isNaN(lngNum)) {
return res.status(400).json({
success: false,
message: 'Ungültige GPS-Koordinaten'
});
}
if (latNum < -90 || latNum > 90 || lngNum < -180 || lngNum > 180) {
return res.status(400).json({
success: false,
message: 'GPS-Koordinaten außerhalb des gültigen Bereichs'
});
}
const user = await User.findByIdAndUpdate(
id,
{ gps: { lat: latNum, lng: lngNum } },
{ new: true, runValidators: true }
);
if (!user) {
return res.status(404).json({
success: false,
message: 'Benutzer nicht gefunden'
});
}
res.json({
success: true,
data: user
});
} catch (error) {
logger.error('Fehler beim Aktualisieren der GPS-Koordinaten:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Aktualisieren der GPS-Koordinaten'
});
}
};
const exportUsers = async (req, res) => {
try {
const format = req.query.format || 'json';
const users = await User.find({ deleted: { $ne: true } })
.sort({ name: 1 })
.select('-__v -passwordHash -deleted -deletedAt -deletedBy');
if (format === 'csv') {
const escapeCell = (val) => {
if (val == null) return '';
const str = String(val);
return str.includes(',') || str.includes('"') || str.includes('\n')
? `"${str.replace(/"/g, '""')}"`
: str;
};
const csv = [
['Name', 'Adresse', 'Telefon', 'Festnetz', 'E-Mail', 'Typ', 'Verfügbar', 'Latitude', 'Longitude'].join(','),
...users.map(user => [
escapeCell(user.name),
escapeCell(user.address),
escapeCell(user.phone),
escapeCell(user.landline),
escapeCell(user.email),
escapeCell(user.type),
user.available ? 'Ja' : 'Nein',
user.gps?.lat ?? '',
user.gps?.lng ?? ''
].join(','))
].join('\n');
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition',
`attachment; filename="nachsuchenfuehrer_export_${new Date().toISOString().slice(0, 10)}.csv"`);
return res.send('\uFEFF' + csv); // BOM für korrekte UTF-8-Darstellung in Excel
}
// JSON format sauberes Import-Format ohne MongoDB-interne Felder
const exportData = users.map(u => ({
name: u.name,
address: u.address,
phone: u.phone,
landline: u.landline || null,
email: u.email || null,
type: u.type,
available: u.available,
...(u.gps?.lat != null && u.gps?.lng != null ? { gps: { lat: u.gps.lat, lng: u.gps.lng } } : {})
}));
res.setHeader('Content-Disposition',
`attachment; filename="nachsuchenfuehrer_export_${new Date().toISOString().slice(0, 10)}.json"`);
res.json({
exportedAt: new Date().toISOString(),
count: exportData.length,
users: exportData
});
} catch (error) {
logger.error('Fehler beim Exportieren der Benutzer:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Exportieren der Benutzer'
});
}
};
/**
* Import users from a previously exported JSON file.
* Accepts { users: [...] } same format as the JSON export.
* Existing users (matched by name + address) are skipped unless ?overwrite=true.
*/
const importUsers = async (req, res) => {
try {
const { users: importList } = req.body;
if (!Array.isArray(importList) || importList.length === 0) {
return res.status(400).json({
success: false,
message: 'Kein gültiges Import-Format. Erwartet: { users: [...] }'
});
}
if (importList.length > 500) {
return res.status(400).json({
success: false,
message: 'Zu viele Einträge. Maximal 500 Benutzer pro Import erlaubt.'
});
}
const results = { imported: 0, skipped: 0, errors: [] };
for (const entry of importList) {
const { name, address, phone, type } = entry;
if (!name || !address || !phone || !type) {
results.errors.push({ entry: entry.name || '(unbekannt)', reason: 'Pflichtfelder fehlen (name, address, phone, type)' });
continue;
}
try {
const existing = await User.findOne({ name: name.trim(), deleted: { $ne: true } });
if (existing && !overwrite) {
results.skipped++;
continue;
}
const payload = {
name: name.trim(),
address: address.trim(),
phone: phone.trim(),
landline: entry.landline || null,
email: entry.email || null,
type: type.trim(),
available: entry.available !== undefined ? Boolean(entry.available) : true
};
if (entry.gps?.lat != null && entry.gps?.lng != null) {
payload.gps = { lat: Number(entry.gps.lat), lng: Number(entry.gps.lng) };
} else {
// GPS aus Adresse ermitteln
const coords = await geocodeAddress(payload.address);
if (coords) payload.gps = coords;
}
if (existing && overwrite) {
await User.findByIdAndUpdate(existing._id, payload, { runValidators: true });
} else {
await User.create(payload);
}
results.imported++;
} catch (err) {
results.errors.push({ entry: entry.name || '(unbekannt)', reason: err.message });
}
}
logger.info(`Import abgeschlossen: ${results.imported} importiert, ${results.skipped} übersprungen, ${results.errors.length} Fehler`);
res.json({
success: true,
message: `Import abgeschlossen: ${results.imported} importiert, ${results.skipped} übersprungen, ${results.errors.length} Fehler`,
results
});
} catch (error) {
logger.error('Fehler beim Importieren der Benutzer:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Importieren der Benutzer'
});
}
};
/**
* Bulk update users - update multiple users at once
*/
const bulkUpdateUsers = async (req, res) => {
try {
const { ids, updates } = req.body;
if (!ids || !Array.isArray(ids) || ids.length === 0) {
return res.status(400).json({
success: false,
message: 'IDs-Array ist erforderlich'
});
}
if (!updates || typeof updates !== 'object') {
return res.status(400).json({
success: false,
message: 'Updates-Objekt ist erforderlich'
});
}
// Prevent updating sensitive fields
const allowedFields = ['available', 'type', 'phone', 'address'];
const updateFields = {};
for (const [key, value] of Object.entries(updates)) {
if (allowedFields.includes(key)) {
updateFields[key] = value;
}
}
if (Object.keys(updateFields).length === 0) {
return res.status(400).json({
success: false,
message: 'Keine gültigen Update-Felder angegeben'
});
}
// Perform bulk update
const result = await User.updateMany(
{ _id: { $in: ids } },
{ $set: updateFields }
);
logger.info(`Bulk update: ${result.modifiedCount} users updated by admin ${req.user?.username || 'unknown'}`);
res.json({
success: true,
message: `${result.modifiedCount} Benutzer erfolgreich aktualisiert`,
data: {
matched: result.matchedCount,
modified: result.modifiedCount
}
});
} catch (error) {
logger.error('Fehler beim Bulk-Update:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Bulk-Update'
});
}
};
/**
* Bulk delete users - soft delete multiple users at once
*/
const bulkDeleteUsers = async (req, res) => {
try {
const { ids } = req.body;
if (!ids || !Array.isArray(ids) || ids.length === 0) {
return res.status(400).json({
success: false,
message: 'IDs-Array ist erforderlich'
});
}
// Soft delete all users
const result = await User.updateMany(
{ _id: { $in: ids } },
{
$set: {
deleted: true,
deletedAt: new Date(),
deletedBy: req.user?.id || null
}
}
);
logger.info(`Bulk delete: ${result.modifiedCount} users deleted by admin ${req.user?.username || 'unknown'}`);
res.json({
success: true,
message: `${result.modifiedCount} Benutzer erfolgreich gelöscht`,
data: {
matched: result.matchedCount,
modified: result.modifiedCount
}
});
} catch (error) {
logger.error('Fehler beim Bulk-Delete:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Bulk-Delete'
});
}
};
const uploadUserPhoto = async (req, res) => {
try {
const { photo } = req.body;
const ALLOWED_PHOTO_TYPES = [
'data:image/jpeg;base64,',
'data:image/png;base64,',
'data:image/webp;base64,',
'data:image/gif;base64,'
];
if (!photo || typeof photo !== 'string' || !ALLOWED_PHOTO_TYPES.some(t => photo.startsWith(t))) {
return res.status(400).json({ success: false, message: 'Ungültiges Bildformat. Erlaubt: JPEG, PNG, WebP, GIF' });
}
const user = await User.findOne({ _id: req.params.id, deleted: { $ne: true } });
if (!user) {
return res.status(404).json({ success: false, message: 'Benutzer nicht gefunden' });
}
user.photo = photo;
await user.save();
res.json({ success: true, data: { photo: user.photo } });
} catch (error) {
logger.error('Fehler beim Foto-Upload:', error);
res.status(500).json({ success: false, message: 'Fehler beim Foto-Upload' });
}
};
const deleteUserPhoto = async (req, res) => {
try {
const user = await User.findOne({ _id: req.params.id, deleted: { $ne: true } });
if (!user) {
return res.status(404).json({ success: false, message: 'Benutzer nicht gefunden' });
}
user.photo = null;
await user.save();
res.json({ success: true });
} catch (error) {
logger.error('Fehler beim Löschen des Fotos:', error);
res.status(500).json({ success: false, message: 'Fehler beim Löschen des Fotos' });
}
};
module.exports = {
getAllUsers,
getUserById,
createUser,
updateUser,
deleteUser,
restoreUser,
getDeletedUsers,
getPublicUsers,
updateAvailability,
updateGPS,
exportUsers,
importUsers,
bulkUpdateUsers,
bulkDeleteUsers,
uploadUserPhoto,
deleteUserPhoto
};