Commit Graph

51 Commits

Author SHA1 Message Date
duffyduck 386d206ff1 Vertragsdokumente-Modal: Vorschau-Link pro Dokument
Neben jeder Dokument-Zeile sitzt jetzt ein "Vorschau"-Link mit
ExternalLink-Icon, der die PDF in einem neuen Tab öffnet (via
viewUrl mit Token-Auth, inline-disposition). Klick darauf
schaltet bewusst NICHT die Checkbox um – die Auswahl bleibt,
nur das Dokument geht in einem zweiten Tab auf.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 15:56:16 +02:00
duffyduck 67d6fd4941 E-Mail-Liste: eigener Scrollbalken statt seitenweit wachsen
User-Bug: bei vielen E-Mails wuchs die Liste links unbegrenzt nach
unten, sodass die ganze Seite gescrollt werden musste.

- ContractEmailsSection: flex-Container von minHeight:400 auf
  feste 600px Höhe gestellt. Die linke Liste hatte schon
  overflow-y-auto – jetzt greift's auch.
- EmailClientTab: h-full auf calc(100vh - 240px) (mit
  minHeight:500) bounded. h-full hat im Tab-Container vorher
  nichts gebracht, weil der Parent selbst keine feste Höhe
  hatte.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 07:51:16 +02:00
duffyduck a4895374b9 Kundendaten-Modal: nur Anbieter-Nummern, keine internen CRM-Nummern
Modal ist für Mails AN den Anbieter gedacht – interne CRM-Nummern
interessieren dort niemanden.

- formatCustomerBlock: customer.customerNumber (intern) raus,
  stattdessen contract.customerNumberAtProvider rein.
- formatContractBlock: interne contractNumber raus, restliche
  Anbieter-/Vertriebsplattform-Nummern bleiben.
- Previews ziehen ebenfalls auf customerNumberAtProvider /
  contractNumberAtProvider um, mit Hinweis-Text wenn keine
  Anbieter-Nummer hinterlegt ist.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-21 16:10:20 +02:00
duffyduck ebaee024b6 Kundendaten-Modal: Bank + Ausweis getrennte Text/PDF-Wahl
Bank- und Ausweis-Section haben jetzt jeweils zwei unabhängige
Checkboxen statt der bisherigen Section-Checkbox + Sub-Attach:

- Bank: "Letzte 4 IBAN-Stellen einfügen" + "Bankkarte als PDF
  anhängen". Text-Variante zeigt nur "IBAN endet auf: XXXX" – keine
  volle IBAN/BIC/Bank-Liste mehr (Mail-Hygiene).
- Ausweis: "{Typ}-Nummer einfügen" + "{Typ} als PDF anhängen".
  Text-Variante zeigt nur die Nummer, keine Behörde/Daten.

Alle drei Kombinationen "nur Text", "nur PDF" und "beides" sind
damit möglich, "keins von beidem" entspricht der Section-aus.
Schalter sind disabled wenn der jeweilige Wert (IBAN /
documentNumber / documentPath) nicht vorhanden ist.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-21 16:08:35 +02:00
duffyduck f1b05c56e5 Kundendaten-Modal: E-Mail-Wahl Stammdaten vs. Absender
In der "Anrede & Name"-Section neue Radio-Wahl, sobald die
Section aktiv ist:
- Stammdaten-E-Mail (customer.email) – default wenn vorhanden
- Absender-Adresse (Postfach von dem gesendet wird)
- Keine E-Mail einfügen

Wird in den Customer-Block-Builder durchgereicht und ersetzt die
fix verdrahtete customer.email-Zeile. Wenn die Stammdaten-Mail
fehlt, ist der Radio "Stammdaten" disabled und der Default
springt auf "Absender".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-21 16:04:54 +02:00
duffyduck 5293af18a5 E-Mail-Compose: Vertragsdokumente anhängen + Kundendaten einfügen
Zwei neue Buttons im Compose-Modal (nur sichtbar bei Vertrag-
Kontext):

- Vertragsdokumente: listet alle am Vertrag gespeicherten
  ContractDocuments gruppiert nach documentType. Auswahl →
  Token-Download via fileUrl → base64 → Anhang.
