import express from 'express'; import bcrypt from 'bcrypt'; import { body, validationResult } from 'express-validator'; import crypto from 'crypto'; import pool from '../config/database.js'; import { authenticateToken, requireSuperAdmin } from '../middleware/auth.js'; import { sendDemoInviteMail } from '../lib/mail.js'; import { seedDemoTenant } from '../lib/seed.js'; const router = express.Router(); router.use(authenticateToken, requireSuperAdmin); // GET /api/tenants router.get('/', async (req, res) => { try { const [rows] = await pool.query( `SELECT t.*, (SELECT COUNT(*) FROM mitarbeiter WHERE tenant_id = t.id) AS user_count, (SELECT COUNT(*) FROM kunden WHERE tenant_id = t.id) AS kunden_count FROM tenants t ORDER BY t.erstellt_am DESC` ); res.json(rows); } catch (e) { console.error('GET tenants:', e); res.status(500).json({ error: 'Serverfehler' }); } }); // GET /api/tenants/:id router.get('/:id', async (req, res) => { try { const [rows] = await pool.query('SELECT * FROM tenants WHERE id = ?', [req.params.id]); if (rows.length === 0) return res.status(404).json({ error: 'Tenant nicht gefunden' }); res.json(rows[0]); } catch (e) { res.status(500).json({ error: 'Serverfehler' }); } }); // POST /api/tenants -> legt Demo-Tenant + Admin-User + Sample-Daten an, schickt Invite-Mail // body: { name, kontakt_email, firma?, days? (default 30), seed? (default true) } router.post('/', [ body('name').notEmpty().trim(), body('kontakt_email').isEmail().normalizeEmail(), body('days').optional().isInt({ min: 1, max: 365 }) ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() }); const { name, kontakt_email, firma, days = 30, seed = true } = req.body; const slug = (firma || name).toLowerCase() .replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 50) + '-' + crypto.randomBytes(2).toString('hex'); const conn = await pool.getConnection(); try { await conn.beginTransaction(); // tenant const tenantId = crypto.randomUUID(); await conn.query( `INSERT INTO tenants (id, name, slug, kontakt_email, firma, typ, status, demo_expires_at, max_users) VALUES (?, ?, ?, ?, ?, 'demo', 'aktiv', DATE_ADD(NOW(), INTERVAL ? DAY), 5)`, [tenantId, name, slug, kontakt_email, firma || null, days] ); // admin user const adminId = crypto.randomUUID(); const plainPw = crypto.randomBytes(8).toString('base64url'); const hash = await bcrypt.hash(plainPw, 10); await conn.query( `INSERT INTO mitarbeiter (id, tenant_id, email, password_hash, name, rolle, aktiv) VALUES (?, ?, ?, ?, ?, 'admin', 1)`, [adminId, tenantId, kontakt_email, hash, name] ); if (seed) await seedDemoTenant(conn, tenantId); await conn.commit(); // Mail asynchron - Fehler nicht blockend sendDemoInviteMail({ to: kontakt_email, name, slug, password: plainPw, loginUrl: `https://pfandsystem.dockly.de/login?tenant=${slug}`, days }).catch(err => console.error('Mail-Fehler:', err.message)); res.status(201).json({ tenant_id: tenantId, slug, admin_email: kontakt_email, // Passwort wird ausnahmsweise im Response zurückgegeben, damit der SuperAdmin // es ggf. manuell weitergeben kann, falls die Mail nicht ankommt. admin_password_oneshot: plainPw, demo_expires_in_days: days }); } catch (e) { await conn.rollback(); console.error('Tenant-Anlage-Fehler:', e); res.status(500).json({ error: 'Fehler beim Anlegen des Tenants', detail: e.message }); } finally { conn.release(); } }); // PATCH /api/tenants/:id -> Demo verlaengern, Status ändern, Limits ändern router.patch('/:id', async (req, res) => { const updates = []; const values = []; if (req.body.extend_days) { updates.push('demo_expires_at = DATE_ADD(COALESCE(demo_expires_at, NOW()), INTERVAL ? DAY)'); values.push(parseInt(req.body.extend_days)); updates.push("status = 'aktiv'"); } if (req.body.status) { updates.push('status = ?'); values.push(req.body.status); } if (req.body.max_users) { updates.push('max_users = ?'); values.push(parseInt(req.body.max_users)); } if (req.body.typ) { updates.push('typ = ?'); values.push(req.body.typ); } if (updates.length === 0) return res.status(400).json({ error: 'Keine Änderungen' }); values.push(req.params.id); try { await pool.query(`UPDATE tenants SET ${updates.join(', ')} WHERE id = ?`, values); const [rows] = await pool.query('SELECT * FROM tenants WHERE id = ?', [req.params.id]); res.json(rows[0]); } catch (e) { console.error('Tenant-Update:', e); res.status(500).json({ error: 'Serverfehler' }); } }); // DELETE /api/tenants/:id -> Tenant inkl. aller Daten löschen (CASCADE) router.delete('/:id', async (req, res) => { try { await pool.query('DELETE FROM tenants WHERE id = ?', [req.params.id]); res.json({ message: 'Tenant gelöscht' }); } catch (e) { res.status(500).json({ error: 'Serverfehler' }); } }); export default router;