import express from 'express'; import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; import { body, validationResult } from 'express-validator'; import pool from '../config/database.js'; import { authenticateToken } from '../middleware/auth.js'; const router = express.Router(); const issueToken = (m) => jwt.sign( { id: m.id, email: m.email, rolle: m.rolle, name: m.name, tenant_id: m.tenant_id }, process.env.JWT_SECRET, { expiresIn: '24h' } ); // POST /api/auth/login // body: { email, password, tenant_slug? } // - tenant_slug optional: falls mehrere Tenants mit gleicher Mail (sollte selten sein) // - SuperAdmins logen sich ohne tenant_slug ein. router.post('/login', [ body('email').isEmail().normalizeEmail(), body('password').notEmpty() ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() }); const { email, password, tenant_slug } = req.body; try { let rows; if (tenant_slug) { [rows] = await pool.query( `SELECT m.* FROM mitarbeiter m JOIN tenants t ON m.tenant_id = t.id WHERE m.email = ? AND t.slug = ? AND m.aktiv = 1`, [email, tenant_slug] ); } else { // Versuche zuerst SuperAdmin (tenant_id NULL), dann ersten Tenant-Match [rows] = await pool.query( `SELECT * FROM mitarbeiter WHERE email = ? AND aktiv = 1 ORDER BY (tenant_id IS NULL) DESC LIMIT 1`, [email] ); } if (rows.length === 0) return res.status(401).json({ error: 'Ungültige Anmeldedaten' }); const m = rows[0]; const ok = await bcrypt.compare(password, m.password_hash); if (!ok) return res.status(401).json({ error: 'Ungültige Anmeldedaten' }); // Demo-Ablauf-Prüfung if (m.tenant_id) { const [tr] = await pool.query( 'SELECT status, demo_expires_at, typ, name FROM tenants WHERE id = ?', [m.tenant_id] ); if (tr.length === 0 || tr[0].status !== 'aktiv') { return res.status(403).json({ error: 'Tenant nicht aktiv' }); } if (tr[0].typ === 'demo' && tr[0].demo_expires_at && new Date(tr[0].demo_expires_at) < new Date()) { await pool.query("UPDATE tenants SET status='abgelaufen' WHERE id = ?", [m.tenant_id]); return res.status(403).json({ error: 'Demo-Zugang abgelaufen' }); } } res.json({ token: issueToken(m), user: { id: m.id, email: m.email, name: m.name, rolle: m.rolle, tenant_id: m.tenant_id } }); } catch (e) { console.error('Login-Fehler:', e); res.status(500).json({ error: 'Serverfehler beim Login' }); } }); // GET /api/auth/me router.get('/me', authenticateToken, async (req, res) => { try { const [rows] = await pool.query( `SELECT m.id, m.email, m.name, m.rolle, m.tenant_id, m.erstellt_am, t.name AS tenant_name, t.slug AS tenant_slug, t.typ AS tenant_typ, t.demo_expires_at, t.status AS tenant_status FROM mitarbeiter m LEFT JOIN tenants t ON m.tenant_id = t.id WHERE m.id = ?`, [req.user.id] ); if (rows.length === 0) return res.status(404).json({ error: 'Benutzer nicht gefunden' }); res.json(rows[0]); } catch (e) { console.error('me-Fehler:', e); res.status(500).json({ error: 'Serverfehler' }); } }); // POST /api/auth/change-password router.post('/change-password', authenticateToken, [ body('currentPassword').notEmpty(), body('newPassword').isLength({ min: 6 }) ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() }); try { const [rows] = await pool.query('SELECT password_hash FROM mitarbeiter WHERE id = ?', [req.user.id]); if (rows.length === 0) return res.status(404).json({ error: 'Benutzer nicht gefunden' }); const ok = await bcrypt.compare(req.body.currentPassword, rows[0].password_hash); if (!ok) return res.status(401).json({ error: 'Aktuelles Passwort ist falsch' }); const hash = await bcrypt.hash(req.body.newPassword, 10); await pool.query('UPDATE mitarbeiter SET password_hash = ? WHERE id = ?', [hash, req.user.id]); res.json({ message: 'Passwort erfolgreich geändert' }); } catch (e) { console.error('change-password-Fehler:', e); res.status(500).json({ error: 'Serverfehler' }); } }); export default router;