Refaktoriere Controller-Methoden zur Benutzer-, Event- und Menü-Datenverwaltung, indem die Logik in separate Service-Klassen ausgelagert wird. Implementiere eine verbesserte Fehlerbehandlung und sichere Rückgaben. Füge eine neue Route zur Passwortänderung im Benutzer-Router hinzu.

This commit is contained in:
Torsten Schulz (local)
2025-09-24 10:02:46 +02:00
parent 36e5b05e39
commit 77e3dbde82
13 changed files with 1137 additions and 554 deletions

View File

@@ -1,205 +1,29 @@
const bcrypt = require('bcryptjs');
const { User, PasswordResetToken } = require('../models');
const jwt = require('jsonwebtoken');
const { addTokenToBlacklist } = require('../utils/blacklist');
const { transporter, getPasswordResetEmailTemplate } = require('../config/email');
const crypto = require('crypto');
const AuthService = require('../services/AuthService');
const ErrorHandler = require('../utils/ErrorHandler');
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
exports.register = ErrorHandler.asyncHandler(async (req, res) => {
const result = await AuthService.register(req.body);
ErrorHandler.successResponse(res, result, 'Benutzer erfolgreich registriert', 201);
});
exports.register = async (req, res) => {
const { name, email, password } = req.body;
if (!name || !email || !password) {
return res.status(400).json({ message: 'Alle Felder sind erforderlich' });
}
try {
const hashedPassword = await bcrypt.hash(password, 10);
console.log('Register: creating user', { email });
exports.login = ErrorHandler.asyncHandler(async (req, res) => {
const result = await AuthService.login(req.body);
ErrorHandler.successResponse(res, result, result.message);
});
const maxAttempts = 3;
let attempt = 0;
let createdUser = null;
let lastError = null;
exports.forgotPassword = ErrorHandler.asyncHandler(async (req, res) => {
const result = await AuthService.forgotPassword(req.body.email);
ErrorHandler.successResponse(res, result, result.message);
});
while (attempt < maxAttempts && !createdUser) {
try {
createdUser = await User.create({ name, email, password: hashedPassword, active: true });
} catch (err) {
lastError = err;
// Spezifisch auf Lock-Timeout reagieren und erneut versuchen
if ((err.code === 'ER_LOCK_WAIT_TIMEOUT' || err?.parent?.code === 'ER_LOCK_WAIT_TIMEOUT') && attempt < maxAttempts - 1) {
const backoffMs = 300 * (attempt + 1);
console.warn(`Register: ER_LOCK_WAIT_TIMEOUT, retry in ${backoffMs}ms (attempt ${attempt + 1}/${maxAttempts})`);
await delay(backoffMs);
attempt++;
continue;
}
throw err;
}
}
exports.resetPassword = ErrorHandler.asyncHandler(async (req, res) => {
const result = await AuthService.resetPassword(req.body.token, req.body.password);
ErrorHandler.successResponse(res, result, result.message);
});
if (!createdUser && lastError) {
console.error('Register error (after retries):', lastError);
return res.status(503).json({ message: 'Zeitüberschreitung beim Zugriff auf die Datenbank. Bitte erneut versuchen.' });
}
console.log('Register: user created', { id: createdUser.id });
const safeUser = {
id: createdUser.id,
name: createdUser.name,
email: createdUser.email,
active: createdUser.active,
created_at: createdUser.created_at
};
return res.status(201).json({ message: 'Benutzer erfolgreich registriert', user: safeUser });
} catch (error) {
if (error.name === 'SequelizeUniqueConstraintError') {
return res.status(400).json({ message: 'Email-Adresse bereits in Verwendung' });
}
console.error('Register error:', error);
return res.status(500).json({ message: 'Ein Fehler ist aufgetreten', error: error.message });
}
};
exports.login = async (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ message: 'Email und Passwort sind erforderlich' });
}
try {
const user = await User.findOne({ where: { email } });
if (!user) {
return res.status(401).json({ message: 'Ungültige Anmeldedaten' });
}
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
return res.status(401).json({ message: 'Ungültige Anmeldedaten' });
}
if (!user.active) {
return res.status(403).json({ message: 'Benutzerkonto ist nicht aktiv' });
}
const token = jwt.sign({ id: user.id, name: user.name, email: user.email }, 'zTxVgptmPl9!_dr%xxx9999(dd)', { expiresIn: '1h' });
return res.status(200).json({ message: 'Login erfolgreich', token, 'user': user });
} catch (error) {
return res.status(500).json({ message: 'Ein Fehler ist aufgetreten' });
}
};
exports.forgotPassword = async (req, res) => {
const { email } = req.body;
if (!email) {
return res.status(400).json({ message: 'E-Mail-Adresse ist erforderlich' });
}
try {
const user = await User.findOne({ where: { email } });
if (!user) {
// Aus Sicherheitsgründen immer Erfolg melden, auch wenn E-Mail nicht existiert
return res.status(200).json({ message: 'Falls die E-Mail-Adresse in unserem System registriert ist, erhalten Sie einen Link zum Zurücksetzen des Passworts.' });
}
// Alte Reset-Tokens für diesen User löschen
await PasswordResetToken.destroy({ where: { userId: user.id } });
// Neuen Reset-Token generieren
const token = crypto.randomBytes(32).toString('hex');
const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 Stunde
await PasswordResetToken.create({
userId: user.id,
token,
expiresAt
});
// Reset-URL generieren
const resetUrl = `${process.env.FRONTEND_URL || 'http://localhost:8080'}/reset-password?token=${token}`;
// E-Mail versenden
const emailTemplate = getPasswordResetEmailTemplate(resetUrl, user.name);
const mailOptions = {
from: process.env.SMTP_FROM || 'noreply@miriamgemeinde.de',
to: email,
subject: emailTemplate.subject,
html: emailTemplate.html,
text: emailTemplate.text
};
console.log('=== EMAIL SENDING DEBUG ===');
console.log('From:', mailOptions.from);
console.log('To:', mailOptions.to);
console.log('Subject:', mailOptions.subject);
console.log('Reset URL:', resetUrl);
console.log('===========================');
await transporter.sendMail(mailOptions);
console.log('Password reset email sent to:', email);
return res.status(200).json({ message: 'Falls die E-Mail-Adresse in unserem System registriert ist, erhalten Sie einen Link zum Zurücksetzen des Passworts.' });
} catch (error) {
console.error('Forgot password error:', error);
return res.status(500).json({ message: 'Ein Fehler ist aufgetreten' });
}
};
exports.resetPassword = async (req, res) => {
const { token, password } = req.body;
if (!token || !password) {
return res.status(400).json({ message: 'Token und neues Passwort sind erforderlich' });
}
if (password.length < 6) {
return res.status(400).json({ message: 'Passwort muss mindestens 6 Zeichen lang sein' });
}
try {
// Token validieren
const resetToken = await PasswordResetToken.findOne({
where: {
token,
used: false,
expiresAt: {
[require('sequelize').Op.gt]: new Date()
}
},
include: [{ model: User, as: 'user' }]
});
if (!resetToken) {
return res.status(400).json({ message: 'Ungültiger oder abgelaufener Token' });
}
// Passwort hashen und aktualisieren
const hashedPassword = await bcrypt.hash(password, 10);
await User.update(
{ password: hashedPassword },
{ where: { id: resetToken.userId } }
);
// Token als verwendet markieren
await resetToken.update({ used: true });
console.log('Password reset successful for user:', resetToken.userId);
return res.status(200).json({ message: 'Passwort erfolgreich zurückgesetzt' });
} catch (error) {
console.error('Reset password error:', error);
return res.status(500).json({ message: 'Ein Fehler ist aufgetreten' });
}
};
exports.logout = async (req, res) => {
exports.logout = ErrorHandler.asyncHandler(async (req, res) => {
const authHeader = req.header('Authorization');
if (!authHeader) {
return res.status(400).json({ message: 'Kein Token bereitgestellt' });
}
const token = authHeader.replace('Bearer ', '');
try {
addTokenToBlacklist(token);
return res.status(200).json({ message: 'Logout erfolgreich' });
} catch (error) {
console.log(error);
return res.status(500).json({ message: 'Ein Fehler ist beim Logout aufgetreten' });
}
};
const token = authHeader ? authHeader.replace('Bearer ', '') : null;
const result = await AuthService.logout(token);
ErrorHandler.successResponse(res, result, result.message);
});

