import { Response } from 'express'; import * as contractTaskService from '../services/contractTask.service.js'; import * as contractService from '../services/contract.service.js'; import * as customerService from '../services/customer.service.js'; import * as appSettingService from '../services/appSetting.service.js'; import { logChange } from '../services/audit.service.js'; import { ApiResponse, AuthRequest } from '../types/index.js'; import { canAccessContract, getPortalAllowedCustomerIds } from '../utils/accessControl.js'; // ==================== ALL TASKS (Dashboard & Task List) ==================== export async function getAllTasks(req: AuthRequest, res: Response): Promise { try { const { status, customerId } = req.query; const customerIdNum = customerId ? parseInt(customerId as string) : undefined; // Für Kundenportal: Filter auf erlaubte Kunden (mit Live-Vollmacht-Check) const allowedIds = await getPortalAllowedCustomerIds(req); let customerPortalCustomerIds: number[] | undefined; let customerPortalEmails: string[] | undefined; if (allowedIds) { // Wenn der Portal-User explizit nach einer customerId filtert, die er // nicht (mehr) vertreten darf → 403 statt 200 mit leerem Array // (Pentest Runde 10 – LOW: konsistentes Response-Verhalten nach // Vollmacht-Widerruf). if (customerIdNum !== undefined && !allowedIds.includes(customerIdNum)) { res.status(403).json({ success: false, error: 'Kein Zugriff auf diese Kundendaten' } as ApiResponse); return; } customerPortalCustomerIds = allowedIds; const customers = await customerService.getCustomersByIds(customerPortalCustomerIds); customerPortalEmails = customers .map((c: { id: number; portalEmail: string | null }) => c.portalEmail) .filter((email: string | null): email is string => !!email); } const tasks = await contractTaskService.getAllTasks({ status: status as 'OPEN' | 'COMPLETED' | undefined, customerId: customerIdNum, customerPortalCustomerIds, customerPortalEmails, }); res.json({ success: true, data: tasks } as ApiResponse); } catch (error) { res.status(500).json({ success: false, error: 'Fehler beim Laden der Aufgaben', } as ApiResponse); } } export async function getTaskStats(req: AuthRequest, res: Response): Promise { try { // Für Kundenportal: Filter auf erlaubte Kunden (mit Live-Vollmacht-Check) const allowedIds = await getPortalAllowedCustomerIds(req); let customerPortalCustomerIds: number[] | undefined; let customerPortalEmails: string[] | undefined; if (allowedIds) { customerPortalCustomerIds = allowedIds; const customers = await customerService.getCustomersByIds(customerPortalCustomerIds); customerPortalEmails = customers .map((c: { id: number; portalEmail: string | null }) => c.portalEmail) .filter((email: string | null): email is string => !!email); } const stats = await contractTaskService.getTaskStats({ customerPortalCustomerIds, customerPortalEmails, }); res.json({ success: true, data: stats } as ApiResponse); } catch (error) { res.status(500).json({ success: false, error: 'Fehler beim Laden der Statistik', } as ApiResponse); } } // ==================== TASKS BY CONTRACT ==================== export async function getTasks(req: AuthRequest, res: Response): Promise { try { const contractId = parseInt(req.params.contractId); const { status } = req.query; // Zentraler canAccessContract-Check inkl. Live-Vollmacht-Prüfung über // hasAuthorization (Pentest Runde 6 – HOCH-04: widerrufene Vollmachten // hatten vorher weiter Zugriff, weil nur representedCustomerIds-Array // konsultiert wurde, ohne Status-Check). if (!(await canAccessContract(req, res, contractId))) return; // Für Kundenportal-Benutzer: Lade E-Mails der erlaubten Kunden (mit Live-Vollmacht-Check) let customerPortalEmails: string[] | undefined; const allowedIds = await getPortalAllowedCustomerIds(req); if (allowedIds) { const customers = await customerService.getCustomersByIds(allowedIds); customerPortalEmails = customers .map((c: { id: number; portalEmail: string | null }) => c.portalEmail) .filter((email: string | null): email is string => !!email); } const tasks = await contractTaskService.getTasksByContract({ contractId, status: status as 'OPEN' | 'COMPLETED' | undefined, customerPortalEmails, }); res.json({ success: true, data: tasks } as ApiResponse); } catch (error) { res.status(500).json({ success: false, error: 'Fehler beim Laden der Aufgaben', } as ApiResponse); } } export async function createTask(req: AuthRequest, res: Response): Promise { try { const contractId = parseInt(req.params.contractId); const { title, description, visibleInPortal } = req.body; if (!title) { res.status(400).json({ success: false, error: 'Titel ist erforderlich', } as ApiResponse); return; } const createdBy = req.user?.email; // Für Kundenportal-Benutzer: visibleInPortal wird automatisch auf true gesetzt const finalVisibleInPortal = req.user?.isCustomerPortal ? true : visibleInPortal; const task = await contractTaskService.createTask({ contractId, title, description, visibleInPortal: finalVisibleInPortal, createdBy, }); await logChange({ req, action: 'CREATE', resourceType: 'ContractTask', resourceId: task.id.toString(), label: `Aufgabe "${title}" erstellt`, }); res.status(201).json({ success: true, data: task } as ApiResponse); } catch (error) { res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Aufgabe', } as ApiResponse); } } // Für Kundenportal-Benutzer: Support-Anfrage erstellen (ohne contracts:update Permission) export async function createSupportTicket(req: AuthRequest, res: Response): Promise { try { // Prüfe ob Support-Tickets aktiviert sind const supportEnabled = await appSettingService.getSettingBool('customerSupportTicketsEnabled'); if (!supportEnabled) { res.status(403).json({ success: false, error: 'Support-Anfragen sind nicht aktiviert', } as ApiResponse); return; } const contractId = parseInt(req.params.contractId); const { title, description } = req.body; if (!title) { res.status(400).json({ success: false, error: 'Titel ist erforderlich', } as ApiResponse); return; } // canAccessContract inkl. Live-Vollmacht-Prüfung (siehe getTasks). if (!(await canAccessContract(req, res, contractId))) return; const createdBy = req.user?.email; const task = await contractTaskService.createTask({ contractId, title, description, visibleInPortal: true, // Immer sichtbar im Portal createdBy, }); await logChange({ req, action: 'CREATE', resourceType: 'ContractTask', resourceId: task.id.toString(), label: `Support-Anfrage "${title}" erstellt`, }); res.status(201).json({ success: true, data: task } as ApiResponse); } catch (error) { res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Support-Anfrage', } as ApiResponse); } } export async function updateTask(req: AuthRequest, res: Response): Promise { try { const taskId = parseInt(req.params.taskId); const { title, description, visibleInPortal } = req.body; const task = await contractTaskService.updateTask(taskId, { title, description, visibleInPortal, }); await logChange({ req, action: 'UPDATE', resourceType: 'ContractTask', resourceId: taskId.toString(), label: `Aufgabe aktualisiert`, }); res.json({ success: true, data: task } as ApiResponse); } catch (error) { res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Aufgabe', } as ApiResponse); } } export async function completeTask(req: AuthRequest, res: Response): Promise { try { const taskId = parseInt(req.params.taskId); const task = await contractTaskService.completeTask(taskId); await logChange({ req, action: 'UPDATE', resourceType: 'ContractTask', resourceId: taskId.toString(), label: `Aufgabe abgeschlossen`, }); res.json({ success: true, data: task } as ApiResponse); } catch (error) { res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Fehler beim Abschließen der Aufgabe', } as ApiResponse); } } export async function reopenTask(req: AuthRequest, res: Response): Promise { try { const taskId = parseInt(req.params.taskId); const task = await contractTaskService.reopenTask(taskId); await logChange({ req, action: 'UPDATE', resourceType: 'ContractTask', resourceId: taskId.toString(), label: `Aufgabe wiedereröffnet`, }); res.json({ success: true, data: task } as ApiResponse); } catch (error) { res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Fehler beim Wiedereröffnen der Aufgabe', } as ApiResponse); } } export async function deleteTask(req: AuthRequest, res: Response): Promise { try { const taskId = parseInt(req.params.taskId); await contractTaskService.deleteTask(taskId); await logChange({ req, action: 'DELETE', resourceType: 'ContractTask', resourceId: taskId.toString(), label: `Aufgabe gelöscht`, }); res.json({ success: true, message: 'Aufgabe gelöscht' } as ApiResponse); } catch (error) { res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Fehler beim Löschen der Aufgabe', } as ApiResponse); } } // ==================== SUBTASKS ==================== export async function createSubtask(req: AuthRequest, res: Response): Promise { try { const taskId = parseInt(req.params.taskId); const { title } = req.body; if (!title) { res.status(400).json({ success: false, error: 'Titel ist erforderlich', } as ApiResponse); return; } const createdBy = req.user?.email; const subtask = await contractTaskService.createSubtask({ taskId, title, createdBy, }); await logChange({ req, action: 'CREATE', resourceType: 'ContractSubtask', resourceId: subtask.id.toString(), label: `Unteraufgabe "${title}" erstellt`, }); res.status(201).json({ success: true, data: subtask } as ApiResponse); } catch (error) { res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Unteraufgabe', } as ApiResponse); } } // Kundenportal: Antwort auf eigenes Ticket erstellen export async function createCustomerReply(req: AuthRequest, res: Response): Promise { try { const taskId = parseInt(req.params.taskId); const { title } = req.body; if (!title) { res.status(400).json({ success: false, error: 'Antwort ist erforderlich', } as ApiResponse); return; } // Hole den Task const task = await contractTaskService.getTaskById(taskId); if (!task) { res.status(404).json({ success: false, error: 'Anfrage nicht gefunden', } as ApiResponse); return; } if (!req.user?.isCustomerPortal || !req.user.customerId) { res.status(403).json({ success: false, error: 'Nur für Kundenportal-Benutzer', } as ApiResponse); return; } // Strikter Owner-Check über den Vertrag (mit Live-Vollmacht-Prüfung // via hasAuthorization, Pentest Runde 6 – HOCH-04). Damit kann ein // Portal-User keine fremde Task-ID mit visibleInPortal=true abgreifen. if (!(await canAccessContract(req, res, task.contractId))) return; // Zusätzlich: portal-User darf nur antworten, wenn die Task von ihm // initiiert wurde ODER explizit für ihn sichtbar markiert ist. const allowedCustomerIds = [req.user.customerId, ...(req.user.representedCustomerIds || [])]; const customers = await customerService.getCustomersByIds(allowedCustomerIds); const allowedEmails = customers .map((c: { id: number; portalEmail: string | null }) => c.portalEmail) .filter((email: string | null): email is string => !!email); const isOwnTask = task.createdBy && allowedEmails.includes(task.createdBy); if (!task.visibleInPortal && !isOwnTask) { res.status(403).json({ success: false, error: 'Kein Zugriff auf diese Anfrage', } as ApiResponse); return; } const createdBy = req.user?.email; const subtask = await contractTaskService.createSubtask({ taskId, title, createdBy, }); await logChange({ req, action: 'CREATE', resourceType: 'ContractSubtask', resourceId: subtask.id.toString(), label: `Kundenantwort erstellt`, }); res.status(201).json({ success: true, data: subtask } as ApiResponse); } catch (error) { res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Antwort', } as ApiResponse); } } export async function updateSubtask(req: AuthRequest, res: Response): Promise { try { const subtaskId = parseInt(req.params.subtaskId); const { title } = req.body; if (!title) { res.status(400).json({ success: false, error: 'Titel ist erforderlich', } as ApiResponse); return; } const subtask = await contractTaskService.updateSubtask(subtaskId, { title }); await logChange({ req, action: 'UPDATE', resourceType: 'ContractSubtask', resourceId: subtaskId.toString(), label: `Unteraufgabe aktualisiert`, }); res.json({ success: true, data: subtask } as ApiResponse); } catch (error) { res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Unteraufgabe', } as ApiResponse); } } export async function completeSubtask(req: AuthRequest, res: Response): Promise { try { const subtaskId = parseInt(req.params.subtaskId); const subtask = await contractTaskService.completeSubtask(subtaskId); await logChange({ req, action: 'UPDATE', resourceType: 'ContractSubtask', resourceId: subtaskId.toString(), label: `Unteraufgabe abgeschlossen`, }); res.json({ success: true, data: subtask } as ApiResponse); } catch (error) { res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Fehler beim Abschließen der Unteraufgabe', } as ApiResponse); } } export async function reopenSubtask(req: AuthRequest, res: Response): Promise { try { const subtaskId = parseInt(req.params.subtaskId); const subtask = await contractTaskService.reopenSubtask(subtaskId); await logChange({ req, action: 'UPDATE', resourceType: 'ContractSubtask', resourceId: subtaskId.toString(), label: `Unteraufgabe wiedereröffnet`, }); res.json({ success: true, data: subtask } as ApiResponse); } catch (error) { res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Fehler beim Wiedereröffnen der Unteraufgabe', } as ApiResponse); } } export async function deleteSubtask(req: AuthRequest, res: Response): Promise { try { const subtaskId = parseInt(req.params.subtaskId); await contractTaskService.deleteSubtask(subtaskId); await logChange({ req, action: 'DELETE', resourceType: 'ContractSubtask', resourceId: subtaskId.toString(), label: `Unteraufgabe gelöscht`, }); res.json({ success: true, message: 'Unteraufgabe gelöscht' } as ApiResponse); } catch (error) { res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Fehler beim Löschen der Unteraufgabe', } as ApiResponse); } }