Major refactor: auth, bug fixes, security hardening, modular routes
Bug fixes - Sync database.js schema with actual production DB (tarifoptionen shape was wrong; new installs would have broken). - Fix SQL syntax error `SELECT to.*` (reserved keyword) in single- schaltung provision endpoint, plus the dead fallback hierarchy that selected columns that were never in the query. - Fix PUT /api/tarifoptionen/:id writing to non-existent column provision_betrag (should be provision_euro). - Fix race condition in PUT /api/schaltungen/:id where forEach with async db.run inside a transaction could send duplicate responses. - Delete dokumente rows explicitly before deleting schaltung (existing DBs lack ON DELETE CASCADE on dokumente FK). - Replace hardcoded basisBetrag=100 for percent rules; basis is now configurable per rule via the new basis_euro column. Security - Multi-user authentication: users table, bcryptjs, express-session with SQLite store, login/logout/me/setup/change-password endpoints. - Auth gate on all /api/* except /api/auth/*. - CLI to create users: npm run create-user -- <user> <pass> [--admin]. - helmet for security headers. - CORS restricted to ALLOWED_ORIGINS env (default same-origin). - Rate limits: 300/min global, 20/15min on login. - Path-traversal validation on document download/delete. - XSS hardening in frontend: escapeHtml helper applied to all DB-derived innerHTML interpolations; inline onclick handlers that embedded user-controlled strings replaced with data-* attrs + addEventListener. Architecture - Provision calculation centralised in provisionService.js as the single source of truth (was duplicated and inconsistent). - server.js cut from 2138 to ~140 lines; routes split into per- resource modules under routes/. - Promise wrapper db.js for sqlite3 callbacks; async/await in routes. - middleware/ for auth and async error handling. Tests - 14 unit tests for provisionService covering fallback hierarchy, percent calculation, tarifoptionen, hardware logic, employee commission. All green. Config - dotenv loads .env.local at startup (gitignored). - .gitignore extended for sessions.db and .env*. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
55ec916f35
commit
4303b00b9b
23 files changed
+2427
-2575
No files matched your search
@@ -0,0 +1,172 @@
|
||||
const express = require('express');
|
||||
const db = require('../db');
|
||||
const asyncHandler = require('../middleware/asyncHandler');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Validates that a percent rule has a basis_euro >= 0. Returns error string or null.
|
||||
function validateRule({ provision_typ, betrag, basis_euro, verkaeufer_provision_typ, verkaeufer_provision_betrag }) {
|
||||
if (!['fix', 'prozent'].includes(provision_typ)) {
|
||||
return 'Ungültiger Provisionstyp';
|
||||
}
|
||||
const b = parseFloat(betrag);
|
||||
if (!Number.isFinite(b)) return 'Betrag muss eine Zahl sein';
|
||||
if (provision_typ === 'prozent' && (b < 0 || b > 100)) {
|
||||
return 'Prozentual muss zwischen 0 und 100 sein';
|
||||
}
|
||||
if (provision_typ === 'fix' && b < 0) {
|
||||
return 'Fixer Betrag kann nicht negativ sein';
|
||||
}
|
||||
if (provision_typ === 'prozent') {
|
||||
if (basis_euro === undefined || basis_euro === null || basis_euro === '') {
|
||||
return 'Prozent-Provision benötigt einen Basis-Betrag (basis_euro)';
|
||||
}
|
||||
const bb = parseFloat(basis_euro);
|
||||
if (!Number.isFinite(bb) || bb < 0) {
|
||||
return 'Basis-Betrag muss eine positive Zahl sein';
|
||||
}
|
||||
}
|
||||
if (verkaeufer_provision_typ) {
|
||||
if (!['fix', 'prozent'].includes(verkaeufer_provision_typ)) {
|
||||
return 'Ungültiger Verkäufer-Provisionstyp';
|
||||
}
|
||||
const vb = parseFloat(verkaeufer_provision_betrag);
|
||||
if (!Number.isFinite(vb)) return 'Verkäufer-Betrag muss eine Zahl sein';
|
||||
if (verkaeufer_provision_typ === 'prozent' && (vb < 0 || vb > 100)) {
|
||||
return 'Verkäufer-Prozentwert muss zwischen 0 und 100 sein';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- Tariff-specific rules (provisionsregeln) ---
|
||||
|
||||
router.post('/provisionsregeln', asyncHandler(async (req, res) => {
|
||||
const { tarif_id, provision_typ, betrag, basis_euro, beschreibung, gueltig_ab,
|
||||
verkaeufer_provision_typ, verkaeufer_provision_betrag, verkaeufer_provision_beschreibung } = req.body || {};
|
||||
if (!tarif_id) return res.status(400).json({ error: 'Tarif ist erforderlich' });
|
||||
const err = validateRule({ provision_typ, betrag, basis_euro, verkaeufer_provision_typ, verkaeufer_provision_betrag });
|
||||
if (err) return res.status(400).json({ error: err });
|
||||
|
||||
const result = await db.run(
|
||||
`INSERT INTO provisionsregeln
|
||||
(tarif_id, provision_typ, betrag, basis_euro, beschreibung, gueltig_ab,
|
||||
verkaeufer_provision_typ, verkaeufer_provision_betrag, verkaeufer_provision_beschreibung)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
tarif_id, provision_typ, betrag,
|
||||
provision_typ === 'prozent' ? parseFloat(basis_euro) : null,
|
||||
beschreibung || null,
|
||||
gueltig_ab || new Date().toISOString().split('T')[0],
|
||||
verkaeufer_provision_typ || null,
|
||||
verkaeufer_provision_betrag != null ? parseFloat(verkaeufer_provision_betrag) : null,
|
||||
verkaeufer_provision_beschreibung || null,
|
||||
]
|
||||
);
|
||||
res.json({ id: result.lastID, message: 'Provisionsregel erfolgreich erstellt' });
|
||||
}));
|
||||
|
||||
router.put('/provisionsregeln/:id', asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const current = await db.get('SELECT * FROM provisionsregeln WHERE id = ?', [id]);
|
||||
if (!current) return res.status(404).json({ error: 'Provisionsregel nicht gefunden' });
|
||||
|
||||
const merged = {
|
||||
provision_typ: req.body.provision_typ !== undefined ? req.body.provision_typ : current.provision_typ,
|
||||
betrag: req.body.betrag !== undefined ? req.body.betrag : current.betrag,
|
||||
basis_euro: req.body.basis_euro !== undefined ? req.body.basis_euro : current.basis_euro,
|
||||
beschreibung: req.body.beschreibung !== undefined ? req.body.beschreibung : current.beschreibung,
|
||||
gueltig_ab: req.body.gueltig_ab !== undefined ? req.body.gueltig_ab : current.gueltig_ab,
|
||||
aktiv: req.body.aktiv !== undefined ? req.body.aktiv : current.aktiv,
|
||||
verkaeufer_provision_typ: req.body.verkaeufer_provision_typ !== undefined ? req.body.verkaeufer_provision_typ : current.verkaeufer_provision_typ,
|
||||
verkaeufer_provision_betrag: req.body.verkaeufer_provision_betrag !== undefined ? req.body.verkaeufer_provision_betrag : current.verkaeufer_provision_betrag,
|
||||
verkaeufer_provision_beschreibung: req.body.verkaeufer_provision_beschreibung !== undefined ? req.body.verkaeufer_provision_beschreibung : current.verkaeufer_provision_beschreibung,
|
||||
};
|
||||
|
||||
const verr = validateRule(merged);
|
||||
if (verr) return res.status(400).json({ error: verr });
|
||||
|
||||
await db.run(
|
||||
`UPDATE provisionsregeln
|
||||
SET provision_typ = ?, betrag = ?, basis_euro = ?, beschreibung = ?, gueltig_ab = ?, aktiv = ?,
|
||||
verkaeufer_provision_typ = ?, verkaeufer_provision_betrag = ?, verkaeufer_provision_beschreibung = ?
|
||||
WHERE id = ?`,
|
||||
[
|
||||
merged.provision_typ, merged.betrag,
|
||||
merged.provision_typ === 'prozent' ? merged.basis_euro : null,
|
||||
merged.beschreibung, merged.gueltig_ab, merged.aktiv,
|
||||
merged.verkaeufer_provision_typ,
|
||||
merged.verkaeufer_provision_betrag,
|
||||
merged.verkaeufer_provision_beschreibung,
|
||||
id,
|
||||
]
|
||||
);
|
||||
res.json({ message: 'Provisionsregel erfolgreich aktualisiert' });
|
||||
}));
|
||||
|
||||
// --- Global provider rules (anbieter_provisionsregeln) ---
|
||||
|
||||
router.post('/anbieter-provisionsregeln', asyncHandler(async (req, res) => {
|
||||
const { anbieter_id, provision_typ, betrag, basis_euro, beschreibung, gueltig_ab,
|
||||
verkaeufer_provision_typ, verkaeufer_provision_betrag, verkaeufer_provision_beschreibung } = req.body || {};
|
||||
if (!anbieter_id) return res.status(400).json({ error: 'Anbieter ist erforderlich' });
|
||||
const err = validateRule({ provision_typ, betrag, basis_euro, verkaeufer_provision_typ, verkaeufer_provision_betrag });
|
||||
if (err) return res.status(400).json({ error: err });
|
||||
|
||||
const result = await db.run(
|
||||
`INSERT INTO anbieter_provisionsregeln
|
||||
(anbieter_id, provision_typ, betrag, basis_euro, beschreibung, gueltig_ab,
|
||||
verkaeufer_provision_typ, verkaeufer_provision_betrag, verkaeufer_provision_beschreibung)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
anbieter_id, provision_typ, betrag,
|
||||
provision_typ === 'prozent' ? parseFloat(basis_euro) : null,
|
||||
beschreibung || null,
|
||||
gueltig_ab || new Date().toISOString().split('T')[0],
|
||||
verkaeufer_provision_typ || null,
|
||||
verkaeufer_provision_betrag != null ? parseFloat(verkaeufer_provision_betrag) : null,
|
||||
verkaeufer_provision_beschreibung || null,
|
||||
]
|
||||
);
|
||||
res.json({ id: result.lastID, message: 'Globale Provisionsregel erfolgreich erstellt' });
|
||||
}));
|
||||
|
||||
router.put('/anbieter-provisionsregeln/:id', asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const current = await db.get('SELECT * FROM anbieter_provisionsregeln WHERE id = ?', [id]);
|
||||
if (!current) return res.status(404).json({ error: 'Globale Provisionsregel nicht gefunden' });
|
||||
|
||||
const merged = {
|
||||
provision_typ: req.body.provision_typ !== undefined ? req.body.provision_typ : current.provision_typ,
|
||||
betrag: req.body.betrag !== undefined ? req.body.betrag : current.betrag,
|
||||
basis_euro: req.body.basis_euro !== undefined ? req.body.basis_euro : current.basis_euro,
|
||||
beschreibung: req.body.beschreibung !== undefined ? req.body.beschreibung : current.beschreibung,
|
||||
gueltig_ab: req.body.gueltig_ab !== undefined ? req.body.gueltig_ab : current.gueltig_ab,
|
||||
aktiv: req.body.aktiv !== undefined ? req.body.aktiv : current.aktiv,
|
||||
verkaeufer_provision_typ: req.body.verkaeufer_provision_typ !== undefined ? req.body.verkaeufer_provision_typ : current.verkaeufer_provision_typ,
|
||||
verkaeufer_provision_betrag: req.body.verkaeufer_provision_betrag !== undefined ? req.body.verkaeufer_provision_betrag : current.verkaeufer_provision_betrag,
|
||||
verkaeufer_provision_beschreibung: req.body.verkaeufer_provision_beschreibung !== undefined ? req.body.verkaeufer_provision_beschreibung : current.verkaeufer_provision_beschreibung,
|
||||
};
|
||||
|
||||
const verr = validateRule(merged);
|
||||
if (verr) return res.status(400).json({ error: verr });
|
||||
|
||||
await db.run(
|
||||
`UPDATE anbieter_provisionsregeln
|
||||
SET provision_typ = ?, betrag = ?, basis_euro = ?, beschreibung = ?, gueltig_ab = ?, aktiv = ?,
|
||||
verkaeufer_provision_typ = ?, verkaeufer_provision_betrag = ?, verkaeufer_provision_beschreibung = ?
|
||||
WHERE id = ?`,
|
||||
[
|
||||
merged.provision_typ, merged.betrag,
|
||||
merged.provision_typ === 'prozent' ? merged.basis_euro : null,
|
||||
merged.beschreibung, merged.gueltig_ab, merged.aktiv,
|
||||
merged.verkaeufer_provision_typ,
|
||||
merged.verkaeufer_provision_betrag,
|
||||
merged.verkaeufer_provision_beschreibung,
|
||||
id,
|
||||
]
|
||||
);
|
||||
res.json({ message: 'Globale Provisionsregel erfolgreich aktualisiert' });
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
Reference in new issue
Block a user