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
@@ -260,6 +260,16 @@ export async function getContractFolderCounts(req: AuthRequest, res: Response):
export async function syncAccount(req: AuthRequest, res: Response): Promise<void> {
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';
+5 -1
View File
@@ -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
);
+17 -5
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) => (
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 hover:bg-gray-50 ${
selectedContractId === contract.id ? 'bg-blue-50 border-blue-200' : ''
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'
}`}
>
<div className="font-medium">{contract.contractNumber}</div>
{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 className="p-3 text-gray-500 text-center">
Keine Verträge gefunden.