Merge-Konflikt aufgelöst: AuthService und ErrorHandler wiederhergestellt
- AuthService mit allen Methoden (register, login, forgotPassword, resetPassword, logout) - ErrorHandler für zentrale Fehlerbehandlung - authController.js auf neue Clean Code Architektur umgestellt - Alle Authentifizierungsendpunkte verwenden jetzt Service Layer Pattern
This commit is contained in:
@@ -1,186 +1,292 @@
|
||||
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 { User, PasswordResetToken } = require('../models');
|
||||
const { transporter, getPasswordResetEmailTemplate } = require('../config/email');
|
||||
const { addTokenToBlacklist } = require('../utils/blacklist');
|
||||
|
||||
/**
|
||||
* Service für Authentifizierungslogik
|
||||
*/
|
||||
class AuthService {
|
||||
/**
|
||||
* User registrieren
|
||||
* Benutzerregistrierung
|
||||
* @param {Object} userData - Benutzerdaten
|
||||
* @returns {Object} - Registrierungsergebnis
|
||||
*/
|
||||
async register(userData) {
|
||||
static async register(userData) {
|
||||
const { name, email, password } = userData;
|
||||
|
||||
|
||||
// Validierung
|
||||
if (!name || !email || !password) {
|
||||
throw new Error('VALIDATION_ERROR: Alle Felder sind erforderlich');
|
||||
throw new 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');
|
||||
if (password.length < 6) {
|
||||
throw new Error('Passwort muss mindestens 6 Zeichen lang sein');
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
const user = await User.create({
|
||||
name,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
active: true
|
||||
});
|
||||
// Email-Format validieren
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
throw new Error('Ungültige Email-Adresse');
|
||||
}
|
||||
|
||||
return this.getSafeUserData(user);
|
||||
try {
|
||||
// Passwort hashen
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// Benutzer erstellen mit Retry-Logik für Lock-Timeout
|
||||
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 this.delay(backoffMs);
|
||||
attempt++;
|
||||
continue;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
if (!createdUser && lastError) {
|
||||
throw new Error('Zeitüberschreitung beim Zugriff auf die Datenbank. Bitte erneut versuchen.');
|
||||
}
|
||||
|
||||
// Sichere Benutzerdaten zurückgeben
|
||||
const safeUser = {
|
||||
id: createdUser.id,
|
||||
name: createdUser.name,
|
||||
email: createdUser.email,
|
||||
active: createdUser.active,
|
||||
created_at: createdUser.created_at
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Benutzer erfolgreich registriert',
|
||||
user: safeUser
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
if (error.name === 'SequelizeUniqueConstraintError') {
|
||||
throw new Error('Email-Adresse bereits in Verwendung');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User einloggen
|
||||
* Benutzeranmeldung
|
||||
* @param {string} email - Email-Adresse
|
||||
* @param {string} password - Passwort
|
||||
* @returns {Object} - Login-Ergebnis
|
||||
*/
|
||||
async login(credentials) {
|
||||
const { email, password } = credentials;
|
||||
|
||||
static async login(email, password) {
|
||||
if (!email || !password) {
|
||||
throw new Error('VALIDATION_ERROR: Email und Passwort sind erforderlich');
|
||||
throw new Error('Email und Passwort sind erforderlich');
|
||||
}
|
||||
|
||||
const user = await User.findOne({ where: { email } });
|
||||
if (!user) {
|
||||
throw new Error('INVALID_CREDENTIALS');
|
||||
try {
|
||||
// Benutzer suchen
|
||||
const user = await User.findOne({ where: { email } });
|
||||
if (!user) {
|
||||
throw new Error('Ungültige Anmeldedaten');
|
||||
}
|
||||
|
||||
// Passwort prüfen
|
||||
const validPassword = await bcrypt.compare(password, user.password);
|
||||
if (!validPassword) {
|
||||
throw new Error('Ungültige Anmeldedaten');
|
||||
}
|
||||
|
||||
// Benutzer aktiv?
|
||||
if (!user.active) {
|
||||
throw new Error('Benutzerkonto ist nicht aktiv');
|
||||
}
|
||||
|
||||
// JWT Token erstellen
|
||||
const token = jwt.sign(
|
||||
{ id: user.id, name: user.name, email: user.email },
|
||||
process.env.JWT_SECRET || 'zTxVgptmPl9!_dr%xxx9999(dd)',
|
||||
{ expiresIn: '1h' }
|
||||
);
|
||||
|
||||
// Sichere Benutzerdaten
|
||||
const safeUser = {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
active: user.active,
|
||||
created_at: user.created_at
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Login erfolgreich',
|
||||
token,
|
||||
user: safeUser
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
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
|
||||
* Passwort vergessen - Email senden
|
||||
* @param {string} email - Email-Adresse
|
||||
* @returns {Object} - Ergebnis
|
||||
*/
|
||||
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) {
|
||||
static async forgotPassword(email) {
|
||||
if (!email) {
|
||||
throw new Error('VALIDATION_ERROR: E-Mail-Adresse ist erforderlich');
|
||||
throw new Error('Email-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.' };
|
||||
try {
|
||||
// Benutzer suchen
|
||||
const user = await User.findOne({ where: { email } });
|
||||
if (!user) {
|
||||
// Aus Sicherheitsgründen keine Information über Existenz preisgeben
|
||||
return {
|
||||
success: true,
|
||||
message: 'Falls die Email-Adresse registriert ist, wurde eine Passwort-Reset-Email gesendet'
|
||||
};
|
||||
}
|
||||
|
||||
// Alte Tokens löschen
|
||||
await PasswordResetToken.destroy({ where: { userId: user.id } });
|
||||
|
||||
// Neuen Token erstellen
|
||||
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
|
||||
});
|
||||
|
||||
// Email senden
|
||||
const resetUrl = `${process.env.FRONTEND_URL || 'http://localhost:8080'}/reset-password?token=${token}`;
|
||||
const emailTemplate = getPasswordResetEmailTemplate(user.name, resetUrl);
|
||||
|
||||
await transporter.sendMail({
|
||||
from: process.env.EMAIL_FROM || 'noreply@miriamgemeinde.de',
|
||||
to: email,
|
||||
subject: 'Passwort zurücksetzen - Miriam Gemeinde',
|
||||
html: emailTemplate
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Falls die Email-Adresse registriert ist, wurde eine Passwort-Reset-Email gesendet'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Forgot password error:', error);
|
||||
throw new Error('Fehler beim Senden der Passwort-Reset-Email');
|
||||
}
|
||||
|
||||
// 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
|
||||
* @param {string} token - Reset-Token
|
||||
* @param {string} newPassword - Neues Passwort
|
||||
* @returns {Object} - Ergebnis
|
||||
*/
|
||||
async resetPassword(token, newPassword) {
|
||||
static async resetPassword(token, newPassword) {
|
||||
if (!token || !newPassword) {
|
||||
throw new Error('VALIDATION_ERROR: Token und neues Passwort sind erforderlich');
|
||||
throw new Error('Token und neues Passwort sind erforderlich');
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
throw new Error('VALIDATION_ERROR: Passwort muss mindestens 6 Zeichen lang sein');
|
||||
throw new 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' }]
|
||||
});
|
||||
try {
|
||||
// Token suchen
|
||||
const resetToken = await PasswordResetToken.findOne({
|
||||
where: { token, used: false },
|
||||
include: [{ model: User, as: 'user' }]
|
||||
});
|
||||
|
||||
if (!resetToken) {
|
||||
throw new Error('INVALID_RESET_TOKEN');
|
||||
if (!resetToken) {
|
||||
throw new Error('Ungültiger oder bereits verwendeter Token');
|
||||
}
|
||||
|
||||
// Token abgelaufen?
|
||||
if (new Date() > resetToken.expiresAt) {
|
||||
await resetToken.destroy();
|
||||
throw new Error('Token ist abgelaufen');
|
||||
}
|
||||
|
||||
// Neues Passwort hashen und speichern
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||
await resetToken.user.update({ password: hashedPassword });
|
||||
|
||||
// Token als verwendet markieren
|
||||
await resetToken.update({ used: true });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Passwort erfolgreich zurückgesetzt'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Reset password error:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 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)
|
||||
* Benutzerabmeldung
|
||||
* @param {string} token - JWT Token
|
||||
* @returns {Object} - Ergebnis
|
||||
*/
|
||||
getSafeUserData(user) {
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
active: user.active,
|
||||
created_at: user.created_at
|
||||
};
|
||||
static async logout(token) {
|
||||
if (!token) {
|
||||
throw new Error('Token ist erforderlich');
|
||||
}
|
||||
|
||||
try {
|
||||
// Token zur Blacklist hinzufügen
|
||||
addTokenToBlacklist(token);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Erfolgreich abgemeldet'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
throw new Error('Fehler beim Abmelden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hilfsfunktion für Verzögerung
|
||||
* @param {number} ms - Millisekunden
|
||||
* @returns {Promise} - Promise mit Verzögerung
|
||||
*/
|
||||
static delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new AuthService();
|
||||
module.exports = AuthService;
|
||||
Reference in New Issue
Block a user