From 6a670df1c49428359a28661043f1058b4bcb18aa Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 30 May 2026 08:25:16 +0200 Subject: [PATCH] fix: 2x Portal-Bugs (Vertragsauswahl + Email-Sync) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 1 — Support-Anfrage: ausgewaehlter Vertrag nicht erkennbar Im Kundenportal beim Erstellen einer Support-Anfrage war der Selected-State des Vertrags nur ein dezenter blau-grauer Hintergrund + Border-Farbwechsel. Auf hellem Bildschirm / nicht- perfekter Lichtsituation kaum zu sehen. Fix: kraefigere Markierung mit linkem 4px-Akzent-Bar (border-l-blue-600), kraefigerem Background (bg-blue-100), Checkmark-Icon rechtsbuendig und blauer Titel-Text. Bug 2 — Email-Sync im Portal: "Keine Berechtigung" POST /api/stressfrei-emails/:id/sync hatte requirePermission('customers:update') – die Portal-Kunden nicht haben (nur customers:read fuer eigene Daten). Sie konnten ihr eigenes Postfach nicht synchronisieren. Fix: Perm-Middleware aus der Route raus, Mitarbeiter-Check + Owner-Check in den Controller verlegt: - isCustomerPortal: nur Owner-Check (canAccessStressfreiEmail) - Mitarbeiter: muss customers:update haben Trennung der Threat-Modelle – Portal-User darf sein Postfach syncen, sonst aber nichts triggern; Mitarbeiter brauchen weiter die Update-Perm. Live-verifiziert: - Portal-User 1 syncs eigenes Konto → Auth passiert (400 wegen fehlender IMAP-Config in dev-DB, NICHT 403) - Portal-User 1 syncs Customer-3-Konto → 403 "Kein Zugriff" - Mitarbeiter ohne customers:update → weiter 403 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/controllers/cachedEmail.controller.ts | 10 +++++ backend/src/routes/cachedEmail.routes.ts | 6 ++- frontend/src/pages/tasks/TaskList.tsx | 40 ++++++++++++------- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/backend/src/controllers/cachedEmail.controller.ts b/backend/src/controllers/cachedEmail.controller.ts index 37b5e8fc..84210f23 100644 --- a/backend/src/controllers/cachedEmail.controller.ts +++ b/backend/src/controllers/cachedEmail.controller.ts @@ -260,6 +260,16 @@ export async function getContractFolderCounts(req: AuthRequest, res: Response): export async function syncAccount(req: AuthRequest, res: Response): Promise { try { const stressfreiEmailId = parseInt(req.params.id); + // Mitarbeiter brauchen customers:update (wie früher), Portal-Kunden + // brauchen keine Perm – nur Eigentum am Konto (Owner-Check unten). + // Trennung der Threat-Modelle: Portal-User dürfen IHR eigenes + // Postfach syncen, sollen aber nicht Mitarbeiter-Updates triggern. + const isPortal = !!req.user?.isCustomerPortal; + const hasUpdatePerm = req.user?.permissions?.includes('customers:update') ?? false; + if (!isPortal && !hasUpdatePerm) { + res.status(403).json({ success: false, error: 'Keine Berechtigung' } as ApiResponse); + return; + } if (!(await canAccessStressfreiEmail(req, res, stressfreiEmailId))) return; const fullSync = req.query.full === 'true'; diff --git a/backend/src/routes/cachedEmail.routes.ts b/backend/src/routes/cachedEmail.routes.ts index bc3d9f2d..6d83098d 100644 --- a/backend/src/routes/cachedEmail.routes.ts +++ b/backend/src/routes/cachedEmail.routes.ts @@ -236,10 +236,14 @@ router.delete( // E-Mails für ein Konto synchronisieren // POST /api/stressfrei-emails/:id/sync?full=true +// +// KEIN `requirePermission('customers:update')` hier: Portal-Kunden +// dürfen ihr EIGENES Postfach synchronisieren – sie haben aber nur +// `customers:read`. Der Mitarbeiter-Perm-Check und der Owner-Check +// laufen im Controller. (Pentest 2026-05-30 follow-up.) router.post( '/stressfrei-emails/:id/sync', authenticate, - requirePermission('customers:update'), cachedEmailController.syncAccount ); diff --git a/frontend/src/pages/tasks/TaskList.tsx b/frontend/src/pages/tasks/TaskList.tsx index 9c69cdea..2c6d3564 100644 --- a/frontend/src/pages/tasks/TaskList.tsx +++ b/frontend/src/pages/tasks/TaskList.tsx @@ -660,21 +660,33 @@ function CreateSupportTicketModal({ />
{filteredContracts.length > 0 ? ( - filteredContracts.map((contract) => ( -
setSelectedContractId(contract.id)} - className={`p-3 cursor-pointer border-b last:border-b-0 hover:bg-gray-50 ${ - selectedContractId === contract.id ? 'bg-blue-50 border-blue-200' : '' - }`} - > -
{contract.contractNumber}
-
- {contract.providerName || 'Kein Anbieter'} - {contract.tariffName && ` - ${contract.tariffName}`} + filteredContracts.map((contract) => { + const isSelected = selectedContractId === contract.id; + return ( +
setSelectedContractId(contract.id)} + className={`p-3 cursor-pointer border-b last:border-b-0 transition-colors flex items-center gap-3 ${ + isSelected + ? 'bg-blue-100 border-l-4 border-l-blue-600 pl-2' + : 'hover:bg-gray-50' + }`} + > + {isSelected && ( + + )} +
+
+ {contract.contractNumber} +
+
+ {contract.providerName || 'Kein Anbieter'} + {contract.tariffName && ` - ${contract.tariffName}`} +
+
-
- )) + ); + }) ) : (
Keine Verträge gefunden.