View File

@@ -1,188 +1,32 @@
const { Event, Institution, EventPlace, ContactPerson, EventType } = require('../models');
const { Op } = require('sequelize');
const moment = require('moment'); // Import von Moment.js
const EventService = require('../services/EventService');
const ErrorHandler = require('../utils/ErrorHandler');
const getAllEvents = async (req, res) => {
try {
const events = await Event.findAll({
include: [
{ model: Institution, as: 'institution' },
{ model: EventPlace, as: 'eventPlace' },
{ model: EventType, as: 'eventType' },
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } }
],
order: ['name', 'date', 'time']
});
res.json(events);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch events' });
console.error(error);
}
};
exports.getAllEvents = ErrorHandler.asyncHandler(async (req, res) => {
const events = await EventService.getAllEvents();
ErrorHandler.successResponse(res, events, 'Events erfolgreich abgerufen');
});
const filterEvents = async (req, res) => {
try {
const request = req.body;
const where = {
[Op.or]: [
{
date: {
[Op.or]: [
{ [Op.gte]: moment().startOf('day').toDate() },
{ [Op.eq]: null }
]
}
},
{ dayOfWeek: { [Op.gte]: 0 } }
]
};
const order = [
['date', 'ASC'],
['time', 'ASC']
];
exports.getEventById = ErrorHandler.asyncHandler(async (req, res) => {
const event = await EventService.getEventById(req.params.id);
ErrorHandler.successResponse(res, event, 'Event erfolgreich abgerufen');
});
if (request.id === 'all') {
const events = await Event.findAll({
where,
include: [
{ model: Institution, as: 'institution' },
{ model: EventPlace, as: 'eventPlace' },
{ model: EventType, as: 'eventType' },
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } }
],
order: order,
logging: console.log // Log the generated SQL query
});
return res.json({ events });
}
exports.filterEvents = ErrorHandler.asyncHandler(async (req, res) => {
const result = await EventService.filterEvents(req.body);
ErrorHandler.successResponse(res, result, 'Events erfolgreich gefiltert');
});
if (request.id === 'home') {
const events = await Event.findAll({
where: {
alsoOnHomepage: 1,
date: { [Op.gte]: moment().startOf('day').toDate() }
},
include: [
{ model: Institution, as: 'institution' },
{ model: EventPlace, as: 'eventPlace' },
{ model: EventType, as: 'eventType' },
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } },
],
order: order,
});
return res.json({ events });
}
exports.createEvent = ErrorHandler.asyncHandler(async (req, res) => {
const event = await EventService.createEvent(req.body);
ErrorHandler.successResponse(res, event, 'Event erfolgreich erstellt', 201);
});
if (!request.id && !request.places && !request.types) {
return res.json({ events: [], places: [], types: [], contactPersons: [] });
}
exports.updateEvent = ErrorHandler.asyncHandler(async (req, res) => {
const event = await EventService.updateEvent(req.params.id, req.body);
ErrorHandler.successResponse(res, event, 'Event erfolgreich aktualisiert');
});
if (request.id) {
where.id = request.id;
}
if (request.places && request.places.length > 0) {
where.event_place_id = {
[Op.in]: request.places.map(id => parseInt(id))
};
}
if (request.types && request.types.length > 0) {
where.eventTypeId = {
[Op.in]: request.types.map(id => parseInt(id))
};
}
const events = await Event.findAll({
where,
include: [
{ model: Institution, as: 'institution' },
{ model: EventPlace, as: 'eventPlace' },
{ model: EventType, as: 'eventType' },
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } }
],
order: order,
});
const displayFields = request.display ? request.display : [];
const filteredEvents = events.map(event => {
const filteredEvent = { ...event.toJSON() };
if (!displayFields.includes('name')) delete filteredEvent.name;
if (!displayFields.includes('type')) delete filteredEvent.eventType;
if (!displayFields.includes('place')) delete filteredEvent.eventPlace;
if (!displayFields.includes('description')) delete filteredEvent.description;
if (!displayFields.includes('time')) delete filteredEvent.time;
if (!displayFields.includes('time')) delete filteredEvent.endTime;
if (!displayFields.includes('contactPerson')) delete filteredEvent.contactPersons;
if (!displayFields.includes('day')) delete filteredEvent.dayOfWeek;
if (!displayFields.includes('institution')) delete filteredEvent.institution;
return filteredEvent;
});
res.json({ events: filteredEvents });
} catch (error) {
res.status(500).json({ error: 'Failed to filter events' });
console.error(error);
}
};
const createEvent = async (req, res) => {
try {
const { contactPersonIds, ...eventData } = req.body;
eventData.alsoOnHomepage = eventData.alsoOnHomepage ?? 0;
const event = await Event.create(eventData);
if (contactPersonIds) {
await event.setContactPersons(contactPersonIds);
}
res.status(201).json(event);
} catch (error) {
res.status(500).json({ error: 'Failed to create event' });
console.error(error);
}
};
const updateEvent = async (req, res) => {
try {
const { id } = req.params;
const { contactPersonIds, ...eventData } = req.body;
const event = await Event.findByPk(id);
if (!event) {
return res.status(404).json({ error: 'Event not found' });
}
await event.update(eventData);
if (contactPersonIds) {
await event.setContactPersons(contactPersonIds);
}
res.status(200).json(event);
} catch (error) {
res.status(500).json({ error: 'Failed to update event' });
console.error(error);
}
};
const deleteEvent = async (req, res) => {
try {
const { id } = req.params;
const deleted = await Event.destroy({
where: { id: id }
});
if (deleted) {
res.status(204).json();
} else {
res.status(404).json({ error: 'Event not found' });
}
} catch (error) {
res.status(500).json({ error: 'Failed to delete event' });
console.error(error);
}
};
module.exports = {
getAllEvents,
createEvent,
updateEvent,
deleteEvent,
filterEvents
};
exports.deleteEvent = ErrorHandler.asyncHandler(async (req, res) => {
const result = await EventService.deleteEvent(req.params.id);
ErrorHandler.successResponse(res, result, result.message);
});