- Kundendaten einfügen: zeigt Sections nur wenn Daten vorhanden
  (Customer, Lieferadresse, ggf. Rechnungsadresse, Vertrag, Bank,
  Ausweis). Bei Bank/Ausweis zusätzlich Sub-Checkbox "als PDF
  anhängen" wenn documentPath vorhanden. Text-Blöcke ans Body-
  Ende, PDFs in attachments[]. 25-MB-Limit beidseitig geprüft.

Helpers in composeAttachmentHelpers.ts:
- serverFileToAttachment(path, filename) für Token-URL→Blob→base64
- totalAttachmentBytes mit ~33% base64-Overhead
- sprechende Dateinamen via bankCardAttachmentName /
  identityDocAttachmentName

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-21 15:55:13 +02:00
duffyduck f1102a24b7 CopyButton: Portal-Benutzername + E-Mail-Postfach-Selector
ContractForm: neben dem "Portal Benutzername"-Label sitzt jetzt
ein CopyButton, der je nach Modus den manuellen Eingabewert oder
die ausgewählte Stressfrei-Adresse in die Zwischenablage kopiert.
Erscheint nur wenn der jeweilige Wert nicht leer ist.

EmailClientTab + ContractEmailsSection: rechts neben dem Account-
Selector liegt jetzt ein CopyButton, der die Postfach-Adresse des
aktuell gewählten Mailbox-Kontos kopiert. Funktioniert in beiden
UI-Varianten (Single-Account-Span und Multi-Account-Dropdown).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-21 15:05:25 +02:00
duffyduck 993f2d10f0 E-Mail-Ansicht: Postfach-Filter in Trash/Sent durchreichen
Bug: Im Vertrags-Tab (Gesendet/Gelöscht) und im Kunden-Haupt-
Postfach (Gelöscht) wurden Mails aus ALLEN Postfächern angezeigt,
unabhängig vom ausgewählten Postfach. Im Vertrag fehlte zusätzlich
der Vertrags-Filter im Papierkorb.

Backend:
- getEmailsForContract akzeptiert accountId → stressfreiEmailId
- getTrashEmails (controller + service) nimmt {accountId, contractId}
- getFolderCountsForContract bekommt optional stressfreiEmailId und
  zusätzlich trash/trashUnread im Result

Frontend:
- API-Client (getForContract/getTrash/getContractFolderCounts) nimmt
  Filter entgegen
- ContractEmailsSection reicht selectedAccountId in alle drei Queries
  + queryKey durch. Trash-Badge kommt jetzt aus contract-scoped
  Counts statt account-globalem stressfreiEmailApi
- EmailClientTab reicht selectedAccountId in die Trash-Query durch

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-21 14:06:24 +02:00
duffyduck 523eab30d5 JpgToPdfModal: Bilder auf 2400px runterskalieren
Stage: 2 Handy-JPGs → 23 MB PDF. Smartphone-Fotos haben
4000-6000 px Kante, das macht auch ohne Re-Encode 5-10 MB pro
Bild → PDF wird riesig.

Beim Hinzufügen werden Bilder jetzt auf max 2400 px lange Kante
runterskaliert (~290 DPI auf A4 = Druckqualität) und als JPEG mit
Quality 0.92 (Lightroom-Default) persistiert. Vorschau, Rotation/
Flip und PDF-Embed laufen alle auf dem skalierten Bild.

Erwartete Größe: 2 Handy-Fotos ≈ 1-2 MB PDF.
2026-06-03 18:29:04 +02:00
duffyduck 2fee13d09e EmailDetail: ExternalLink-Icon beim "Zugeordnet zu"-Badge
Klick auf die Vertragsnummer öffnet weiterhin im selben Tab
(via React-Router Link). Neues Icon daneben öffnet den Vertrag in
einem neuen Browser-Tab – analog zum Pro-Tab-Link in CustomerDetail.
2026-06-03 18:20:10 +02:00
duffyduck 84cbf01706 Kunden-Tabs: ExternalLink-Icon neben jedem Reiter
Tabs-Komponente bekommt optionalen tabHrefBuilder(tabId)-Prop.
Wenn gesetzt, erscheint neben jedem Tab-Label ein kleines
ExternalLink-Icon, das den Tab via ?tab=<id> in einem neuen
Browser-Tab öffnet.

CustomerDetail übergibt den Builder. URL-Param wird eh schon
für den Tab-Sync genutzt – Anhängen reicht.

