conn = $conn; } /** * Log an action to the audit trail * * @param int $userId User ID performing the action * @param string $actionType Type of action (e.g., 'create', 'update', 'delete', 'view') * @param string $entityType Type of entity (e.g., 'ticket', 'comment', 'api_key') * @param string|null $entityId ID of the entity affected * @param array|null $details Additional details as associative array * @param string|null $ipAddress IP address of the user * @return bool Success status */ public function log($userId, $actionType, $entityType, $entityId = null, $details = null, $ipAddress = null) { // Convert details array to JSON $detailsJson = null; if ($details !== null) { $detailsJson = json_encode($details); } // Get IP address if not provided if ($ipAddress === null) { $ipAddress = $this->getClientIP(); } $stmt = $this->conn->prepare( "INSERT INTO audit_log (user_id, action_type, entity_type, entity_id, details, ip_address) VALUES (?, ?, ?, ?, ?, ?)" ); $stmt->bind_param("isssss", $userId, $actionType, $entityType, $entityId, $detailsJson, $ipAddress); $success = $stmt->execute(); $stmt->close(); return $success; } /** * Get audit logs for a specific entity * * @param string $entityType Type of entity * @param string $entityId ID of the entity * @param int $limit Maximum number of logs to return * @return array Array of audit log records */ public function getLogsByEntity($entityType, $entityId, $limit = 100) { $stmt = $this->conn->prepare( "SELECT al.*, u.username, u.display_name FROM audit_log al LEFT JOIN users u ON al.user_id = u.user_id WHERE al.entity_type = ? AND al.entity_id = ? ORDER BY al.created_at DESC LIMIT ?" ); $stmt->bind_param("ssi", $entityType, $entityId, $limit); $stmt->execute(); $result = $stmt->get_result(); $logs = []; while ($row = $result->fetch_assoc()) { // Decode JSON details if ($row['details']) { $row['details'] = json_decode($row['details'], true); } $logs[] = $row; } $stmt->close(); return $logs; } /** * Get audit logs for a specific user * * @param int $userId User ID * @param int $limit Maximum number of logs to return * @return array Array of audit log records */ public function getLogsByUser($userId, $limit = 100) { $stmt = $this->conn->prepare( "SELECT al.*, u.username, u.display_name FROM audit_log al LEFT JOIN users u ON al.user_id = u.user_id WHERE al.user_id = ? ORDER BY al.created_at DESC LIMIT ?" ); $stmt->bind_param("ii", $userId, $limit); $stmt->execute(); $result = $stmt->get_result(); $logs = []; while ($row = $result->fetch_assoc()) { // Decode JSON details if ($row['details']) { $row['details'] = json_decode($row['details'], true); } $logs[] = $row; } $stmt->close(); return $logs; } /** * Get recent audit logs (for admin panel) * * @param int $limit Maximum number of logs to return * @param int $offset Offset for pagination * @return array Array of audit log records */ public function getRecentLogs($limit = 50, $offset = 0) { $stmt = $this->conn->prepare( "SELECT al.*, u.username, u.display_name FROM audit_log al LEFT JOIN users u ON al.user_id = u.user_id ORDER BY al.created_at DESC LIMIT ? OFFSET ?" ); $stmt->bind_param("ii", $limit, $offset); $stmt->execute(); $result = $stmt->get_result(); $logs = []; while ($row = $result->fetch_assoc()) { // Decode JSON details if ($row['details']) { $row['details'] = json_decode($row['details'], true); } $logs[] = $row; } $stmt->close(); return $logs; } /** * Get audit logs filtered by action type * * @param string $actionType Action type to filter by * @param int $limit Maximum number of logs to return * @return array Array of audit log records */ public function getLogsByAction($actionType, $limit = 100) { $stmt = $this->conn->prepare( "SELECT al.*, u.username, u.display_name FROM audit_log al LEFT JOIN users u ON al.user_id = u.user_id WHERE al.action_type = ? ORDER BY al.created_at DESC LIMIT ?" ); $stmt->bind_param("si", $actionType, $limit); $stmt->execute(); $result = $stmt->get_result(); $logs = []; while ($row = $result->fetch_assoc()) { // Decode JSON details if ($row['details']) { $row['details'] = json_decode($row['details'], true); } $logs[] = $row; } $stmt->close(); return $logs; } /** * Get total count of audit logs * * @return int Total count */ public function getTotalCount() { $result = $this->conn->query("SELECT COUNT(*) as count FROM audit_log"); $row = $result->fetch_assoc(); return (int)$row['count']; } /** * Delete old audit logs (for maintenance) * * @param int $daysToKeep Number of days of logs to keep * @return int Number of deleted records */ public function deleteOldLogs($daysToKeep = 90) { $stmt = $this->conn->prepare( "DELETE FROM audit_log WHERE created_at < DATE_SUB(NOW(), INTERVAL ? DAY)" ); $stmt->bind_param("i", $daysToKeep); $stmt->execute(); $affectedRows = $stmt->affected_rows; $stmt->close(); return $affectedRows; } /** * Get client IP address (handles proxies) * * @return string Client IP address */ private function getClientIP() { $ipAddress = ''; // Check for proxy headers if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) { // Cloudflare $ipAddress = $_SERVER['HTTP_CF_CONNECTING_IP']; } elseif (!empty($_SERVER['HTTP_X_REAL_IP'])) { // Nginx proxy $ipAddress = $_SERVER['HTTP_X_REAL_IP']; } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { // Standard proxy header $ipAddress = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]; } elseif (!empty($_SERVER['REMOTE_ADDR'])) { // Direct connection $ipAddress = $_SERVER['REMOTE_ADDR']; } return trim($ipAddress); } /** * Helper: Log ticket creation * * @param int $userId User ID * @param string $ticketId Ticket ID * @param array $ticketData Ticket data * @return bool Success status */ public function logTicketCreate($userId, $ticketId, $ticketData) { return $this->log( $userId, 'create', 'ticket', $ticketId, ['title' => $ticketData['title'], 'priority' => $ticketData['priority'] ?? null] ); } /** * Helper: Log ticket update * * @param int $userId User ID * @param string $ticketId Ticket ID * @param array $changes Array of changed fields * @return bool Success status */ public function logTicketUpdate($userId, $ticketId, $changes) { return $this->log($userId, 'update', 'ticket', $ticketId, $changes); } /** * Helper: Log comment creation * * @param int $userId User ID * @param int $commentId Comment ID * @param string $ticketId Associated ticket ID * @return bool Success status */ public function logCommentCreate($userId, $commentId, $ticketId) { return $this->log( $userId, 'create', 'comment', (string)$commentId, ['ticket_id' => $ticketId] ); } /** * Helper: Log ticket view * * @param int $userId User ID * @param string $ticketId Ticket ID * @return bool Success status */ public function logTicketView($userId, $ticketId) { return $this->log($userId, 'view', 'ticket', $ticketId); } // ======================================== // Security Event Logging Methods // ======================================== /** * Log a security event * * @param string $eventType Type of security event * @param array $details Additional details * @param int|null $userId User ID if known * @return bool Success status */ public function logSecurityEvent($eventType, $details = [], $userId = null) { $details['event_type'] = $eventType; $details['user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown'; return $this->log($userId, 'security_event', 'security', null, $details); } /** * Log a failed authentication attempt * * @param string $username Username attempted * @param string $reason Reason for failure * @return bool Success status */ public function logFailedAuth($username, $reason = 'Invalid credentials') { return $this->logSecurityEvent('failed_auth', [ 'username' => $username, 'reason' => $reason ]); } /** * Log a CSRF token failure * * @param string $endpoint The endpoint that was accessed * @param int|null $userId User ID if session exists * @return bool Success status */ public function logCsrfFailure($endpoint, $userId = null) { return $this->logSecurityEvent('csrf_failure', [ 'endpoint' => $endpoint, 'method' => $_SERVER['REQUEST_METHOD'] ?? 'Unknown' ], $userId); } /** * Log a rate limit exceeded event * * @param string $endpoint The endpoint that was rate limited * @param int|null $userId User ID if session exists * @return bool Success status */ public function logRateLimitExceeded($endpoint, $userId = null) { return $this->logSecurityEvent('rate_limit_exceeded', [ 'endpoint' => $endpoint ], $userId); } /** * Log an unauthorized access attempt * * @param string $resource The resource that was accessed * @param int|null $userId User ID if session exists * @return bool Success status */ public function logUnauthorizedAccess($resource, $userId = null) { return $this->logSecurityEvent('unauthorized_access', [ 'resource' => $resource ], $userId); } /** * Get security events (for admin review) * * @param int $limit Maximum number of events * @param int $offset Offset for pagination * @return array Security events */ public function getSecurityEvents($limit = 100, $offset = 0) { $stmt = $this->conn->prepare( "SELECT al.*, u.username, u.display_name FROM audit_log al LEFT JOIN users u ON al.user_id = u.user_id WHERE al.action_type = 'security_event' ORDER BY al.created_at DESC LIMIT ? OFFSET ?" ); $stmt->bind_param("ii", $limit, $offset); $stmt->execute(); $result = $stmt->get_result(); $events = []; while ($row = $result->fetch_assoc()) { if ($row['details']) { $row['details'] = json_decode($row['details'], true); } $events[] = $row; } $stmt->close(); return $events; } /** * Get formatted timeline for a specific ticket * Includes all ticket updates and comments * * @param string $ticketId Ticket ID * @return array Timeline events */ public function getTicketTimeline($ticketId) { $stmt = $this->conn->prepare( "SELECT al.*, u.username, u.display_name FROM audit_log al LEFT JOIN users u ON al.user_id = u.user_id WHERE (al.entity_type = 'ticket' AND al.entity_id = ?) OR (al.entity_type = 'comment' AND JSON_EXTRACT(al.details, '$.ticket_id') = ?) ORDER BY al.created_at DESC" ); $stmt->bind_param("ss", $ticketId, $ticketId); $stmt->execute(); $result = $stmt->get_result(); $timeline = []; while ($row = $result->fetch_assoc()) { if ($row['details']) { $row['details'] = json_decode($row['details'], true); } $timeline[] = $row; } $stmt->close(); return $timeline; } /** * Get filtered audit logs with advanced search * * @param array $filters Associative array of filter criteria * @param int $limit Maximum number of logs to return * @param int $offset Offset for pagination * @return array Array containing logs and total count */ public function getFilteredLogs($filters = [], $limit = 50, $offset = 0) { $whereConditions = []; $params = []; $paramTypes = ''; // Action type filter if (!empty($filters['action_type'])) { $actions = explode(',', $filters['action_type']); $placeholders = str_repeat('?,', count($actions) - 1) . '?'; $whereConditions[] = "al.action_type IN ($placeholders)"; $params = array_merge($params, $actions); $paramTypes .= str_repeat('s', count($actions)); } // Entity type filter if (!empty($filters['entity_type'])) { $entities = explode(',', $filters['entity_type']); $placeholders = str_repeat('?,', count($entities) - 1) . '?'; $whereConditions[] = "al.entity_type IN ($placeholders)"; $params = array_merge($params, $entities); $paramTypes .= str_repeat('s', count($entities)); } // User filter if (!empty($filters['user_id'])) { $whereConditions[] = "al.user_id = ?"; $params[] = (int)$filters['user_id']; $paramTypes .= 'i'; } // Entity ID filter (for specific ticket/comment) if (!empty($filters['entity_id'])) { $whereConditions[] = "al.entity_id = ?"; $params[] = $filters['entity_id']; $paramTypes .= 's'; } // Date range filters if (!empty($filters['date_from'])) { $whereConditions[] = "DATE(al.created_at) >= ?"; $params[] = $filters['date_from']; $paramTypes .= 's'; } if (!empty($filters['date_to'])) { $whereConditions[] = "DATE(al.created_at) <= ?"; $params[] = $filters['date_to']; $paramTypes .= 's'; } // IP address filter if (!empty($filters['ip_address'])) { $whereConditions[] = "al.ip_address LIKE ?"; $params[] = '%' . $filters['ip_address'] . '%'; $paramTypes .= 's'; } // Build WHERE clause $whereClause = ''; if (!empty($whereConditions)) { $whereClause = 'WHERE ' . implode(' AND ', $whereConditions); } // Get total count for pagination $countSql = "SELECT COUNT(*) as total FROM audit_log al $whereClause"; $countStmt = $this->conn->prepare($countSql); if (!empty($params)) { $countStmt->bind_param($paramTypes, ...$params); } $countStmt->execute(); $totalResult = $countStmt->get_result(); $totalCount = $totalResult->fetch_assoc()['total']; $countStmt->close(); // Get filtered logs $sql = "SELECT al.*, u.username, u.display_name FROM audit_log al LEFT JOIN users u ON al.user_id = u.user_id $whereClause ORDER BY al.created_at DESC LIMIT ? OFFSET ?"; $stmt = $this->conn->prepare($sql); // Add limit and offset parameters $params[] = $limit; $params[] = $offset; $paramTypes .= 'ii'; if (!empty($params)) { $stmt->bind_param($paramTypes, ...$params); } $stmt->execute(); $result = $stmt->get_result(); $logs = []; while ($row = $result->fetch_assoc()) { if ($row['details']) { $row['details'] = json_decode($row['details'], true); } $logs[] = $row; } $stmt->close(); return [ 'logs' => $logs, 'total' => $totalCount, 'pages' => ceil($totalCount / $limit) ]; } }