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

@@ -27,6 +27,7 @@ try {
require_once $configPath;
require_once $commentModelPath;
require_once $auditLogModelPath;
require_once dirname(__DIR__) . '/helpers/Database.php';
// Check authentication via session
session_start();
@@ -49,17 +50,8 @@ try {
$currentUser = $_SESSION['user'];
$userId = $currentUser['user_id'];
// 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();
// Get POST data
$data = json_decode(file_get_contents('php://input'), true);

View File

@@ -5,6 +5,7 @@ RateLimitMiddleware::apply('api');
session_start();
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/helpers/Database.php';
require_once dirname(__DIR__) . '/models/TicketModel.php';
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
require_once dirname(__DIR__) . '/models/UserModel.php';
@@ -40,18 +41,8 @@ if (!$ticketId) {
exit;
}
// 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) {
echo json_encode(['success' => false, 'error' => 'Database connection failed']);
exit;
}
// Use centralized database connection
$conn = Database::getConnection();
$ticketModel = new TicketModel($conn);
$auditLogModel = new AuditLogModel($conn);
@@ -69,7 +60,6 @@ if ($assignedTo === null || $assignedTo === '') {
$targetUser = $userModel->getUserById($assignedTo);
if (!$targetUser) {
echo json_encode(['success' => false, 'error' => 'Invalid user ID']);
$conn->close();
exit;
}
@@ -80,6 +70,4 @@ if ($assignedTo === null || $assignedTo === '') {
}
}
$conn->close();
echo json_encode(['success' => $success]);

View File

@@ -6,6 +6,7 @@
*/
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/helpers/Database.php';
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
session_start();
@@ -26,19 +27,8 @@ if (!$isAdmin) {
exit;
}
// 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) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => 'Database connection failed']);
exit;
}
// Use centralized database connection
$conn = Database::getConnection();
$auditLogModel = new AuditLogModel($conn);
@@ -90,7 +80,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
}
fclose($output);
$conn->close();
exit;
}

View File

@@ -5,6 +5,7 @@ RateLimitMiddleware::apply('api');
session_start();
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/helpers/Database.php';
require_once dirname(__DIR__) . '/models/BulkOperationsModel.php';
require_once dirname(__DIR__) . '/models/TicketModel.php';
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
@@ -55,21 +56,8 @@ foreach ($ticketIds as $ticketId) {
}
}
// Create database connection (needed for visibility check)
require_once dirname(__DIR__) . '/models/TicketModel.php';
// 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) {
echo json_encode(['success' => false, 'error' => 'Database connection failed']);
exit;
}
// Use centralized database connection
$conn = Database::getConnection();
$bulkOpsModel = new BulkOperationsModel($conn);
$ticketModel = new TicketModel($conn);
@@ -92,7 +80,6 @@ foreach ($ticketIds as $ticketId) {
}
if (empty($accessibleTicketIds)) {
$conn->close();
echo json_encode(['success' => false, 'error' => 'No accessible tickets in selection']);
exit;
}
@@ -104,7 +91,6 @@ $ticketIds = $accessibleTicketIds;
$operationId = $bulkOpsModel->createBulkOperation($operationType, $ticketIds, $_SESSION['user']['user_id'], $parameters);
if (!$operationId) {
$conn->close();
echo json_encode(['success' => false, 'error' => 'Failed to create bulk operation']);
exit;
}

View File

@@ -11,6 +11,7 @@ RateLimitMiddleware::apply('api');
session_start();
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/helpers/Database.php';
require_once dirname(__DIR__) . '/helpers/ResponseHelper.php';
header('Content-Type: application/json');
@@ -32,17 +33,8 @@ if (strlen($title) < 5) {
ResponseHelper::success(['duplicates' => []]);
}
// 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) {
ResponseHelper::serverError('Database connection failed');
}
// Use centralized database connection
$conn = Database::getConnection();
// Search for similar titles
// Use both LIKE for substring matching and SOUNDEX for phonetic matching
@@ -112,6 +104,4 @@ usort($duplicates, function($a, $b) {
// Limit to top 5
$duplicates = array_slice($duplicates, 0, 5);
$conn->close();
ResponseHelper::success(['duplicates' => $duplicates]);

View File

@@ -12,6 +12,7 @@ RateLimitMiddleware::apply('api');
try {
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/helpers/Database.php';
require_once dirname(__DIR__) . '/models/CustomFieldModel.php';
// Check authentication
@@ -40,16 +41,8 @@ try {
}
}
$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");
}
// Use centralized database connection
$conn = Database::getConnection();
header('Content-Type: application/json');
@@ -103,8 +96,6 @@ try {
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
}
$conn->close();
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);

