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

@@ -222,39 +222,99 @@ class TicketModel {
];
}
public function updateTicket(array $ticketData, ?int $updatedBy = null): bool {
$sql = "UPDATE tickets SET
title = ?,
priority = ?,
status = ?,
description = ?,
category = ?,
type = ?,
updated_by = ?,
updated_at = NOW()
WHERE ticket_id = ?";
/**
* Update a ticket with optional optimistic locking
*
* @param array $ticketData Ticket data including ticket_id
* @param int|null $updatedBy User ID performing the update
* @param string|null $expectedUpdatedAt If provided, update will fail if ticket was modified since this timestamp
* @return array ['success' => bool, 'error' => string|null, 'conflict' => bool]
*/
public function updateTicket(array $ticketData, ?int $updatedBy = null, ?string $expectedUpdatedAt = null): array {
// Build query with optional optimistic locking
if ($expectedUpdatedAt !== null) {
// Optimistic locking enabled - check that updated_at hasn't changed
$sql = "UPDATE tickets SET
title = ?,
priority = ?,
status = ?,
description = ?,
category = ?,
type = ?,
updated_by = ?,
updated_at = NOW()
WHERE ticket_id = ? AND updated_at = ?";
} else {
// No optimistic locking
$sql = "UPDATE tickets SET
title = ?,
priority = ?,
status = ?,
description = ?,
category = ?,
type = ?,
updated_by = ?,
updated_at = NOW()
WHERE ticket_id = ?";
}
$stmt = $this->conn->prepare($sql);
if (!$stmt) {
return false;
return ['success' => false, 'error' => 'Failed to prepare statement', 'conflict' => false];
}
$stmt->bind_param(
"sissssii",
$ticketData['title'],
$ticketData['priority'],
$ticketData['status'],
$ticketData['description'],
$ticketData['category'],
$ticketData['type'],
$updatedBy,
$ticketData['ticket_id']
);
if ($expectedUpdatedAt !== null) {
$stmt->bind_param(
"sissssiis",
$ticketData['title'],
$ticketData['priority'],
$ticketData['status'],
$ticketData['description'],
$ticketData['category'],
$ticketData['type'],
$updatedBy,
$ticketData['ticket_id'],
$expectedUpdatedAt
);
} else {
$stmt->bind_param(
"sissssii",
$ticketData['title'],
$ticketData['priority'],
$ticketData['status'],
$ticketData['description'],
$ticketData['category'],
$ticketData['type'],
$updatedBy,
$ticketData['ticket_id']
);
}
$result = $stmt->execute();
$affectedRows = $stmt->affected_rows;
$stmt->close();
return $result;
if (!$result) {
return ['success' => false, 'error' => 'Database error: ' . $this->conn->error, 'conflict' => false];
}
// Check for optimistic locking conflict
if ($expectedUpdatedAt !== null && $affectedRows === 0) {
// Either ticket doesn't exist or was modified by someone else
$ticket = $this->getTicketById($ticketData['ticket_id']);
if ($ticket) {
return [
'success' => false,
'error' => 'This ticket was modified by another user. Please refresh and try again.',
'conflict' => true,
'current_updated_at' => $ticket['updated_at']
];
} else {
return ['success' => false, 'error' => 'Ticket not found', 'conflict' => false];
}
}
return ['success' => true, 'error' => null, 'conflict' => false];
}
public function createTicket(array $ticketData, ?int $createdBy = null): array {