Commit Graph

27 Commits

Author SHA1 Message Date
duffyduck c9f4fcf8de fix: Portal-Passwort-Card im Vertragsdetail wieder sichtbar
Folge-Symptom zum PW-Save-Fix: das Speichern hat funktioniert,
aber die "Zugangsdaten"-Card im Read-Only-View hat das Passwort-
Feld nicht angezeigt. Ursache: das Frontend nutzte
`c.portalPasswordEncrypted` als Truthy-Check, aber
sanitizeContract strippt das Feld bewusst aus jeder Response
(Pentest Runde 15 - kein verschlüsselter Blob in /contracts/:id).

Fix: getContractById hängt jetzt ein virtuelles `hasPortalPassword`-
Bool-Flag an die Response. Frontend nutzt das statt
portalPasswordEncrypted. Der verschlüsselte Wert bleibt
server-seitig; der Klartext kommt weiterhin über
GET /contracts/:id/password mit Audit-Log.

Live-verifiziert: PUT setzt PW, GET liefert hasPortalPassword:true
+ portalPasswordEncrypted ist NICHT in der Response.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:30:54 +02:00
duffyduck 771f46d2ac Vertragsansicht: Kunden-Schnellansicht-Modal + Cent/Euro-Input
Feature 1 – Kunden-Schnellansicht:
Info-Icon neben dem Kundenlink im Vertragsdetail oeffnet ein Modal
mit den wichtigsten Kundendaten (Firma, Name, Geburtsdatum/-ort,
Gruendungsdatum, Adresse, Telefon, Mobil, E-Mail, Portal-E-Mail,
Steuer-/Handelsregisternr). Jedes Feld hat einen Copy-Button.
Lazy-Fetch via customerApi.getById, staleTime 30s.

Feature 2 – Cent/Euro-Doppel-Input:
Neben dem €/kWh-Arbeitspreis-Feld jetzt ein zweites ct/kWh-Feld.
Bidirektional gekoppelt – Tippen in € aktualisiert ct (×100),
Tippen in ct aktualisiert € (÷100). Backend speichert weiterhin
nur den Euro-Wert; Cent ist reine UI-Hilfe. Float-Rausch-Schutz
verhindert "0.25 → 25.0000000000004". Greift fuer unitPrice und
unitPriceNt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:41:37 +02:00
duffyduck 20d42c5270 Energie-Bonus aufgeteilt in Sofort + Neukunden
EnergyContractDetails.bonus war ein einzelnes Feld. Strom-/Gas-
Verträge haben aber typischerweise zwei Boni (Sofort beim Wechsel
+ Neukunden-Bonus nach 12 Monaten), die getrennt verbucht werden
müssen.

Migration 20260524100000_split_energy_bonus:
- ADD COLUMN IF NOT EXISTS instantBonus, newCustomerBonus
- bestehende `bonus`-Werte → instantBonus (Annahme: Sofort)
- DROP COLUMN IF EXISTS bonus

UI:
- ContractForm zeigt zwei Input-Felder
- Detail-Ansicht zeigt beide einzeln + Gesamtbonus
- Kostenvorschau listet beide einzeln, dann Gesamt, dann effektive
  Jahreskosten

Cost-Calc: calculateCosts() bekommt beide Boni; CostCalculation
liefert instantBonus, newCustomerBonus, totalBonus.

PDF-Template: drei neue Variablen energyDetails.instantBonus,
.newCustomerBonus, .totalBonus.

Live-verifiziert auf dev: PUT mit beiden Werten → DB persistiert,
GET liefert zurueck.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:27:54 +02:00
duffyduck 69b9a35674 Security-Hardening Runde 11: Pentest Runde 7 (Portal-PW + Download-Tokens)
Hit-List vom Pentester abgearbeitet. Hauptpunkte:

1) Contract/Mail-Credentials (password/internet/sip/simcard, mailbox/send/
   reset-password): ALLE bereits durch canAccess* gesichert, keine Lücke.