Click-stopPropagation verhindert, dass der Klick auf das Icon
gleichzeitig den Tab-Wechsel auslöst.
2026-06-03 18:15:23 +02:00
duffyduck 101369c205 EmailDetail: Links immer im neuen Tab öffnen
Nach DOMPurify-Sanitize alle <a>-Elemente auf target="_blank" +
rel="noopener noreferrer" setzen. Letzteres verhindert
window.opener-Tab-Hijacking. Sanitize + DOM-Walk in useMemo, läuft
nur bei Wechsel der Email neu.
2026-06-03 18:06:19 +02:00
duffyduck 7c18343a95 Bugfixes: Adresse-Modal + Upload-Limit auf 25 MB
1. AddressModal: Straße-Feld ließ sich nicht editieren. setFormData
   wurde im Render-Body aufgerufen, wenn formData.street !==
   address.street → Reset bei jedem Tastendruck. In useEffect mit
   [address?.id]-Dependency umgezogen.

2. Multer-Limit von 10 MB auf 25 MB in upload.routes.ts,
   gdpr.routes.ts, contract.routes.ts. Zwei Handy-Fotos zu PDF
   kratzten am alten Limit. FileUpload-Hinweistext angepasst.
2026-06-03 16:37:09 +02:00
duffyduck 431792e8d9 JpgToPdfModal: PDF-Größe massiv reduziert
Stage-Bug: 2 Handy-JPGs à 2 MB → PDF >10 MB → Multer 413. Ursache:
Canvas-Re-Encode mit JPEG-Quality 1.0 blies jedes Bild auf 8-15 MB
auf (Quality 100 % ≠ "identisch zum Original", sondern "möglichst
viele Bits pro Pixel" – ein schon JPEG-komprimiertes Smartphone-
Foto wird so künstlich 4-8× größer).

Fix 1: Wenn Rotation/Flip unverändert (Standardfall), Original-
DataURL 1:1 in die PDF einbetten – kein Canvas-Roundtrip, keine
Quality-Aufblähung. 2-MB-JPEG bleibt 2 MB. Format-Detection per
data:image/png-Prefix (PNG vs JPEG).

Fix 2: Bei Transformation toDataURL('image/jpeg', 0.95) statt 1.0.
Visuell identisch für Foto-Inhalte, 50-70 % kleiner.

Kombiniert: 2 untransformierte Handy-Fotos ≈ 4 MB PDF (vorher
16-30 MB), 2 gedrehte ≈ 5-8 MB.
2026-06-03 16:06:05 +02:00
duffyduck ec577e6d76 Pentest 68.1 (LOW) + 68.2 (INFO): PDF-Active-Content-Filter + Modal-Limit
68.1: Magic-Byte-Check prüfte nur %PDF-. PDFs mit /JavaScript, /JS,
/Launch, /EmbeddedFile, /RichMedia (Flash) kamen durch und wurden
inline ausgeliefert – Browser-Viewer ignorieren JS, Adobe Acrobat
nicht.

- Neuer Helper assertSafePdf(buf) in utils/sanitize.ts mit
  case-sensitivem String-Scan auf die fünf Action-Patterns
  (\b-Word-Boundary verhindert False-Positives bei /JSXForm etc.).
- Neue Middleware pdfUploadSafety.ts mit zwei Varianten:
  requireSafeUploadedPdf (PDF-only) und scanUploadedPdfIfPresent
  (durchwinkt JPG/PNG, scannt nur PDFs).
- Eingehängt in: upload.routes (Magic-Byte-Validator erweitert),
  gdpr.routes Vollmacht-Upload, pdfTemplate.routes Template-Upload,
  contract.routes Vertragsdokumente, cachedEmail.controller
  (saveAttachmentTo, saveAttachmentAsInvoice,
  saveAttachmentAsContractDocument).
- Inline-Vorschau bleibt – Pentester-Empfehlung "disposition=inline
  abschalten" wurde bewusst nicht umgesetzt (löst Acrobat-Risiko
  nicht, bricht aber ~20 UI-Stellen).
- Smoke-Test: 5 Payload-Typen abgelehnt, clean PDF + Non-PDF + JSXForm
  durchgewinkt.

