feat(backend): multi-tenant refactor + Geraete + Kundenbilder

Middleware:
- requireTenant prueft tenant_id, Status und Demo-Ablauf (autom. Status-Update)
- requireSuperAdmin fuer cross-tenant Endpoints
- JWT enthaelt jetzt tenant_id

Auth:
- Login: SuperAdmin (tenant_id=NULL) ODER tenant-scoped User
- Demo-Ablaufs-Check beim Login

Neue Routes:
- /api/tenants  (SuperAdmin: Demo-Accounts anlegen mit 30-Tage-Default,
                 Mail-Invite via Resend, Sample-Daten-Seed automatisch,
                 verlaengern, deaktivieren)
- /api/geraete  (Seriennummern-Verwaltung mit Kunde-Zuordnung)
- /api/kunden-bilder/:kunde_id (Foto-Upload, max 5/Kunde)

Bestehende Routes:
- alle tenant_id-scoped (SELECT/UPDATE/DELETE)
- kunden: anlieferungshinweis + lieferzeit_bis Felder
- Cross-tenant-Refs (kunde_id/artikel_id) werden gegen Tenant validiert

Lib:
- lib/mail.js (Resend, mit Dev-Fallback)
- lib/seed.js (5 Kunden, 3 Artikel, 6 Lieferungen, 2 Geraete)
- scripts/bootstrap.js (legt SuperAdmin aus env-Vars beim Start an)

server.js: neue Routes + bootstrap-Call
This commit is contained in:
christian
2026-05-26 12:47:17 +00:00
parent d86c898455
commit 43c75e5dd3
14 changed files with 844 additions and 576 deletions

32
backend/lib/mail.js Normal file
View File

@@ -0,0 +1,32 @@
import { Resend } from 'resend';
const resend = process.env.RESEND_API_KEY ? new Resend(process.env.RESEND_API_KEY) : null;
const FROM = process.env.MAIL_FROM || 'Pfandsystem Demo <demo@dockly.de>';
export async function sendDemoInviteMail({ to, name, slug, password, loginUrl, days }) {
if (!resend) {
console.log('[mail-dev] Demo-Invite an', to, 'Slug:', slug, 'Pw:', password);
return { dev: true };
}
const html = `
<div style="font-family:system-ui,Segoe UI,Helvetica,Arial,sans-serif;max-width:560px;margin:auto">
<h2 style="color:#1e293b">Willkommen im Pfandsystem-Demo</h2>
<p>Hallo ${name},</p>
<p>dein <strong>${days}-Tage Demo-Zugang</strong> ist bereit. Du kannst alle Funktionen
in Ruhe testen und eigene Beispieldaten anlegen.</p>
<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin:16px 0">
<p style="margin:4px 0"><strong>Login-URL:</strong> <a href="${loginUrl}">${loginUrl}</a></p>
<p style="margin:4px 0"><strong>E-Mail:</strong> ${to}</p>
<p style="margin:4px 0"><strong>Passwort:</strong> <code>${password}</code></p>
<p style="margin:4px 0"><strong>Tenant-Kennung:</strong> ${slug}</p>
</div>
<p>Bitte aendere dein Passwort nach dem ersten Login.</p>
<p style="color:#64748b;font-size:12px">Diese Mail wurde automatisch versandt vom Pfandsystem-Demo-Portal.</p>
</div>`;
return resend.emails.send({
from: FROM,
to,
subject: `Dein Pfandsystem-Demo-Zugang (${days} Tage)`,
html
});
}

62
backend/lib/seed.js Normal file
View File

@@ -0,0 +1,62 @@
import crypto from 'crypto';
// Befuellt einen frischen Demo-Tenant mit Beispieldaten
// (5 Kunden, 3 Artikel, 6 Lieferungen, 2 Geraete).
export async function seedDemoTenant(conn, tenantId) {
// Artikel
const artikel = [
{ id: crypto.randomUUID(), name: 'Brotkorb klein', pfand: 2.50 },
{ id: crypto.randomUUID(), name: 'Brotkorb gross', pfand: 5.00 },
{ id: crypto.randomUUID(), name: 'Transportbox', pfand: 10.00 }
];
for (const a of artikel) {
await conn.query(
'INSERT INTO artikel (id, tenant_id, bezeichnung, pfand_betrag) VALUES (?, ?, ?, ?)',
[a.id, tenantId, a.name, a.pfand]
);
}
// Kunden
const kunden = [
{ name: 'Cafe Sonnenschein', adresse: 'Bahnhofstr. 12, 10115 Berlin', zeit: '05:00', hinweis: 'Anlieferung Hintertuer. Code 1234#.' },
{ name: 'Restaurant Roma', adresse: 'Hauptstr. 88, 10117 Berlin', zeit: '06:30', hinweis: 'Schluesselkasten links neben Eingang.' },
{ name: 'Hotel Adlerhorst', adresse: 'Seestrasse 5, 14193 Berlin', zeit: '05:30', hinweis: 'Anlieferung Tiefgarage Ebene -1, Tor 3.' },
{ name: 'Kantine Werk Nord', adresse: 'Industriestr. 200, 13407 Berlin', zeit: '06:00', hinweis: 'Werkschutz Bescheid geben, Tor B.' },
{ name: 'Baeckerei Schmitt', adresse: 'Marktplatz 4, 10178 Berlin', zeit: '04:45', hinweis: 'Direkt in den Verkaufsraum stellen.' }
];
const kundenIds = [];
for (const k of kunden) {
const id = crypto.randomUUID();
kundenIds.push(id);
await conn.query(
`INSERT INTO kunden (id, tenant_id, name, adresse, lieferzeit_bis, anlieferungshinweis)
VALUES (?, ?, ?, ?, ?, ?)`,
[id, tenantId, k.name, k.adresse, k.zeit, k.hinweis]
);
}
// Lieferungen (verteilt ueber die letzten 14 Tage)
for (let i = 0; i < 6; i++) {
const kid = kundenIds[i % kundenIds.length];
const aid = artikel[i % artikel.length].id;
const anz = (i % 3) + 1;
await conn.query(
`INSERT INTO lieferungen (tenant_id, kunde_id, artikel_id, anzahl, erstellt_am)
VALUES (?, ?, ?, ?, DATE_SUB(NOW(), INTERVAL ? DAY))`,
[tenantId, kid, aid, anz, i * 2]
);
}
// Geraete
const geraete = [
{ sn: 'OF-2024-001', typ: 'Ofen', bez: 'Miwe Condo 3-Etagen', kid: kundenIds[0] },
{ sn: 'KS-2024-007', typ: 'Kuehlschrank', bez: 'Liebherr GKv 5710', kid: kundenIds[2] }
];
for (const g of geraete) {
await conn.query(
`INSERT INTO geraete (id, tenant_id, seriennummer, typ, bezeichnung, kunde_id, status)
VALUES (UUID(), ?, ?, ?, ?, ?, 'aktiv')`,
[tenantId, g.sn, g.typ, g.bez, g.kid]
);
}
}

View File