View File

@@ -19,6 +19,7 @@ if (session_status() === PHP_SESSION_NONE) {
}
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/helpers/Database.php';
require_once dirname(__DIR__) . '/helpers/ResponseHelper.php';
require_once dirname(__DIR__) . '/models/AttachmentModel.php';
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
@@ -87,27 +88,19 @@ try {
}
// Log the deletion
$conn = new mysqli(
$GLOBALS['config']['DB_HOST'],
$GLOBALS['config']['DB_USER'],
$GLOBALS['config']['DB_PASS'],
$GLOBALS['config']['DB_NAME']
$conn = Database::getConnection();
$auditLog = new AuditLogModel($conn);
$auditLog->log(
$_SESSION['user']['user_id'],
'attachment_delete',
'ticket_attachments',
(string)$attachmentId,
[
'ticket_id' => $attachment['ticket_id'],
'filename' => $attachment['original_filename'],
'size' => $attachment['file_size']
]
);
if (!$conn->connect_error) {
$auditLog = new AuditLogModel($conn);
$auditLog->log(
$_SESSION['user']['user_id'],
'attachment_delete',
'ticket_attachments',
(string)$attachmentId,
[
'ticket_id' => $attachment['ticket_id'],
'filename' => $attachment['original_filename'],
'size' => $attachment['file_size']
]
);
$conn->close();
}
ResponseHelper::success([], 'Attachment deleted successfully');

View File

@@ -16,6 +16,7 @@ ob_start();
try {
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/helpers/Database.php';
require_once dirname(__DIR__) . '/models/CommentModel.php';
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
@@ -39,17 +40,8 @@ try {
$userId = $currentUser['user_id'];
$isAdmin = $currentUser['is_admin'] ?? false;
// 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");
}
// Use centralized database connection
$conn = Database::getConnection();
// Get data - support both POST body and query params
$data = json_decode(file_get_contents('php://input'), true);

View File

@@ -7,6 +7,7 @@
session_start();
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/helpers/Database.php';
require_once dirname(__DIR__) . '/models/AttachmentModel.php';
require_once dirname(__DIR__) . '/models/TicketModel.php';
@@ -42,24 +43,12 @@ try {
}
// Verify the associated ticket exists and user has access
$conn = new mysqli(
$GLOBALS['config']['DB_HOST'],
$GLOBALS['config']['DB_USER'],
$GLOBALS['config']['DB_PASS'],
$GLOBALS['config']['DB_NAME']
);
if ($conn->connect_error) {
http_response_code(500);
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => 'Database connection failed']);
exit;
}
$conn = Database::getConnection();
$ticketModel = new TicketModel($conn);
$ticket = $ticketModel->getTicketById($attachment['ticket_id']);
if (!$ticket) {
$conn->close();
http_response_code(404);
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => 'Associated ticket not found']);
@@ -68,7 +57,6 @@ try {
// Check if user has access to this ticket based on visibility settings
if (!$ticketModel->canUserAccessTicket($ticket, $_SESSION['user'])) {
$conn->close();
http_response_code(403);
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => 'Access denied to this ticket']);

View File

