From 9e462f7f006eaee0ce415a6b0e3e8bf8ce385560 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Thu, 24 Sep 2026 19:13:31 -0400 Subject: [PATCH] Extract ticket assignment from assign_ticket.php into AssignmentService (#111) The access check, the admin/creator/current-assignee permission rule, unassign/assign, the audit log, the optional Matrix assignment notification and the stats-cache invalidation move into services/AssignmentService.php for reuse by the MCP assign_ticket tool. Error messages and status codes are unchanged. One deliberate difference: assign_ticket.php's early error responses (400/403/404) used a bare echo and so omitted the CSRF token that bootstrap had just rotated. Every response now goes through apiRespond(), which includes it. The front end already resyncs its token from any response body, so this is compatible, and a failed assign can no longer leave the page holding a stale token. Verified over real HTTP: the assignee can reassign (200); a user who can see the ticket but isn't admin/creator/assignee gets 403 'Permission denied'. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MGDKHiU5RJdo3dqQUDow3X --- api/assign_ticket.php | 88 ++++-------------------------- services/AssignmentService.php | 98 ++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 79 deletions(-) create mode 100644 services/AssignmentService.php diff --git a/api/assign_ticket.php b/api/assign_ticket.php index 1046909..28ced6c 100644 --- a/api/assign_ticket.php +++ b/api/assign_ticket.php @@ -1,11 +1,7 @@ false, 'error' => 'Ticket ID required']); - exit; -} -$ticketId = $ticketIdRaw; - -$ticketModel = new TicketModel($conn); -$auditLogModel = new AuditLogModel($conn); -$userModel = new UserModel($conn); - -// Verify ticket exists and user can access it -$ticket = $ticketModel->getTicketById($ticketId); -if (!$ticket || !$ticketModel->canUserAccessTicket($ticket, $currentUser)) { - http_response_code(404); - echo json_encode(['success' => false, 'error' => 'Ticket not found']); - exit; -} - -// Authorization: only admins or the ticket creator/assignee can reassign -if (!$isAdmin && (int)$ticket['created_by'] !== (int)$userId && (int)$ticket['assigned_to'] !== (int)$userId) { - http_response_code(403); - echo json_encode(['success' => false, 'error' => 'Permission denied']); - exit; -} - -if ($assignedTo === null || $assignedTo === '') { - // Unassign ticket - $success = $ticketModel->unassignTicket($ticketId, $userId); - if ($success) { - $auditLogModel->log($userId, 'unassign', 'ticket', $ticketId); - } -} else { - // Validate assigned_to is a valid user ID - $assignedTo = (int)$assignedTo; - $targetUser = $userModel->getUserById($assignedTo); - if (!$targetUser) { - http_response_code(400); - echo json_encode(['success' => false, 'error' => 'Invalid user ID']); - exit; - } - - // Assign ticket - $success = $ticketModel->assignTicket($ticketId, $assignedTo, $userId); - if ($success) { - $auditLogModel->log($userId, 'assign', 'ticket', $ticketId, ['assigned_to' => $assignedTo]); - - if (!empty($GLOBALS['config']['MATRIX_NOTIFY_ASSIGNMENTS'])) { - $changedByDisplay = $currentUser['display_name'] ?? $currentUser['username'] ?? null; - $assigneeName = $targetUser['display_name'] ?? $targetUser['username'] ?? null; - $assigneeMatrix = isset($targetUser['username']) - ? SynapseHelper::resolveUsername($targetUser['username']) - : null; - NotificationHelper::sendAssignmentNotification( - $ticketId, - $ticket['title'] ?? "Ticket #{$ticketId}", - $assigneeName, - $assigneeMatrix, - $changedByDisplay, - $ticket['visibility'] ?? 'public' - ); - } - } -} - -if (!$success) { - http_response_code(500); - apiRespond(['success' => false, 'error' => 'Failed to update ticket assignment']); -} else { - require_once dirname(__DIR__) . '/models/StatsModel.php'; - (new StatsModel($conn))->invalidateCache(); - apiRespond(['success' => true]); +if (!empty($result['http_status'])) { + http_response_code($result['http_status']); + unset($result['http_status']); } +apiRespond($result); diff --git a/services/AssignmentService.php b/services/AssignmentService.php new file mode 100644 index 0000000..706fcba --- /dev/null +++ b/services/AssignmentService.php @@ -0,0 +1,98 @@ + false, 'error' => 'Ticket ID required', 'http_status' => 400]; + } + $ticketId = $ticketIdRaw; + + $ticketModel = new TicketModel($conn); + $auditLogModel = new AuditLogModel($conn); + $userModel = new UserModel($conn); + + // Verify ticket exists and user can access it + $ticket = $ticketModel->getTicketById($ticketId); + if (!$ticket || !$ticketModel->canUserAccessTicket($ticket, $currentUser)) { + return ['success' => false, 'error' => 'Ticket not found', 'http_status' => 404]; + } + + // Authorization: only admins or the ticket creator/assignee can reassign + if (!$isAdmin && (int)$ticket['created_by'] !== (int)$userId && (int)$ticket['assigned_to'] !== (int)$userId) { + return ['success' => false, 'error' => 'Permission denied', 'http_status' => 403]; + } + + if ($assignedTo === null || $assignedTo === '') { + // Unassign ticket + $success = $ticketModel->unassignTicket($ticketId, $userId); + if ($success) { + $auditLogModel->log($userId, 'unassign', 'ticket', $ticketId); + } + } else { + // Validate assigned_to is a valid user ID + $assignedTo = (int)$assignedTo; + $targetUser = $userModel->getUserById($assignedTo); + if (!$targetUser) { + return ['success' => false, 'error' => 'Invalid user ID', 'http_status' => 400]; + } + + // Assign ticket + $success = $ticketModel->assignTicket($ticketId, $assignedTo, $userId); + if ($success) { + $auditLogModel->log($userId, 'assign', 'ticket', $ticketId, ['assigned_to' => $assignedTo]); + + if (!empty($GLOBALS['config']['MATRIX_NOTIFY_ASSIGNMENTS'])) { + $changedByDisplay = $currentUser['display_name'] ?? $currentUser['username'] ?? null; + $assigneeName = $targetUser['display_name'] ?? $targetUser['username'] ?? null; + $assigneeMatrix = isset($targetUser['username']) + ? SynapseHelper::resolveUsername($targetUser['username']) + : null; + NotificationHelper::sendAssignmentNotification( + $ticketId, + $ticket['title'] ?? "Ticket #{$ticketId}", + $assigneeName, + $assigneeMatrix, + $changedByDisplay, + $ticket['visibility'] ?? 'public' + ); + } + } + } + + if (!$success) { + return ['success' => false, 'error' => 'Failed to update ticket assignment', 'http_status' => 500]; + } + + (new StatsModel($conn))->invalidateCache(); + return ['success' => true]; + } +}