@@ -1,8 +1,12 @@
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import pool from '../config/database.js';
// Verifiziert JWT und stellt req.user bereit
// req.user enthaelt: { id, email, name, rolle, tenant_id }
// tenant_id ist NULL fuer SuperAdmins.
export const authenticateToken = (req, res, next) => { export const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization']; const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN const token = authHeader && authHeader.split(' ')[1];
if (!token) { if (!token) {
return res.status(401).json({ error: 'Kein Token bereitgestellt' }); return res.status(401).json({ error: 'Kein Token bereitgestellt' });
@@ -10,23 +14,52 @@ export const authenticateToken = (req, res, next) => {
jwt.verify(token, process.env.JWT_SECRET, (err, user) => { jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) { if (err) {
return res.status(403).json({ error: 'Ungültiger oder abgelaufener Token' }); return res.status(403).json({ error: 'Ungueltiger oder abgelaufener Token' });
} }
req.user = user; req.user = user;
next(); next();
}); });
}; };
export const requireRole = (roles) => { // Stellt sicher, dass der User einen Tenant hat und der Tenant aktiv ist.
return (req, res, next) => { // SuperAdmins ohne Tenant werden mit 403 abgelehnt (sollen tenants-Route nutzen).
if (!req.user) { export const requireTenant = async (req, res, next) => {
return res.status(401).json({ error: 'Nicht authentifiziert' }); if (!req.user) return res.status(401).json({ error: 'Nicht authentifiziert' });
if (!req.user.tenant_id) {
return res.status(403).json({ error: 'Tenant-Kontext erforderlich (SuperAdmin nicht erlaubt)' });
}
try {
const [rows] = await pool.query(
'SELECT id, status, demo_expires_at, typ FROM tenants WHERE id = ?',
[req.user.tenant_id]
);
if (rows.length === 0) return res.status(403).json({ error: 'Tenant nicht gefunden' });
const t = rows[0];
if (t.status !== 'aktiv') return res.status(403).json({ error: `Tenant-Status: ${t.status}` });
if (t.typ === 'demo' && t.demo_expires_at && new Date(t.demo_expires_at) < new Date()) {
await pool.query("UPDATE tenants SET status='abgelaufen' WHERE id = ?", [t.id]);
return res.status(403).json({ error: 'Demo-Zugang abgelaufen' });
} }
req.tenant = t;
if (!roles.includes(req.user.rolle)) {
return res.status(403).json({ error: 'Keine Berechtigung' });
}
next(); next();
}; } catch (e) {
console.error('requireTenant-Fehler:', e);
res.status(500).json({ error: 'Serverfehler' });
}
};
export const requireRole = (roles) => (req, res, next) => {
if (!req.user) return res.status(401).json({ error: 'Nicht authentifiziert' });
if (!roles.includes(req.user.rolle)) {
return res.status(403).json({ error: 'Keine Berechtigung' });
}
next();
};
export const requireSuperAdmin = (req, res, next) => {
if (!req.user) return res.status(401).json({ error: 'Nicht authentifiziert' });
if (req.user.rolle !== 'superadmin') {
return res.status(403).json({ error: 'SuperAdmin erforderlich' });
}
next();
}; };

View File

@@ -1,116 +1,76 @@
import express from 'express'; import express from 'express';
import { body, validationResult } from 'express-validator'; import { body, validationResult } from 'express-validator';
import pool from '../config/database.js'; import pool from '../config/database.js';
import { authenticateToken } from '../middleware/auth.js'; import { authenticateToken, requireTenant } from '../middleware/auth.js';
const router = express.Router(); const router = express.Router();
router.use(authenticateToken, requireTenant);
// Alle Routen erfordern Authentifizierung
router.use(authenticateToken);
// GET alle Artikel
router.get('/', async (req, res) => { router.get('/', async (req, res) => {
try { try {
const [rows] = await pool.query( const [rows] = await pool.query(
'SELECT * FROM artikel ORDER BY bezeichnung ASC' 'SELECT * FROM artikel WHERE tenant_id = ? ORDER BY bezeichnung ASC',
[req.user.tenant_id]
); );
res.json(rows); res.json(rows);
} catch (error) { } catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
console.error('Fehler beim Abrufen der Artikel:', error);
res.status(500).json({ error: 'Serverfehler' });
}
}); });
// GET einzelner Artikel
router.get('/:id', async (req, res) => { router.get('/:id', async (req, res) => {
try { try {
const [rows] = await pool.query( const [rows] = await pool.query(
'SELECT * FROM artikel WHERE id = ?', 'SELECT * FROM artikel WHERE id = ? AND tenant_id = ?',
[req.params.id] [req.params.id, req.user.tenant_id]
); );
if (rows.length === 0) return res.status(404).json({ error: 'Artikel nicht gefunden' });
if (rows.length === 0) {
return res.status(404).json({ error: 'Artikel nicht gefunden' });
}
res.json(rows[0]); res.json(rows[0]);
} catch (error) { } catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
console.error('Fehler beim Abrufen des Artikels:', error);
res.status(500).json({ error: 'Serverfehler' });
}
}); });
// POST neuer Artikel
router.post('/', [ router.post('/', [
body('bezeichnung').notEmpty().trim(), body('bezeichnung').notEmpty().trim(),
body('pfand_betrag').isFloat({ min: 0 }) body('pfand_betrag').isFloat({ min: 0 })
], async (req, res) => { ], async (req, res) => {
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) { if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
return res.status(400).json({ errors: errors.array() });
}
const { bezeichnung, pfand_betrag } = req.body;
try { try {
const [result] = await pool.query( await pool.query(
'INSERT INTO artikel (bezeichnung, pfand_betrag) VALUES (?, ?)', 'INSERT INTO artikel (id, tenant_id, bezeichnung, pfand_betrag) VALUES (UUID(), ?, ?, ?)',
[bezeichnung, pfand_betrag] [req.user.tenant_id, req.body.bezeichnung, req.body.pfand_betrag]
); );
const [rows] = await pool.query(
const [newArtikel] = await pool.query('SELECT * FROM artikel WHERE id = ?', [result.insertId]); 'SELECT * FROM artikel WHERE tenant_id = ? ORDER BY erstellt_am DESC LIMIT 1',
res.status(201).json(newArtikel[0]); [req.user.tenant_id]
} catch (error) { );
console.error('Fehler beim Anlegen des Artikels:', error); res.status(201).json(rows[0]);
res.status(500).json({ error: 'Serverfehler' }); } catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
}
}); });
// PUT Artikel aktualisieren
router.put('/:id', [ router.put('/:id', [
body('bezeichnung').notEmpty().trim(), body('bezeichnung').notEmpty().trim(),
body('pfand_betrag').isFloat({ min: 0 }) body('pfand_betrag').isFloat({ min: 0 })
], async (req, res) => { ], async (req, res) => {
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) { if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
return res.status(400).json({ errors: errors.array() });
}
const { bezeichnung, pfand_betrag } = req.body;
try { try {
await pool.query( const [r] = await pool.query(
'UPDATE artikel SET bezeichnung = ?, pfand_betrag = ? WHERE id = ?', 'UPDATE artikel SET bezeichnung=?, pfand_betrag=? WHERE id=? AND tenant_id=?',
[bezeichnung, pfand_betrag, req.params.id] [req.body.bezeichnung, req.body.pfand_betrag, req.params.id, req.user.tenant_id]
); );
if (r.affectedRows === 0) return res.status(404).json({ error: 'Artikel nicht gefunden' });
const [updated] = await pool.query('SELECT * FROM artikel WHERE id = ?', [req.params.id]); const [rows] = await pool.query('SELECT * FROM artikel WHERE id=? AND tenant_id=?',
[req.params.id, req.user.tenant_id]);
if (updated.length === 0) { res.json(rows[0]);
return res.status(404).json({ error: 'Artikel nicht gefunden' }); } catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
}
res.json(updated[0]);
} catch (error) {
console.error('Fehler beim Aktualisieren des Artikels:', error);
res.status(500).json({ error: 'Serverfehler' });
}
}); });
// DELETE Artikel
router.delete('/:id', async (req, res) => { router.delete('/:id', async (req, res) => {
try { try {
const [result] = await pool.query('DELETE FROM artikel WHERE id = ?', [req.params.id]); const [r] = await pool.query('DELETE FROM artikel WHERE id=? AND tenant_id=?',
[req.params.id, req.user.tenant_id]);
if (result.affectedRows === 0) { if (r.affectedRows === 0) return res.status(404).json({ error: 'Artikel nicht gefunden' });
return res.status(404).json({ error: 'Artikel nicht gefunden' }); res.json({ message: 'Artikel geloescht' });
} } catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
res.json({ message: 'Artikel erfolgreich gelöscht' });
} catch (error) {
console.error('Fehler beim Löschen des Artikels:', error);
res.status(500).json({ error: 'Serverfehler' });
}
}); });
export default router; export default router;

View File