@@ -3,6 +3,7 @@
* Export Tickets API
*
* Exports tickets to CSV format with optional filtering
* Respects ticket visibility settings
*/
// Disable error display in the output
@@ -16,6 +17,7 @@ RateLimitMiddleware::apply('api');
try {
// Include required files
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/helpers/Database.php';
require_once dirname(__DIR__) . '/models/TicketModel.php';
// Check authentication via session
@@ -29,17 +31,8 @@ try {
$currentUser = $_SESSION['user'];
// 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");
}
// Use centralized database connection
$conn = Database::getConnection();
// Get filter parameters
$status = isset($_GET['status']) ? $_GET['status'] : null;
@@ -64,11 +57,18 @@ try {
}
// Get specific tickets by IDs
$tickets = $ticketModel->getTicketsByIds($ticketIdArray);
// Convert associative array to indexed array
$tickets = array_values($tickets);
$allTickets = $ticketModel->getTicketsByIds($ticketIdArray);
// Filter tickets based on visibility - only export tickets the user can access
$tickets = [];
foreach ($allTickets as $ticket) {
if ($ticketModel->canUserAccessTicket($ticket, $currentUser)) {
$tickets[] = $ticket;
}
}
} else {
// Get all tickets with filters (no pagination for export)
// getAllTickets already applies visibility filtering via getVisibilityFilter
$result = $ticketModel->getAllTickets(1, 10000, $status, 'created_at', 'desc', $category, $type, $search);
$tickets = $result['tickets'];
}

View File

@@ -12,6 +12,7 @@ ob_start();
try {
// Load config
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/helpers/Database.php';
// Load models
require_once dirname(__DIR__) . '/models/ApiKeyModel.php';
@@ -71,17 +72,8 @@ try {
$expiresInDays = null;
}
// 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");
}
// Use centralized database connection
$conn = Database::getConnection();
// Generate API key
$apiKeyModel = new ApiKeyModel($conn);
@@ -101,8 +93,6 @@ try {
['key_name' => $keyName, 'expires_in_days' => $expiresInDays]
);
$conn->close();
// Clear output buffer
ob_end_clean();

110
api/health.php Normal file
View File

@@ -0,0 +1,110 @@
<?php
/**
* Health Check Endpoint
*
* Returns system health status for monitoring tools.
* Does not require authentication - suitable for load balancer health checks.
*
* Returns:
* - 200 OK: System is healthy
* - 503 Service Unavailable: System has issues
*/
// Don't apply rate limiting to health checks - they should always respond
header('Content-Type: application/json');
header('Cache-Control: no-cache, no-store, must-revalidate');
$startTime = microtime(true);
$checks = [];
$healthy = true;
// Check 1: Database connectivity
try {
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/helpers/Database.php';
$conn = Database::getConnection();
// Quick query to verify connection is actually working
$result = $conn->query('SELECT 1');
if ($result && $result->fetch_row()) {
$checks['database'] = [
'status' => 'ok',
'message' => 'Connected'
];
} else {
$checks['database'] = [
'status' => 'error',
'message' => 'Query failed'
];
$healthy = false;
}
} catch (Exception $e) {
$checks['database'] = [
'status' => 'error',
'message' => 'Connection failed'
];
$healthy = false;
}
// Check 2: File system (uploads directory writable)
$uploadDir = $GLOBALS['config']['UPLOAD_DIR'] ?? dirname(__DIR__) . '/uploads';
if (is_dir($uploadDir) && is_writable($uploadDir)) {
$checks['filesystem'] = [
'status' => 'ok',
'message' => 'Writable'
];
} else {
$checks['filesystem'] = [
'status' => 'warning',
'message' => 'Upload directory not writable'
];
// Don't mark as unhealthy - this might be intentional
}
// Check 3: Session storage
$sessionPath = session_save_path() ?: sys_get_temp_dir();
if (is_dir($sessionPath) && is_writable($sessionPath)) {
$checks['sessions'] = [
'status' => 'ok',
'message' => 'Writable'
];
} else {
$checks['sessions'] = [
'status' => 'error',
'message' => 'Session storage not writable'
];
$healthy = false;
}
// Check 4: Rate limit storage
$rateLimitDir = sys_get_temp_dir() . '/tinker_tickets_ratelimit';
if (!is_dir($rateLimitDir)) {
@mkdir($rateLimitDir, 0755, true);
}
if (is_dir($rateLimitDir) && is_writable($rateLimitDir)) {
$checks['rate_limit'] = [
'status' => 'ok',
'message' => 'Writable'
];
} else {
$checks['rate_limit'] = [
'status' => 'warning',
'message' => 'Rate limit storage not writable'
];
}
// Calculate response time
$responseTime = round((microtime(true) - $startTime) * 1000, 2);
// Set status code
http_response_code($healthy ? 200 : 503);
// Return response
echo json_encode([
'status' => $healthy ? 'healthy' : 'unhealthy',
'timestamp' => date('c'),
'response_time_ms' => $responseTime,
'checks' => $checks,
'version' => '1.0.0'
], JSON_PRETTY_PRINT);

