From 5bbb7dfffe05dd9a83d67c705c0cf6a8d4c94a57 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Thu, 30 Jul 2026 14:51:52 +0200 Subject: [PATCH] PdfDragButton komplett entfernt (Plattformgrenze) Test-Ergebnis: Browser kann einer fremden Desktop-App keine echte lokale Datei zum Anhaengen uebergeben. URL-Drag -> Thunderbird nur Link (0 Bytes, Fehler beim Senden); Datei-Drop in den Text -> nur Dateiname als Text; auf die Anhang-Leiste (Thunderbird/Linux) -> ebenfalls kein echter Anhang; Webmail -> gar nicht moeglich. Entscheidung: Feature raus. Download- und Anzeigen-Button decken den Bedarf zuverlaessig ab. PdfDragButton geloescht, fileUrl()-Token-Param zurueckgebaut. Pentest R131 damit gegenstandslos (kein Drag mehr). Die separaten Copy-Buttons fuer IBAN + Ausweisnummer im Vertrags- formular bleiben erhalten. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/todo.md | 46 ++--- frontend/src/components/ui/PdfDragButton.tsx | 170 ------------------ .../src/pages/contracts/ContractDetail.tsx | 17 -- frontend/src/pages/contracts/ContractForm.tsx | 13 -- .../src/pages/customers/CustomerDetail.tsx | 9 - frontend/src/utils/fileUrl.ts | 10 +- 6 files changed, 18 insertions(+), 247 deletions(-) delete mode 100644 frontend/src/components/ui/PdfDragButton.tsx diff --git a/docs/todo.md b/docs/todo.md index ee64b6f3..668c6115 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,36 +97,22 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt -- [x] **🖱️ PDF-Drag-and-Drop aus Bankkarte/Ausweis heraus** (2026-07-30) - - Neues wiederverwendbares `PdfDragButton`: ziehbares „📄 PDF ziehen"- - Element, mit dem der hinterlegte Scan direkt aus dem Browser in ein - Mail-Fenster (Anhang) oder den Datei-Explorer gezogen werden kann - (Chromium-`DownloadURL`, Format `::`). - - **Technik-Grenze bewusst:** „PDF per Strg+V als DATEI einfügen" geht im - Browser NICHT (Web-Clipboard darf keine OS-Datei-Zwischenablage/CF_HDROP - befüllen) → Drag-and-Drop statt Clipboard. `DownloadURL` nur Chrome/Edge, - nicht Firefox (dort Klick-Fallback: Datei im Tab öffnen). - - Eingebaut an: Vertrag-Neu/Bearbeiten (`ContractForm`, neben IBAN-/ - Ausweisnummer-Copy-Button), Vertragsansicht (`ContractDetail`, Bankkarte- - + Ausweis-Card), Kundenakte-Tabs Bankkarten + Ausweise (`CustomerDetail`, - neben Anzeigen/Download). Erscheint nur bei vorhandenem `documentPath`. - - Nutzt bestehende `fileUrl()`-Download-URL (Per-File-Ownership-Check - unverändert) – kein neuer Endpoint, keine neue Angriffsfläche. Ausweitung - auf weitere PDF-Stellen (Rechnungen, Vertragsdoks, Datenschutz) folgt bei - Bedarf über dieselbe Komponente. - - **Nachtrag Pentest R131 + Funktions-Iteration → Blob-Ansatz (final):** - Zwischenstände (Access-Token → 60s-Download-Token, Text-Formate raus/rein) - hatten entweder ein Token-Leak-Risiko ODER hängten nur einen LINK an - (Thunderbird lud die URL erst beim Senden → 0 Bytes / Fehler, Token - abgelaufen). Endstand: Datei wird VORAB per Bearer-Auth als **Blob** - geladen und beim Drag als echter Datei-Inhalt übergeben - (`dataTransfer.items.add(File)` + `DownloadURL` mit lokaler `blob:`-URL). - → Es wird der DATEI-INHALT übertragen, kein Link. Sicherheit R131 damit - vollständig erledigt: im Drag steckt WEDER Server-URL NOCH Token, ein - Fehl-Drop kann nichts mehr leaken. Blob wird beim Mount vorgeladen - (Ladezustand „lädt …", `/files/download` schreibt kein Audit-Log, daher - kein Audit-Spam). Zuverlässiges Datei-Drop v.a. in Chromium; Ziele ohne - File-Annahme → Klick-Fallback (Tab öffnen, manuell anhängen). +- [~] **🖱️ PDF-Drag-and-Drop aus Bankkarte/Ausweis — VERWORFEN** (2026-07-30) + - Idee: hinterlegten Scan per Drag direkt in Mail-Anhang/Datei-Explorer + ziehen. Über mehrere Iterationen durchgespielt (Chromium-`DownloadURL` + mit Access-Token → 60s-Download-Token → Blob/`items.add(File)`). + - **Ergebnis: harte Plattformgrenze.** Ein Browser kann einer fremden + Desktop-App keine echte lokale Datei zum Anhängen übergeben. URL-Drag → + Thunderbird speichert nur einen Link (0 Bytes, lädt beim Senden nach → + Fehler). Datei-Drop in den Nachrichtentext → nur Dateiname als Text. + Auf die Anhang-Leiste gezogen (Thunderbird/Linux/Chromium) → ebenfalls + kein echter Anhang. Webmail im Browser → gar keine Datei möglich. + - **Entscheidung (User):** komplett wieder entfernt. Download- + Anzeigen- + Button decken den Bedarf zuverlässig ab. Komponente `PdfDragButton` + gelöscht, `fileUrl()`-Token-Param zurückgebaut. Pentest R131 damit + gegenstandslos (kein Drag-Feature mehr). + - **Bleibt erhalten:** die separaten Copy-Buttons für IBAN (Bankkarte) und + Ausweisnummer im Vertragsformular (s.u.). - [x] **📋 Vertragsformular: Copy-Buttons für Bankkarte (IBAN) + Ausweis** (2026-07-30) - In „Kundendaten verknüpfen" je ein Kopieren-Button neben dem Label, diff --git a/frontend/src/components/ui/PdfDragButton.tsx b/frontend/src/components/ui/PdfDragButton.tsx deleted file mode 100644 index bd312aad..00000000 --- a/frontend/src/components/ui/PdfDragButton.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import { GripVertical, FileDown, Info, Loader2 } from 'lucide-react'; -import api from '../../services/api'; -import { fileUrl } from '../../utils/fileUrl'; - -/** - * Ziehbares Element, mit dem eine im Backend hinterlegte Datei (i.d.R. ein - * PDF-Scan) als ECHTE DATEI aus dem Browser heraus in eine fremde Anwendung - * – Mail-Anhang, Datei-Explorer – gezogen werden kann. - * - * Ansatz (bewusst Blob statt URL): - * Die Datei wird VORAB per authentifiziertem Request (Bearer-Header über die - * Axios-Instanz) als Blob geladen und beim `dragstart` als Datei-Inhalt in - * den DataTransfer gelegt: - * - `dataTransfer.items.add(File)` → echte Datei für Web-/App-Ziele, - * - `DownloadURL` mit einer `blob:`-Object-URL → Chromium liefert die Bytes - * beim Drop selbst aus (Explorer/Datei-Manager). - * Dadurch wird der DATEI-INHALT übertragen, NICHT ein Link. Das behebt den - * Fall, dass Mail-Programme (z.B. Thunderbird) nur eine URL als Anhang - * speichern und die Datei erst beim Senden nachladen (→ 0 Bytes / Fehler, - * weil kurzlebiger Token längst abgelaufen ist). - * - * SICHERHEIT (Pentest R131): Im Drag steckt jetzt WEDER eine Server-URL NOCH - * ein Token – nur der Datei-Inhalt bzw. eine lokale `blob:`-URL. Ein Fehl-Drop - * kann also keinen Token (weder Access- noch Download-Token) mehr leaken. - * - * Grenzen der Browser-Plattform: - * - „PDF per Strg+V als DATEI einfügen" bleibt im Browser unmöglich → Drag. - * - Zuverlässiges Datei-Drop nach draußen funktioniert am besten in - * Chromium (Chrome/Edge). Ziele, die weder `File` noch `DownloadURL` - * annehmen (manche Webmail-Compose-Felder), erhalten keine echte Datei – - * dort bleibt der Klick-Fallback (Datei im Tab öffnen → manuell anhängen). - */ -interface PdfDragButtonProps { - /** Server-Pfad der Datei (`documentPath`). Ohne Pfad wird nichts gerendert. */ - path: string | null | undefined; - /** Dateiname beim Ablegen. Ohne Endung wird sie aus dem Pfad ergänzt. */ - filename?: string; - label?: string; - title?: string; - className?: string; - size?: 'sm' | 'md'; -} - -const MIME_BY_EXT: Record = { - pdf: 'application/pdf', - png: 'image/png', - jpg: 'image/jpeg', - jpeg: 'image/jpeg', - gif: 'image/gif', - webp: 'image/webp', -}; - -function extOf(path: string): string { - const clean = path.split('?')[0].split('#')[0]; - const dot = clean.lastIndexOf('.'); - return dot === -1 ? '' : clean.slice(dot + 1).toLowerCase(); -} - -export default function PdfDragButton({ - path, - filename, - label = 'PDF ziehen', - title, - className = '', - size = 'sm', -}: PdfDragButtonProps) { - const blobRef = useRef(null); - const objectUrlRef = useRef(null); - const fetchingRef = useRef(false); - const [state, setState] = useState<'idle' | 'loading' | 'ready'>('idle'); - - // Datei-Bytes einmal laden (auth über Bearer-Header der Axios-Instanz). - const ensureBlob = () => { - if (!path || blobRef.current || fetchingRef.current) return; - fetchingRef.current = true; - setState('loading'); - api - .get('/files/download', { params: { path }, responseType: 'blob' }) - .then((res) => { - blobRef.current = res.data as Blob; - setState('ready'); - }) - .catch(() => { - setState('idle'); - }) - .finally(() => { - fetchingRef.current = false; - }); - }; - - // Beim Mount vorladen, damit der erste Drag sofort eine echte Datei liefert. - useEffect(() => { - if (path) ensureBlob(); - return () => { - if (objectUrlRef.current) URL.revokeObjectURL(objectUrlRef.current); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [path]); - - if (!path) return null; - - const ext = extOf(path); - const mime = MIME_BY_EXT[ext] || 'application/octet-stream'; - const base = (filename && filename.trim()) || 'dokument'; - const safeBase = base.replace(/[\\/:*?"<>|]+/g, '_'); - const name = safeBase.includes('.') ? safeBase : `${safeBase}${ext ? `.${ext}` : ''}`; - - const handleDragStart = (e: React.DragEvent) => { - const blob = blobRef.current; - if (!blob) { - // Bytes noch nicht da → keinen halben (Link-)Anhang erzeugen. Laden - // anstoßen; der nächste Versuch liefert dann die echte Datei. - ensureBlob(); - e.preventDefault(); - return; - } - // Frische Object-URL, vorherige freigeben. - if (objectUrlRef.current) URL.revokeObjectURL(objectUrlRef.current); - const url = URL.createObjectURL(blob); - objectUrlRef.current = url; - - const file = new File([blob], name, { type: mime }); - try { - e.dataTransfer.items.add(file); - } catch { - /* ältere Browser: dann trägt DownloadURL den Drop */ - } - try { - e.dataTransfer.setData('DownloadURL', `${mime}:${name}:${url}`); - } catch { - /* noop */ - } - e.dataTransfer.effectAllowed = 'copy'; - }; - - const iconSize = size === 'sm' ? 'w-3.5 h-3.5' : 'w-4 h-4'; - const loading = state === 'loading'; - - return ( - - window.open(fileUrl(path, { inline: true }), '_blank', 'noopener')} - title={ - title || - (loading - ? 'Datei wird geladen …' - : 'In Mail-Anhang/Explorer ziehen (am besten Chrome/Edge). Klick: im Tab öffnen.') - } - className={`inline-flex items-center gap-1 select-none rounded border border-gray-200 bg-gray-50 px-1.5 py-0.5 text-xs text-gray-600 transition-colors hover:text-blue-600 hover:border-blue-300 hover:bg-blue-50 ${ - state === 'ready' ? 'cursor-grab active:cursor-grabbing' : 'cursor-default' - }`} - > - - {loading ? : } - {loading ? 'lädt …' : label} - - - - - - ); -} diff --git a/frontend/src/pages/contracts/ContractDetail.tsx b/frontend/src/pages/contracts/ContractDetail.tsx index 2784d3e2..e212d3cd 100644 --- a/frontend/src/pages/contracts/ContractDetail.tsx +++ b/frontend/src/pages/contracts/ContractDetail.tsx @@ -19,7 +19,6 @@ import toast from 'react-hot-toast'; import JpgToPdfModal from '../../components/ui/JpgToPdfModal'; import { calculateConsumption, calculateCosts, calculateMultiMeterConsumption } from '../../utils/energyCalculations'; import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton'; -import PdfDragButton from '../../components/ui/PdfDragButton'; import AutosaveDateInput from '../../components/ui/AutosaveDateInput'; import { formatDate } from '../../utils/dateFormat'; import { useProviderSettings } from '../../hooks/useProviderSettings'; @@ -2652,14 +2651,6 @@ export default function ContractDetail() { {c.bankCard.iban}

- {c.bankCard.documentPath && ( -
- -
- )} {c.bankCard.bankName &&

{c.bankCard.bankName}

} {c.bankCard.description && (

@@ -2675,14 +2666,6 @@ export default function ContractDetail() {

{c.identityDocument.type}

- {c.identityDocument.documentPath && ( -
- -
- )} )} diff --git a/frontend/src/pages/contracts/ContractForm.tsx b/frontend/src/pages/contracts/ContractForm.tsx index 8169c1e5..1c195061 100644 --- a/frontend/src/pages/contracts/ContractForm.tsx +++ b/frontend/src/pages/contracts/ContractForm.tsx @@ -10,7 +10,6 @@ import Button from '../../components/ui/Button'; import Input from '../../components/ui/Input'; import Select from '../../components/ui/Select'; import CopyButton from '../../components/ui/CopyButton'; -import PdfDragButton from '../../components/ui/PdfDragButton'; import CustomerInfoModal from '../../components/contracts/CustomerInfoModal'; import { buildContractLabelParts } from '../../utils/contractLabel'; import type { ContractType } from '../../types'; @@ -979,12 +978,6 @@ export default function ContractForm() { {selectedBankCard && ( )} - {selectedBankCard?.documentPath && ( - - )} } {...register('bankCardId')} @@ -1011,12 +1004,6 @@ export default function ContractForm() { {selectedDocument && ( )} - {selectedDocument?.documentPath && ( - - )} } {...register('identityDocumentId')} diff --git a/frontend/src/pages/customers/CustomerDetail.tsx b/frontend/src/pages/customers/CustomerDetail.tsx index 4fec7e9b..9e465ad0 100644 --- a/frontend/src/pages/customers/CustomerDetail.tsx +++ b/frontend/src/pages/customers/CustomerDetail.tsx @@ -18,7 +18,6 @@ import FileUpload from '../../components/ui/FileUpload'; import { Edit, Plus, Trash2, MapPin, CreditCard, FileText, Gauge, Eye, EyeOff, Download, Globe, UserPlus, X, Search, Mail, Copy, Check, ChevronDown, ChevronRight, Info, Shield, ShieldCheck, ShieldX, ShieldAlert, Lock, ArrowLeft, Cake, RefreshCw, ExternalLink, Images } from 'lucide-react'; import JpgToPdfModal from '../../components/ui/JpgToPdfModal'; import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton'; -import PdfDragButton from '../../components/ui/PdfDragButton'; import BirthdayManagementModal from '../../components/BirthdayManagementModal'; import { formatDate } from '../../utils/dateFormat'; import { getContractTypeInfo } from '../../utils/contractInfo'; @@ -1003,10 +1002,6 @@ function BankCardsTab({ Download - {canEdit && ( <> Download - {canEdit && ( <>