Add performance, security, and reliability improvements

- Consolidate all 20 API files to use centralized Database helper
- Add optimistic locking to ticket updates to prevent concurrent conflicts
- Add caching to StatsModel (60s TTL) for dashboard performance
- Add health check endpoint (api/health.php) for monitoring
- Improve rate limit cleanup with cron script and efficient DirectoryIterator
- Enable rate limit response headers (X-RateLimit-*)
- Add audit logging for workflow transitions
- Log Discord webhook failures instead of silencing
- Fix visibility check on export_tickets.php
- Add database migration system with performance indexes
- Fix cron recurring tickets to use assignTicket method

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 14:39:13 -05:00
parent c3f7593f3c
commit 7575d6a277
31 changed files with 825 additions and 398 deletions

View File

@@ -14,6 +14,7 @@ try {
// Load config
$configPath = dirname(__DIR__) . '/config/config.php';
require_once $configPath;
require_once dirname(__DIR__) . '/helpers/Database.php';
// Load environment variables (for Discord webhook)
$envPath = dirname(__DIR__) . '/.env';
@@ -141,11 +142,25 @@ try {
}
}
// Update ticket with user tracking
$result = $this->ticketModel->updateTicket($updateData, $this->userId);
// Update ticket with user tracking and optional optimistic locking
$expectedUpdatedAt = $data['expected_updated_at'] ?? null;
$result = $this->ticketModel->updateTicket($updateData, $this->userId, $expectedUpdatedAt);
// Handle conflict case
if (!$result['success']) {
$response = [
'success' => false,
'error' => $result['error'] ?? 'Failed to update ticket in database'
];
if (!empty($result['conflict'])) {
$response['conflict'] = true;
$response['current_updated_at'] = $result['current_updated_at'] ?? null;
}
return $response;
}
// Handle visibility update if provided
if ($result && isset($data['visibility'])) {
if (isset($data['visibility'])) {
$visibilityGroups = $data['visibility_groups'] ?? null;
// Convert array to comma-separated string if needed
if (is_array($visibilityGroups)) {
@@ -163,27 +178,20 @@ try {
$this->ticketModel->updateVisibility($id, $data['visibility'], $visibilityGroups, $this->userId);
}
if ($result) {
// Log ticket update to audit log
if ($this->userId) {
$this->auditLog->logTicketUpdate($this->userId, $id, $data);
}
// Discord webhook disabled for updates - only send for new tickets
// $this->sendDiscordWebhook($id, $currentTicket, $updateData, $data);
return [
'success' => true,
'status' => $updateData['status'],
'priority' => $updateData['priority'],
'message' => 'Ticket updated successfully'
];
} else {
return [
'success' => false,
'error' => 'Failed to update ticket in database'
];
// Log ticket update to audit log
if ($this->userId) {
$this->auditLog->logTicketUpdate($this->userId, $id, $data);
}
// Discord webhook disabled for updates - only send for new tickets
// $this->sendDiscordWebhook($id, $currentTicket, $updateData, $data);
return [
'success' => true,
'status' => $updateData['status'],
'priority' => $updateData['priority'],
'message' => 'Ticket updated successfully'
];
}
private function sendDiscordWebhook($ticketId, $oldData, $newData, $changedFields) {
@@ -260,22 +268,18 @@ try {
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
// Silently handle errors - webhook is optional
// Log webhook errors instead of silencing them
if ($curlError) {
error_log("Discord webhook cURL error for ticket #{$ticketId}: {$curlError}");
} elseif ($httpCode !== 204 && $httpCode !== 200) {
error_log("Discord webhook failed for ticket #{$ticketId}. HTTP Code: {$httpCode}, Response: " . substr($webhookResult, 0, 200));
}
}
}
// Create database connection
$conn = new mysqli(
$GLOBALS['config']['DB_HOST'],
$GLOBALS['config']['DB_USER'],
$GLOBALS['config']['DB_PASS'],
$GLOBALS['config']['DB_NAME']
);
if ($conn->connect_error) {
throw new Exception("Database connection failed: " . $conn->connect_error);
}
// Use centralized database connection
$conn = Database::getConnection();
// Check request method
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
@@ -302,9 +306,6 @@ try {
// Update ticket
$result = $controller->update($ticketId, $data);
// Close database connection
$conn->close();
// Discard any output that might have been generated
ob_end_clean();