fix: 2x Portal-Bugs (Vertragsauswahl + Email-Sync)

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 08:25:16 +02:00
parent 7dcdf9d6ef
commit 6a670df1c4
3 changed files with 41 additions and 15 deletions
+26 -14
View File
@@ -660,21 +660,33 @@ function CreateSupportTicketModal({
/>
<div className="max-h-48 overflow-y-auto border rounded-lg">
{filteredContracts.length > 0 ? (
filteredContracts.map((contract) => (
<div
key={contract.id}
onClick={() => 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' : ''
}`}
>
<div className="font-medium">{contract.contractNumber}</div>
<div className="text-sm text-gray-500">
{contract.providerName || 'Kein Anbieter'}
{contract.tariffName && ` - ${contract.tariffName}`}
filteredContracts.map((contract) => {
const isSelected = selectedContractId === contract.id;
return (
<div
key={contract.id}
onClick={() => 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 && (
<CheckCircle className="w-5 h-5 text-blue-600 flex-shrink-0" />
)}
<div className="flex-1 min-w-0">
<div className={`font-medium ${isSelected ? 'text-blue-900' : ''}`}>
{contract.contractNumber}
</div>
<div className="text-sm text-gray-500">
{contract.providerName || 'Kein Anbieter'}
{contract.tariffName && ` - ${contract.tariffName}`}
</div>
</div>
</div>
</div>
))
);
})
) : (
<div className="p-3 text-gray-500 text-center">
Keine Verträge gefunden.