View File

@@ -12,6 +12,7 @@ RateLimitMiddleware::apply('api');
try {
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/helpers/Database.php';
require_once dirname(__DIR__) . '/models/RecurringTicketModel.php';
// Check authentication
@@ -42,16 +43,8 @@ try {
}
}
$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");
}
// Use centralized database connection
$conn = Database::getConnection();
header('Content-Type: application/json');
@@ -130,8 +123,6 @@ try {
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
}
$conn->close();
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);

View File

@@ -12,6 +12,7 @@ RateLimitMiddleware::apply('api');
try {
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/helpers/Database.php';
// Check authentication
session_start();
@@ -39,16 +40,8 @@ try {
}
}
$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");
}
// Use centralized database connection
$conn = Database::getConnection();
header('Content-Type: application/json');
@@ -145,8 +138,6 @@ try {
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
}
$conn->close();
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);

View File

@@ -13,6 +13,8 @@ RateLimitMiddleware::apply('api');
try {
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/helpers/Database.php';
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
// Check authentication
session_start();
@@ -40,16 +42,12 @@ try {
}
}
$conn = new mysqli(
$GLOBALS['config']['DB_HOST'],
$GLOBALS['config']['DB_USER'],
$GLOBALS['config']['DB_PASS'],
$GLOBALS['config']['DB_NAME']
);
// Use centralized database connection
$conn = Database::getConnection();
if ($conn->connect_error) {
throw new Exception("Database connection failed");
}
// Initialize audit log
$auditLog = new AuditLogModel($conn);
$userId = $_SESSION['user']['user_id'];
header('Content-Type: application/json');
@@ -92,8 +90,18 @@ try {
);
if ($stmt->execute()) {
$transitionId = $conn->insert_id;
WorkflowModel::clearCache(); // Clear workflow cache
echo json_encode(['success' => true, 'transition_id' => $conn->insert_id]);
// Audit log: workflow transition created
$auditLog->log($userId, 'create', 'workflow_transition', (string)$transitionId, [
'from_status' => $data['from_status'],
'to_status' => $data['to_status'],
'requires_comment' => $data['requires_comment'] ?? 0,
'requires_admin' => $data['requires_admin'] ?? 0
]);
echo json_encode(['success' => true, 'transition_id' => $transitionId]);
} else {
echo json_encode(['success' => false, 'error' => $stmt->error]);
}
@@ -123,6 +131,14 @@ try {
$success = $stmt->execute();
if ($success) {
WorkflowModel::clearCache(); // Clear workflow cache
// Audit log: workflow transition updated
$auditLog->log($userId, 'update', 'workflow_transition', (string)$id, [
'from_status' => $data['from_status'],
'to_status' => $data['to_status'],
'requires_comment' => $data['requires_comment'] ?? 0,
'requires_admin' => $data['requires_admin'] ?? 0
]);
}
echo json_encode(['success' => $success]);
$stmt->close();
@@ -134,11 +150,25 @@ try {
exit;
}
// Get transition details before deletion for audit log
$getStmt = $conn->prepare("SELECT from_status, to_status FROM status_transitions WHERE transition_id = ?");
$getStmt->bind_param('i', $id);
$getStmt->execute();
$getResult = $getStmt->get_result();
$transitionData = $getResult->fetch_assoc();
$getStmt->close();
$stmt = $conn->prepare("DELETE FROM status_transitions WHERE transition_id = ?");
$stmt->bind_param('i', $id);
$success = $stmt->execute();
if ($success) {
WorkflowModel::clearCache(); // Clear workflow cache
// Audit log: workflow transition deleted
$auditLog->log($userId, 'delete', 'workflow_transition', (string)$id, [
'from_status' => $transitionData['from_status'] ?? 'unknown',
'to_status' => $transitionData['to_status'] ?? 'unknown'
]);
}
echo json_encode(['success' => $success]);
$stmt->close();
@@ -149,8 +179,6 @@ try {
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
}
$conn->close();
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);