View File

@@ -1,30 +1,12 @@
const { MenuItem } = require('../models');
const fetchMenuData = require('../utils/fetchMenuData');
const MenuDataService = require('../services/MenuDataService');
const ErrorHandler = require('../utils/ErrorHandler');
exports.getMenuData = async (req, res) => {
try {
const menuData = await fetchMenuData();
res.json(menuData);
} catch (error) {
res.status(500).send('Error fetching menu data');
}
};
exports.getMenuData = ErrorHandler.asyncHandler(async (req, res) => {
const menuData = await MenuDataService.getMenuData();
ErrorHandler.successResponse(res, menuData, 'Menü-Daten erfolgreich abgerufen');
});
exports.saveMenuData = async (req, res) => {
try {
const menuData = req.body;
const adjustedMenuData = menuData.map(item => {
item.parent_id = item.parent_id < 0 ? null : item.parent_id;
return item;
})
.sort((a, b) => (a.parent_id === null ? -1 : 1) - (b.parent_id === null ? -1 : 1));
await MenuItem.destroy({ where: {} });
for (const item of adjustedMenuData) {
await MenuItem.create(item);
}
res.status(200).send('Menü-Daten erfolgreich gespeichert');
} catch (error) {
console.error('Fehler beim Speichern der Menü-Daten:', error);
res.status(500).send('Fehler beim Speichern der Menü-Daten');
}
};
exports.saveMenuData = ErrorHandler.asyncHandler(async (req, res) => {
const result = await MenuDataService.saveMenuData(req.body);
ErrorHandler.successResponse(res, result, result.message);
});

View File

@@ -1,48 +1,27 @@
// controllers/pageController.js
const { Page } = require('../models');
const PageService = require('../services/PageService');
const ErrorHandler = require('../utils/ErrorHandler');
exports.getMenuData = async (req, res) => {
try {
const pages = await Page.findAll({
attributes: ['link', 'name']
});
res.json(pages);
} catch (error) {
console.error('Fehler beim Abrufen der Seiten:', error);
res.status(500).json({ message: 'Fehler beim Abrufen der Seiten' });
}
};
exports.getMenuData = ErrorHandler.asyncHandler(async (req, res) => {
const pages = await PageService.getAllPages();
ErrorHandler.successResponse(res, pages, 'Seiten erfolgreich abgerufen');
});
exports.getPageContent = async (req, res) => {
try {
const page = await Page.findOne({
where: { link: req.query.link }
});
if (page) {
res.json({ content: page.content });
} else {
res.json({ content: "" });
}
} catch (error) {
console.error('Fehler beim Laden des Seiteninhalts:', error);
res.status(500).json({ message: 'Fehler beim Laden des Seiteninhalts' });
}
};
exports.getPageContent = ErrorHandler.asyncHandler(async (req, res) => {
const result = await PageService.getPageContent(req.query.link);
ErrorHandler.successResponse(res, result, 'Seiteninhalt erfolgreich abgerufen');
});
exports.savePageContent = async (req, res) => {
try {
const { link, name, content } = req.body;
let page = await Page.findOne({ where: { link } });
if (page) {
page.content = content;
page.name = name;
} else {
page = await Page.create({ link, name, content });
}
await page.save();
res.json({ message: 'Seiteninhalt gespeichert', page });
} catch (error) {
console.error('Fehler beim Speichern des Seiteninhalts:', error);
res.status(500).json({ message: 'Fehler beim Speichern des Seiteninhalts' });
}
};
exports.savePageContent = ErrorHandler.asyncHandler(async (req, res) => {
const result = await PageService.savePageContent(req.body);
ErrorHandler.successResponse(res, result, result.message);
});
exports.getPageById = ErrorHandler.asyncHandler(async (req, res) => {
const page = await PageService.getPageById(req.params.id);
ErrorHandler.successResponse(res, page, 'Seite erfolgreich abgerufen');
});
exports.deletePage = ErrorHandler.asyncHandler(async (req, res) => {
const result = await PageService.deletePage(req.params.id);
ErrorHandler.successResponse(res, result, result.message);
});

