Files
pfandsystem-demo/backend/routes/tenants.js
christian 0535fbe72a fix: Umlaute, Foto-404, Account-Menu responsive
Bugs:
- nginx: /uploads/ wurde von der .jpg/.png-Regex-Location geschluckt
  -> ^~ Prefix gibt /uploads/ und /api/ Vorrang vor Regex
  -> Fotos werden jetzt korrekt vom Backend ausgeliefert
- TopNav menu-item Klasse war via <style>@apply definiert -
  Tailwind verarbeitet @apply nur in .css-Dateien, nicht in JSX-style-Tags
  -> alle Klassen direkt am Element + neuer responsive Aufbau
- TopNav mobile-friendlier: Brand-Truncate, Demo-Badge erst ab sm,
  Dropdown max-w fuer Viewport, Touch-Outside-Close, groessere Touch-Targets

Umlaute:
- Alle ASCII-Substitutionen (fuer, ueber, aendern, loeschen, Stueck, Geraete,
  Ruecknahme, Hintertuer, Schluessel, Kuehlschrank, Pruefe, gueltig, etc.)
  konsequent auf Unicode (für, über, ä, ö, ü, ß) umgestellt.
- 26 Dateien betroffen, SQL-Identifier + Import-Pfade + API-Routen ASCII gelassen.
- seed.js: Baeckerei -> Baeckerei.. ah Baeckerei -> Bäckerei
2026-05-26 14:01:30 +00:00

141 lines
5.1 KiB
JavaScript

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;