2) GET /customers/:id/portal/password (Klartext-Portal-PW-Abruf):
   fehlender canAccessCustomer-Check ergänzt. Defense in depth gegen
   versehentliche customers:update-Permission an Portal/eingeschränkte
   Mitarbeiter.

3) Admin-Endpoints (factory-reset, developer/*, audit-logs/rehash,
   audit-logs/customer): durch admin-Permissions geschützt – Portal-User
   haben diese nicht.

4) Token-in-URL (NIEDRIG): Langlebige Access-JWTs landeten als ?token= in
   URLs für iframe-PDFs, Audit-Export-Window etc. → nginx-Logs +
   Browser-History + Referer.
   Lösung: kurzlebige Download-Tokens.
   - signDownloadToken() liefert JWT mit type='download', exp=60s
   - Auth-Middleware akzeptiert type='download' AUSSCHLIESSLICH via
     ?token=, niemals als Bearer-Header
   - POST /api/auth/download-token Endpoint (authenticated)
   - Frontend: authApi.getDownloadToken() utility
   - 4 Stellen migriert: AuditLog-Export, PdfTemplate-Preview-iframe,
     PdfTemplate-Generate, ContractDetail-PDF-Generate (2x),
     Portal-Privacy-PDF
   - fileUrl/getAttachmentUrl sind synchron + breit gestreut – Migration
     bleibt für Folge-PR

Live-verifiziert:
- Download-Token: 1773 Zeichen, type=download, exp-iat=60s
- als Header → 401 (Falscher Token-Typ), als ?token= → 200
- portal-user (Customer 3) auf customers/2/portal/password → 403

Rate-Limiter-Check: express-rate-limit Fixed-Window, kein Reset bei jedem
Request (Pentester-Klage „Fenster reseted sich" stimmt mit dem Code nicht
überein – wahrscheinlich Retry-After-Misinterpretation). Kein Code-Bug
identifiziert; ggf. später Admin-Override-Endpoint nachrüsten.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 00:40:00 +02:00
duffyduck 9830ac29a5 security: JWT raus aus localStorage – Refresh-Cookie-Pattern für SPA
Behebt das Pentest-Finding „JWT in localStorage (MITTEL)": bei XSS war
der Token JS-erreichbar → Angreifer könnte alle Anbieter-Credentials
abrufen. Branchenstandard-Lösung für SPAs jetzt umgesetzt.

Architektur:
- Access-Token: 15 min Lifetime, lebt NUR im JavaScript-Memory
  (api.ts tokenStore + AuthContext). Kein localStorage mehr.
- Refresh-Token: 7 Tage, im httpOnly-Cookie (Secure bei HTTPS_ENABLED,
  SameSite=Strict, Path=/api/auth). JavaScript hat keinen Zugriff →
  XSS klaut max. einen 15-min-Access-Token.

Backend:
- signAccessToken/signRefreshToken mit `type`-Claim
- Auth-Middleware verweigert Tokens mit type=refresh
- POST /api/auth/login + /customer-login: setzt refresh_token-Cookie,
  gibt access-Token im Body
- POST /api/auth/refresh: liest Cookie, rotiert ihn, gibt neuen Access
  aus. Prüft tokenInvalidatedAt (Logout/Rollenänderung = sofortige
  Invalidation auch des Refresh-Tokens)
- POST /api/auth/logout: löscht Cookie + setzt tokenInvalidatedAt
- cookie-parser als neue Dependency

Frontend:
- api.ts: in-memory tokenStore (kein localStorage); withCredentials=true
  für Cookie-Roundtrip; axios-Response-Interceptor mit
  Single-Flight-Refresh-Retry bei 401 (Original-Request wird
  transparent retried mit neuem Token)
- AuthContext: beim App-Start /auth/refresh aufrufen → wenn Cookie
  noch gültig, ist der User automatisch eingeloggt. Tab-Reload
  funktioniert weiterhin obwohl Access-Token nur in memory ist.
- 9 alte `localStorage.getItem('token')`-Stellen migriert auf
  `getAccessToken()` (PDF-Preview-iframe, Audit-Log-CSV-Export,
  DB-Backup-Download, File-Download-URLs, Portal-PDF-Link)

Live verifiziert:
- Login setzt Cookie (httpOnly, SameSite=Strict, Path=/api/auth) + Bearer
- API mit Bearer: 200; ohne: 401
- Refresh mit Cookie: rotiert sauber + neuer Access-Token im Body
- Refresh-Token als Bearer abgewiesen: 401 ("Falscher Token-Typ")
- Logout: Cookie gelöscht, danach /refresh → 401

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 16:06:17 +02:00
duffyduck 77602bb4ac contracts: VVL (Vertragsverlängerung) als Split-Button neben Folgevertrag
VVL = Vertragsverlängerung beim selben Anbieter (vs. Folgevertrag = i.d.R.
Anbieterwechsel).

Im Gegensatz zu createFollowUpContract wird ALLES kopiert:
- Provider, Tarif, Portal-Username/Passwort (verschlüsselt)
- Preise (basePrice/unitPrice/bonus etc.)
- Notes, Commission, Internet-Zugangsdaten, SIP-Daten, SIM-PINs
- ContractDocuments (1:1, gleiche Datei-Referenz)
- Detail-Tabellen (Energy/Internet/Mobile/TV/CarInsurance) komplett

Berechnet:
- newStartDate = oldStartDate + Vertragslaufzeit (Monate aus
  ContractDuration.code/description geparsed: "24M" / "24 Monate" / "2J")
- newEndDate = newStartDate + Laufzeit
- status = DRAFT (User bestätigt manuell)

NICHT kopiert:
- documentType "Auftragsformular" (das wird neu unterschrieben)
- cancellation*-Felder (alter Cancel-Flow nicht relevant)

Frontend:
- Split-Button: Hauptaktion "Folgevertrag anlegen" + ChevronDown-Pfeil
- Dropdown: "VVL anlegen" mit Bestätigungs-Modal
- Modal zeigt Vorhersage des neuen Startdatums (alter Start +
  Vertragslaufzeit als Hinweis)

History-Einträge wie bei Folgevertrag, mit eigenem VVL-Wording.
Doppel-Schutz: maximal 1 Folge-/VVL-Vertrag pro Vorgänger.

Live-verifiziert:
- Contract #17 (FIBER, 2026-05-01, 24M) → VVL mit Start 2028-05-01 ✓
- Provider/Tarif/Preise/Credentials 1:1 übernommen
- 2 Dokumente kopiert (außer Auftragsformular)
- History-Einträge in beiden Verträgen vorhanden

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:12:39 +02:00
duffyduck 35745ce3bb Security-Hardening Runde 5: Hack-Das-Ding (DSGVO-GAU + Timing + XSS)
Live-Pentest gegen Dev-Server + 3 parallele Audit-Agents.

🚨 CRITICAL: /api/uploads/* war ohne Auth erreichbar
- express.static('/api/uploads', ...) → jeder konnte mit ratbarer URL
  sensible PDFs (Kündigungsbestätigungen, Ausweise, Bankkarten,
  Vollmachten) ziehen. Live-verifiziert: 23-KB-PDF eines echten Kunden
  ohne Login geladen.
- Fix: authenticate-Middleware vor static-Handler (req.query.token
  unterstützung war schon da, jetzt aktiv genutzt).
- Frontend: utils/fileUrl.ts hängt JWT als ?token=... an. 24 direkte
  /api${...Path}-URLs in 5 Dateien per Skript migriert (CustomerDetail,
  ContractDetail, InvoicesSection, PdfTemplates, GDPRDashboard).

🚨 HIGH: Login-Timing User-Enumeration
- bcrypt.compare wurde nur bei existierenden Usern ausgeführt → 110ms
  vs 10ms Differenz, Email-Enumeration trivial messbar.
- Fix: Dummy-bcrypt-compare bei invalid user (Cost 12). Plus Lazy-
  Rehash bei erfolgreichem Login: alte Cost-10-Hashes (z.B. admin aus
  Installation) werden auf BCRYPT_COST upgraded, damit Dummy- und
  Echt-Hash-Cost zusammenpassen.
- Live-verifiziert nach Admin-Rehash: 422ms (invalid) vs 423ms (valid)
  – Side-Channel dicht.

🚨 HIGH: XSS via Privacy-Policy/Imprint-HTML
- 4 Frontend-Seiten renderten Backend-HTML ohne DOMPurify
  (PortalPrivacy, ConsentPage, PortalWebsitePrivacy, PortalImprint).
  Admin-eingegebene <script>-Tags wären bei jedem Portal-Kunden-
  Besuch ausgeführt worden – auch auf der öffentlichen Consent-Seite.
- Fix: DOMPurify.sanitize mit strikter FORBID_TAGS/ATTR Config.

🛡 HIGH: IDOR-Härtung an Upload-/Document-Endpoints
- canAccessContract jetzt in: uploadContractDocument,
  deleteContractDocument, handleContractDocumentUpload (Kündigungs-
  Letter+Confirmation), handleContractDocumentDelete,
  saveAttachmentAsContractDocument.
- Defense-in-Depth: aktuell durch requirePermission abgesichert,
  schützt auch gegen künftige Staff-Scoping-Rollen.

Offen für v1.1:
- Per-File-Ownership-Check für /api/uploads (Kontroll-Lookup
  welche Ressource zur Datei gehört)
- TipTap-Link-Tool javascript:-Protokoll blockieren
- Prisma-Error-Messages in Admin-Endpoints generisch sanitisieren

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:37 +02:00
duffyduck dea2da0271 Vertragsstatus-Trigger: Datum beim Upload miterfassen
Beim automatischen Status-Wechsel wird jetzt auch das passende Datum gesetzt,
damit Status und Datumsfeld konsistent sind (Cockpit-Warnung "Datum fehlt"
verschwindet sofort nach Upload).

Backend:
- Upload-Handler für Kündigungsbestätigung(s-Optionen) nimmt optional
  `confirmationDate` aus multipart an, speichert als
  cancellationConfirmationDate / cancellationConfirmationOptionsDate.
  Fallback: heute (nur falls Feld noch leer war).
- maybeActivateOnDeliveryConfirmation nimmt optional deliveryDate, setzt
  Contract.startDate falls leer. Fallback: heute.

Frontend:
- ContractDetail: neues kleines Modal beim Kündigungsbestätigungs-Upload
  fragt das Bestätigungs-Datum ab (Default: heute oder bereits gesetzter
  Wert). Der bestehende inline-Datums-Editor bleibt für spätere Korrekturen.
- ContractDocumentsSection: Datums-Input erscheint conditional im
  Upload-Bereich, sobald Typ "Lieferbestätigung" gewählt ist.
- SaveAttachmentModal (E-Mail-Anhang → Vertragsdokument): gleicher
  Datums-Input conditional für "Lieferbestätigung".
- API-Methoden uploadCancellationConfirmation / uploadDocument /
  saveAttachmentAsContractDocument nehmen optional Datum entgegen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 13:40:04 +02:00
duffyduck cdde7b4ab7 Mandantenfähigkeit: Domain + Kunden-E-Mail-Label dynamisch pro Provider
Alle hardcoded Referenzen auf 'stressfrei-wechseln.de' und 'Stressfrei-Wechseln'
durch dynamische Werte aus der EmailProviderConfig ersetzt. Notwendig für
Multi-Mandanten-Betrieb, wenn das CRM an Dritte vermietet wird.

Schema:
- Neues Feld EmailProviderConfig.customerEmailLabel (String?)
- Wenn leer, wird Label aus Domain abgeleitet ('stressfrei-wechseln.de' → 'Stressfrei-Wechseln')

Backend:
- Neuer Endpoint GET /api/email-providers/public-settings liefert { domain, customerEmailLabel }
- Neue Service-Funktionen: getProviderPublicSettings(), deriveLabelFromDomain()
- create/updateProviderConfig erweitert um customerEmailLabel

Frontend:
- Neuer Hook useProviderSettings() mit Auto-Caching
- Neues Eingabefeld 'Bezeichnung für Kunden-E-Mails' im Provider-Modal
- Dynamische Domain-Suffix im Adress-Hinzufügen-Dialog (@<domain>)
- Tab-Label 'Stressfrei-Wechseln' im Kunden-Detail → dynamisch
- 'Stressfrei-Wechseln Adresse' in ContractForm → dynamisch
- '(Stressfrei-Wechseln)' Badge in ContractDetail → dynamisch
- 'Stressfrei-Wechseln E-Mail' im Generate-Modal → dynamisch
- Leere-Zustand-Meldungen in Tab und E-Mail-Client → dynamisch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 15:43:19 +02:00
duffyduck 9a84e2d3cb PDF-Auftragsvorlagen-System, Objekttyp/Lage-Felder, Eigentümer-Fallback bei Bankverbindung
- PDF-Template-Editor in Einstellungen: Vorlagen hochladen, Formularfelder automatisch auslesen, CRM-Felder zuordnen
- PDF-Vorschau mit annotierten Feldnamen, seitenweise Sortierung der Felder
- Auftrag generieren aus Vertragsdaten (Button im Vertrags-Detail)
- Dynamische Rufnummern-Felder mit Vorwahl-Extraktion und konfigurierbarer Maximalanzahl
- Nicht zugeordnete Felder bleiben editierbar im generierten PDF
- Eigentümer-Felder mit Namens-Kombinationen (Firma+Name etc.) und Fallback auf Kundendaten
- Stressfrei-E-Mail als Feld-Option im Template-Editor
- Objekttyp, Lage und Lage des Anschlusses als neue Felder bei Festnetz-Verträgen (DSL, Glasfaser, Kabel)
- Bankverbindung-Fallback: wenn keine am Vertrag verknüpft, wird automatisch die neueste aktive des Kunden genommen

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 19:16:47 +02:00
duffyduck 0e75e6c8e5 added place to telecommunication, added contract documents, added invoice to other contracts 2026-03-25 16:55:48 +01:00
duffyduck fd55742c57 complete new audit system 2026-03-21 18:23:54 +01:00
duffyduck 38b3b7da73 Datenschutz vollmacht fixed, two time counter added 2026-03-21 16:42:31 +01:00
duffyduck eecc6cd73e fixed back button with source, and customer in customer lsit clickable 2026-03-21 12:16:04 +01:00
duffyduck d7b42f64b1 fixed all back buttons 2026-03-21 12:03:32 +01:00
duffyduck c3edb8ad2e gdpr audit implemented, email log, vollmachten, pdf delete cancel data privacy and vollmachten, removed message no id card in engergy car, and other contracts that are not telecom contracts, added insert counter for engery 2026-03-21 11:59:53 +01:00
duffyduck e0fc26795e added contract history 2026-02-08 19:24:37 +01:00
duffyduck 4f588015a4 contractnumber provider added, old provider number field only if no previous contact exist 2026-02-08 14:34:56 +01:00
duffyduck 2ab2bb7562 snooze vor expired, contracts, display snoozed contracts if an item is missing, un snooze implemented, fixed invoice upload bug 2026-02-08 13:08:58 +01:00
duffyduck ad2b8ea5b6 added invoices and status in cockpit, created info button for contract status types 2026-02-08 01:18:12 +01:00
duffyduck 89d528bb77 addes cost and usage calculation 2026-02-06 00:14:38 +01:00
duffyduck e8919cfa81 added extra field kwh at m3, expand cost field to 10 komma, added maloid,counter add dialog, auto set unit 2026-02-05 20:34:45 +01:00
duffyduck 80dd5cc157 contractmodaldetail date format and before contract and next contract question to add 2026-02-04 21:17:13 +01:00
duffyduck f33d157b9b save email as pdf likae attachment version 2 2026-02-04 19:49:09 +01:00
duffyduck f21eb20715 seperate delivery and billig adresses in contract added 2026-02-04 08:48:25 +01:00
duffyduck 8c9e61cf17 added backup and email client 2026-02-01 00:02:35 +01:00
Stefan Hacker e209e9bbca first commit 2026-01-29 01:16:54 +01:00