View File

@@ -1,104 +1,42 @@
const { User } = require('../models');
const UserService = require('../services/UserService');
const UserValidator = require('../validators/UserValidator');
const ErrorHandler = require('../utils/ErrorHandler');
exports.getAllUsers = async (req, res) => {
try {
const users = await User.findAll({
order: [['name', 'ASC']],
attributes: ['id', 'name', 'email', 'active', 'created_at'] // Passwort ausschließen
});
res.status(200).json(users);
} catch (error) {
console.error('Error fetching users:', error);
res.status(500).json({ message: 'Error fetching users' });
}
};
exports.getAllUsers = ErrorHandler.asyncHandler(async (req, res) => {
const users = await UserService.getAllUsers();
ErrorHandler.successResponse(res, users, 'Benutzer erfolgreich abgerufen');
});
exports.getUserById = async (req, res) => {
try {
const user = await User.findByPk(req.params.id, {
attributes: ['id', 'name', 'email', 'active', 'created_at'] // Passwort ausschließen
});
if (user) {
res.status(200).json(user);
} else {
res.status(404).json({ message: 'User not found' });
}
} catch (error) {
console.error('Error fetching user:', error);
res.status(500).json({ message: 'Error fetching user' });
}
};
exports.getUserById = ErrorHandler.asyncHandler(async (req, res) => {
UserValidator.validateId(req.params.id);
const user = await UserService.getUserById(req.params.id);
ErrorHandler.successResponse(res, user, 'Benutzer erfolgreich abgerufen');
});
exports.createUser = async (req, res) => {
try {
const user = await User.create(req.body);
// Sichere User-Daten zurückgeben (ohne Passwort)
const safeUser = {
id: user.id,
name: user.name,
email: user.email,
active: user.active,
created_at: user.created_at
};
res.status(201).json(safeUser);
} catch (error) {
console.error('Error creating user:', error);
res.status(500).json({ message: 'Error creating user' });
}
};
exports.createUser = ErrorHandler.asyncHandler(async (req, res) => {
UserValidator.validateCreateUser(req.body);
const user = await UserService.createUser(req.body);
ErrorHandler.successResponse(res, user, 'Benutzer erfolgreich erstellt', 201);
});
exports.updateUser = async (req, res) => {
try {
const user = await User.findByPk(req.params.id);
if (user) {
// Erstelle eine Kopie der Request-Daten ohne sensible Felder
const updateData = { ...req.body };
// Entferne sensible Felder, die niemals über diese Route geändert werden dürfen
delete updateData.password;
delete updateData.id;
delete updateData.created_at;
// Setze updated_at auf aktuelle Zeit
updateData.updated_at = new Date();
// Logging für Debugging
console.log('Updating user:', req.params.id, 'with data:', updateData);
await user.update(updateData);
// Sichere User-Daten zurückgeben (ohne Passwort)
const safeUser = {
id: user.id,
name: user.name,
email: user.email,
active: user.active,
created_at: user.created_at
};
res.status(200).json(safeUser);
} else {
res.status(404).json({ message: 'User not found' });
}
} catch (error) {
console.error('Error updating user:', error);
res.status(500).json({ message: 'Error updating user' });
}
};
exports.updateUser = ErrorHandler.asyncHandler(async (req, res) => {
UserValidator.validateId(req.params.id);
UserValidator.validateUpdateUser(req.body);
const user = await UserService.updateUser(req.params.id, req.body);
ErrorHandler.successResponse(res, user, 'Benutzer erfolgreich aktualisiert');
});
exports.deleteUser = async (req, res) => {
try {
const user = await User.findByPk(req.params.id);
if (user) {
await user.destroy();
res.status(200).json({ message: 'User deleted successfully' });
} else {
res.status(404).json({ message: 'User not found' });
}
} catch (error) {
console.error('Error deleting user:', error);
res.status(500).json({ message: 'Error deleting user' });
}
};
exports.deleteUser = ErrorHandler.asyncHandler(async (req, res) => {
UserValidator.validateId(req.params.id);
await UserService.deleteUser(req.params.id);
ErrorHandler.successResponse(res, null, 'Benutzer erfolgreich gelöscht');
});
// Neue Route für Passwort-Änderung
exports.changePassword = ErrorHandler.asyncHandler(async (req, res) => {
const { currentPassword, newPassword } = req.body;
UserValidator.validateId(req.params.id);
UserValidator.validatePasswordChange(currentPassword, newPassword);
await UserService.changePassword(req.params.id, currentPassword, newPassword);
ErrorHandler.successResponse(res, null, 'Passwort erfolgreich geändert');
});

View File

@@ -1,12 +1,13 @@
const express = require('express');
const router = express.Router();
const { getAllUsers, createUser, updateUser, deleteUser, getUserById } = require('../controllers/userController');
const { getAllUsers, createUser, updateUser, deleteUser, getUserById, changePassword } = require('../controllers/userController');
const authMiddleware = require('../middleware/authMiddleware');
router.get('/', authMiddleware, getAllUsers);
router.get('/:id', authMiddleware, getUserById);
router.post('/', authMiddleware, createUser);
router.put('/:id', authMiddleware, updateUser);
router.put('/:id/change-password', authMiddleware, changePassword);
router.delete('/:id', authMiddleware, deleteUser);
module.exports = router;

186
services/AuthService.js Normal file
View File