68.2: JpgToPdfModal-Self-DoS – MAX_IMAGES=50, MAX_IMAGE_BYTES=25MB.
2026-06-03 13:18:23 +02:00
duffyduck 30f528596c JPGs → PDF: neuer Button überall bei PDF-Upload
- Neue Komponente JpgToPdfModal (jsPDF clientseitig, kein Backend-Roundtrip).
- Bilder hinzufügen per Klick, Drag&Drop oder Strg+V (Clipboard).
- Reihenfolge per Drag&Drop sortierbar; pro Bild 90°/180°-Drehung +
  horizontal/vertikal-Spiegelung.
- Jedes Bild = eine A4-Seite, Orientation automatisch nach Bild,
  JPEG-Qualität 100%.
- FileUpload-Komponente zeigt den Sekundär-Button automatisch, sobald
  accept PDF einschließt (Datenschutz, Vollmacht, Bankkarten, Ausweise,
  Gewerbeanmeldung, Handelsregister, Kündigungsschreiben/-bestätigung
  + jeweilige Optionen).
- Direktinputs ebenfalls erweitert: Vertragsdokumente (ContractDetail),
  Vollmacht-Tab (CustomerDetail), Rechnungen (InvoicesSection).
- PdfTemplates bewusst ausgenommen – braucht AcroForm-Felder.
2026-06-03 12:27:37 +02:00
duffyduck 0f4ffe3c32 E-Mail als PDF speichern: Tab "Vertragsdokument" ergänzt
Bisher hatte das "E-Mail als PDF speichern"-Modal nur die Tabs
"Als Dokument" + "Als Rechnung" (nur Energieverträge). Wenn die
E-Mail einem Vertrag zugeordnet ist, fehlte die Möglichkeit, sie
direkt als Vertragsdokument (Auftragsformular, Lieferbestätigung
etc.) zu hinterlegen – analog zum Anhang-Modal.

Backend: neuer Endpoint POST /api/emails/:id/save-as-contract-document
{ documentType, notes?, deliveryDate? } – generiert das Mail-PDF,
speichert es unter /uploads/contract-documents und legt einen
ContractDocument-Eintrag an. Bei documentType "Lieferbestätigung"
wird der bestehende maybeActivateOnDeliveryConfirmation-Workflow
getriggert (DRAFT → ACTIVE, startDate-Übernahme).

Frontend: SaveEmailAsPdfModal bekommt den dritten Tab parallel zu
SaveAttachmentModal. Tab erscheint, sobald die E-Mail einem Vertrag
zugeordnet ist (auch bei Nicht-Energieverträgen); Tab "Als Rechnung"
bleibt auf Energieverträge beschränkt. Dokumenttyp-Dropdown und
Notizen-Feld werden aus dem Anhang-Modal übernommen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 19:15:23 +02:00
duffyduck cd7075e96f Quicklinks auch im "Kein Postfach"-Zustand der E-Mails-Card anzeigen
Der "Stressfrei wechseln Adressen"-Link (sowie "Postfach öffnen")
war nur im Normal-Zweig sichtbar, nicht aber wenn der Kunde noch
gar kein Mailbox-Konto hat. cardTitle in einer gemeinsamen Variable
extrahiert und in beiden Branches verwendet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 08:23:40 +02:00
duffyduck 9e3bce85f0 Vorgängervertrag-Modal: Kundennr./Vertragsnr. beim Anbieter auch ohne Provider/Tarif anzeigen
Die "Anbieter & Tarif"-Card war nur sichtbar, wenn provider oder
tariff gesetzt waren. Bei Entwürfen ohne Anbieter wurden dadurch
auch customerNumberAtProvider + contractNumberAtProvider versteckt,
obwohl sie pflegbar sind und für den Wechsel-Workflow wichtig sind.
Fix: Card-Sichtbarkeitsbedingung um die beiden Felder erweitert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 07:51:36 +02:00
duffyduck 4fb700cf57 Vertrags-Forms: Mini-Links zu Stammdaten in neuem Tab
ContractEmailsSection (Vertragsansicht): Zusätzlich zu "Postfach
öffnen" gibt es jetzt "Stressfrei wechseln Adressen" → Tab in der
Kundenakte.

ContractForm (Bearbeiten): Kleine ExternalLink-Icons neben den
Select-Labels:
- Lieferadresse + Rechnungsadresse → Kundenakte/Adressen
- Bankkarte → Kundenakte/Bankkarten
- Ausweis → Kundenakte/Ausweise
- Anbieter + Tarif → Settings/Anbieter & Tarife
- Vertriebsplattform → Settings/Vertriebsplattformen

