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:
@@ -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 = 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 });
|
||||
|
||||
const maxAttempts = 3;
|
||||
let attempt = 0;
|
||||
let createdUser = null;
|
||||
let lastError = null;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
exports.register = ErrorHandler.asyncHandler(async (req, res) => {
|
||||
const result = await AuthService.register(req.body);
|
||||
ErrorHandler.successResponse(res, result, 'Benutzer erfolgreich registriert', 201);
|
||||
});
|
||||
|
||||
// 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' }]
|
||||
exports.login = ErrorHandler.asyncHandler(async (req, res) => {
|
||||
const result = await AuthService.login(req.body);
|
||||
ErrorHandler.successResponse(res, result, result.message);
|
||||
});
|
||||
|
||||
if (!resetToken) {
|
||||
return res.status(400).json({ message: 'Ungültiger oder abgelaufener Token' });
|
||||
}
|
||||
exports.forgotPassword = ErrorHandler.asyncHandler(async (req, res) => {
|
||||
const result = await AuthService.forgotPassword(req.body.email);
|
||||
ErrorHandler.successResponse(res, result, result.message);
|
||||
});
|
||||
|
||||
// Passwort hashen und aktualisieren
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
await User.update(
|
||||
{ password: hashedPassword },
|
||||
{ where: { id: resetToken.userId } }
|
||||
);
|
||||
exports.resetPassword = ErrorHandler.asyncHandler(async (req, res) => {
|
||||
const result = await AuthService.resetPassword(req.body.token, req.body.password);
|
||||
ErrorHandler.successResponse(res, result, result.message);
|
||||
});
|
||||
|
||||
// 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);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
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']
|
||||
];
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
if (!request.id && !request.places && !request.types) {
|
||||
return res.json({ events: [], places: [], types: [], contactPersons: [] });
|
||||
}
|
||||
|
||||
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;
|
||||
exports.getAllEvents = ErrorHandler.asyncHandler(async (req, res) => {
|
||||
const events = await EventService.getAllEvents();
|
||||
ErrorHandler.successResponse(res, events, 'Events erfolgreich abgerufen');
|
||||
});
|
||||
|
||||
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 }
|
||||
exports.getEventById = ErrorHandler.asyncHandler(async (req, res) => {
|
||||
const event = await EventService.getEventById(req.params.id);
|
||||
ErrorHandler.successResponse(res, event, 'Event erfolgreich abgerufen');
|
||||
});
|
||||
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.filterEvents = ErrorHandler.asyncHandler(async (req, res) => {
|
||||
const result = await EventService.filterEvents(req.body);
|
||||
ErrorHandler.successResponse(res, result, 'Events erfolgreich gefiltert');
|
||||
});
|
||||
|
||||
exports.createEvent = ErrorHandler.asyncHandler(async (req, res) => {
|
||||
const event = await EventService.createEvent(req.body);
|
||||
ErrorHandler.successResponse(res, event, 'Event erfolgreich erstellt', 201);
|
||||
});
|
||||
|
||||
exports.updateEvent = ErrorHandler.asyncHandler(async (req, res) => {
|
||||
const event = await EventService.updateEvent(req.params.id, req.body);
|
||||
ErrorHandler.successResponse(res, event, 'Event erfolgreich aktualisiert');
|
||||
});
|
||||
|
||||
exports.deleteEvent = ErrorHandler.asyncHandler(async (req, res) => {
|
||||
const result = await EventService.deleteEvent(req.params.id);
|
||||
ErrorHandler.successResponse(res, result, result.message);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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']
|
||||
exports.getMenuData = ErrorHandler.asyncHandler(async (req, res) => {
|
||||
const pages = await PageService.getAllPages();
|
||||
ErrorHandler.successResponse(res, pages, 'Seiten erfolgreich abgerufen');
|
||||
});
|
||||
res.json(pages);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Seiten:', error);
|
||||
res.status(500).json({ message: 'Fehler beim Abrufen der Seiten' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.getPageContent = async (req, res) => {
|
||||
try {
|
||||
const page = await Page.findOne({
|
||||
where: { link: req.query.link }
|
||||
exports.getPageContent = ErrorHandler.asyncHandler(async (req, res) => {
|
||||
const result = await PageService.getPageContent(req.query.link);
|
||||
ErrorHandler.successResponse(res, result, 'Seiteninhalt erfolgreich abgerufen');
|
||||
});
|
||||
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.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);
|
||||
});
|
||||
@@ -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
|
||||
exports.getAllUsers = ErrorHandler.asyncHandler(async (req, res) => {
|
||||
const users = await UserService.getAllUsers();
|
||||
ErrorHandler.successResponse(res, users, 'Benutzer erfolgreich abgerufen');
|
||||
});
|
||||
res.status(200).json(users);
|
||||
} catch (error) {
|
||||
console.error('Error fetching users:', error);
|
||||
res.status(500).json({ message: 'Error fetching users' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.getUserById = async (req, res) => {
|
||||
try {
|
||||
const user = await User.findByPk(req.params.id, {
|
||||
attributes: ['id', 'name', 'email', 'active', 'created_at'] // Passwort ausschließen
|
||||
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');
|
||||
});
|
||||
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.createUser = async (req, res) => {
|
||||
try {
|
||||
const user = await User.create(req.body);
|
||||
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);
|
||||
});
|
||||
|
||||
// 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
|
||||
};
|
||||
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');
|
||||
});
|
||||
|
||||
res.status(201).json(safeUser);
|
||||
} catch (error) {
|
||||
console.error('Error creating user:', error);
|
||||
res.status(500).json({ message: 'Error creating 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');
|
||||
});
|
||||
|
||||
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.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' });
|
||||
}
|
||||
};
|
||||
// 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');
|
||||
});
|
||||
|
||||
@@ -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
186
services/AuthService.js
Normal 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
276
services/EventService.js
Normal 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
101
services/MenuDataService.js
Normal 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
132
services/PageService.js
Normal 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
140
services/UserService.js
Normal 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
71
utils/ErrorHandler.js
Normal 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
109
validators/UserValidator.js
Normal 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();
|
||||
Reference in New Issue
Block a user