@@ -7,171 +7,117 @@ import { authenticateToken } from '../middleware/auth.js';
const router = express.Router(); const router = express.Router();
// Login 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', [ router.post('/login', [
body('email').isEmail().normalizeEmail(), body('email').isEmail().normalizeEmail(),
body('password').notEmpty() body('password').notEmpty()
], async (req, res) => { ], async (req, res) => {
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) { if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
return res.status(400).json({ errors: errors.array() });
}
const { email, password } = req.body; const { email, password, tenant_slug } = req.body;
try { try {
const [rows] = await pool.query( let rows;
'SELECT * FROM mitarbeiter WHERE email = ?', if (tenant_slug) {
[email] [rows] = await pool.query(
); `SELECT m.* FROM mitarbeiter m
JOIN tenants t ON m.tenant_id = t.id
if (rows.length === 0) { WHERE m.email = ? AND t.slug = ? AND m.aktiv = 1`,
return res.status(401).json({ error: 'Ungültige Anmeldedaten' }); [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]
);
} }
const mitarbeiter = rows[0]; if (rows.length === 0) return res.status(401).json({ error: 'Ungueltige Anmeldedaten' });
const validPassword = await bcrypt.compare(password, mitarbeiter.password_hash); const m = rows[0];
if (!validPassword) { const ok = await bcrypt.compare(password, m.password_hash);
return res.status(401).json({ error: 'Ungültige Anmeldedaten' }); if (!ok) return res.status(401).json({ error: 'Ungueltige Anmeldedaten' });
// Demo-Ablauf-Pruefung
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' });
}
} }
const token = jwt.sign(
{
id: mitarbeiter.id,
email: mitarbeiter.email,
rolle: mitarbeiter.rolle,
name: mitarbeiter.name
},
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({ res.json({
token, token: issueToken(m),
user: { user: {
id: mitarbeiter.id, id: m.id, email: m.email, name: m.name, rolle: m.rolle, tenant_id: m.tenant_id
email: mitarbeiter.email,
name: mitarbeiter.name,
rolle: mitarbeiter.rolle
} }
}); });
} catch (error) { } catch (e) {
console.error('Login-Fehler:', error); console.error('Login-Fehler:', e);
res.status(500).json({ error: 'Serverfehler beim Login' }); res.status(500).json({ error: 'Serverfehler beim Login' });
} }
}); });
// Register (nur für Admin) // GET /api/auth/me
router.post('/register', authenticateToken, [
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 6 }),
body('name').notEmpty(),
body('rolle').isIn(['user', 'admin'])
], async (req, res) => {
// Nur Admins dürfen neue Benutzer anlegen
if (req.user.rolle !== 'admin') {
return res.status(403).json({ error: 'Keine Berechtigung' });
}
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { email, password, name, rolle } = req.body;
try {
// Prüfe ob Email bereits existiert
const [existing] = await pool.query(
'SELECT id FROM mitarbeiter WHERE email = ?',
[email]
);
if (existing.length > 0) {
return res.status(400).json({ error: 'Email bereits registriert' });
}
const passwordHash = await bcrypt.hash(password, 10);
const [result] = await pool.query(
'INSERT INTO mitarbeiter (email, password_hash, name, rolle) VALUES (?, ?, ?, ?)',
[email, passwordHash, name, rolle]
);
res.status(201).json({
message: 'Mitarbeiter erfolgreich angelegt',
id: result.insertId
});
} catch (error) {
console.error('Registrierungs-Fehler:', error);
res.status(500).json({ error: 'Serverfehler bei der Registrierung' });
}
});
// Get current user
router.get('/me', authenticateToken, async (req, res) => { router.get('/me', authenticateToken, async (req, res) => {
try { try {
const [rows] = await pool.query( const [rows] = await pool.query(
'SELECT id, email, name, rolle, erstellt_am FROM mitarbeiter WHERE id = ?', `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] [req.user.id]
); );
if (rows.length === 0) return res.status(404).json({ error: 'Benutzer nicht gefunden' });
if (rows.length === 0) {
return res.status(404).json({ error: 'Benutzer nicht gefunden' });
}
res.json(rows[0]); res.json(rows[0]);
} catch (error) { } catch (e) {
console.error('Fehler beim Abrufen des Benutzers:', error); console.error('me-Fehler:', e);
res.status(500).json({ error: 'Serverfehler' }); res.status(500).json({ error: 'Serverfehler' });
} }
}); });
// Change password // POST /api/auth/change-password
router.post('/change-password', authenticateToken, [ router.post('/change-password', authenticateToken, [
body('currentPassword').notEmpty(), body('currentPassword').notEmpty(),
body('newPassword').isLength({ min: 6 }) body('newPassword').isLength({ min: 6 })
], async (req, res) => { ], async (req, res) => {
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) { if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
return res.status(400).json({ errors: errors.array() });
}
const { currentPassword, newPassword } = req.body;
try { try {
// Hole aktuellen Benutzer const [rows] = await pool.query('SELECT password_hash FROM mitarbeiter WHERE id = ?', [req.user.id]);
const [rows] = await pool.query( if (rows.length === 0) return res.status(404).json({ error: 'Benutzer nicht gefunden' });
'SELECT * FROM mitarbeiter WHERE id = ?', const ok = await bcrypt.compare(req.body.currentPassword, rows[0].password_hash);
[req.user.id] 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]);
if (rows.length === 0) { res.json({ message: 'Passwort erfolgreich geaendert' });
return res.status(404).json({ error: 'Benutzer nicht gefunden' }); } catch (e) {
} console.error('change-password-Fehler:', e);
res.status(500).json({ error: 'Serverfehler' });
const mitarbeiter = rows[0];
// Prüfe aktuelles Passwort
const validPassword = await bcrypt.compare(currentPassword, mitarbeiter.password_hash);
if (!validPassword) {
return res.status(401).json({ error: 'Aktuelles Passwort ist falsch' });
}
// Hash neues Passwort
const newPasswordHash = await bcrypt.hash(newPassword, 10);
// Update Passwort
await pool.query(
'UPDATE mitarbeiter SET password_hash = ? WHERE id = ?',
[newPasswordHash, req.user.id]
);
res.json({ message: 'Passwort erfolgreich geändert' });
} catch (error) {
console.error('Fehler beim Ändern des Passworts:', error);
res.status(500).json({ error: 'Serverfehler beim Ändern des Passworts' });
} }
}); });

110
backend/routes/geraete.js Normal file
View File

@@ -0,0 +1,110 @@
import express from 'express';
import { body, validationResult } from 'express-validator';
import pool from '../config/database.js';
import { authenticateToken, requireTenant } from '../middleware/auth.js';
const router = express.Router();
router.use(authenticateToken, requireTenant);
router.get('/', async (req, res) => {
try {
const [rows] = await pool.query(
`SELECT g.*, k.name AS kunde_name
FROM geraete g
LEFT JOIN kunden k ON g.kunde_id = k.id
WHERE g.tenant_id = ?
ORDER BY g.typ, g.seriennummer`,
[req.user.tenant_id]
);
res.json(rows);
} catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
});
router.get('/:id', async (req, res) => {
try {
const [rows] = await pool.query(
`SELECT g.*, k.name AS kunde_name FROM geraete g
LEFT JOIN kunden k ON g.kunde_id = k.id
WHERE g.id = ? AND g.tenant_id = ?`,
[req.params.id, req.user.tenant_id]
);
if (rows.length === 0) return res.status(404).json({ error: 'Geraet nicht gefunden' });
res.json(rows[0]);
} catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
});
router.post('/', [
body('seriennummer').notEmpty().trim(),
body('typ').notEmpty().trim(),
body('bezeichnung').optional({ checkFalsy: true }).trim(),
body('kunde_id').optional({ checkFalsy: true }),
body('status').optional().isIn(['aktiv', 'in_reparatur', 'ausgemustert', 'lager']),
body('notiz').optional({ checkFalsy: true }).trim()
], async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
const { seriennummer, typ, bezeichnung, kunde_id, status, notiz } = req.body;
try {
if (kunde_id) {
const [[k]] = await pool.query('SELECT id FROM kunden WHERE id=? AND tenant_id=?',
[kunde_id, req.user.tenant_id]);
if (!k) return res.status(400).json({ error: 'Kunde nicht im Tenant' });
}
await pool.query(
`INSERT INTO geraete (id, tenant_id, seriennummer, typ, bezeichnung, kunde_id, status, notiz)
VALUES (UUID(), ?, ?, ?, ?, ?, ?, ?)`,
[req.user.tenant_id, seriennummer, typ, bezeichnung || null,
kunde_id || null, status || 'aktiv', notiz || null]
);
const [rows] = await pool.query(
'SELECT * FROM geraete WHERE tenant_id = ? AND seriennummer = ?',
[req.user.tenant_id, seriennummer]
);
res.status(201).json(rows[0]);
} catch (e) {
if (e.code === 'ER_DUP_ENTRY') return res.status(400).json({ error: 'Seriennummer bereits vergeben' });
console.error('POST geraete:', e);
res.status(500).json({ error: 'Serverfehler' });
}
});
router.put('/:id', [
body('seriennummer').notEmpty().trim(),
body('typ').notEmpty().trim(),
body('status').optional().isIn(['aktiv', 'in_reparatur', 'ausgemustert', 'lager'])
], async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
const { seriennummer, typ, bezeichnung, kunde_id, status, notiz } = req.body;
try {
if (kunde_id) {
const [[k]] = await pool.query('SELECT id FROM kunden WHERE id=? AND tenant_id=?',
[kunde_id, req.user.tenant_id]);
if (!k) return res.status(400).json({ error: 'Kunde nicht im Tenant' });
}
const [r] = await pool.query(
`UPDATE geraete SET seriennummer=?, typ=?, bezeichnung=?, kunde_id=?, status=?, notiz=?
WHERE id=? AND tenant_id=?`,
[seriennummer, typ, bezeichnung || null, kunde_id || null,
status || 'aktiv', notiz || null, req.params.id, req.user.tenant_id]
);
if (r.affectedRows === 0) return res.status(404).json({ error: 'Geraet nicht gefunden' });
const [rows] = await pool.query('SELECT * FROM geraete WHERE id=? AND tenant_id=?',
[req.params.id, req.user.tenant_id]);
res.json(rows[0]);
} catch (e) {
if (e.code === 'ER_DUP_ENTRY') return res.status(400).json({ error: 'Seriennummer bereits vergeben' });
res.status(500).json({ error: 'Serverfehler' });
}
});
router.delete('/:id', async (req, res) => {
try {
const [r] = await pool.query('DELETE FROM geraete WHERE id=? AND tenant_id=?',
[req.params.id, req.user.tenant_id]);
if (r.affectedRows === 0) return res.status(404).json({ error: 'Geraet nicht gefunden' });
res.json({ message: 'Geraet geloescht' });
} catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
});
export default router;

View File

@@ -0,0 +1,84 @@
import express from 'express';
import fs from 'fs';
import path from 'path';
import pool from '../config/database.js';
import { authenticateToken, requireTenant } from '../middleware/auth.js';
import { upload } from '../middleware/upload.js';
const router = express.Router();
router.use(authenticateToken, requireTenant);
// POST /api/kunden-bilder/:kunde_id (Multipart, Feld: foto)
router.post('/:kunde_id', upload.single('foto'), async (req, res) => {
if (!req.file) return res.status(400).json({ error: 'Kein Bild hochgeladen' });
try {
const [[k]] = await pool.query('SELECT id FROM kunden WHERE id=? AND tenant_id=?',
[req.params.kunde_id, req.user.tenant_id]);
if (!k) {
// Datei aufraeumen
try { fs.unlinkSync(req.file.path); } catch {}
return res.status(404).json({ error: 'Kunde nicht gefunden' });
}
const [[c]] = await pool.query(
'SELECT COUNT(*) AS n FROM kunden_bilder WHERE kunde_id = ? AND tenant_id = ?',
[req.params.kunde_id, req.user.tenant_id]
);
if (c.n >= 5) {
try { fs.unlinkSync(req.file.path); } catch {}
return res.status(400).json({ error: 'Max. 5 Bilder pro Kunde' });
}
await pool.query(
`INSERT INTO kunden_bilder (id, tenant_id, kunde_id, dateiname, beschreibung, sortierung)
VALUES (UUID(), ?, ?, ?, ?, ?)`,
[req.user.tenant_id, req.params.kunde_id, `/uploads/${req.file.filename}`,
req.body.beschreibung || null, parseInt(req.body.sortierung) || c.n]
);
const [rows] = await pool.query(
`SELECT * FROM kunden_bilder WHERE kunde_id = ? AND tenant_id = ?
ORDER BY sortierung, erstellt_am`,
[req.params.kunde_id, req.user.tenant_id]
);
res.status(201).json(rows);
} catch (e) {
console.error('POST kunden-bilder:', e);
res.status(500).json({ error: 'Serverfehler' });
}
});
// PATCH /api/kunden-bilder/:id -> beschreibung / sortierung
router.patch('/bild/:id', async (req, res) => {
const updates = [];
const values = [];
if (req.body.beschreibung !== undefined) { updates.push('beschreibung = ?'); values.push(req.body.beschreibung); }
if (req.body.sortierung !== undefined) { updates.push('sortierung = ?'); values.push(parseInt(req.body.sortierung)); }
if (updates.length === 0) return res.status(400).json({ error: 'Keine Aenderungen' });
values.push(req.params.id, req.user.tenant_id);
try {
const [r] = await pool.query(
`UPDATE kunden_bilder SET ${updates.join(', ')} WHERE id = ? AND tenant_id = ?`,
values
);
if (r.affectedRows === 0) return res.status(404).json({ error: 'Bild nicht gefunden' });
res.json({ message: 'Aktualisiert' });
} catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
});
// DELETE /api/kunden-bilder/bild/:id
router.delete('/bild/:id', async (req, res) => {
try {
const [rows] = await pool.query(
'SELECT dateiname FROM kunden_bilder WHERE id = ? AND tenant_id = ?',
[req.params.id, req.user.tenant_id]
);
if (rows.length === 0) return res.status(404).json({ error: 'Bild nicht gefunden' });
await pool.query('DELETE FROM kunden_bilder WHERE id = ? AND tenant_id = ?',
[req.params.id, req.user.tenant_id]);
// Datei loeschen (best effort)
const filename = rows[0].dateiname.replace(/^\/uploads\//, '');
const fullPath = path.join(process.env.UPLOAD_DIR || '/app/uploads', filename);
try { fs.unlinkSync(fullPath); } catch {}
res.json({ message: 'Bild geloescht' });
} catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
});
export default router;

View File

@@ -1,126 +1,100 @@
import express from 'express'; import express from 'express';
import { body, validationResult } from 'express-validator'; import { body, validationResult } from 'express-validator';
import pool from '../config/database.js'; import pool from '../config/database.js';
import { authenticateToken } from '../middleware/auth.js'; import { authenticateToken, requireTenant } from '../middleware/auth.js';
const router = express.Router(); const router = express.Router();
router.use(authenticateToken, requireTenant);
// Alle Routen erfordern Authentifizierung
router.use(authenticateToken);
// GET alle Kunden
router.get('/', async (req, res) => { router.get('/', async (req, res) => {
try { try {
const [rows] = await pool.query( const [rows] = await pool.query(
'SELECT * FROM kunden ORDER BY name ASC' 'SELECT * FROM kunden WHERE tenant_id = ? ORDER BY name ASC',
[req.user.tenant_id]
); );
res.json(rows); res.json(rows);
} catch (error) { } catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
console.error('Fehler beim Abrufen der Kunden:', error);
res.status(500).json({ error: 'Serverfehler' });
}
}); });
// GET einzelner Kunde
router.get('/:id', async (req, res) => { router.get('/:id', async (req, res) => {
try { try {
const [rows] = await pool.query( const [rows] = await pool.query(
'SELECT * FROM kunden WHERE id = ?', 'SELECT * FROM kunden WHERE id = ? AND tenant_id = ?',
[req.params.id] [req.params.id, req.user.tenant_id]
); );
if (rows.length === 0) return res.status(404).json({ error: 'Kunde nicht gefunden' });
if (rows.length === 0) { const [bilder] = await pool.query(
return res.status(404).json({ error: 'Kunde nicht gefunden' }); 'SELECT id, dateiname, beschreibung, sortierung FROM kunden_bilder WHERE kunde_id = ? AND tenant_id = ? ORDER BY sortierung, erstellt_am',
} [req.params.id, req.user.tenant_id]
);
res.json(rows[0]); res.json({ ...rows[0], bilder });
} catch (error) { } catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
console.error('Fehler beim Abrufen des Kunden:', error);
res.status(500).json({ error: 'Serverfehler' });
}
}); });
// POST neuer Kunde
router.post('/', [ router.post('/', [
body('name').notEmpty().trim(), body('name').notEmpty().trim(),
body('email').optional().isEmail().normalizeEmail(), body('email').optional({ checkFalsy: true }).isEmail().normalizeEmail(),
body('telefon').optional().trim(), body('lieferzeit_bis').optional({ checkFalsy: true }).matches(/^\d{1,2}:\d{2}$/),
body('adresse').optional().trim(), body('anrede').optional({ checkFalsy: true }).isIn(['Herr', 'Frau', 'Firma'])
body('ansprechpartner').optional().trim(),
body('notiz').optional().trim(),
body('anrede').optional().isIn(['Herr', 'Frau', 'Firma', ''])
], async (req, res) => { ], async (req, res) => {
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) { if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
return res.status(400).json({ errors: errors.array() }); const { name, email, telefon, adresse, ansprechpartner, notiz, anrede,
} anlieferungshinweis, lieferzeit_bis } = req.body;
const { name, email, telefon, adresse, ansprechpartner, notiz, anrede } = req.body;
try { try {
const [result] = await pool.query( const [result] = await pool.query(
'INSERT INTO kunden (name, email, telefon, adresse, ansprechpartner, notiz, anrede) VALUES (?, ?, ?, ?, ?, ?, ?)', `INSERT INTO kunden (id, tenant_id, name, email, telefon, adresse, ansprechpartner, notiz, anrede,
[name, email || null, telefon || null, adresse || null, ansprechpartner || null, notiz || null, anrede || null] anlieferungshinweis, lieferzeit_bis)
VALUES (UUID(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[req.user.tenant_id, name, email || null, telefon || null, adresse || null,
ansprechpartner || null, notiz || null, anrede || null,
anlieferungshinweis || null, lieferzeit_bis || null]
); );
// Re-fetch via tenant + name (id ist UUID, neuer Wert kennen wir nicht direkt)
const [newKunde] = await pool.query('SELECT * FROM kunden WHERE id = ?', [result.insertId]); const [rows] = await pool.query(
res.status(201).json(newKunde[0]); 'SELECT * FROM kunden WHERE tenant_id = ? ORDER BY erstellt_am DESC LIMIT 1',
} catch (error) { [req.user.tenant_id]
console.error('Fehler beim Anlegen des Kunden:', error); );
res.status(201).json(rows[0]);
} catch (e) {
console.error('POST kunden:', e);
res.status(500).json({ error: 'Serverfehler' }); res.status(500).json({ error: 'Serverfehler' });
} }
}); });
// PUT Kunde aktualisieren
router.put('/:id', [ router.put('/:id', [
body('name').notEmpty().trim(), body('name').notEmpty().trim(),
body('email').optional().isEmail().normalizeEmail(), body('email').optional({ checkFalsy: true }).isEmail().normalizeEmail(),
body('telefon').optional().trim(), body('lieferzeit_bis').optional({ checkFalsy: true }).matches(/^\d{1,2}:\d{2}$/),
body('adresse').optional().trim(), body('anrede').optional({ checkFalsy: true }).isIn(['Herr', 'Frau', 'Firma'])
body('ansprechpartner').optional().trim(),
body('notiz').optional().trim(),
body('anrede').optional().isIn(['Herr', 'Frau', 'Firma', ''])
], async (req, res) => { ], async (req, res) => {
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) { if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
return res.status(400).json({ errors: errors.array() }); const { name, email, telefon, adresse, ansprechpartner, notiz, anrede,
} anlieferungshinweis, lieferzeit_bis } = req.body;
const { name, email, telefon, adresse, ansprechpartner, notiz, anrede } = req.body;
try { try {
await pool.query( const [r] = await pool.query(
'UPDATE kunden SET name = ?, email = ?, telefon = ?, adresse = ?, ansprechpartner = ?, notiz = ?, anrede = ? WHERE id = ?', `UPDATE kunden SET name=?, email=?, telefon=?, adresse=?, ansprechpartner=?, notiz=?, anrede=?,
[name, email || null, telefon || null, adresse || null, ansprechpartner || null, notiz || null, anrede || null, req.params.id] anlieferungshinweis=?, lieferzeit_bis=?
WHERE id=? AND tenant_id=?`,
[name, email || null, telefon || null, adresse || null, ansprechpartner || null,
notiz || null, anrede || null, anlieferungshinweis || null, lieferzeit_bis || null,
req.params.id, req.user.tenant_id]
); );
if (r.affectedRows === 0) return res.status(404).json({ error: 'Kunde nicht gefunden' });
const [updated] = await pool.query('SELECT * FROM kunden WHERE id = ?', [req.params.id]); const [rows] = await pool.query('SELECT * FROM kunden WHERE id = ? AND tenant_id = ?',
[req.params.id, req.user.tenant_id]);
if (updated.length === 0) { res.json(rows[0]);
return res.status(404).json({ error: 'Kunde nicht gefunden' }); } catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
}
res.json(updated[0]);
} catch (error) {
console.error('Fehler beim Aktualisieren des Kunden:', error);
res.status(500).json({ error: 'Serverfehler' });
}
}); });
// DELETE Kunde
router.delete('/:id', async (req, res) => { router.delete('/:id', async (req, res) => {
try { try {
const [result] = await pool.query('DELETE FROM kunden WHERE id = ?', [req.params.id]); const [r] = await pool.query('DELETE FROM kunden WHERE id=? AND tenant_id=?',
[req.params.id, req.user.tenant_id]);
if (result.affectedRows === 0) { if (r.affectedRows === 0) return res.status(404).json({ error: 'Kunde nicht gefunden' });
return res.status(404).json({ error: 'Kunde nicht gefunden' }); res.json({ message: 'Kunde geloescht' });
} } catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
res.json({ message: 'Kunde erfolgreich gelöscht' });
} catch (error) {
console.error('Fehler beim Löschen des Kunden:', error);
res.status(500).json({ error: 'Serverfehler' });
}
}); });
export default router; export default router;

View File

@@ -1,116 +1,77 @@
import express from 'express'; import express from 'express';
import { body, validationResult } from 'express-validator'; import { body, validationResult } from 'express-validator';
import pool from '../config/database.js'; import pool from '../config/database.js';
import { authenticateToken } from '../middleware/auth.js'; import { authenticateToken, requireTenant } from '../middleware/auth.js';
import { upload } from '../middleware/upload.js'; import { upload } from '../middleware/upload.js';
const router = express.Router(); const router = express.Router();
router.use(authenticateToken, requireTenant);
// Alle Routen erfordern Authentifizierung const baseSelect = `
router.use(authenticateToken); SELECT l.*, k.name AS kunde_name, a.bezeichnung AS artikel_bezeichnung,
a.pfand_betrag, m.name AS mitarbeiter_name
FROM lieferungen l
LEFT JOIN kunden k ON l.kunde_id = k.id
LEFT JOIN artikel a ON l.artikel_id = a.id
LEFT JOIN mitarbeiter m ON l.mitarbeiter_id = m.id`;
// GET alle Lieferungen
router.get('/', async (req, res) => { router.get('/', async (req, res) => {
try { try {
const [rows] = await pool.query(` const [rows] = await pool.query(
SELECT `${baseSelect} WHERE l.tenant_id = ? ORDER BY l.erstellt_am DESC`,
l.*, [req.user.tenant_id]
k.name as kunde_name, );
a.bezeichnung as artikel_bezeichnung,
a.pfand_betrag,
m.name as mitarbeiter_name
FROM lieferungen l
LEFT JOIN kunden k ON l.kunde_id = k.id
LEFT JOIN artikel a ON l.artikel_id = a.id
LEFT JOIN mitarbeiter m ON l.mitarbeiter_id = m.id
ORDER BY l.erstellt_am DESC
`);
res.json(rows); res.json(rows);
} catch (error) { } catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
console.error('Fehler beim Abrufen der Lieferungen:', error);
res.status(500).json({ error: 'Serverfehler' });
}
}); });
// GET Lieferungen nach Kunde
router.get('/kunde/:kunde_id', async (req, res) => { router.get('/kunde/:kunde_id', async (req, res) => {
try { try {
const [rows] = await pool.query(` const [rows] = await pool.query(
SELECT `${baseSelect} WHERE l.tenant_id = ? AND l.kunde_id = ? ORDER BY l.erstellt_am DESC`,
l.*, [req.user.tenant_id, req.params.kunde_id]
k.name as kunde_name, );
a.bezeichnung as artikel_bezeichnung,
a.pfand_betrag,
m.name as mitarbeiter_name
FROM lieferungen l
LEFT JOIN kunden k ON l.kunde_id = k.id
LEFT JOIN artikel a ON l.artikel_id = a.id
LEFT JOIN mitarbeiter m ON l.mitarbeiter_id = m.id
WHERE l.kunde_id = ?
ORDER BY l.erstellt_am DESC
`, [req.params.kunde_id]);
res.json(rows); res.json(rows);
} catch (error) { } catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
console.error('Fehler beim Abrufen der Lieferungen:', error);
res.status(500).json({ error: 'Serverfehler' });
}
}); });
// POST neue Lieferung (mit optionalem Foto)
router.post('/', upload.single('foto'), [ router.post('/', upload.single('foto'), [
body('kunde_id').notEmpty(), body('kunde_id').notEmpty(),
body('artikel_id').notEmpty(), body('artikel_id').notEmpty(),
body('anzahl').isInt({ min: 1 }) body('anzahl').isInt({ min: 1 })
], async (req, res) => { ], async (req, res) => {
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) { if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
return res.status(400).json({ errors: errors.array() });
}
const { kunde_id, artikel_id, anzahl } = req.body;
const foto_url = req.file ? `/uploads/${req.file.filename}` : null; const foto_url = req.file ? `/uploads/${req.file.filename}` : null;
try { try {
// Sicherstellen: kunde_id + artikel_id gehoeren dem Tenant
const [[k]] = await pool.query('SELECT id FROM kunden WHERE id=? AND tenant_id=?',
[req.body.kunde_id, req.user.tenant_id]);
const [[a]] = await pool.query('SELECT id FROM artikel WHERE id=? AND tenant_id=?',
[req.body.artikel_id, req.user.tenant_id]);
if (!k || !a) return res.status(400).json({ error: 'Kunde oder Artikel nicht im Tenant' });
const [result] = await pool.query( const [result] = await pool.query(
'INSERT INTO lieferungen (kunde_id, artikel_id, anzahl, foto_url, mitarbeiter_id) VALUES (?, ?, ?, ?, ?)', `INSERT INTO lieferungen (tenant_id, kunde_id, artikel_id, anzahl, foto_url, mitarbeiter_id)
[kunde_id, artikel_id, anzahl, foto_url, req.user.id] VALUES (?, ?, ?, ?, ?, ?)`,
[req.user.tenant_id, req.body.kunde_id, req.body.artikel_id, req.body.anzahl,
foto_url, req.user.id]
); );
const [rows] = await pool.query(`${baseSelect} WHERE l.id = ?`, [result.insertId]);
const [newLieferung] = await pool.query(` res.status(201).json(rows[0]);
SELECT } catch (e) {
l.*, console.error('POST lieferungen:', e);
k.name as kunde_name,
a.bezeichnung as artikel_bezeichnung,
a.pfand_betrag,
m.name as mitarbeiter_name
FROM lieferungen l
LEFT JOIN kunden k ON l.kunde_id = k.id
LEFT JOIN artikel a ON l.artikel_id = a.id
LEFT JOIN mitarbeiter m ON l.mitarbeiter_id = m.id
WHERE l.id = ?
`, [result.insertId]);
res.status(201).json(newLieferung[0]);
} catch (error) {
console.error('Fehler beim Anlegen der Lieferung:', error);
res.status(500).json({ error: 'Serverfehler' }); res.status(500).json({ error: 'Serverfehler' });
} }
}); });
// DELETE Lieferung
router.delete('/:id', async (req, res) => { router.delete('/:id', async (req, res) => {
try { try {
const [result] = await pool.query('DELETE FROM lieferungen WHERE id = ?', [req.params.id]); const [r] = await pool.query('DELETE FROM lieferungen WHERE id=? AND tenant_id=?',
[req.params.id, req.user.tenant_id]);
if (result.affectedRows === 0) { if (r.affectedRows === 0) return res.status(404).json({ error: 'Lieferung nicht gefunden' });
return res.status(404).json({ error: 'Lieferung nicht gefunden' }); res.json({ message: 'Lieferung geloescht' });
} } catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
res.json({ message: 'Lieferung erfolgreich gelöscht' });
} catch (error) {
console.error('Fehler beim Löschen der Lieferung:', error);
res.status(500).json({ error: 'Serverfehler' });
}
}); });
export default router; export default router;

View File

@@ -1,126 +1,102 @@
import express from 'express'; import express from 'express';
import bcrypt from 'bcrypt';
import { body, validationResult } from 'express-validator'; import { body, validationResult } from 'express-validator';
import pool from '../config/database.js'; import pool from '../config/database.js';
import { authenticateToken, requireRole } from '../middleware/auth.js'; import { authenticateToken, requireTenant, requireRole } from '../middleware/auth.js';
const router = express.Router(); const router = express.Router();
router.use(authenticateToken, requireTenant);
// Alle Routen erfordern Authentifizierung router.get('/', requireRole(['admin', 'superadmin']), async (req, res) => {
router.use(authenticateToken);
// GET alle Mitarbeiter (nur Admin)
router.get('/', requireRole(['admin']), async (req, res) => {
try { try {
const [rows] = await pool.query( const [rows] = await pool.query(
'SELECT id, email, name, rolle, erstellt_am FROM mitarbeiter ORDER BY name ASC' `SELECT id, email, name, rolle, aktiv, erstellt_am FROM mitarbeiter
WHERE tenant_id = ? ORDER BY name ASC`,
[req.user.tenant_id]
); );
res.json(rows); res.json(rows);
} catch (error) { } catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
console.error('Fehler beim Abrufen der Mitarbeiter:', error);
res.status(500).json({ error: 'Serverfehler' });
}
}); });
// GET einzelner Mitarbeiter router.post('/', requireRole(['admin']), [
router.get('/:id', async (req, res) => { body('email').isEmail().normalizeEmail(),
try { body('password').isLength({ min: 6 }),
const [rows] = await pool.query( body('name').notEmpty().trim(),
'SELECT id, email, name, rolle, erstellt_am FROM mitarbeiter WHERE id = ?', body('rolle').isIn(['user', 'admin', 'fahrer'])
[req.params.id]
);
if (rows.length === 0) {
return res.status(404).json({ error: 'Mitarbeiter nicht gefunden' });
}
res.json(rows[0]);
} catch (error) {
console.error('Fehler beim Abrufen des Mitarbeiters:', error);
res.status(500).json({ error: 'Serverfehler' });
}
});
// PUT Mitarbeiter aktualisieren (nur Admin oder eigener Account)
router.put('/:id', [
body('name').optional().trim(),
body('rolle').optional().isIn(['user', 'admin'])
], async (req, res) => { ], async (req, res) => {
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) { if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
return res.status(400).json({ errors: errors.array() }); try {
} // Pruefe Limit
const [[t]] = await pool.query('SELECT max_users FROM tenants WHERE id = ?', [req.user.tenant_id]);
const [[c]] = await pool.query('SELECT COUNT(*) AS n FROM mitarbeiter WHERE tenant_id = ?',
[req.user.tenant_id]);
if (c.n >= t.max_users) return res.status(403).json({ error: `Limit erreicht (${t.max_users} User)` });
const [[ex]] = await pool.query(
'SELECT id FROM mitarbeiter WHERE email = ? AND tenant_id = ?',
[req.body.email, req.user.tenant_id]
);
if (ex) return res.status(400).json({ error: 'Email bereits vergeben' });
const hash = await bcrypt.hash(req.body.password, 10);
await pool.query(
`INSERT INTO mitarbeiter (id, tenant_id, email, password_hash, name, rolle, aktiv)
VALUES (UUID(), ?, ?, ?, ?, ?, 1)`,
[req.user.tenant_id, req.body.email, hash, req.body.name, req.body.rolle]
);
res.status(201).json({ message: 'Mitarbeiter angelegt' });
} catch (e) {
console.error('POST mitarbeiter:', e);
res.status(500).json({ error: 'Serverfehler' });
}
});
router.put('/:id', [
body('name').optional().trim(),
body('rolle').optional().isIn(['user', 'admin', 'fahrer']),
body('aktiv').optional().isBoolean()
], async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
// Nur Admin darf andere Mitarbeiter bearbeiten oder Rollen ändern
if (req.params.id !== req.user.id && req.user.rolle !== 'admin') { if (req.params.id !== req.user.id && req.user.rolle !== 'admin') {
return res.status(403).json({ error: 'Keine Berechtigung' }); return res.status(403).json({ error: 'Keine Berechtigung' });
} }
if ((req.body.rolle || req.body.aktiv !== undefined) && req.user.rolle !== 'admin') {
// Nur Admin darf Rollen ändern return res.status(403).json({ error: 'Nur Admin darf Rolle/Aktiv aendern' });
if (req.body.rolle && req.user.rolle !== 'admin') {
return res.status(403).json({ error: 'Keine Berechtigung zum Ändern der Rolle' });
} }
const { name, rolle } = req.body;
const updates = []; const updates = [];
const values = []; const values = [];
if (req.body.name) { updates.push('name = ?'); values.push(req.body.name); }
if (name) { if (req.body.rolle) { updates.push('rolle = ?'); values.push(req.body.rolle); }
updates.push('name = ?'); if (req.body.aktiv !== undefined) { updates.push('aktiv = ?'); values.push(req.body.aktiv ? 1 : 0); }
values.push(name); if (updates.length === 0) return res.status(400).json({ error: 'Keine Aenderungen' });
} values.push(req.params.id, req.user.tenant_id);
if (rolle && req.user.rolle === 'admin') {
updates.push('rolle = ?');
values.push(rolle);
}
if (updates.length === 0) {
return res.status(400).json({ error: 'Keine Änderungen angegeben' });
}
values.push(req.params.id);
try { try {
await pool.query( const [r] = await pool.query(
`UPDATE mitarbeiter SET ${updates.join(', ')} WHERE id = ?`, `UPDATE mitarbeiter SET ${updates.join(', ')} WHERE id = ? AND tenant_id = ?`,
values values
); );
if (r.affectedRows === 0) return res.status(404).json({ error: 'Mitarbeiter nicht gefunden' });
const [updated] = await pool.query( const [rows] = await pool.query(
'SELECT id, email, name, rolle, erstellt_am FROM mitarbeiter WHERE id = ?', 'SELECT id, email, name, rolle, aktiv FROM mitarbeiter WHERE id = ? AND tenant_id = ?',
[req.params.id] [req.params.id, req.user.tenant_id]
); );
res.json(rows[0]);
if (updated.length === 0) { } catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
return res.status(404).json({ error: 'Mitarbeiter nicht gefunden' });
}
res.json(updated[0]);
} catch (error) {
console.error('Fehler beim Aktualisieren des Mitarbeiters:', error);
res.status(500).json({ error: 'Serverfehler' });
}
}); });
// DELETE Mitarbeiter (nur Admin)
router.delete('/:id', requireRole(['admin']), async (req, res) => { router.delete('/:id', requireRole(['admin']), async (req, res) => {
// Verhindere Selbstlöschung if (req.params.id === req.user.id) return res.status(400).json({ error: 'Sie koennen sich nicht selbst loeschen' });
if (req.params.id === req.user.id) {
return res.status(400).json({ error: 'Sie können sich nicht selbst löschen' });
}
try { try {
const [result] = await pool.query('DELETE FROM mitarbeiter WHERE id = ?', [req.params.id]); const [r] = await pool.query('DELETE FROM mitarbeiter WHERE id=? AND tenant_id=?',
[req.params.id, req.user.tenant_id]);
if (result.affectedRows === 0) { if (r.affectedRows === 0) return res.status(404).json({ error: 'Mitarbeiter nicht gefunden' });
return res.status(404).json({ error: 'Mitarbeiter nicht gefunden' }); res.json({ message: 'Mitarbeiter geloescht' });
} } catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
res.json({ message: 'Mitarbeiter erfolgreich gelöscht' });
} catch (error) {
console.error('Fehler beim Löschen des Mitarbeiters:', error);
res.status(500).json({ error: 'Serverfehler' });
}
}); });
export default router; export default router;

View File

@@ -1,116 +1,76 @@
import express from 'express'; import express from 'express';
import { body, validationResult } from 'express-validator'; import { body, validationResult } from 'express-validator';
import pool from '../config/database.js'; import pool from '../config/database.js';
import { authenticateToken } from '../middleware/auth.js'; import { authenticateToken, requireTenant } from '../middleware/auth.js';
import { upload } from '../middleware/upload.js'; import { upload } from '../middleware/upload.js';
const router = express.Router(); const router = express.Router();
router.use(authenticateToken, requireTenant);
// Alle Routen erfordern Authentifizierung const baseSelect = `
router.use(authenticateToken); SELECT r.*, k.name AS kunde_name, a.bezeichnung AS artikel_bezeichnung,
a.pfand_betrag, m.name AS mitarbeiter_name
FROM ruecknahmen r
LEFT JOIN kunden k ON r.kunde_id = k.id
LEFT JOIN artikel a ON r.artikel_id = a.id
LEFT JOIN mitarbeiter m ON r.mitarbeiter_id = m.id`;
// GET alle Rücknahmen
router.get('/', async (req, res) => { router.get('/', async (req, res) => {
try { try {
const [rows] = await pool.query(` const [rows] = await pool.query(
SELECT `${baseSelect} WHERE r.tenant_id = ? ORDER BY r.erstellt_am DESC`,
r.*, [req.user.tenant_id]
k.name as kunde_name, );
a.bezeichnung as artikel_bezeichnung,
a.pfand_betrag,
m.name as mitarbeiter_name
FROM ruecknahmen r
LEFT JOIN kunden k ON r.kunde_id = k.id
LEFT JOIN artikel a ON r.artikel_id = a.id
LEFT JOIN mitarbeiter m ON r.mitarbeiter_id = m.id
ORDER BY r.erstellt_am DESC
`);
res.json(rows); res.json(rows);
} catch (error) { } catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
console.error('Fehler beim Abrufen der Rücknahmen:', error);
res.status(500).json({ error: 'Serverfehler' });
}
}); });
// GET Rücknahmen nach Kunde
router.get('/kunde/:kunde_id', async (req, res) => { router.get('/kunde/:kunde_id', async (req, res) => {
try { try {
const [rows] = await pool.query(` const [rows] = await pool.query(
SELECT `${baseSelect} WHERE r.tenant_id = ? AND r.kunde_id = ? ORDER BY r.erstellt_am DESC`,
r.*, [req.user.tenant_id, req.params.kunde_id]
k.name as kunde_name, );
a.bezeichnung as artikel_bezeichnung,
a.pfand_betrag,
m.name as mitarbeiter_name
FROM ruecknahmen r
LEFT JOIN kunden k ON r.kunde_id = k.id
LEFT JOIN artikel a ON r.artikel_id = a.id
LEFT JOIN mitarbeiter m ON r.mitarbeiter_id = m.id
WHERE r.kunde_id = ?
ORDER BY r.erstellt_am DESC
`, [req.params.kunde_id]);
res.json(rows); res.json(rows);
} catch (error) { } catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
console.error('Fehler beim Abrufen der Rücknahmen:', error);
res.status(500).json({ error: 'Serverfehler' });
}
}); });
// POST neue Rücknahme (mit optionalem Foto)
router.post('/', upload.single('foto'), [ router.post('/', upload.single('foto'), [
body('kunde_id').notEmpty(), body('kunde_id').notEmpty(),
body('artikel_id').notEmpty(), body('artikel_id').notEmpty(),
body('anzahl').isInt({ min: 1 }) body('anzahl').isInt({ min: 1 })
], async (req, res) => { ], async (req, res) => {
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) { if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
return res.status(400).json({ errors: errors.array() });
}
const { kunde_id, artikel_id, anzahl } = req.body;
const foto_url = req.file ? `/uploads/${req.file.filename}` : null; const foto_url = req.file ? `/uploads/${req.file.filename}` : null;
try { try {
const [[k]] = await pool.query('SELECT id FROM kunden WHERE id=? AND tenant_id=?',
[req.body.kunde_id, req.user.tenant_id]);
const [[a]] = await pool.query('SELECT id FROM artikel WHERE id=? AND tenant_id=?',
[req.body.artikel_id, req.user.tenant_id]);
if (!k || !a) return res.status(400).json({ error: 'Kunde oder Artikel nicht im Tenant' });
const [result] = await pool.query( const [result] = await pool.query(
'INSERT INTO ruecknahmen (kunde_id, artikel_id, anzahl, foto_url, mitarbeiter_id) VALUES (?, ?, ?, ?, ?)', `INSERT INTO ruecknahmen (tenant_id, kunde_id, artikel_id, anzahl, foto_url, mitarbeiter_id)
[kunde_id, artikel_id, anzahl, foto_url, req.user.id] VALUES (?, ?, ?, ?, ?, ?)`,
[req.user.tenant_id, req.body.kunde_id, req.body.artikel_id, req.body.anzahl,
foto_url, req.user.id]
); );
const [rows] = await pool.query(`${baseSelect} WHERE r.id = ?`, [result.insertId]);
const [newRuecknahme] = await pool.query(` res.status(201).json(rows[0]);
SELECT } catch (e) {
r.*, console.error('POST ruecknahmen:', e);
k.name as kunde_name,
a.bezeichnung as artikel_bezeichnung,
a.pfand_betrag,
m.name as mitarbeiter_name
FROM ruecknahmen r
LEFT JOIN kunden k ON r.kunde_id = k.id
LEFT JOIN artikel a ON r.artikel_id = a.id
LEFT JOIN mitarbeiter m ON r.mitarbeiter_id = m.id
WHERE r.id = ?
`, [result.insertId]);
res.status(201).json(newRuecknahme[0]);
} catch (error) {
console.error('Fehler beim Anlegen der Rücknahme:', error);
res.status(500).json({ error: 'Serverfehler' }); res.status(500).json({ error: 'Serverfehler' });
} }
}); });
// DELETE Rücknahme
router.delete('/:id', async (req, res) => { router.delete('/:id', async (req, res) => {
try { try {
const [result] = await pool.query('DELETE FROM ruecknahmen WHERE id = ?', [req.params.id]); const [r] = await pool.query('DELETE FROM ruecknahmen WHERE id=? AND tenant_id=?',
[req.params.id, req.user.tenant_id]);
if (result.affectedRows === 0) { if (r.affectedRows === 0) return res.status(404).json({ error: 'Ruecknahme nicht gefunden' });
return res.status(404).json({ error: 'Rücknahme nicht gefunden' }); res.json({ message: 'Ruecknahme geloescht' });
} } catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
res.json({ message: 'Rücknahme erfolgreich gelöscht' });
} catch (error) {
console.error('Fehler beim Löschen der Rücknahme:', error);
res.status(500).json({ error: 'Serverfehler' });
}
}); });
export default router; export default router;

140
backend/routes/tenants.js Normal file
View File

@@ -0,0 +1,140 @@
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 zurueckgegeben, 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 aendern, Limits aendern
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 Aenderungen' });
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 loeschen (CASCADE)
router.delete('/:id', async (req, res) => {
try {
await pool.query('DELETE FROM tenants WHERE id = ?', [req.params.id]);
res.json({ message: 'Tenant geloescht' });
} catch (e) {
res.status(500).json({ error: 'Serverfehler' });
}
});
export default router;

38
backend/scripts/bootstrap.js vendored Normal file
View File

@@ -0,0 +1,38 @@
import bcrypt from 'bcrypt';
import pool from '../config/database.js';
// Stellt sicher dass mindestens ein SuperAdmin existiert.
// Nutzt env SUPERADMIN_EMAIL + SUPERADMIN_PASSWORD beim ersten Start.
export async function bootstrapSuperAdmin() {
const email = process.env.SUPERADMIN_EMAIL;
const password = process.env.SUPERADMIN_PASSWORD;
if (!email || !password) {
console.warn('[bootstrap] SUPERADMIN_EMAIL/PASSWORD nicht gesetzt - skip');
return;
}
// Auf Datenbank-Bereitschaft warten (max 30s)
for (let i = 0; i < 30; i++) {
try {
await pool.query('SELECT 1');
break;
} catch {
await new Promise(r => setTimeout(r, 1000));
}
}
const [rows] = await pool.query(
"SELECT id FROM mitarbeiter WHERE email = ? AND tenant_id IS NULL AND rolle = 'superadmin'",
[email]
);
if (rows.length > 0) {
console.log('[bootstrap] SuperAdmin existiert:', email);
return;
}
const hash = await bcrypt.hash(password, 10);
await pool.query(
`INSERT INTO mitarbeiter (id, tenant_id, email, password_hash, name, rolle, aktiv)
VALUES (UUID(), NULL, ?, ?, 'SuperAdmin', 'superadmin', 1)`,
[email, hash]
);
console.log('[bootstrap] SuperAdmin angelegt:', email);
}

View File

@@ -4,13 +4,17 @@ import dotenv from 'dotenv';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
// Routes
import authRoutes from './routes/auth.js'; import authRoutes from './routes/auth.js';
import tenantRoutes from './routes/tenants.js';
import kundenRoutes from './routes/kunden.js'; import kundenRoutes from './routes/kunden.js';
import kundenBilderRoutes from './routes/kunden-bilder.js';
import artikelRoutes from './routes/artikel.js'; import artikelRoutes from './routes/artikel.js';
import lieferungenRoutes from './routes/lieferungen.js'; import lieferungenRoutes from './routes/lieferungen.js';
import ruecknahmenRoutes from './routes/ruecknahmen.js'; import ruecknahmenRoutes from './routes/ruecknahmen.js';
import mitarbeiterRoutes from './routes/mitarbeiter.js'; import mitarbeiterRoutes from './routes/mitarbeiter.js';
import geraeteRoutes from './routes/geraete.js';
import { bootstrapSuperAdmin } from './scripts/bootstrap.js';
dotenv.config(); dotenv.config();
@@ -20,53 +24,41 @@ const __dirname = path.dirname(__filename);
const app = express(); const app = express();
const PORT = process.env.PORT || 3001; const PORT = process.env.PORT || 3001;
// Middleware
app.use(cors()); app.use(cors());
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.use('/uploads', express.static(process.env.UPLOAD_DIR || path.join(__dirname, 'uploads')));
// Statische Dateien (Uploads) app.get('/health', (req, res) => res.json({ status: 'OK', timestamp: new Date().toISOString() }));
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
// Health Check
app.get('/health', (req, res) => {
res.json({ status: 'OK', timestamp: new Date().toISOString() });
});
// API Routes
app.use('/api/auth', authRoutes); app.use('/api/auth', authRoutes);
app.use('/api/tenants', tenantRoutes);
app.use('/api/kunden', kundenRoutes); app.use('/api/kunden', kundenRoutes);
app.use('/api/kunden-bilder', kundenBilderRoutes);
app.use('/api/artikel', artikelRoutes); app.use('/api/artikel', artikelRoutes);
app.use('/api/lieferungen', lieferungenRoutes); app.use('/api/lieferungen', lieferungenRoutes);
app.use('/api/ruecknahmen', ruecknahmenRoutes); app.use('/api/ruecknahmen', ruecknahmenRoutes);
app.use('/api/mitarbeiter', mitarbeiterRoutes); app.use('/api/mitarbeiter', mitarbeiterRoutes);
app.use('/api/geraete', geraeteRoutes);
// Error Handler
app.use((err, req, res, next) => { app.use((err, req, res, next) => {
console.error('Fehler:', err); console.error('Fehler:', err);
if (err.name === 'MulterError') { if (err.name === 'MulterError') {
if (err.code === 'LIMIT_FILE_SIZE') { if (err.code === 'LIMIT_FILE_SIZE') return res.status(400).json({ error: 'Datei zu gross (max. 10MB)' });
return res.status(400).json({ error: 'Datei zu groß (max. 10MB)' });
}
return res.status(400).json({ error: err.message }); return res.status(400).json({ error: err.message });
} }
res.status(err.status || 500).json({ error: err.message || 'Interner Serverfehler' });
});
res.status(err.status || 500).json({ app.use((req, res) => res.status(404).json({ error: 'Route nicht gefunden' }));
error: err.message || 'Interner Serverfehler'
// Bootstrap SuperAdmin beim Start, dann Server starten
bootstrapSuperAdmin()
.catch(err => console.error('Bootstrap-Fehler:', err))
.finally(() => {
app.listen(PORT, '0.0.0.0', () => {
console.log(`Backend laeuft auf Port ${PORT}`);
});
}); });
});
// 404 Handler
app.use((req, res) => {
res.status(404).json({ error: 'Route nicht gefunden' });
});
// Server starten
app.listen(PORT, '0.0.0.0', () => {
console.log(`🚀 Backend-Server läuft auf Port ${PORT}`);
console.log(`📊 Health Check: http://localhost:${PORT}/health`);
console.log(`🔐 API Base URL: http://localhost:${PORT}/api`);
});
export default app; export default app;