Select-Komponente nimmt jetzt ReactNode als label (statt nur string),
um JSX-Labels mit eingebettetem Link zu erlauben. Rückwärts-
kompatibel zu allen bestehenden String-Aufrufen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 07:47:40 +02:00
duffyduck 3a9cece929 Vertragshistorie: Vertragsnummern als Link in neuem Tab
Erwähnte Vertragsnummern (Pattern PREFIX-RANDOM) in Title und
Description werden gegen previousContract + followUpContract des
aktuellen Vertrags aufgelöst und als Link mit target="_blank"
gerendert. Nicht aufgelöste Nummern bleiben als Text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 14:33:11 +02:00
duffyduck d92d0b1eaf Vertrags-Mail-Card: Link "Postfach öffnen" → Kunden-Postfach in neuem Tab
Neben dem "E-Mails"-Titel der Card im Vertragsdetail jetzt ein
kleiner Link mit ExternalLink-Icon, der den Tab "E-Mail-Postfach"
in der Kundenakte in einem neuen Tab öffnet (target="_blank",
rel="noopener noreferrer"). Greift auf das bereits unterstützte
?tab=emails-Deep-Link-Pattern in CustomerDetail zurück.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 12:23:29 +02:00
duffyduck 0bd2f9be7e fix: "Anzeigen"-Buttons öffnen Datei wieder im Browser-Tab
Folge-Symptom des Pen-30.13-Fixes: alle file-downloads liefen mit
Content-Disposition: attachment – das ist gegen Stored-XSS richtig,
hat aber die "Anzeigen"-Buttons (Bankkarten / Ausweise /
Verträge / etc.) kaputtgemacht, weil der Browser jetzt
herunterlud statt im Tab zu öffnen.

Magic-Byte-basierter Whitelist-Pfad eingebaut: optional ?disposition=
inline am Download-Endpoint, ABER nur wenn die ersten Bytes der
Datei das Magic eines safe Typs zeigen (PDF, PNG, JPEG, GIF, WebP).
Bei Mismatch fällt's auf attachment zurück – Stored-XSS bleibt
weiterhin unmöglich, falls jemand HTML als .pdf hochlädt.

Frontend: neuer viewUrl(path)-Alias = fileUrl(path, {inline: true}).
Alle Stellen mit `<a href={fileUrl(...)} target="_blank">` oder
`window.open(fileUrl(...), '_blank')` (13 Stellen über CustomerDetail,
ContractDetail, PdfTemplates, GDPRDashboard, InvoicesSection)
nutzen jetzt viewUrl. Download-Stellen bleiben fileUrl
(= attachment, byte-genaues File-Save).

Live-verifiziert auf dev:
- ohne Param: attachment (default, Stored-XSS-Schutz)
- ?disposition=inline + echte PDF: inline + application/pdf
- ?disposition=inline + HTML als .pdf: attachment (Magic-Mismatch
  → Browser lädt herunter statt zu rendern)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 09:19:04 +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 f6df97226d feat(email): Weiterleiten + Erneut senden im Detail-Pane
Zwei Aktionen, die der existierende Reply-Pfad bisher nicht abdeckte:

1. Weiterleiten (Compose-Modal-Forward-Modus):
   - Neuer Button im EmailDetail, neben "Antworten"
   - ComposeEmailModal akzeptiert jetzt einen `forwardOf` prop und
     füllt das Formular im Forward-Stil vor:
     * To leer (User trägt selbst ein)
     * Subject mit "Fwd:"-Prefix
     * Body mit zitierten Headern (Von, An, Datum, Betreff) +
       Original-Text
   - Titel des Modals reagiert ("Antworten" / "Weiterleiten" /
     "Neue E-Mail")

