From 9385fc0f118449adf9c26a5b475fa6151412b4ee Mon Sep 17 00:00:00 2001 From: duffyduck Date: Thu, 28 May 2026 09:20:36 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20Portal-E-Mail-Feld=20konnte=20nur=20per?= =?UTF-8?q?=20Paste=20bef=C3=BCllt=20werden?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Folge-Symptom des Pentest-29.4-Email-Validators: das Portal-Email- Input feuerte bei jedem Keystroke einen PUT /customers/:id/portal mit dem Zwischenstand ("p", "po", "por@") – der Backend-Validator lehnte das mit 400 ab, der Server-State blieb unverändert, das Input re-renderte mit dem alten Wert. Effekt: man konnte nichts tippen, nur per Paste in einem Event eine vollständige Adresse setzen. Fix: lokaler emailDraft-State. Während getippt wird, bleibt der Wert nur im Client. Commit erfolgt erst onBlur oder bei Enter – oder wird mit Escape verworfen. Bei Mutations-Error gibt's jetzt auch einen toast statt stiller Revert. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/pages/customers/CustomerDetail.tsx | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/customers/CustomerDetail.tsx b/frontend/src/pages/customers/CustomerDetail.tsx index ef3afaa2..4f2824f4 100644 --- a/frontend/src/pages/customers/CustomerDetail.tsx +++ b/frontend/src/pages/customers/CustomerDetail.tsx @@ -1902,6 +1902,14 @@ function PortalTab({ const [searchTerm, setSearchTerm] = useState(''); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); + // Lokaler Eingabezustand für die Portal-E-Mail. Wenn wir den Input + // direkt vom Server-State (`portal?.portalEmail`) speisen und auf + // jedes Keystroke einen PUT feuern, lehnt der Backend-Validator + // (Pentest 29.4) die Zwischenstände ("p", "po", "por") als + // ungültige E-Mail mit 400 ab → Server-State bleibt unverändert, + // Input rendert mit altem Wert → man konnte nur per Paste + // tippen. Lokaler State + Commit on Blur löst das. + const [emailDraft, setEmailDraft] = useState(null); // Lade Portal-Einstellungen const { data: portalData, isLoading: portalLoading } = useQuery({ @@ -1921,6 +1929,11 @@ function PortalTab({ customerApi.updatePortalSettings(customerId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['customer-portal', customerId] }); + // Lokalen Draft verwerfen – kommt jetzt frisch vom Server. + setEmailDraft(null); + }, + onError: (err: Error) => { + toast.error(err.message || 'Speichern fehlgeschlagen'); }, }); @@ -2036,8 +2049,30 @@ function PortalTab({
updatePortalMutation.mutate({ portalEmail: e.target.value || null })} + value={emailDraft ?? portal?.portalEmail ?? ''} + onChange={(e) => setEmailDraft(e.target.value)} + onBlur={() => { + if (emailDraft === null) return; + const trimmed = emailDraft.trim(); + // Nur committen wenn sich was geändert hat – sonst + // hauen wir bei jedem Tab-Wechsel einen sinnlosen + // PUT raus. + if (trimmed === (portal?.portalEmail || '')) { + setEmailDraft(null); + return; + } + updatePortalMutation.mutate({ portalEmail: trimmed || null }); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + (e.target as HTMLInputElement).blur(); + } + if (e.key === 'Escape') { + setEmailDraft(null); + (e.target as HTMLInputElement).blur(); + } + }} placeholder="portal@example.com" disabled={!canEdit || !portal?.portalEnabled} className="flex-1" @@ -2045,6 +2080,7 @@ function PortalTab({

Diese E-Mail wird für den Login ins Kundenportal verwendet. + Speichern erfolgt automatisch beim Verlassen des Felds (oder Enter).