@@ -0,0 +1,186 @@
const bcrypt = require('bcryptjs');
const { User, PasswordResetToken } = require('../models');
const jwt = require('jsonwebtoken');
const { addTokenToBlacklist } = require('../utils/blacklist');
const { transporter, getPasswordResetEmailTemplate } = require('../config/email');
const crypto = require('crypto');
class AuthService {
/**
* User registrieren
*/
async register(userData) {
const { name, email, password } = userData;
if (!name || !email || !password) {
throw new Error('VALIDATION_ERROR: Alle Felder sind erforderlich');
}
// Prüfen ob E-Mail bereits existiert
const existingUser = await User.findOne({ where: { email } });
if (existingUser) {
throw new Error('EMAIL_ALREADY_EXISTS');
}
const hashedPassword = await bcrypt.hash(password, 10);
const user = await User.create({
name,
email,
password: hashedPassword,
active: true
});
return this.getSafeUserData(user);
}
/**
* User einloggen
*/
async login(credentials) {
const { email, password } = credentials;
if (!email || !password) {
throw new Error('VALIDATION_ERROR: Email und Passwort sind erforderlich');
}
const user = await User.findOne({ where: { email } });
if (!user) {
throw new Error('INVALID_CREDENTIALS');
}
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
throw new Error('INVALID_CREDENTIALS');
}
if (!user.active) {
throw new Error('ACCOUNT_INACTIVE');
}
const token = jwt.sign(
{ id: user.id, name: user.name, email: user.email },
'zTxVgptmPl9!_dr%xxx9999(dd)',
{ expiresIn: '1h' }
);
return {
message: 'Login erfolgreich',
token,
user: this.getSafeUserData(user)
};
}
/**
* User ausloggen
*/
async logout(token) {
if (!token) {
throw new Error('VALIDATION_ERROR: Kein Token bereitgestellt');
}
addTokenToBlacklist(token);
return { message: 'Logout erfolgreich' };
}
/**
* Passwort vergessen - E-Mail senden
*/
async forgotPassword(email) {
if (!email) {
throw new Error('VALIDATION_ERROR: E-Mail-Adresse ist erforderlich');
}
const user = await User.findOne({ where: { email } });
if (!user) {
// Aus Sicherheitsgründen immer Erfolg melden
return { message: 'Falls die E-Mail-Adresse in unserem System registriert ist, erhalten Sie einen Link zum Zurücksetzen des Passworts.' };
}
// Alte Reset-Tokens für diesen User löschen
await PasswordResetToken.destroy({ where: { userId: user.id } });
// Neuen Reset-Token generieren
const token = crypto.randomBytes(32).toString('hex');
const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 Stunde
await PasswordResetToken.create({
userId: user.id,
token,
expiresAt
});
// Reset-URL generieren
const resetUrl = `${process.env.FRONTEND_URL || 'http://localhost:8080'}/reset-password?token=${token}`;
// E-Mail versenden
const emailTemplate = getPasswordResetEmailTemplate(resetUrl, user.name);
await transporter.sendMail({
from: process.env.SMTP_FROM || 'noreply@miriamgemeinde.de',
to: email,
subject: emailTemplate.subject,
html: emailTemplate.html,
text: emailTemplate.text
});
console.log('Password reset email sent to:', email);
return { message: 'Falls die E-Mail-Adresse in unserem System registriert ist, erhalten Sie einen Link zum Zurücksetzen des Passworts.' };
}
/**
* Passwort zurücksetzen
*/
async resetPassword(token, newPassword) {
if (!token || !newPassword) {
throw new Error('VALIDATION_ERROR: Token und neues Passwort sind erforderlich');
}
if (newPassword.length < 6) {
throw new Error('VALIDATION_ERROR: Passwort muss mindestens 6 Zeichen lang sein');
}
// Token validieren
const resetToken = await PasswordResetToken.findOne({
where: {
token,
used: false,
expiresAt: {
[require('sequelize').Op.gt]: new Date()
}
},
include: [{ model: User, as: 'user' }]
});
if (!resetToken) {
throw new Error('INVALID_RESET_TOKEN');
}
// Passwort hashen und aktualisieren
const hashedPassword = await bcrypt.hash(newPassword, 10);
await User.update(
{ password: hashedPassword },
{ where: { id: resetToken.userId } }
);
// Token als verwendet markieren
await resetToken.update({ used: true });
console.log('Password reset successful for user:', resetToken.userId);
return { message: 'Passwort erfolgreich zurückgesetzt' };
}
/**
* Sichere User-Daten extrahieren (ohne Passwort)
*/
getSafeUserData(user) {
return {
id: user.id,
name: user.name,
email: user.email,
active: user.active,
created_at: user.created_at
};
}
}
module.exports = new AuthService();

276
services/EventService.js Normal file
View File

