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 && (
<>