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, // Not accessible via JavaScript (XSS protection) secure: secureCookie, sameSite: 'strict', path: '/', // Ensure cookie is sent for all app routes, including /drohnenfuehrer/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: '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) { 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) { logger.error(`[forgotPassword] Fehler beim Senden der Reset-Mail: ${mailErr.message}`); } } else { 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 };