View File

@@ -12,6 +12,7 @@ ob_start();
try {
// Load config
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/helpers/Database.php';
// Load models
require_once dirname(__DIR__) . '/models/ApiKeyModel.php';
@@ -56,17 +57,8 @@ try {
throw new Exception("Valid key ID is required");
}
// 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");
}
// Use centralized database connection
$conn = Database::getConnection();
// Get key info for audit log
$apiKeyModel = new ApiKeyModel($conn);
@@ -97,8 +89,6 @@ try {
['key_name' => $keyInfo['key_name'], 'key_prefix' => $keyInfo['key_prefix']]
);
$conn->close();
// Clear output buffer
ob_end_clean();

View File

@@ -5,6 +5,7 @@
*/
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/helpers/Database.php';
require_once dirname(__DIR__) . '/models/SavedFiltersModel.php';
session_start();
@@ -30,19 +31,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' || $_SERVER['REQUEST_METHOD'] === 'PUT
$userId = $_SESSION['user']['user_id'];
// 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) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => 'Database connection failed']);
exit;
}
// Use centralized database connection
$conn = Database::getConnection();
$filtersModel = new SavedFiltersModel($conn);
@@ -72,7 +62,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
http_response_code(500);
echo json_encode(['success' => false, 'error' => 'Failed to fetch filters']);
}
$conn->close();
exit;
}
@@ -83,8 +72,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isset($data['filter_name']) || !isset($data['filter_criteria'])) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Missing filter_name or filter_criteria']);
$conn->close();
exit;
exit;
}
$filterName = trim($data['filter_name']);
@@ -95,8 +83,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (empty($filterName) || strlen($filterName) > 100) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Invalid filter name']);
$conn->close();
exit;
exit;
}
try {
@@ -106,7 +93,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
http_response_code(500);
echo json_encode(['success' => false, 'error' => 'Failed to save filter']);
}
$conn->close();
exit;
}
@@ -117,8 +103,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
if (!isset($data['filter_id'])) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Missing filter_id']);
$conn->close();
exit;
exit;
}
$filterId = (int)$data['filter_id'];
@@ -132,16 +117,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
http_response_code(500);
echo json_encode(['success' => false, 'error' => 'Failed to set default filter']);
}
$conn->close();
exit;
exit;
}
// Handle full filter update
if (!isset($data['filter_name']) || !isset($data['filter_criteria'])) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Missing filter_name or filter_criteria']);
$conn->close();
exit;
exit;
}
$filterName = trim($data['filter_name']);
@@ -155,7 +138,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
http_response_code(500);
echo json_encode(['success' => false, 'error' => 'Failed to update filter']);
}
$conn->close();
exit;
}
@@ -166,8 +148,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
if (!isset($data['filter_id'])) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Missing filter_id']);
$conn->close();
exit;
exit;
}
$filterId = (int)$data['filter_id'];
@@ -179,7 +160,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
http_response_code(500);
echo json_encode(['success' => false, 'error' => 'Failed to delete filter']);
}
$conn->close();
exit;
}

View File

