Lint / PHP (phpcs PSR-12) (push) Successful in 41s
Lint / JS (eslint) (push) Successful in 11s
Lint / PHP requirements (version + extensions) (push) Successful in 44s
Security / PHP Security (semgrep) (push) Successful in 2m47s
Lint / Deploy (push) Successful in 2s
Lint / Notify on failure (push) Has been skipped
Extends the Bearer-key API beyond create-only (all rate-limited, scope- enforced, per-key-label attribution): - GET /api/tickets_api.php: triage the queue (status/priority/host title match + pagination) or read one ticket + its comments. read scope. - POST /api/ticket_comment_api.php: post a comment as the key (user_name = key name, linked to the key owner). read_write scope. - POST /api/ticket_status_api.php: change/close status with workflow validation + requires_comment; posts the close reason in the same call, fires the Matrix status notification, invalidates stats. read_write scope. Reuses TicketModel/CommentModel/WorkflowModel/NotificationHelper; a read key cannot mutate. Reachability requires the reverse-proxy Authelia bypass (handled separately). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
126 lines
3.8 KiB
PHP
126 lines
3.8 KiB
PHP
<?php
|
|
|
|
/**
|
|
* ticket_comment_api.php — Bearer-key endpoint to post a comment on a ticket.
|
|
*
|
|
* POST only. Requires 'read_write' scope.
|
|
*
|
|
* Identity = PER-KEY LABEL: the comment author (ticket_comments.user_name) is the
|
|
* API key's key_name and the linked user_id is the key's created_by.
|
|
*
|
|
* Body (JSON): {
|
|
* "ticket_id": "NNN" (required),
|
|
* "comment_text": "..." (required, non-empty),
|
|
* "markdown_enabled": bool (optional)
|
|
* }
|
|
* Response: {success:true, comment_id:...}
|
|
*/
|
|
|
|
header('Content-Type: application/json');
|
|
|
|
error_reporting(E_ALL);
|
|
ini_set('display_errors', 0);
|
|
|
|
require_once dirname(__DIR__) . '/middleware/RateLimitMiddleware.php';
|
|
RateLimitMiddleware::apply('api');
|
|
|
|
require_once dirname(__DIR__) . '/config/config.php';
|
|
require_once dirname(__DIR__) . '/helpers/Database.php';
|
|
require_once dirname(__DIR__) . '/middleware/ApiKeyAuth.php';
|
|
require_once dirname(__DIR__) . '/models/TicketModel.php';
|
|
require_once dirname(__DIR__) . '/models/CommentModel.php';
|
|
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
|
|
|
|
try {
|
|
$conn = Database::getConnection();
|
|
} catch (Throwable $e) {
|
|
error_log('ticket_comment_api: DB connection failed: ' . $e->getMessage());
|
|
http_response_code(500);
|
|
echo json_encode(['success' => false, 'error' => 'Internal server error']);
|
|
exit;
|
|
}
|
|
|
|
$apiKeyAuth = new ApiKeyAuth($conn);
|
|
|
|
try {
|
|
$apiKeyAuth->authenticate();
|
|
} catch (Exception $e) {
|
|
// ApiKeyAuth already sent the 401 response.
|
|
exit;
|
|
}
|
|
|
|
// Posting a comment is a write — reject 'read' keys with 403 before any mutation.
|
|
$apiKeyAuth->requireScope('read_write');
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
http_response_code(405);
|
|
echo json_encode(['success' => false, 'error' => 'Method not allowed. Use POST.']);
|
|
exit;
|
|
}
|
|
|
|
$context = $apiKeyAuth->getKeyContext();
|
|
$keyName = $context['key_name'] ?? 'API';
|
|
$createdBy = ($context['created_by'] ?? null) !== null ? (int)$context['created_by'] : null;
|
|
|
|
$rawInput = file_get_contents('php://input');
|
|
$data = json_decode($rawInput, true);
|
|
if (!is_array($data)) {
|
|
http_response_code(400);
|
|
echo json_encode(['success' => false, 'error' => 'Invalid JSON body']);
|
|
exit;
|
|
}
|
|
|
|
$ticketId = isset($data['ticket_id']) ? trim((string)$data['ticket_id']) : '';
|
|
if ($ticketId === '') {
|
|
http_response_code(400);
|
|
echo json_encode(['success' => false, 'error' => 'ticket_id is required']);
|
|
exit;
|
|
}
|
|
|
|
$commentText = isset($data['comment_text']) ? trim((string)$data['comment_text']) : '';
|
|
if ($commentText === '') {
|
|
http_response_code(400);
|
|
echo json_encode(['success' => false, 'error' => 'comment_text is required']);
|
|
exit;
|
|
}
|
|
|
|
$markdownEnabled = !empty($data['markdown_enabled']);
|
|
|
|
// Validate the ticket exists.
|
|
$ticketModel = new TicketModel($conn);
|
|
$ticket = $ticketModel->getTicketById($ticketId);
|
|
if (!$ticket) {
|
|
http_response_code(404);
|
|
echo json_encode(['success' => false, 'error' => 'Ticket not found']);
|
|
exit;
|
|
}
|
|
|
|
// Post the comment under the key's label / owner.
|
|
$commentModel = new CommentModel($conn);
|
|
$result = $commentModel->addComment($ticketId, [
|
|
'user_name' => $keyName,
|
|
'comment_text' => $commentText,
|
|
'markdown_enabled' => $markdownEnabled,
|
|
], $createdBy);
|
|
|
|
if (empty($result['success'])) {
|
|
error_log('ticket_comment_api: addComment failed for ticket ' . $ticketId
|
|
. ': ' . ($result['error'] ?? 'unknown'));
|
|
http_response_code(500);
|
|
echo json_encode(['success' => false, 'error' => 'Failed to add comment']);
|
|
exit;
|
|
}
|
|
|
|
$commentId = $result['comment_id'] ?? null;
|
|
|
|
// Audit trail (action 'comment' / entity 'comment' are both whitelisted).
|
|
$auditLog = new AuditLogModel($conn);
|
|
$auditLog->log($createdBy, 'comment', 'comment', (string)$commentId, [
|
|
'ticket_id' => $ticketId,
|
|
'key_name' => $keyName,
|
|
'via_api' => true,
|
|
]);
|
|
|
|
echo json_encode(['success' => true, 'comment_id' => $commentId]);
|
|
exit;
|