Files
miriamgemeinde/services/AuthService.js
Torsten Schulz (local) 7861b9cffb 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
2025-09-24 11:59:13 +02:00

292 lines
7.9 KiB
JavaScript

const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
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 {
/**
* Benutzerregistrierung
* @param {Object} userData - Benutzerdaten
* @returns {Object} - Registrierungsergebnis
*/
static async register(userData) {
const { name, email, password } = userData;
// Validierung
if (!name || !email || !password) {
throw new Error('Alle Felder sind erforderlich');
}
if (password.length < 6) {
throw new Error('Passwort muss mindestens 6 Zeichen lang sein');
}
// Email-Format validieren
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new Error('Ungültige Email-Adresse');
}
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;
}
}
/**
* Benutzeranmeldung
* @param {string} email - Email-Adresse
* @param {string} password - Passwort
* @returns {Object} - Login-Ergebnis
*/
static async login(email, password) {
if (!email || !password) {
throw new Error('Email und Passwort sind erforderlich');
}
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;
}
}
/**
* Passwort vergessen - Email senden
* @param {string} email - Email-Adresse
* @returns {Object} - Ergebnis
*/
static async forgotPassword(email) {
if (!email) {
throw new Error('Email-Adresse ist erforderlich');
}
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');
}
}
/**
* Passwort zurücksetzen
* @param {string} token - Reset-Token
* @param {string} newPassword - Neues Passwort
* @returns {Object} - Ergebnis
*/
static async resetPassword(token, newPassword) {
if (!token || !newPassword) {
throw new Error('Token und neues Passwort sind erforderlich');
}
if (newPassword.length < 6) {
throw new Error('Passwort muss mindestens 6 Zeichen lang sein');
}
try {
// Token suchen
const resetToken = await PasswordResetToken.findOne({
where: { token, used: false },
include: [{ model: User, as: 'user' }]
});
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;
}
}
/**
* Benutzerabmeldung
* @param {string} token - JWT Token
* @returns {Object} - Ergebnis
*/
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 = AuthService;