Kundenakte: Tab "Geworben / angeworben" (Kundenempfehlungen)

Neuer Tab vor "Datenschutz", nur Mitarbeiter/Admin (nicht Portal),
ohne Consent-Pflicht. Zwei Abschnitte:
1. "<Kunde> wurde an Board geholt durch:" – max. 1 Werber
   (DB-Unique auf recruitedId).
2. "<Kunde> hat folgende Kunden an Board geholt:" – beliebig viele.

Jede Zeile: Kunde per Lupe-Such-Modal (breite Suche über Name/
Kundennr./Firma/E-Mail/Telefon) + Beziehungs-Dropdown. Löschen +
Externtab-Link zur Kundenakte pro Zeile.

Bidirektional aus EINEM Datensatz: "A geworben durch B" erscheint
automatisch bei B unter "hat geworben"; von beiden Akten
hinzufügbar/löschbar.

Backend: neues Model CustomerReferral (recruiter/recruited FKs,
recruitedId @unique, relationship) + Migration. Beziehungs-Whitelist
serverseitig; Self-Werbung + Doppel-Werber (409) abgefangen.
Portal-Token wird explizit geblockt (Defense-in-Depth, nicht nur
UI-Ausblendung). CREATE/DELETE auditiert.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 17:50:15 +02:00
co-authored by Claude Opus 4.7
parent 6b08762ff9
commit cda6d2814e
10 changed files with 744 additions and 1 deletions
+9
View File
@@ -1,5 +1,6 @@
import { Router } from 'express';
import * as customerController from '../controllers/customer.controller.js';
import * as customerReferralController from '../controllers/customerReferral.controller.js';
import * as stressfreiEmailController from '../controllers/stressfreiEmail.controller.js';
import { authenticate, requirePermission, requireCustomerAccess } from '../middleware/auth.js';
@@ -53,4 +54,12 @@ router.post('/:customerId/representatives', authenticate, requirePermission('cus
router.delete('/:customerId/representatives/:representativeId', authenticate, requirePermission('customers:update'), customerController.removeRepresentative);
router.get('/:customerId/representatives/search', authenticate, requirePermission('customers:read'), customerController.searchForRepresentative);
// Werbung ("Geworben / angeworben") nur Mitarbeiter/Admins (Controller
// blockt Portal zusätzlich explizit). /search VOR der Liste ist unkritisch,
// da unterschiedliche Pfade, aber der Klarheit halber gruppiert.
router.get('/:customerId/referrals/search', authenticate, requirePermission('customers:read'), customerReferralController.searchCustomers);
router.get('/:customerId/referrals', authenticate, requirePermission('customers:read'), customerReferralController.getReferrals);
router.post('/:customerId/referrals', authenticate, requirePermission('customers:update'), customerReferralController.createReferral);
router.delete('/:customerId/referrals/:referralId', authenticate, requirePermission('customers:update'), customerReferralController.deleteReferral);
export default router;