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:
@@ -260,6 +260,16 @@ export async function getContractFolderCounts(req: AuthRequest, res: Response):
|
|||||||
export async function syncAccount(req: AuthRequest, res: Response): Promise<void> {
|
export async function syncAccount(req: AuthRequest, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const stressfreiEmailId = parseInt(req.params.id);
|
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;
|
if (!(await canAccessStressfreiEmail(req, res, stressfreiEmailId))) return;
|
||||||
const fullSync = req.query.full === 'true';
|
const fullSync = req.query.full === 'true';
|
||||||
|
|
||||||
|
|||||||
@@ -236,10 +236,14 @@ router.delete(
|
|||||||
|
|
||||||
// E-Mails für ein Konto synchronisieren
|
// E-Mails für ein Konto synchronisieren
|
||||||
// POST /api/stressfrei-emails/:id/sync?full=true
|
// 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(
|
router.post(
|
||||||
'/stressfrei-emails/:id/sync',
|
'/stressfrei-emails/:id/sync',
|
||||||
authenticate,
|
authenticate,
|
||||||
requirePermission('customers:update'),
|
|
||||||
cachedEmailController.syncAccount
|
cachedEmailController.syncAccount
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -660,21 +660,33 @@ function CreateSupportTicketModal({
|
|||||||
/>
|
/>
|
||||||
<div className="max-h-48 overflow-y-auto border rounded-lg">
|
<div className="max-h-48 overflow-y-auto border rounded-lg">
|
||||||
{filteredContracts.length > 0 ? (
|
{filteredContracts.length > 0 ? (
|
||||||
filteredContracts.map((contract) => (
|
filteredContracts.map((contract) => {
|
||||||
|
const isSelected = selectedContractId === contract.id;
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={contract.id}
|
key={contract.id}
|
||||||
onClick={() => setSelectedContractId(contract.id)}
|
onClick={() => setSelectedContractId(contract.id)}
|
||||||
className={`p-3 cursor-pointer border-b last:border-b-0 hover:bg-gray-50 ${
|
className={`p-3 cursor-pointer border-b last:border-b-0 transition-colors flex items-center gap-3 ${
|
||||||
selectedContractId === contract.id ? 'bg-blue-50 border-blue-200' : ''
|
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">
|
<div className="text-sm text-gray-500">
|
||||||
{contract.providerName || 'Kein Anbieter'}
|
{contract.providerName || 'Kein Anbieter'}
|
||||||
{contract.tariffName && ` - ${contract.tariffName}`}
|
{contract.tariffName && ` - ${contract.tariffName}`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
) : (
|
) : (
|
||||||
<div className="p-3 text-gray-500 text-center">
|
<div className="p-3 text-gray-500 text-center">
|
||||||
Keine Verträge gefunden.
|
Keine Verträge gefunden.
|
||||||
|
|||||||
Reference in New Issue
Block a user