263 lines
7.2 KiB
JavaScript
263 lines
7.2 KiB
JavaScript
const jwt = require('jsonwebtoken');
|
||
const crypto = require('crypto');
|
||
const Admin = require('../models/Admin');
|
||
const ResetToken = require('../models/ResetToken');
|
||
const config = require('../config/env');
|
||
const logger = require('../utils/logger');
|
||
const { auditAuth } = require('../middleware/auditLogger');
|
||
const { sendPasswordResetMail } = require('../utils/mailer');
|
||
|
||
const login = async (req, res) => {
|
||
try {
|
||
const { username, password } = req.body;
|
||
|
||
if (!username || !password) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: 'Benutzername und Passwort sind erforderlich'
|
||
});
|
||
}
|
||
|
||
// Find admin (case-insensitive username match)
|
||
const admin = await Admin.findOne({ username: { $regex: new RegExp(`^${username.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i') } });
|
||
if (!admin) {
|
||
await auditAuth(req, false, username, 'Benutzer nicht gefunden');
|
||
return res.status(401).json({
|
||
success: false,
|
||
message: 'Benutzername oder Passwort falsch'
|
||
});
|
||
}
|
||
|
||
// Check password
|
||
const isPasswordValid = await admin.comparePassword(password);
|
||
if (!isPasswordValid) {
|
||
await auditAuth(req, false, username, 'Falsches Passwort');
|
||
return res.status(401).json({
|
||
success: false,
|
||
message: 'Benutzername oder Passwort falsch'
|
||
});
|
||
}
|
||
|
||
// Generate token
|
||
const token = jwt.sign(
|
||
{ id: admin._id, username: admin.username, app: config.appName },
|
||
config.jwtSecret,
|
||
{ expiresIn: config.jwtExpiresIn }
|
||
);
|
||
|
||
// Set token in httpOnly cookie (XSS protection)
|
||
const secureCookie = config.nodeEnv === 'production'
|
||
? (req.secure || req.headers['x-forwarded-proto'] === 'https')
|
||
: false;
|
||
|
||
res.cookie('token', token, {
|
||
httpOnly: true,
|
||
secure: secureCookie,
|
||
sameSite: 'strict',
|
||
path: '/',
|
||
maxAge: 24 * 60 * 60 * 1000 // 24 hours in milliseconds
|
||
});
|
||
|
||
// Log successful login
|
||
await auditAuth(req, true, username);
|
||
|
||
res.json({
|
||
success: true,
|
||
user: {
|
||
id: admin._id,
|
||
username: admin.username
|
||
}
|
||
});
|
||
} catch (error) {
|
||
logger.error('Login-Fehler:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Serverfehler beim Anmelden'
|
||
});
|
||
}
|
||
};
|
||
|
||
const logout = async (req, res) => {
|
||
try {
|
||
// Log logout (get username from token if available)
|
||
const username = req.user?.username || 'unknown';
|
||
await auditAuth(req, true, username, null);
|
||
|
||
const secureCookie = config.nodeEnv === 'production'
|
||
? (req.secure || req.headers['x-forwarded-proto'] === 'https')
|
||
: false;
|
||
|
||
// Clear the token cookie
|
||
res.clearCookie('token', {
|
||
httpOnly: true,
|
||
secure: secureCookie,
|
||
sameSite: 'strict',
|
||
path: '/'
|
||
});
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Erfolgreich abgemeldet'
|
||
});
|
||
} catch (error) {
|
||
logger.error('Logout-Fehler:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Serverfehler beim Abmelden'
|
||
});
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Request password reset - generates reset token
|
||
*/
|
||
const forgotPassword = async (req, res) => {
|
||
try {
|
||
const { username } = req.body;
|
||
|
||
if (!username) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: 'Benutzername ist erforderlich'
|
||
});
|
||
}
|
||
|
||
// Find admin
|
||
const admin = await Admin.findOne({ username });
|
||
|
||
// Don't reveal if user exists (security best practice)
|
||
if (!admin) {
|
||
logger.warn(`Password reset requested for non-existent user: ${username}`);
|
||
return res.json({
|
||
success: true,
|
||
message: 'Falls der Benutzer existiert, wurde ein Reset-Token generiert. Bitte kontaktieren Sie den Administrator.'
|
||
});
|
||
}
|
||
|
||
// Generate secure random token
|
||
const token = crypto.randomBytes(32).toString('hex');
|
||
const expiresAt = new Date(Date.now() + 3600000); // 1 hour from now
|
||
|
||
// Delete any existing tokens for this admin
|
||
await ResetToken.deleteMany({ adminId: admin._id });
|
||
|
||
// Create new reset token
|
||
await ResetToken.create({
|
||
adminId: admin._id,
|
||
token,
|
||
expiresAt
|
||
});
|
||
|
||
// Build reset URL pointing at the SPA's reset page
|
||
const resetUrl = `${config.appUrl}/passwort-zuruecksetzen?token=${token}`;
|
||
|
||
// Send reset email if admin has an email address and SMTP is configured
|
||
if (admin.email) {
|
||
try {
|
||
const sent = await sendPasswordResetMail(admin.email, resetUrl);
|
||
if (!sent) {
|
||
// SMTP not configured – fall back to logging the URL
|
||
logger.warn(`[forgotPassword] SMTP nicht konfiguriert. Reset-URL für ${username}: ${resetUrl}`);
|
||
} else {
|
||
logger.info(`[forgotPassword] Reset-Mail an ${admin.email} gesendet (Benutzer: ${username})`);
|
||
}
|
||
} catch (mailErr) {
|
||
// Mail failure must not block the response (token is already stored)
|
||
logger.error(`[forgotPassword] Fehler beim Senden der Reset-Mail: ${mailErr.message}`);
|
||
}
|
||
} else {
|
||
// Admin has no email – fall back to logging
|
||
logger.warn(
|
||
`[forgotPassword] Admin "${username}" hat keine E-Mail-Adresse. ` +
|
||
`Reset-URL: ${resetUrl}`
|
||
);
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Falls der Benutzer existiert, wurde eine Reset-E-Mail gesendet.'
|
||
});
|
||
} catch (error) {
|
||
logger.error('Fehler bei Passwort-Reset-Anfrage:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Serverfehler bei Passwort-Reset-Anfrage'
|
||
});
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Reset password with token
|
||
*/
|
||
const resetPassword = async (req, res) => {
|
||
try {
|
||
const { token, newPassword } = req.body;
|
||
|
||
if (!token || !newPassword) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: 'Token und neues Passwort sind erforderlich'
|
||
});
|
||
}
|
||
|
||
// Validate password length
|
||
if (newPassword.length < 6) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: 'Passwort muss mindestens 6 Zeichen lang sein'
|
||
});
|
||
}
|
||
|
||
// Find valid, unused token
|
||
const resetToken = await ResetToken.findOne({
|
||
token,
|
||
expiresAt: { $gt: new Date() },
|
||
used: false
|
||
});
|
||
|
||
if (!resetToken) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: 'Ungültiger oder abgelaufener Reset-Token'
|
||
});
|
||
}
|
||
|
||
// Update admin password
|
||
const admin = await Admin.findById(resetToken.adminId);
|
||
if (!admin) {
|
||
return res.status(404).json({
|
||
success: false,
|
||
message: 'Admin nicht gefunden'
|
||
});
|
||
}
|
||
|
||
admin.password = newPassword;
|
||
await admin.save();
|
||
|
||
// Mark token as used
|
||
resetToken.used = true;
|
||
await resetToken.save();
|
||
|
||
// Delete all other tokens for this admin
|
||
await ResetToken.deleteMany({
|
||
adminId: admin._id,
|
||
_id: { $ne: resetToken._id }
|
||
});
|
||
|
||
logger.info(`Password successfully reset for admin: ${admin.username}`);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Passwort erfolgreich zurückgesetzt'
|
||
});
|
||
} catch (error) {
|
||
logger.error('Fehler beim Zurücksetzen des Passworts:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Serverfehler beim Zurücksetzen des Passworts'
|
||
});
|
||
}
|
||
};
|
||
|
||
module.exports = { login, logout, forgotPassword, resetPassword };
|