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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MGDKHiU5RJdo3dqQUDow3X
99 lines
4.3 KiB
PHP
99 lines
4.3 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Assigning / unassigning a ticket: access check, permission (admin, creator,
|
|
* or current assignee), audit log, optional Matrix assignment notification,
|
|
* stats-cache invalidation.
|
|
*
|
|
* Shared by the web UI (api/assign_ticket.php) and the MCP assign_ticket tool
|
|
* so both run one code path (tinker_tickets#111). Extracted verbatim from
|
|
* assign_ticket.php. Returns result arrays; failures carry 'http_status'.
|
|
*/
|
|
|
|
require_once dirname(__DIR__) . '/models/TicketModel.php';
|
|
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
|
|
require_once dirname(__DIR__) . '/models/UserModel.php';
|
|
require_once dirname(__DIR__) . '/models/StatsModel.php';
|
|
require_once dirname(__DIR__) . '/helpers/NotificationHelper.php';
|
|
require_once dirname(__DIR__) . '/helpers/SynapseHelper.php';
|
|
|
|
class AssignmentService
|
|
{
|
|
/**
|
|
* @param array $currentUser Authenticated user row (user_id, username, display_name, is_admin, ...)
|
|
* @param array $data ticket_id, assigned_to (user_id; null/'' to unassign)
|
|
*/
|
|
public static function assign(mysqli $conn, array $currentUser, array $data): array
|
|
{
|
|
$userId = $currentUser['user_id'];
|
|
$isAdmin = $currentUser['is_admin'] ?? false;
|
|
|
|
$ticketIdRaw = isset($data['ticket_id']) ? trim((string)$data['ticket_id']) : '';
|
|
$assignedTo = $data['assigned_to'] ?? null;
|
|
|
|
if (!ctype_digit($ticketIdRaw) || (int)$ticketIdRaw <= 0) {
|
|
return ['success' => 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];
|
|
}
|
|
}
|