From 77e3dbde823e60ebfa600050f176fbde17230454 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 24 Sep 2025 10:02:46 +0200 Subject: [PATCH] =?UTF-8?q?Refaktoriere=20Controller-Methoden=20zur=20Benu?= =?UTF-8?q?tzer-,=20Event-=20und=20Men=C3=BC-Datenverwaltung,=20indem=20di?= =?UTF-8?q?e=20Logik=20in=20separate=20Service-Klassen=20ausgelagert=20wir?= =?UTF-8?q?d.=20Implementiere=20eine=20verbesserte=20Fehlerbehandlung=20un?= =?UTF-8?q?d=20sichere=20R=C3=BCckgaben.=20F=C3=BCge=20eine=20neue=20Route?= =?UTF-8?q?=20zur=20Passwort=C3=A4nderung=20im=20Benutzer-Router=20hinzu.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controllers/authController.js | 222 +++--------------------- controllers/eventController.js | 208 +++------------------- controllers/menuDataController.js | 38 ++-- controllers/pageController.js | 69 +++----- controllers/userController.js | 136 ++++----------- routes/users.js | 3 +- services/AuthService.js | 186 ++++++++++++++++++++ services/EventService.js | 276 ++++++++++++++++++++++++++++++ services/MenuDataService.js | 101 +++++++++++ services/PageService.js | 132 ++++++++++++++ services/UserService.js | 140 +++++++++++++++ utils/ErrorHandler.js | 71 ++++++++ validators/UserValidator.js | 109 ++++++++++++ 13 files changed, 1137 insertions(+), 554 deletions(-) create mode 100644 services/AuthService.js create mode 100644 services/EventService.js create mode 100644 services/MenuDataService.js create mode 100644 services/PageService.js create mode 100644 services/UserService.js create mode 100644 utils/ErrorHandler.js create mode 100644 validators/UserValidator.js diff --git a/controllers/authController.js b/controllers/authController.js index 333857c..1cc1a6c 100644 --- a/controllers/authController.js +++ b/controllers/authController.js @@ -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); +}); \ No newline at end of file diff --git a/controllers/eventController.js b/controllers/eventController.js index 3078e1a..ac20ad5 100644 --- a/controllers/eventController.js +++ b/controllers/eventController.js @@ -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); +}); \ No newline at end of file diff --git a/controllers/menuDataController.js b/controllers/menuDataController.js index 33c5407..035df1d 100644 --- a/controllers/menuDataController.js +++ b/controllers/menuDataController.js @@ -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); +}); \ No newline at end of file diff --git a/controllers/pageController.js b/controllers/pageController.js index 5640e00..dd0a8d2 100644 --- a/controllers/pageController.js +++ b/controllers/pageController.js @@ -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); +}); \ No newline at end of file diff --git a/controllers/userController.js b/controllers/userController.js index 43f56d2..3f818e7 100644 --- a/controllers/userController.js +++ b/controllers/userController.js @@ -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'); +}); diff --git a/routes/users.js b/routes/users.js index 3dcd57c..11c3299 100644 --- a/routes/users.js +++ b/routes/users.js @@ -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; diff --git a/services/AuthService.js b/services/AuthService.js new file mode 100644 index 0000000..8c2c9be --- /dev/null +++ b/services/AuthService.js @@ -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(); diff --git a/services/EventService.js b/services/EventService.js new file mode 100644 index 0000000..acaceb9 --- /dev/null +++ b/services/EventService.js @@ -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(); diff --git a/services/MenuDataService.js b/services/MenuDataService.js new file mode 100644 index 0000000..4a06c24 --- /dev/null +++ b/services/MenuDataService.js @@ -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(); diff --git a/services/PageService.js b/services/PageService.js new file mode 100644 index 0000000..e64fd4f --- /dev/null +++ b/services/PageService.js @@ -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(); diff --git a/services/UserService.js b/services/UserService.js new file mode 100644 index 0000000..360a71a --- /dev/null +++ b/services/UserService.js @@ -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(); diff --git a/utils/ErrorHandler.js b/utils/ErrorHandler.js new file mode 100644 index 0000000..e3e7088 --- /dev/null +++ b/utils/ErrorHandler.js @@ -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(); diff --git a/validators/UserValidator.js b/validators/UserValidator.js new file mode 100644 index 0000000..b1c9e0c --- /dev/null +++ b/validators/UserValidator.js @@ -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();