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:
32
backend/lib/mail.js
Normal file
32
backend/lib/mail.js
Normal 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
62
backend/lib/seed.js
Normal 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]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
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)) {
|
if (!roles.includes(req.user.rolle)) {
|
||||||
return res.status(403).json({ error: 'Keine Berechtigung' });
|
return res.status(403).json({ error: 'Keine Berechtigung' });
|
||||||
}
|
}
|
||||||
|
|
||||||
next();
|
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();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
[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]
|
[email]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (rows.length === 0) {
|
|
||||||
return res.status(401).json({ error: 'Ungültige Anmeldedaten' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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' });
|
||||||
}
|
|
||||||
|
|
||||||
const token = jwt.sign(
|
// Demo-Ablauf-Pruefung
|
||||||
{
|
if (m.tenant_id) {
|
||||||
id: mitarbeiter.id,
|
const [tr] = await pool.query(
|
||||||
email: mitarbeiter.email,
|
'SELECT status, demo_expires_at, typ, name FROM tenants WHERE id = ?',
|
||||||
rolle: mitarbeiter.rolle,
|
[m.tenant_id]
|
||||||
name: mitarbeiter.name
|
|
||||||
},
|
|
||||||
process.env.JWT_SECRET,
|
|
||||||
{ expiresIn: '24h' }
|
|
||||||
);
|
);
|
||||||
|
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({
|
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
110
backend/routes/geraete.js
Normal 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;
|
||||||
84
backend/routes/kunden-bilder.js
Normal file
84
backend/routes/kunden-bilder.js
Normal 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;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
140
backend/routes/tenants.js
Normal 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
38
backend/scripts/bootstrap.js
vendored
Normal 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);
|
||||||
|
}
|
||||||
@@ -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({
|
|
||||||
error: err.message || 'Interner Serverfehler'
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 404 Handler
|
app.use((req, res) => res.status(404).json({ error: 'Route nicht gefunden' }));
|
||||||
app.use((req, res) => {
|
|
||||||
res.status(404).json({ error: 'Route nicht gefunden' });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Server starten
|
// Bootstrap SuperAdmin beim Start, dann Server starten
|
||||||
|
bootstrapSuperAdmin()
|
||||||
|
.catch(err => console.error('Bootstrap-Fehler:', err))
|
||||||
|
.finally(() => {
|
||||||
app.listen(PORT, '0.0.0.0', () => {
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
console.log(`🚀 Backend-Server läuft auf Port ${PORT}`);
|
console.log(`Backend laeuft 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;
|
||||||
|
|||||||
Reference in New Issue
Block a user