feat(auth): implement password reset functionality
- Added new endpoints for requesting and resetting passwords in the authController. - Updated User model to include resetToken and resetTokenExpires fields for managing password reset requests. - Enhanced emailService to send password reset emails with secure links. - Updated frontend routes and views to support password reset flow, including new ForgotPassword and ResetPassword components. - Improved internationalization files with new translation keys for password reset messages across multiple languages.
This commit is contained in:
@@ -2,7 +2,8 @@ import bcrypt from 'bcrypt';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import User from '../models/User.js';
|
||||
import UserToken from '../models/UserToken.js';
|
||||
import { sendActivationEmail } from './emailService.js';
|
||||
import crypto from 'crypto';
|
||||
import { sendActivationEmail, sendPasswordResetEmail } from './emailService.js';
|
||||
|
||||
import { devLog, errorLog } from '../utils/logger.js';
|
||||
|
||||
@@ -101,4 +102,80 @@ const logout = async (token) => {
|
||||
return { message: 'Logout erfolgreich' };
|
||||
};
|
||||
|
||||
export { register, activateUser, login, logout };
|
||||
const requestPasswordReset = async (email) => {
|
||||
if (!email) {
|
||||
const err = new Error('E-Mail-Adresse ist erforderlich.');
|
||||
err.status = 400;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const user = await User.findOne({ where: { email } });
|
||||
|
||||
// Aus Sicherheitsgründen IMMER Erfolg melden (verhindert E-Mail-Enumeration)
|
||||
if (!user) {
|
||||
return { message: 'Falls ein Konto mit dieser E-Mail existiert, wurde ein Link zum Zurücksetzen gesendet.' };
|
||||
}
|
||||
|
||||
// Token erzeugen (kryptographisch sicher)
|
||||
const resetToken = crypto.randomBytes(32).toString('hex');
|
||||
const resetTokenExpires = new Date(Date.now() + 60 * 60 * 1000); // 1 Stunde gültig
|
||||
|
||||
user.resetToken = resetToken;
|
||||
user.resetTokenExpires = resetTokenExpires;
|
||||
await user.save();
|
||||
|
||||
try {
|
||||
await sendPasswordResetEmail(email, resetToken);
|
||||
} catch (mailError) {
|
||||
errorLog('[authService.requestPasswordReset] Fehler beim E-Mail-Versand', {
|
||||
message: mailError?.message,
|
||||
code: mailError?.code,
|
||||
});
|
||||
const err = new Error('E-Mail konnte nicht gesendet werden. Bitte Administrator kontaktieren.');
|
||||
err.status = 500;
|
||||
throw err;
|
||||
}
|
||||
|
||||
return { message: 'Falls ein Konto mit dieser E-Mail existiert, wurde ein Link zum Zurücksetzen gesendet.' };
|
||||
};
|
||||
|
||||
const resetPassword = async (token, newPassword) => {
|
||||
if (!token || !newPassword) {
|
||||
const err = new Error('Token und neues Passwort sind erforderlich.');
|
||||
err.status = 400;
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
const err = new Error('Das Passwort muss mindestens 6 Zeichen lang sein.');
|
||||
err.status = 400;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const { Op } = await import('sequelize');
|
||||
const user = await User.findOne({
|
||||
where: {
|
||||
resetToken: token,
|
||||
resetTokenExpires: { [Op.gt]: new Date() }
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
const err = new Error('Der Link ist ungültig oder abgelaufen. Bitte fordere einen neuen an.');
|
||||
err.status = 400;
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Neues Passwort setzen (wird automatisch durch beforeUpdate-Hook gehasht)
|
||||
user.password = newPassword;
|
||||
user.resetToken = null;
|
||||
user.resetTokenExpires = null;
|
||||
await user.save();
|
||||
|
||||
// Alle bestehenden Tokens des Users löschen (erzwingt Neuanmeldung)
|
||||
await UserToken.destroy({ where: { userId: user.id } });
|
||||
|
||||
return { message: 'Passwort wurde erfolgreich geändert.' };
|
||||
};
|
||||
|
||||
export { register, activateUser, login, logout, requestPasswordReset, resetPassword };
|
||||
|
||||
@@ -18,4 +18,36 @@ const sendActivationEmail = async (email, activationCode) => {
|
||||
await transporter.sendMail(mailOptions);
|
||||
};
|
||||
|
||||
export { sendActivationEmail };
|
||||
const sendPasswordResetEmail = async (email, resetToken) => {
|
||||
const resetLink = `${process.env.BASE_URL}/reset-password/${resetToken}`;
|
||||
const mailOptions = {
|
||||
from: process.env.EMAIL_USER,
|
||||
to: email,
|
||||
subject: 'Passwort zurücksetzen',
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: #333;">Passwort zurücksetzen</h2>
|
||||
<p>Du hast eine Anfrage zum Zurücksetzen deines Passworts gestellt.</p>
|
||||
<p>Klicke auf den folgenden Link, um ein neues Passwort zu vergeben:</p>
|
||||
<p style="margin: 20px 0;">
|
||||
<a href="${resetLink}"
|
||||
style="background-color: #4CAF50; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">
|
||||
Passwort zurücksetzen
|
||||
</a>
|
||||
</p>
|
||||
<p style="color: #666; font-size: 14px;">
|
||||
Dieser Link ist <strong>1 Stunde</strong> gültig.<br>
|
||||
Falls du diese Anfrage nicht gestellt hast, ignoriere diese E-Mail einfach.
|
||||
</p>
|
||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||
<p style="color: #999; font-size: 12px;">
|
||||
Falls der Button nicht funktioniert, kopiere diesen Link in deinen Browser:<br>
|
||||
<a href="${resetLink}">${resetLink}</a>
|
||||
</p>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
await transporter.sendMail(mailOptions);
|
||||
};
|
||||
|
||||
export { sendActivationEmail, sendPasswordResetEmail };
|
||||
|
||||
Reference in New Issue
Block a user