2. Erneut senden (One-Click-Resend):
   - Neuer Button im EmailDetail; schickt die Mail nochmal an die
     ursprüngliche toAddresses (= die Stressfrei-Adresse selbst).
     Plesk routet dann gemäß der HEUTE hinterlegten Forwards –
     Use-Case: die Stressfrei-Forward-Adresse wurde nach Empfang
     umgestellt, der Empfang soll beim neuen Forward-Empfänger
     landen.
   - Confirm-Dialog erklärt den Vorgang und warnt explizit, dass
     Anhänge nicht erneut mit gesendet werden (Anhänge wären
     IMAP-Refetch, dafür "Weiterleiten" nutzen).
   - Toast-Feedback für Erfolg/Fehler.
   - Im TRASH-Folder wird der Resend-Button bewusst nicht
     eingeblendet (kein sinnvoller Use-Case dort).

Backend braucht keine neuen Endpoints – beide Aktionen nutzen die
bestehenden `stressfreiEmailApi.sendEmail` + `cachedEmailApi.getById`
(letztere für den Body, der ohnehin schon im Detail-View geladen ist).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 14:46:44 +02:00
duffyduck 185b38dc55 feat(email): Suchleiste + erweiterte Filter im Email-Postfach
Variante B aus der Trade-off-Diskussion: Suchleiste über der Email-Liste
plus eine ausklappbare Box mit Detail-Filtern, alle AND-verknüpft.

Backend:
- EmailListOptions um search + 9 Detail-Filter erweitert (fromFilter,
  toFilter, subjectFilter, bodyFilter, attachmentNameFilter,
  hasAttachments, isRead, isStarred, receivedFrom, receivedTo)
- getCachedEmails baut die where-Klausel:
  * `search` → OR über Subject/From-Address/From-Name/Body (Volltext-
    Quicksearch)
  * Feldspezifische Filter werden AND-verknüpft an die where gehängt;
    From-/Body-Filter intern als kleine OR-Subqueries (Match in
    Adresse ODER Name; Match in textBody ODER htmlBody)
- Controller-Parser akzeptiert die Filter als Query-Parameter
  (parseBoolParam/parseDateParam tolerieren leere/invalide Werte)

Frontend:
- Suchleiste mit X-Button zum Leeren + Filter-Toggle mit Badge (zeigt
  Anzahl aktiver Filter)
- Ausklappbare Filter-Box: Von, An, Betreff, Inhalt, Datum von/bis,
  Anhang-Dateiname, Mit/Ohne Anhang, Gelesen-Status, Markiert-Status
- Filter-State fließt via useMemo + queryKey in den useQuery → React
  Query macht automatisch ein Re-Fetch bei jeder Änderung
- "Alle zurücksetzen"-Button räumt komplett auf
- Nicht für TRASH-Folder eingeblendet (eigener Pfad ohne Filter-API)

Bewusst nicht gebaut: voller AND/OR-Builder mit Plus-Button und
Bool-Verschachtelung. Reale Such-Use-Cases im Email-Kontext sind
quasi immer AND-verknüpft; Bool-Builder bringt mehr Bedienprobleme
als Mehrwert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 14:31:43 +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 1c46d7345c Security-Hardening: IDOR-Fixes, XSS-Sanitizer, CORS+Helmet, Data-Exposure
Umfassender Security-Review vor öffentlichem Deployment.
Detaillierter Report in docs/SECURITY-REVIEW.md.

🔴 KRITISCHE FIXES:

1. CORS offen → jetzt nur explizite Origins (via CORS_ORIGINS env),
   in Production per default komplett aus (gleiche Origin erzwingt Browser).

2. Keine Security-Headers → helmet-Middleware hinzugefügt.
   X-Frame-Options, X-Content-Type-Options, HSTS, Referrer-Policy, CORP.

3. JWT-Fallback-Secret entfernt. Beim Server-Start wird jetzt geprüft ob
   JWT_SECRET (min 32 Zeichen) und ENCRYPTION_KEY (exakt 64 Hex) gesetzt sind,
   sonst Fail-Fast mit klarer Fehlermeldung.

4. IDOR bei 7 Contract-Endpoints. Portal-Kunden mit 'contracts:read'
   konnten über geratene IDs fremde Daten abrufen (Passwort, SIM-PIN/PUK,
   Internet-Zugangsdaten, SIP-Credentials, Vertragsdokumente, Rechnungen).
   Neuer Helper canAccessContract() in utils/accessControl.ts in allen
   betroffenen Endpoints eingebaut. Prüft Vertrag-Besitzer + Vollmachten.