@@ -0,0 +1,276 @@
const { Event, Institution, EventPlace, ContactPerson, EventType } = require('../models');
const { Op } = require('sequelize');
const moment = require('moment');
class EventService {
/**
* Alle Events abrufen
*/
async getAllEvents() {
try {
const events = await Event.findAll({
include: [
{ model: Institution, as: 'institution' },
{ model: EventPlace, as: 'eventPlace' },
{ model: EventType, as: 'eventType' },
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } }
],
order: ['name', 'date', 'time']
});
return events;
} catch (error) {
console.error('Error fetching all events:', error);
throw new Error('EVENTS_FETCH_ERROR');
}
}
/**
* Event anhand ID abrufen
*/
async getEventById(id) {
try {
if (!id || isNaN(parseInt(id))) {
throw new Error('VALIDATION_ERROR: Ungültige ID');
}
const event = await Event.findByPk(id, {
include: [
{ model: Institution, as: 'institution' },
{ model: EventPlace, as: 'eventPlace' },
{ model: EventType, as: 'eventType' },
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } }
]
});
if (!event) {
throw new Error('EVENT_NOT_FOUND');
}
return event;
} catch (error) {
console.error('Error fetching event by ID:', error);
throw new Error('EVENT_FETCH_ERROR');
}
}
/**
* Events filtern
*/
async filterEvents(filterData) {
try {
const { id, places, types, display } = filterData;
// Basis-Where-Klausel für zukünftige Events
const where = {
[Op.or]: [
{
date: {
[Op.or]: [
{ [Op.gte]: moment().startOf('day').toDate() },
{ [Op.eq]: null }
]
}
},
{ dayOfWeek: { [Op.gte]: 0 } }
]
};
const order = [
['date', 'ASC'],
['time', 'ASC']
];
// Spezielle Filter
if (id === 'all') {
return await this._getAllFutureEvents(where, order);
}
if (id === 'home') {
return await this._getHomepageEvents(where, order);
}
if (!id && !places && !types) {
return { events: [], places: [], types: [], contactPersons: [] };
}
// Weitere Filter anwenden
if (id) {
where.id = id;
}
if (places && places.length > 0) {
where.event_place_id = {
[Op.in]: places.map(id => parseInt(id))
};
}
if (types && types.length > 0) {
where.eventTypeId = {
[Op.in]: types.map(id => parseInt(id))
};
}
const events = await Event.findAll({
where,
include: [
{ model: Institution, as: 'institution' },
{ model: EventPlace, as: 'eventPlace' },
{ model: EventType, as: 'eventType' },
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } }
],
order: order,
});
// Events basierend auf Display-Feldern filtern
const displayFields = display || [];
const filteredEvents = this._filterEventFields(events, displayFields);
return { events: filteredEvents };
} catch (error) {
console.error('Error filtering events:', error);
throw new Error('EVENT_FILTER_ERROR');
}
}
/**
* Event erstellen
*/
async createEvent(eventData) {
try {
const { contactPersonIds, ...eventDataWithoutContacts } = eventData;
// Validierung
if (!eventDataWithoutContacts.name) {
throw new Error('VALIDATION_ERROR: Event-Name ist erforderlich');
}
eventDataWithoutContacts.alsoOnHomepage = eventDataWithoutContacts.alsoOnHomepage ?? 0;
const event = await Event.create(eventDataWithoutContacts);
if (contactPersonIds && contactPersonIds.length > 0) {
await event.setContactPersons(contactPersonIds);
}
return event;
} catch (error) {
console.error('Error creating event:', error);
throw new Error('EVENT_CREATE_ERROR');
}
}
/**
* Event aktualisieren
*/
async updateEvent(id, eventData) {
try {
if (!id || isNaN(parseInt(id))) {
throw new Error('VALIDATION_ERROR: Ungültige ID');
}
const { contactPersonIds, ...eventDataWithoutContacts } = eventData;
const event = await Event.findByPk(id);
if (!event) {
throw new Error('EVENT_NOT_FOUND');
}
await event.update(eventDataWithoutContacts);
if (contactPersonIds !== undefined) {
await event.setContactPersons(contactPersonIds || []);
}
return event;
} catch (error) {
console.error('Error updating event:', error);
throw new Error('EVENT_UPDATE_ERROR');
}
}
/**
* Event löschen
*/
async deleteEvent(id) {
try {
if (!id || isNaN(parseInt(id))) {
throw new Error('VALIDATION_ERROR: Ungültige ID');
}
const event = await Event.findByPk(id);
if (!event) {
throw new Error('EVENT_NOT_FOUND');
}
await event.destroy();
return { message: 'Event erfolgreich gelöscht' };
} catch (error) {
console.error('Error deleting event:', error);
throw new Error('EVENT_DELETE_ERROR');
}
}
/**
* Alle zukünftigen Events abrufen
*/
async _getAllFutureEvents(where, order) {
const events = await Event.findAll({
where,
include: [
{ model: Institution, as: 'institution' },
{ model: EventPlace, as: 'eventPlace' },
{ model: EventType, as: 'eventType' },
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } }
],
order: order,
logging: console.log
});
return { events };
}
/**
* Homepage Events abrufen
*/
async _getHomepageEvents(where, order) {
const events = await Event.findAll({
where: {
alsoOnHomepage: 1,
date: { [Op.gte]: moment().startOf('day').toDate() }
},
include: [
{ model: Institution, as: 'institution' },
{ model: EventPlace, as: 'eventPlace' },
{ model: EventType, as: 'eventType' },
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } },
],
order: order,
});
return { events };
}
/**
* Event-Felder basierend auf Display-Feldern filtern
*/
_filterEventFields(events, displayFields) {
return events.map(event => {
const filteredEvent = { ...event.toJSON() };
if (!displayFields.includes('name')) delete filteredEvent.name;
if (!displayFields.includes('type')) delete filteredEvent.eventType;
if (!displayFields.includes('place')) delete filteredEvent.eventPlace;
if (!displayFields.includes('description')) delete filteredEvent.description;
if (!displayFields.includes('time')) delete filteredEvent.time;
if (!displayFields.includes('time')) delete filteredEvent.endTime;
if (!displayFields.includes('contactPerson')) delete filteredEvent.contactPersons;
if (!displayFields.includes('day')) delete filteredEvent.dayOfWeek;
if (!displayFields.includes('institution')) delete filteredEvent.institution;
return filteredEvent;
});
}
}
module.exports = new EventService();

101
services/MenuDataService.js Normal file
View File

