Files
tinker_tickets/api/tickets_api.php
T

140 lines
4.2 KiB
PHP
Raw Normal View History

<?php
/**
* tickets_api.php — Bearer-key read endpoint (list/triage + read-one).
*
* GET only. Requires 'read' scope (a 'read_write' key also satisfies it).
* Acts as a trusted automation/server credential: reads return the full queue
* (no per-user visibility filtering).
*
* GET ?ticket_id=NNN -> {success, ticket, comments}
* GET ?status=&priority=&host= -> {success, tickets, page, total, pages}
* &page=&limit=
*/
header('Content-Type: application/json');
error_reporting(E_ALL);
ini_set('display_errors', 0);
// Rate limiting (same pattern as the other Bearer API endpoints)
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';
try {
$conn = Database::getConnection();
} catch (Throwable $e) {
error_log('tickets_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;
}
// Reads only need the 'read' scope.
$apiKeyAuth->requireScope('read');
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Method not allowed. Use GET.']);
exit;
}
$ticketModel = new TicketModel($conn);
// ── READ ONE ──────────────────────────────────────────────────────────────
if (isset($_GET['ticket_id']) && trim((string)$_GET['ticket_id']) !== '') {
$ticketId = trim((string)$_GET['ticket_id']);
$ticket = $ticketModel->getTicketById($ticketId);
if (!$ticket) {
http_response_code(404);
echo json_encode(['success' => false, 'error' => 'Ticket not found']);
exit;
}
// Flat list of comments (newest first) — same fetch the ticket view uses.
$commentModel = new CommentModel($conn);
$comments = $commentModel->getCommentsByTicketId($ticketId, false);
echo json_encode([
'success' => true,
'ticket' => $ticket,
'comments' => $comments,
]);
exit;
}
// ── LIST / TRIAGE ───────────────────────────────────────────────────────────
$status = (isset($_GET['status']) && trim((string)$_GET['status']) !== '')
? trim((string)$_GET['status'])
: 'Open';
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
if ($page < 1) {
$page = 1;
}
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 25;
if ($limit < 1) {
$limit = 25;
}
if ($limit > 100) {
$limit = 100; // cap
}
$filters = [];
if (isset($_GET['priority']) && trim((string)$_GET['priority']) !== '') {
$priority = (int)$_GET['priority'];
if ($priority >= 1 && $priority <= 5) {
// Exact-priority match via the min/max range filter.
$filters['priority_min'] = $priority;
$filters['priority_max'] = $priority;
}
}
// hwmon puts the host in the title (e.g. "[hostname] ..."), so a host filter is a
// title substring match — served by getAllTickets's `search` param (title search).
$search = null;
if (isset($_GET['host']) && trim((string)$_GET['host']) !== '') {
$search = trim((string)$_GET['host']);
}
// user = null => getAllTickets skips visibility filtering and returns the full
// queue (this is a trusted server credential, not an end user).
$result = $ticketModel->getAllTickets(
$page,
$limit,
$status,
'ticket_id',
'desc',
null,
null,
$search,
$filters,
null
);
echo json_encode([
'success' => true,
'tickets' => $result['tickets'],
'page' => $result['current_page'],
'total' => $result['total'],
'pages' => $result['pages'],
]);
exit;