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

238 lines
6.5 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 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 },
config.jwtSecret,
{ expiresIn: config.jwtExpiresIn }
);
// Set token in httpOnly cookie (XSS protection)
res.cookie('token', token, {
httpOnly: true, // Not accessible via JavaScript (XSS protection)
secure: false, // Allow over HTTP in development (localhost)
sameSite: 'lax', // Relaxed CSRF protection (allows cross-port cookies on localhost)
path: '/', // Ensure cookie is sent for all app routes, including /stoeberhunde/api
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);
// Clear the token cookie
res.clearCookie('token', {
httpOnly: true,
secure: config.nodeEnv === 'production',
sameSite: 'lax',
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
});
// In production, send email here
// For development, log the token
if (config.nodeEnv !== 'production') {
logger.info(`Password reset token for ${username}: ${token} (expires in 1 hour)`);
} else {
logger.info(`Password reset token generiert für Benutzer: ${username}`);
}
res.json({
success: true,
message: 'Reset-Token wurde generiert. In der Entwicklungsumgebung finden Sie den Token in den Logs.',
...(config.nodeEnv === 'development' && { token }) // Only in dev!
});
} 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 };