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

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

186
services/AuthService.js Normal file
View File

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