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:3003'}/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;