jagd-apps/nachsuche/backend/controllers/authController.js

263 lines
7.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 };