@@ -0,0 +1,101 @@
const { MenuItem } = require('../models');
class MenuDataService {
/**
* Alle Menü-Daten abrufen
*/
async getMenuData() {
try {
const menuItems = await MenuItem.findAll({
order: [['order_id', 'ASC']],
include: [{
model: MenuItem,
as: 'submenu',
required: false,
order: [['order_id', 'ASC']]
}]
});
const menuData = this.buildMenuStructure(menuItems);
return menuData;
} catch (error) {
console.error('Error fetching menu data:', error);
throw new Error('MENU_DATA_FETCH_ERROR');
}
}
/**
* Menü-Daten speichern
*/
async saveMenuData(menuData) {
try {
if (!Array.isArray(menuData)) {
throw new Error('VALIDATION_ERROR: Menü-Daten müssen ein Array sein');
}
// Menü-Daten anpassen
const adjustedMenuData = menuData.map(item => {
item.parent_id = item.parent_id < 0 ? null : item.parent_id;
return item;
}).sort((a, b) => (a.parent_id === null ? -1 : 1) - (b.parent_id === null ? -1 : 1));
// Alle bestehenden Menü-Items löschen
await MenuItem.destroy({ where: {} });
// Neue Menü-Items erstellen
for (const item of adjustedMenuData) {
await MenuItem.create(item);
}
return { message: 'Menü-Daten erfolgreich gespeichert' };
} catch (error) {
console.error('Error saving menu data:', error);
throw new Error('MENU_DATA_SAVE_ERROR');
}
}
/**
* Menü-Struktur aufbauen
*/
buildMenuStructure(menuItems) {
const menu = [];
const itemMap = {};
// Alle Items in Map speichern
menuItems.forEach(item => {
itemMap[item.id] = {
id: item.id,
name: item.name,
link: item.link,
component: item.component,
showInMenu: item.show_in_menu,
requiresAuth: item.requires_auth,
order_id: item.order_id,
pageTitle: item.page_title,
image: item.image,
submenu: []
};
});
// Hierarchie aufbauen
menuItems.forEach(item => {
if (item.parent_id) {
if (itemMap[item.parent_id]) {
itemMap[item.parent_id].submenu.push(itemMap[item.id]);
}
} else {
menu.push(itemMap[item.id]);
}
});
// Sortierung anwenden
menu.sort((a, b) => a.order_id - b.order_id);
menu.forEach(item => {
item.submenu.sort((a, b) => a.order_id - b.order_id);
});
return menu;
}
}
module.exports = new MenuDataService();

132
services/PageService.js Normal file
View File

@@ -0,0 +1,132 @@
const { Page } = require('../models');
class PageService {
/**
* Seiteninhalt anhand Link abrufen
*/
async getPageContent(link) {
try {
if (!link) {
throw new Error('VALIDATION_ERROR: Link ist erforderlich');
}
const page = await Page.findOne({ where: { link } });
if (!page) {
throw new Error('PAGE_NOT_FOUND');
}
return {
content: page.content || '',
title: page.title || '',
link: page.link
};
} catch (error) {
console.error('Error fetching page content:', error);
throw new Error('PAGE_CONTENT_FETCH_ERROR');
}
}
/**
* Seiteninhalt speichern
*/
async savePageContent(pageData) {
try {
const { link, name, content } = pageData;
if (!link || !name) {
throw new Error('VALIDATION_ERROR: Link und Name sind erforderlich');
}
// Prüfen ob Seite bereits existiert
const existingPage = await Page.findOne({ where: { link } });
if (existingPage) {
// Seite aktualisieren
await existingPage.update({
content: content || '',
title: name,
updated_at: new Date()
});
} else {
// Neue Seite erstellen
await Page.create({
link,
title: name,
content: content || '',
created_at: new Date(),
updated_at: new Date()
});
}
return { message: 'Seiteninhalt erfolgreich gespeichert' };
} catch (error) {
console.error('Error saving page content:', error);
throw new Error('PAGE_CONTENT_SAVE_ERROR');
}
}
/**
* Alle Seiten abrufen
*/
async getAllPages() {
try {
const pages = await Page.findAll({
order: [['title', 'ASC']],
attributes: ['id', 'link', 'title', 'created_at', 'updated_at']
});
return pages;
} catch (error) {
console.error('Error fetching all pages:', error);
throw new Error('PAGES_FETCH_ERROR');
}
}
/**
* Seite anhand ID abrufen
*/
async getPageById(id) {
try {
if (!id || isNaN(parseInt(id))) {
throw new Error('VALIDATION_ERROR: Ungültige ID');
}
const page = await Page.findByPk(id);
if (!page) {
throw new Error('PAGE_NOT_FOUND');
}
return page;
} catch (error) {
console.error('Error fetching page by ID:', error);
throw new Error('PAGE_FETCH_ERROR');
}
}
/**
* Seite löschen
*/
async deletePage(id) {
try {
if (!id || isNaN(parseInt(id))) {
throw new Error('VALIDATION_ERROR: Ungültige ID');
}
const page = await Page.findByPk(id);
if (!page) {
throw new Error('PAGE_NOT_FOUND');
}
await page.destroy();
return { message: 'Seite erfolgreich gelöscht' };
} catch (error) {
console.error('Error deleting page:', error);
throw new Error('PAGE_DELETE_ERROR');
}
}
}
module.exports = new PageService();

140
services/UserService.js Normal file
View File