@@ -64,6 +64,7 @@ if (session_status() === PHP_SESSION_NONE) {
}
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/helpers/Database.php';
require_once dirname(__DIR__) . '/models/DependencyModel.php';
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
require_once dirname(__DIR__) . '/helpers/ResponseHelper.php';
@@ -86,17 +87,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' || $_SERVER['REQUEST_METHOD'] === 'DEL
}
}
// 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) {
ResponseHelper::serverError('Database connection failed');
}
// Use centralized database connection
$conn = Database::getConnection();
// Check if ticket_dependencies table exists
$tableCheck = $conn->query("SHOW TABLES LIKE 'ticket_dependencies'");
@@ -211,6 +203,4 @@ switch ($method) {
// Log detailed error server-side
error_log('Ticket dependencies API error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
ResponseHelper::serverError('An error occurred while processing the dependency request');
}
$conn->close();
};

View File

@@ -16,6 +16,7 @@ ob_start();
try {
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/helpers/Database.php';
require_once dirname(__DIR__) . '/models/CommentModel.php';
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
@@ -41,17 +42,8 @@ try {
$userId = $currentUser['user_id'];
$isAdmin = $currentUser['is_admin'] ?? false;
// 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");
}
// Use centralized database connection
$conn = Database::getConnection();
// Get POST/PUT data
$data = json_decode(file_get_contents('php://input'), true);

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();

View File

@@ -19,6 +19,7 @@ if (session_status() === PHP_SESSION_NONE) {
}
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/helpers/Database.php';
require_once dirname(__DIR__) . '/helpers/ResponseHelper.php';
require_once dirname(__DIR__) . '/models/AttachmentModel.php';
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
@@ -171,28 +172,20 @@ try {
}
// Log the upload
$conn = new mysqli(
$GLOBALS['config']['DB_HOST'],
$GLOBALS['config']['DB_USER'],
$GLOBALS['config']['DB_PASS'],
$GLOBALS['config']['DB_NAME']
$conn = Database::getConnection();
$auditLog = new AuditLogModel($conn);
$auditLog->log(
$_SESSION['user']['user_id'],
'attachment_upload',
'ticket_attachments',
(string)$attachmentId,
[
'ticket_id' => $ticketId,
'filename' => $originalFilename,
'size' => $file['size'],
'mime_type' => $mimeType
]
);
if (!$conn->connect_error) {
$auditLog = new AuditLogModel($conn);
$auditLog->log(
$_SESSION['user']['user_id'],
'attachment_upload',
'ticket_attachments',
(string)$attachmentId,
[
'ticket_id' => $ticketId,
'filename' => $originalFilename,
'size' => $file['size'],
'mime_type' => $mimeType
]
);
$conn->close();
}
ResponseHelper::created([
'attachment_id' => $attachmentId,

View File

@@ -5,6 +5,7 @@
*/
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/helpers/Database.php';
require_once dirname(__DIR__) . '/models/UserPreferencesModel.php';
session_start();
@@ -30,19 +31,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' || $_SERVER['REQUEST_METHOD'] === 'DEL
$userId = $_SESSION['user']['user_id'];
// 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) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => 'Database connection failed']);
exit;
}
// Use centralized database connection
$conn = Database::getConnection();
$prefsModel = new UserPreferencesModel($conn);
@@ -55,7 +45,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
http_response_code(500);
echo json_encode(['success' => false, 'error' => 'Failed to fetch preferences']);
}
$conn->close();
exit;
}
@@ -66,8 +55,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isset($data['key']) || !isset($data['value'])) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Missing key or value']);
$conn->close();
exit;
exit;
}
$key = trim($data['key']);
@@ -86,8 +74,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!in_array($key, $validKeys)) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Invalid preference key']);
$conn->close();
exit;
exit;
}
try {
@@ -103,7 +90,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
http_response_code(500);
echo json_encode(['success' => false, 'error' => 'Failed to save preference']);
}
$conn->close();
exit;
}
@@ -114,8 +100,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
if (!isset($data['key'])) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Missing key']);
$conn->close();
exit;
exit;
}
try {
@@ -125,7 +110,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
http_response_code(500);
echo json_encode(['success' => false, 'error' => 'Failed to delete preference']);
}
$conn->close();
exit;
}