5. XSS via Email-Body. email.htmlBody wurde ungefiltert via
   dangerouslySetInnerHTML gerendert. Angreifer konnte Mail mit <script>
   schicken → Token-Diebstahl aus localStorage. Jetzt mit DOMPurify
   sanitized: verbietet script/iframe/form/inline-handler, erlaubt
   normale Formatierung + Bilder.

6. Customer-API leakte sensible Felder:
   - portalPasswordHash (bcrypt-Hash)
   - portalPasswordEncrypted (symmetrisch, mit ENCRYPTION_KEY entschlüsselbar)
   - portalPasswordResetToken (gültig 2h)
   Neuer Sanitizer in utils/sanitize.ts, angewendet in getCustomer/getCustomers.
   Admin mit customers:update darf portalPasswordEncrypted sehen (für UI-Anzeige),
   alle anderen Rollen nicht.

🟡 WICHTIGE FIXES:

7. Portal-JWT-Invalidation nach Passwort-Reset. Neues Feld
   Customer.portalTokenInvalidatedAt, wird beim Reset auf now() gesetzt.
   Auth-Middleware prüft Portal-Sessions dagegen. Alte Sessions werden
   dadurch invalidiert.

8. express.json() mit 5 MB Size-Limit (statt Default 100 KB unklar).

Neue Files:
- backend/src/utils/accessControl.ts - IDOR-Schutz
- backend/src/utils/sanitize.ts - Response-Sanitizer
- docs/SECURITY-REVIEW.md - vollständiger Report + Deployment-Checkliste

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 22:06:16 +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 cf4370c905 Toast-Benachrichtigungen bei IMAP-Sync- und SMTP-Send-Fehlern
Bisher blieb ein fehlgeschlagener IMAP-Sync oder E-Mail-Versand still – der User
sah nur im Browser-Devtools, dass etwas schief lief. Jetzt erscheint eine rote
Toast-Benachrichtigung (8 Sekunden) mit der konkreten Fehlermeldung des Servers,
z.B. 'Sync fehlgeschlagen: IMAP-Authentifizierung fehlgeschlagen: NO [AUTHENTICATIONFAILED]'.

EmailClientTab (Synchronisieren-Button):
- toast.success bei erfolgreichem Sync
- toast.error bei Fehler + bei Backend-Response mit success=false

ComposeEmailModal (Senden):
- toast.success bei erfolgreichem Versand
- toast.error bei SMTP-Fehler mit Server-Response (zusätzlich zum Inline-Fehler)

Außerdem im imapService.testImapConnection:
- Roh-Error wird jetzt geloggt (code, response, responseStatus, authenticationFailed)
- ImapFlow-spezifische Felder werden in die Fehlermeldung übernommen, sodass
  z.B. '2 NO [AUTHENTICATIONFAILED] Authentication failed.' direkt sichtbar wird

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 15:16:04 +02:00
duffyduck 2879bd64d6 Email-Anhänge als Vertragsdokumente + Rechnungen für alle Vertragstypen
Der SaveAttachmentModal hat jetzt drei Modi (wenn E-Mail einem Vertrag zugeordnet ist):

1. Als Dokument – in feste Slots (Kündigungsschreiben etc.), unverändert
2. Als Vertragsdokument – NEU: flexible ContractDocument-Tabelle mit Typ-Dropdown
   (Auftragsformular, Lieferbestätigung, Vertragsunterlagen, Vollmacht,
   Widerrufsbelehrung, Preisblatt, Sonstiges) + optionalen Notizen
3. Als Rechnung – jetzt für ALLE Vertragstypen (vorher nur Strom/Gas)

Backend:
- Neuer Endpoint POST /api/emails/:id/attachments/:filename/save-as-contract-document
- saveAttachmentAsInvoice + saveEmailAsInvoice: ELECTRICITY/GAS-Einschränkung entfernt,
  nutzt jetzt addInvoiceByContract als Fallback für Nicht-Energie-Verträge

Frontend:
- cachedEmailApi.saveAttachmentAsContractDocument hinzugefügt
- SaveAttachmentModal: neuer Mode 'contractDocument' mit Typ+Notizen
- Mode-Toggle zeigt jetzt alle drei Optionen wenn Vertrag zugeordnet

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 13:06:10 +02:00
duffyduck 9d6bd68ddc Geburtstag-Management-Modal mit Reset + Send + Auto-Flag
Neuer Cake-Button neben dem Geburtsdatum in den Stammdaten öffnet ein Modal
mit drei Funktionen:

1. **Gruß-Marker zurücksetzen** (lastBirthdayGreetingYear → null)
   - Für Debugging oder als Fallback, wenn der Kunde den Gruß erneut sehen soll
   - Mit Bestätigungsdialog

2. **Geburtstagsgruß jetzt senden** (Email / WhatsApp / Telegram / Signal)
   - Email: direkt via System-SMTP mit HTML-Template (Du/Sie-abhängig)
   - WhatsApp/Telegram/Signal: öffnet vorbefülltes Fenster mit Gruß-Text
   - Text beachtet Du/Sie-Verhältnis (pronomen, possessiv, etc.)
   - Mit Bestätigungsdialog

3. **Automatisch senden** – neue Einstellung am Customer
   - autoBirthdayGreeting (Boolean) + autoBirthdayChannel (String)
   - Für späteren Cron-basierten Automatik-Versand vorbereitet

Backend:
- birthday.service.ts: resetBirthdayGreeting, buildBirthdayGreetingText, getBirthdayGreetingData
- birthday.controller.ts: resetBirthdayGreeting, sendBirthdayGreeting
- Routes: POST /birthdays/:customerId/reset + /send
- Audit-Log bei beiden Aktionen

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 12:46:03 +02:00
duffyduck 2a3928d0e7 Anrede-Verhältnis Du/Sie pro Kunde + Geburtstagsgruß respektiert Anrede
Schema:
- Customer.useInformalAddress: Boolean (Default: false = Sie)
- Auch bei Firmenkunden verfügbar (Chef kann man auch duzen)

Frontend:
- Neues Pflichtfeld "Anrede per" (Du/Sie) im Kunden-Formular
- Anzeige als Badge in CustomerDetail-Stammdaten

Geburtstagsgruß im Portal:
- Bei Du: "Herzlichen Glückwunsch, Max! Alles Gute zu deinem 42. Geburtstag!"
- Bei Sie: "Herzlichen Glückwunsch, Herr Müller! Alles Gute zu Ihrem 42. Geburtstag!"
- Konsistent auch bei nachträglichen Glückwünschen (hattest/hatten, bist/sind etc.)
- Backend liefert firstName, lastName, salutation und useInformalAddress

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 12:27:23 +02:00
duffyduck 018784cca6 Geburtstagskalender + Geburtstagsgruß-Modal im Kundenportal
Admin (Vertrags-Cockpit):
- Neue Section "Geburtstage" zeigt Kunden mit Geburtstag
- Fenster: -7 bis +30 Tage um heute
- Farbcodierung: heute (pink), vergangen (amber), bevorstehend (grau)
- Anzeige: Name, Kundennummer, Geburtsdatum, Alter, "Heute!" / "In X Tagen" / "Vor X Tagen"

Portal (Kundenportal):
- Modal mit Geburtstagsgruß wenn Geburtstag heute oder in den letzten 7 Tagen war
- Unterscheidet zwischen aktuellem Geburtstag und nachträglichen Glückwünschen
- Schönes Gradient-Design mit Konfetti-Emojis
- Wird pro Jahr nur einmal angezeigt (Customer.lastBirthdayGreetingYear)
- Bestätigung speichert das aktuelle Jahr

Backend:
- Neues Feld Customer.lastBirthdayGreetingYear (Int?)
- Service birthday.service.ts mit Fenster-Logik + Alter-Berechnung
- Endpoints /api/birthdays/upcoming (Admin),
  /api/birthdays/my-birthday (Portal GET + POST /acknowledge)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 11:51:20 +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 a15772cb54 impressum datenschutz added 2026-03-25 15:25:34 +01:00
duffyduck fd55742c57 complete new audit system 2026-03-21 18:23:54 +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 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 8c65fecef0 remove uploads from repo but keep empty folder 2026-02-04 19:19:18 +01:00
duffyduck d8ced5cb24 added new view in contracts customer and contracts 2026-02-04 00:52:04 +01:00
duffyduck b87053760e save attachment from email in customer data and - or contracts 2026-02-03 23:58:00 +01:00
duffyduck bf068276b5 all email views the same 2026-02-03 23:04:42 +01:00
duffyduck 8c9e61cf17 added backup and email client 2026-02-01 00:02:35 +01:00