@@ -0,0 +1,140 @@
const { User } = require('../models');
const bcrypt = require('bcryptjs');
class UserService {
/**
* Alle User abrufen (ohne sensible Daten)
*/
async getAllUsers() {
const users = await User.findAll({
order: [['name', 'ASC']],
attributes: ['id', 'name', 'email', 'active', 'created_at', 'updated_at']
});
return users;
}
/**
* User anhand ID abrufen (ohne sensible Daten)
*/
async getUserById(id) {
const user = await User.findByPk(id, {
attributes: ['id', 'name', 'email', 'active', 'created_at', 'updated_at']
});
if (!user) {
throw new Error('USER_NOT_FOUND');
}
return user;
}
/**
* Neuen User erstellen
*/
async createUser(userData) {
// Passwort hashen falls vorhanden
if (userData.password) {
userData.password = await bcrypt.hash(userData.password, 10);
}
const user = await User.create(userData);
// Sichere User-Daten zurückgeben
return this.getSafeUserData(user);
}
/**
* User aktualisieren (ohne sensible Felder)
*/
async updateUser(id, updateData) {
const user = await User.findByPk(id);
if (!user) {
throw new Error('USER_NOT_FOUND');
}
// Erstelle sichere Update-Daten
const safeUpdateData = this.getSafeUpdateData(updateData);
await user.update(safeUpdateData);
return this.getSafeUserData(user);
}
/**
* User löschen
*/
async deleteUser(id) {
const user = await User.findByPk(id);
if (!user) {
throw new Error('USER_NOT_FOUND');
}
await user.destroy();
return true;
}
/**
* User anhand E-Mail abrufen (für interne Verwendung)
*/
async getUserByEmail(email) {
return await User.findOne({ where: { email } });
}
/**
* Passwort ändern (separate Methode für sichere Passwort-Änderung)
*/
async changePassword(id, currentPassword, newPassword) {
const user = await User.findByPk(id);
if (!user) {
throw new Error('USER_NOT_FOUND');
}
// Aktuelles Passwort prüfen
const isValidPassword = await bcrypt.compare(currentPassword, user.password);
if (!isValidPassword) {
throw new Error('INVALID_CURRENT_PASSWORD');
}
// Neues Passwort hashen und speichern
const hashedPassword = await bcrypt.hash(newPassword, 10);
await user.update({ password: hashedPassword });
return true;
}
/**
* Sichere User-Daten extrahieren (ohne Passwort)
*/
getSafeUserData(user) {
return {
id: user.id,
name: user.name,
email: user.email,
active: user.active,
created_at: user.created_at,
updated_at: user.updated_at
};
}
/**
* Sichere Update-Daten erstellen (ohne sensible Felder)
*/
getSafeUpdateData(updateData) {
const safeData = { ...updateData };
// Entferne sensible Felder
delete safeData.password;
delete safeData.id;
delete safeData.created_at;
// Setze updated_at
safeData.updated_at = new Date();
return safeData;
}
}
module.exports = new UserService();

71
utils/ErrorHandler.js Normal file
View File

@@ -0,0 +1,71 @@
class ErrorHandler {
/**
* Error in HTTP Response umwandeln
*/
handleError(error, res) {
console.error('Error:', error);
// Validation Errors
if (error.message.startsWith('VALIDATION_ERROR:')) {
const message = error.message.replace('VALIDATION_ERROR: ', '');
return res.status(400).json({
success: false,
message: message,
type: 'VALIDATION_ERROR'
});
}
// Business Logic Errors
switch (error.message) {
case 'USER_NOT_FOUND':
return res.status(404).json({
success: false,
message: 'Benutzer nicht gefunden',
type: 'NOT_FOUND'
});
case 'INVALID_CURRENT_PASSWORD':
return res.status(400).json({
success: false,
message: 'Aktuelles Passwort ist falsch',
type: 'INVALID_PASSWORD'
});
case 'EMAIL_ALREADY_EXISTS':
return res.status(409).json({
success: false,
message: 'E-Mail-Adresse bereits vorhanden',
type: 'DUPLICATE_EMAIL'
});
default:
return res.status(500).json({
success: false,
message: 'Ein interner Fehler ist aufgetreten',
type: 'INTERNAL_ERROR'
});
}
}
/**
* Success Response erstellen
*/
successResponse(res, data, message = 'Erfolgreich', statusCode = 200) {
return res.status(statusCode).json({
success: true,
message: message,
data: data
});
}
/**
* Async Error Wrapper für Controller
*/
asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
}
module.exports = new ErrorHandler();

109
validators/UserValidator.js Normal file
View File

@@ -0,0 +1,109 @@
class UserValidator {
/**
* User-Erstellungsdaten validieren
*/
validateCreateUser(userData) {
const errors = [];
if (!userData.name || userData.name.trim().length < 2) {
errors.push('Name muss mindestens 2 Zeichen lang sein');
}
if (!userData.email || !this.isValidEmail(userData.email)) {
errors.push('Gültige E-Mail-Adresse ist erforderlich');
}
if (!userData.password || userData.password.length < 6) {
errors.push('Passwort muss mindestens 6 Zeichen lang sein');
}
if (errors.length > 0) {
throw new Error(`VALIDATION_ERROR: ${errors.join(', ')}`);
}
return true;
}
/**
* User-Update-Daten validieren
*/
validateUpdateUser(updateData) {
const errors = [];
if (updateData.name !== undefined && (updateData.name.trim().length < 2)) {
errors.push('Name muss mindestens 2 Zeichen lang sein');
}
if (updateData.email !== undefined && !this.isValidEmail(updateData.email)) {
errors.push('Gültige E-Mail-Adresse ist erforderlich');
}
if (updateData.active !== undefined && typeof updateData.active !== 'boolean') {
errors.push('Active muss ein Boolean-Wert sein');
}
// Warnung für sensible Felder
if (updateData.password !== undefined) {
throw new Error('VALIDATION_ERROR: Passwort kann nicht über diese Route geändert werden');
}
if (updateData.id !== undefined) {
throw new Error('VALIDATION_ERROR: ID kann nicht geändert werden');
}
if (updateData.created_at !== undefined) {
throw new Error('VALIDATION_ERROR: Erstellungsdatum kann nicht geändert werden');
}
if (errors.length > 0) {
throw new Error(`VALIDATION_ERROR: ${errors.join(', ')}`);
}
return true;
}
/**
* Passwort-Änderung validieren
*/
validatePasswordChange(currentPassword, newPassword) {
const errors = [];
if (!currentPassword) {
errors.push('Aktuelles Passwort ist erforderlich');
}
if (!newPassword || newPassword.length < 6) {
errors.push('Neues Passwort muss mindestens 6 Zeichen lang sein');
}
if (currentPassword === newPassword) {
errors.push('Neues Passwort muss sich vom aktuellen unterscheiden');
}
if (errors.length > 0) {
throw new Error(`VALIDATION_ERROR: ${errors.join(', ')}`);
}
return true;
}
/**
* E-Mail-Format validieren
*/
isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
/**
* ID validieren
*/
validateId(id) {
if (!id || isNaN(parseInt(id))) {
throw new Error('VALIDATION_ERROR: Ungültige ID');
}
return true;
}
}
module.exports = new UserValidator();