Compare commits
10 Commits
962724d811
...
837c4baf56
| Author | SHA1 | Date | |
|---|---|---|---|
| 837c4baf56 | |||
| becee84821 | |||
| 4a05c82852 | |||
| e801eee6ee | |||
| 58f2e9d143 | |||
| 783bf52552 | |||
| 8137a007a1 | |||
| f46b1c31b5 | |||
| fa9d9dfe0f | |||
| f096766e5d |
@@ -30,6 +30,18 @@ try {
|
|||||||
throw new Exception("Authentication required");
|
throw new Exception("Authentication required");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CSRF Protection
|
||||||
|
require_once dirname(__DIR__) . '/middleware/CsrfMiddleware.php';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$csrfToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||||
|
if (!CsrfMiddleware::validateToken($csrfToken)) {
|
||||||
|
http_response_code(403);
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$currentUser = $_SESSION['user'];
|
$currentUser = $_SESSION['user'];
|
||||||
$userId = $currentUser['user_id'];
|
$userId = $currentUser['user_id'];
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,17 @@ if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CSRF Protection
|
||||||
|
require_once dirname(__DIR__) . '/middleware/CsrfMiddleware.php';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$csrfToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||||
|
if (!CsrfMiddleware::validateToken($csrfToken)) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$userId = $_SESSION['user']['user_id'];
|
$userId = $_SESSION['user']['user_id'];
|
||||||
|
|
||||||
// Get request data
|
// Get request data
|
||||||
|
|||||||
@@ -13,6 +13,17 @@ if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CSRF Protection
|
||||||
|
require_once dirname(__DIR__) . '/middleware/CsrfMiddleware.php';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$csrfToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||||
|
if (!CsrfMiddleware::validateToken($csrfToken)) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check admin status - bulk operations are admin-only
|
// Check admin status - bulk operations are admin-only
|
||||||
$isAdmin = $_SESSION['user']['is_admin'] ?? false;
|
$isAdmin = $_SESSION['user']['is_admin'] ?? false;
|
||||||
if (!$isAdmin) {
|
if (!$isAdmin) {
|
||||||
|
|||||||
@@ -17,6 +17,17 @@ if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CSRF Protection
|
||||||
|
require_once dirname(__DIR__) . '/middleware/CsrfMiddleware.php';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' || $_SERVER['REQUEST_METHOD'] === 'PUT' || $_SERVER['REQUEST_METHOD'] === 'DELETE') {
|
||||||
|
$csrfToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||||
|
if (!CsrfMiddleware::validateToken($csrfToken)) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$userId = $_SESSION['user']['user_id'];
|
$userId = $_SESSION['user']['user_id'];
|
||||||
|
|
||||||
// Create database connection
|
// Create database connection
|
||||||
|
|||||||
@@ -59,6 +59,19 @@ try {
|
|||||||
if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
|
if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
|
||||||
throw new Exception("Authentication required");
|
throw new Exception("Authentication required");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CSRF Protection
|
||||||
|
require_once dirname(__DIR__) . '/middleware/CsrfMiddleware.php';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' || $_SERVER['REQUEST_METHOD'] === 'PUT') {
|
||||||
|
$csrfToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||||
|
if (!CsrfMiddleware::validateToken($csrfToken)) {
|
||||||
|
http_response_code(403);
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$currentUser = $_SESSION['user'];
|
$currentUser = $_SESSION['user'];
|
||||||
$userId = $currentUser['user_id'];
|
$userId = $currentUser['user_id'];
|
||||||
$isAdmin = $currentUser['is_admin'] ?? false;
|
$isAdmin = $currentUser['is_admin'] ?? false;
|
||||||
|
|||||||
@@ -17,6 +17,17 @@ if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CSRF Protection
|
||||||
|
require_once dirname(__DIR__) . '/middleware/CsrfMiddleware.php';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' || $_SERVER['REQUEST_METHOD'] === 'DELETE') {
|
||||||
|
$csrfToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||||
|
if (!CsrfMiddleware::validateToken($csrfToken)) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$userId = $_SESSION['user']['user_id'];
|
$userId = $_SESSION['user']['user_id'];
|
||||||
|
|
||||||
// Create database connection
|
// Create database connection
|
||||||
|
|||||||
@@ -156,7 +156,10 @@ async function saveCurrentFilter() {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch('/api/saved_filters.php', {
|
const response = await fetch('/api/saved_filters.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': window.CSRF_TOKEN
|
||||||
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
filter_name: filterName.trim(),
|
filter_name: filterName.trim(),
|
||||||
filter_criteria: filterCriteria
|
filter_criteria: filterCriteria
|
||||||
@@ -319,7 +322,10 @@ async function deleteSavedFilter() {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch('/api/saved_filters.php', {
|
const response = await fetch('/api/saved_filters.php', {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': window.CSRF_TOKEN
|
||||||
|
},
|
||||||
body: JSON.stringify({ filter_id: filterId })
|
body: JSON.stringify({ filter_id: filterId })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -276,7 +276,8 @@ function quickSave() {
|
|||||||
fetch('/api/update_ticket.php', {
|
fetch('/api/update_ticket.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': window.CSRF_TOKEN
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
})
|
})
|
||||||
@@ -355,7 +356,8 @@ function saveTicket() {
|
|||||||
fetch('/api/update_ticket.php', {
|
fetch('/api/update_ticket.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': window.CSRF_TOKEN
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
ticket_id: ticketId,
|
ticket_id: ticketId,
|
||||||
@@ -492,7 +494,10 @@ function bulkClose() {
|
|||||||
|
|
||||||
fetch('/api/bulk_operation.php', {
|
fetch('/api/bulk_operation.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': window.CSRF_TOKEN
|
||||||
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
operation_type: 'bulk_close',
|
operation_type: 'bulk_close',
|
||||||
ticket_ids: ticketIds
|
ticket_ids: ticketIds
|
||||||
@@ -593,7 +598,10 @@ function performBulkAssign() {
|
|||||||
|
|
||||||
fetch('/api/bulk_operation.php', {
|
fetch('/api/bulk_operation.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': window.CSRF_TOKEN
|
||||||
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
operation_type: 'bulk_assign',
|
operation_type: 'bulk_assign',
|
||||||
ticket_ids: ticketIds,
|
ticket_ids: ticketIds,
|
||||||
@@ -681,7 +689,10 @@ function performBulkPriority() {
|
|||||||
|
|
||||||
fetch('/api/bulk_operation.php', {
|
fetch('/api/bulk_operation.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': window.CSRF_TOKEN
|
||||||
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
operation_type: 'bulk_priority',
|
operation_type: 'bulk_priority',
|
||||||
ticket_ids: ticketIds,
|
ticket_ids: ticketIds,
|
||||||
@@ -808,7 +819,10 @@ function performBulkStatusChange() {
|
|||||||
|
|
||||||
fetch('/api/bulk_operation.php', {
|
fetch('/api/bulk_operation.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': window.CSRF_TOKEN
|
||||||
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
operation_type: 'bulk_status',
|
operation_type: 'bulk_status',
|
||||||
ticket_ids: ticketIds,
|
ticket_ids: ticketIds,
|
||||||
@@ -888,7 +902,10 @@ function performBulkDelete() {
|
|||||||
|
|
||||||
fetch('/api/bulk_operation.php', {
|
fetch('/api/bulk_operation.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': window.CSRF_TOKEN
|
||||||
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
operation_type: 'bulk_delete',
|
operation_type: 'bulk_delete',
|
||||||
ticket_ids: ticketIds
|
ticket_ids: ticketIds
|
||||||
|
|||||||
@@ -81,7 +81,10 @@ async function saveSettings() {
|
|||||||
for (const [key, value] of Object.entries(prefs)) {
|
for (const [key, value] of Object.entries(prefs)) {
|
||||||
const response = await fetch('/api/user_preferences.php', {
|
const response = await fetch('/api/user_preferences.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': window.CSRF_TOKEN
|
||||||
|
},
|
||||||
body: JSON.stringify({ key, value })
|
body: JSON.stringify({ key, value })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ function saveTicket() {
|
|||||||
fetch(apiUrl, {
|
fetch(apiUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': window.CSRF_TOKEN
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
ticket_id: ticketId,
|
ticket_id: ticketId,
|
||||||
@@ -140,7 +141,8 @@ function addComment() {
|
|||||||
fetch('/api/add_comment.php', {
|
fetch('/api/add_comment.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': window.CSRF_TOKEN
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
ticket_id: ticketId,
|
ticket_id: ticketId,
|
||||||
@@ -178,18 +180,33 @@ function addComment() {
|
|||||||
.replace(/\n/g, '<br>');
|
.replace(/\n/g, '<br>');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new comment to the list
|
// Add new comment to the list (using safe DOM API to prevent XSS)
|
||||||
const commentsList = document.querySelector('.comments-list');
|
const commentsList = document.querySelector('.comments-list');
|
||||||
const newComment = `
|
|
||||||
<div class="comment">
|
const commentDiv = document.createElement('div');
|
||||||
<div class="comment-header">
|
commentDiv.className = 'comment';
|
||||||
<span class="comment-user">${data.user_name}</span>
|
|
||||||
<span class="comment-date">${data.created_at}</span>
|
const headerDiv = document.createElement('div');
|
||||||
</div>
|
headerDiv.className = 'comment-header';
|
||||||
<div class="comment-text">${displayText}</div>
|
|
||||||
</div>
|
const userSpan = document.createElement('span');
|
||||||
`;
|
userSpan.className = 'comment-user';
|
||||||
commentsList.insertAdjacentHTML('afterbegin', newComment);
|
userSpan.textContent = data.user_name; // Safe - auto-escapes
|
||||||
|
|
||||||
|
const dateSpan = document.createElement('span');
|
||||||
|
dateSpan.className = 'comment-date';
|
||||||
|
dateSpan.textContent = data.created_at; // Safe - auto-escapes
|
||||||
|
|
||||||
|
const textDiv = document.createElement('div');
|
||||||
|
textDiv.className = 'comment-text';
|
||||||
|
textDiv.innerHTML = displayText; // displayText already sanitized above
|
||||||
|
|
||||||
|
headerDiv.appendChild(userSpan);
|
||||||
|
headerDiv.appendChild(dateSpan);
|
||||||
|
commentDiv.appendChild(headerDiv);
|
||||||
|
commentDiv.appendChild(textDiv);
|
||||||
|
|
||||||
|
commentsList.insertBefore(commentDiv, commentsList.firstChild);
|
||||||
} else {
|
} else {
|
||||||
console.error('Error adding comment:', data.error || 'Unknown error');
|
console.error('Error adding comment:', data.error || 'Unknown error');
|
||||||
}
|
}
|
||||||
@@ -283,7 +300,10 @@ function handleAssignmentChange() {
|
|||||||
|
|
||||||
fetch('/api/assign_ticket.php', {
|
fetch('/api/assign_ticket.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': window.CSRF_TOKEN
|
||||||
|
},
|
||||||
body: JSON.stringify({ ticket_id: ticketId, assigned_to: assignedTo })
|
body: JSON.stringify({ ticket_id: ticketId, assigned_to: assignedTo })
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
@@ -316,7 +336,10 @@ function handleMetadataChanges() {
|
|||||||
|
|
||||||
fetch('/api/update_ticket.php', {
|
fetch('/api/update_ticket.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': window.CSRF_TOKEN
|
||||||
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
ticket_id: ticketId,
|
ticket_id: ticketId,
|
||||||
[fieldName]: fieldName === 'priority' ? parseInt(newValue) : newValue
|
[fieldName]: fieldName === 'priority' ? parseInt(newValue) : newValue
|
||||||
@@ -418,7 +441,8 @@ function updateTicketStatus() {
|
|||||||
fetch('/api/update_ticket.php', {
|
fetch('/api/update_ticket.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': window.CSRF_TOKEN
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
ticket_id: ticketId,
|
ticket_id: ticketId,
|
||||||
|
|||||||
@@ -20,8 +20,14 @@ class AuthMiddleware {
|
|||||||
* @throws Exception if authentication fails
|
* @throws Exception if authentication fails
|
||||||
*/
|
*/
|
||||||
public function authenticate() {
|
public function authenticate() {
|
||||||
// Start session if not already started
|
// Start session if not already started with secure settings
|
||||||
if (session_status() === PHP_SESSION_NONE) {
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
// Configure secure session settings
|
||||||
|
ini_set('session.cookie_httponly', 1);
|
||||||
|
ini_set('session.cookie_secure', 1); // Requires HTTPS
|
||||||
|
ini_set('session.cookie_samesite', 'Strict');
|
||||||
|
ini_set('session.use_strict_mode', 1);
|
||||||
|
|
||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,10 +72,17 @@ class AuthMiddleware {
|
|||||||
throw new Exception("Failed to sync user from Authelia");
|
throw new Exception("Failed to sync user from Authelia");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regenerate session ID to prevent session fixation attacks
|
||||||
|
session_regenerate_id(true);
|
||||||
|
|
||||||
// Store user in session
|
// Store user in session
|
||||||
$_SESSION['user'] = $user;
|
$_SESSION['user'] = $user;
|
||||||
$_SESSION['last_activity'] = time();
|
$_SESSION['last_activity'] = time();
|
||||||
|
|
||||||
|
// Generate new CSRF token on login
|
||||||
|
require_once __DIR__ . '/CsrfMiddleware.php';
|
||||||
|
CsrfMiddleware::generateToken();
|
||||||
|
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
55
middleware/CsrfMiddleware.php
Normal file
55
middleware/CsrfMiddleware.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* CSRF Protection Middleware
|
||||||
|
* Generates and validates CSRF tokens for all state-changing operations
|
||||||
|
*/
|
||||||
|
class CsrfMiddleware {
|
||||||
|
private static $tokenName = 'csrf_token';
|
||||||
|
private static $tokenTime = 'csrf_token_time';
|
||||||
|
private static $tokenLifetime = 3600; // 1 hour
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a new CSRF token
|
||||||
|
*/
|
||||||
|
public static function generateToken() {
|
||||||
|
$_SESSION[self::$tokenName] = bin2hex(random_bytes(32));
|
||||||
|
$_SESSION[self::$tokenTime] = time();
|
||||||
|
return $_SESSION[self::$tokenName];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current CSRF token, regenerate if expired
|
||||||
|
*/
|
||||||
|
public static function getToken() {
|
||||||
|
if (!isset($_SESSION[self::$tokenName]) || self::isTokenExpired()) {
|
||||||
|
return self::generateToken();
|
||||||
|
}
|
||||||
|
return $_SESSION[self::$tokenName];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate CSRF token (constant-time comparison)
|
||||||
|
*/
|
||||||
|
public static function validateToken($token) {
|
||||||
|
if (!isset($_SESSION[self::$tokenName])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::isTokenExpired()) {
|
||||||
|
self::generateToken(); // Auto-regenerate expired token
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constant-time comparison to prevent timing attacks
|
||||||
|
return hash_equals($_SESSION[self::$tokenName], $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if token is expired
|
||||||
|
*/
|
||||||
|
private static function isTokenExpired() {
|
||||||
|
return !isset($_SESSION[self::$tokenTime]) ||
|
||||||
|
(time() - $_SESSION[self::$tokenTime]) > self::$tokenLifetime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
11
migrations/013_add_performance_indexes.sql
Normal file
11
migrations/013_add_performance_indexes.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-- Migration 013: Add performance indexes for critical queries
|
||||||
|
|
||||||
|
-- Index on ticket_comments.ticket_id (foreign key without index)
|
||||||
|
-- Speeds up comment loading by 10-100x on large tables
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ticket_comments_ticket_id
|
||||||
|
ON ticket_comments(ticket_id);
|
||||||
|
|
||||||
|
-- Composite index on audit_log for entity lookups with date sorting
|
||||||
|
-- Optimizes activity timeline queries
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_audit_entity_created
|
||||||
|
ON audit_log(entity_type, entity_id, created_at DESC);
|
||||||
4
migrations/013_rollback.sql
Normal file
4
migrations/013_rollback.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
-- Rollback for migration 013: Remove performance indexes
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_ticket_comments_ticket_id ON ticket_comments;
|
||||||
|
DROP INDEX IF EXISTS idx_audit_entity_created ON audit_log;
|
||||||
@@ -70,6 +70,9 @@ class BulkOperationsModel {
|
|||||||
$ticketModel = new TicketModel($this->conn);
|
$ticketModel = new TicketModel($this->conn);
|
||||||
$auditLogModel = new AuditLogModel($this->conn);
|
$auditLogModel = new AuditLogModel($this->conn);
|
||||||
|
|
||||||
|
// Batch load all tickets in one query to eliminate N+1 problem
|
||||||
|
$ticketsById = $ticketModel->getTicketsByIds($ticketIds);
|
||||||
|
|
||||||
foreach ($ticketIds as $ticketId) {
|
foreach ($ticketIds as $ticketId) {
|
||||||
$ticketId = trim($ticketId);
|
$ticketId = trim($ticketId);
|
||||||
$success = false;
|
$success = false;
|
||||||
@@ -77,8 +80,8 @@ class BulkOperationsModel {
|
|||||||
try {
|
try {
|
||||||
switch ($operation['operation_type']) {
|
switch ($operation['operation_type']) {
|
||||||
case 'bulk_close':
|
case 'bulk_close':
|
||||||
// Get current ticket to preserve other fields
|
// Get current ticket from pre-loaded batch
|
||||||
$currentTicket = $ticketModel->getTicketById($ticketId);
|
$currentTicket = $ticketsById[$ticketId] ?? null;
|
||||||
if ($currentTicket) {
|
if ($currentTicket) {
|
||||||
$success = $ticketModel->updateTicket([
|
$success = $ticketModel->updateTicket([
|
||||||
'ticket_id' => $ticketId,
|
'ticket_id' => $ticketId,
|
||||||
@@ -109,7 +112,7 @@ class BulkOperationsModel {
|
|||||||
|
|
||||||
case 'bulk_priority':
|
case 'bulk_priority':
|
||||||
if (isset($parameters['priority'])) {
|
if (isset($parameters['priority'])) {
|
||||||
$currentTicket = $ticketModel->getTicketById($ticketId);
|
$currentTicket = $ticketsById[$ticketId] ?? null;
|
||||||
if ($currentTicket) {
|
if ($currentTicket) {
|
||||||
$success = $ticketModel->updateTicket([
|
$success = $ticketModel->updateTicket([
|
||||||
'ticket_id' => $ticketId,
|
'ticket_id' => $ticketId,
|
||||||
@@ -131,7 +134,7 @@ class BulkOperationsModel {
|
|||||||
|
|
||||||
case 'bulk_status':
|
case 'bulk_status':
|
||||||
if (isset($parameters['status'])) {
|
if (isset($parameters['status'])) {
|
||||||
$currentTicket = $ticketModel->getTicketById($ticketId);
|
$currentTicket = $ticketsById[$ticketId] ?? null;
|
||||||
if ($currentTicket) {
|
if ($currentTicket) {
|
||||||
$success = $ticketModel->updateTicket([
|
$success = $ticketModel->updateTicket([
|
||||||
'ticket_id' => $ticketId,
|
'ticket_id' => $ticketId,
|
||||||
|
|||||||
@@ -377,4 +377,50 @@ class TicketModel {
|
|||||||
$stmt->close();
|
$stmt->close();
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get multiple tickets by IDs in a single query (batch loading)
|
||||||
|
* Eliminates N+1 query problem in bulk operations
|
||||||
|
*
|
||||||
|
* @param array $ticketIds Array of ticket IDs
|
||||||
|
* @return array Associative array keyed by ticket_id
|
||||||
|
*/
|
||||||
|
public function getTicketsByIds($ticketIds) {
|
||||||
|
if (empty($ticketIds)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize ticket IDs
|
||||||
|
$ticketIds = array_map('intval', $ticketIds);
|
||||||
|
|
||||||
|
// Create placeholders for IN clause
|
||||||
|
$placeholders = str_repeat('?,', count($ticketIds) - 1) . '?';
|
||||||
|
|
||||||
|
$sql = "SELECT t.*,
|
||||||
|
u_created.username as creator_username,
|
||||||
|
u_created.display_name as creator_display_name,
|
||||||
|
u_updated.username as updater_username,
|
||||||
|
u_updated.display_name as updater_display_name,
|
||||||
|
u_assigned.username as assigned_username,
|
||||||
|
u_assigned.display_name as assigned_display_name
|
||||||
|
FROM tickets t
|
||||||
|
LEFT JOIN users u_created ON t.created_by = u_created.user_id
|
||||||
|
LEFT JOIN users u_updated ON t.updated_by = u_updated.user_id
|
||||||
|
LEFT JOIN users u_assigned ON t.assigned_to = u_assigned.user_id
|
||||||
|
WHERE t.ticket_id IN ($placeholders)";
|
||||||
|
|
||||||
|
$stmt = $this->conn->prepare($sql);
|
||||||
|
$types = str_repeat('i', count($ticketIds));
|
||||||
|
$stmt->bind_param($types, ...$ticketIds);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
$tickets = [];
|
||||||
|
while ($row = $result->fetch_assoc()) {
|
||||||
|
$tickets[$row['ticket_id']] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt->close();
|
||||||
|
return $tickets;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,12 +4,50 @@
|
|||||||
*/
|
*/
|
||||||
class UserModel {
|
class UserModel {
|
||||||
private $conn;
|
private $conn;
|
||||||
private static $userCache = [];
|
private static $userCache = []; // ['key' => ['data' => ..., 'expires' => timestamp]]
|
||||||
|
private static $cacheTTL = 300; // 5 minutes
|
||||||
|
|
||||||
public function __construct($conn) {
|
public function __construct($conn) {
|
||||||
$this->conn = $conn;
|
$this->conn = $conn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached user data if not expired
|
||||||
|
*/
|
||||||
|
private static function getCached($key) {
|
||||||
|
if (isset(self::$userCache[$key])) {
|
||||||
|
$cached = self::$userCache[$key];
|
||||||
|
if ($cached['expires'] > time()) {
|
||||||
|
return $cached['data'];
|
||||||
|
}
|
||||||
|
// Expired - remove from cache
|
||||||
|
unset(self::$userCache[$key]);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store user data in cache with expiration
|
||||||
|
*/
|
||||||
|
private static function setCached($key, $data) {
|
||||||
|
self::$userCache[$key] = [
|
||||||
|
'data' => $data,
|
||||||
|
'expires' => time() + self::$cacheTTL
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate specific user cache entry
|
||||||
|
*/
|
||||||
|
public static function invalidateCache($userId = null, $username = null) {
|
||||||
|
if ($userId !== null) {
|
||||||
|
unset(self::$userCache["user_id_$userId"]);
|
||||||
|
}
|
||||||
|
if ($username !== null) {
|
||||||
|
unset(self::$userCache["user_$username"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync user from Authelia headers (create or update)
|
* Sync user from Authelia headers (create or update)
|
||||||
*
|
*
|
||||||
@@ -22,8 +60,9 @@ class UserModel {
|
|||||||
public function syncUserFromAuthelia($username, $displayName = '', $email = '', $groups = '') {
|
public function syncUserFromAuthelia($username, $displayName = '', $email = '', $groups = '') {
|
||||||
// Check cache first
|
// Check cache first
|
||||||
$cacheKey = "user_$username";
|
$cacheKey = "user_$username";
|
||||||
if (isset(self::$userCache[$cacheKey])) {
|
$cached = self::getCached($cacheKey);
|
||||||
return self::$userCache[$cacheKey];
|
if ($cached !== null) {
|
||||||
|
return $cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if user is admin based on groups
|
// Determine if user is admin based on groups
|
||||||
@@ -72,8 +111,8 @@ class UserModel {
|
|||||||
|
|
||||||
$stmt->close();
|
$stmt->close();
|
||||||
|
|
||||||
// Cache user
|
// Cache user with TTL
|
||||||
self::$userCache[$cacheKey] = $user;
|
self::setCached($cacheKey, $user);
|
||||||
|
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
@@ -85,8 +124,9 @@ class UserModel {
|
|||||||
*/
|
*/
|
||||||
public function getSystemUser() {
|
public function getSystemUser() {
|
||||||
// Check cache first
|
// Check cache first
|
||||||
if (isset(self::$userCache['system'])) {
|
$cached = self::getCached('system');
|
||||||
return self::$userCache['system'];
|
if ($cached !== null) {
|
||||||
|
return $cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $this->conn->prepare("SELECT * FROM users WHERE username = 'system'");
|
$stmt = $this->conn->prepare("SELECT * FROM users WHERE username = 'system'");
|
||||||
@@ -95,7 +135,7 @@ class UserModel {
|
|||||||
|
|
||||||
if ($result->num_rows > 0) {
|
if ($result->num_rows > 0) {
|
||||||
$user = $result->fetch_assoc();
|
$user = $result->fetch_assoc();
|
||||||
self::$userCache['system'] = $user;
|
self::setCached('system', $user);
|
||||||
$stmt->close();
|
$stmt->close();
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
@@ -113,8 +153,9 @@ class UserModel {
|
|||||||
public function getUserById($userId) {
|
public function getUserById($userId) {
|
||||||
// Check cache first
|
// Check cache first
|
||||||
$cacheKey = "user_id_$userId";
|
$cacheKey = "user_id_$userId";
|
||||||
if (isset(self::$userCache[$cacheKey])) {
|
$cached = self::getCached($cacheKey);
|
||||||
return self::$userCache[$cacheKey];
|
if ($cached !== null) {
|
||||||
|
return $cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $this->conn->prepare("SELECT * FROM users WHERE user_id = ?");
|
$stmt = $this->conn->prepare("SELECT * FROM users WHERE user_id = ?");
|
||||||
@@ -124,7 +165,7 @@ class UserModel {
|
|||||||
|
|
||||||
if ($result->num_rows > 0) {
|
if ($result->num_rows > 0) {
|
||||||
$user = $result->fetch_assoc();
|
$user = $result->fetch_assoc();
|
||||||
self::$userCache[$cacheKey] = $user;
|
self::setCached($cacheKey, $user);
|
||||||
$stmt->close();
|
$stmt->close();
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
@@ -142,8 +183,9 @@ class UserModel {
|
|||||||
public function getUserByUsername($username) {
|
public function getUserByUsername($username) {
|
||||||
// Check cache first
|
// Check cache first
|
||||||
$cacheKey = "user_$username";
|
$cacheKey = "user_$username";
|
||||||
if (isset(self::$userCache[$cacheKey])) {
|
$cached = self::getCached($cacheKey);
|
||||||
return self::$userCache[$cacheKey];
|
if ($cached !== null) {
|
||||||
|
return $cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $this->conn->prepare("SELECT * FROM users WHERE username = ?");
|
$stmt = $this->conn->prepare("SELECT * FROM users WHERE username = ?");
|
||||||
@@ -153,7 +195,7 @@ class UserModel {
|
|||||||
|
|
||||||
if ($result->num_rows > 0) {
|
if ($result->num_rows > 0) {
|
||||||
$user = $result->fetch_assoc();
|
$user = $result->fetch_assoc();
|
||||||
self::$userCache[$cacheKey] = $user;
|
self::setCached($cacheKey, $user);
|
||||||
$stmt->close();
|
$stmt->close();
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,13 @@
|
|||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
|
||||||
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js"></script>
|
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js"></script>
|
||||||
|
<script>
|
||||||
|
// CSRF Token for AJAX requests
|
||||||
|
window.CSRF_TOKEN = '<?php
|
||||||
|
require_once __DIR__ . '/../middleware/CsrfMiddleware.php';
|
||||||
|
echo CsrfMiddleware::getToken();
|
||||||
|
?>';
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
|
|||||||
@@ -14,6 +14,13 @@
|
|||||||
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
|
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
|
||||||
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/markdown.js"></script>
|
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/markdown.js"></script>
|
||||||
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js"></script>
|
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js"></script>
|
||||||
|
<script>
|
||||||
|
// CSRF Token for AJAX requests
|
||||||
|
window.CSRF_TOKEN = '<?php
|
||||||
|
require_once __DIR__ . '/../middleware/CsrfMiddleware.php';
|
||||||
|
echo CsrfMiddleware::getToken();
|
||||||
|
?>';
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body data-categories='<?php echo json_encode($categories); ?>' data-types='<?php echo json_encode($types); ?>'>
|
<body data-categories='<?php echo json_encode($categories); ?>' data-types='<?php echo json_encode($types); ?>'>
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,13 @@ function formatDetails($details, $actionType) {
|
|||||||
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js"></script>
|
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js"></script>
|
||||||
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/ticket.js"></script>
|
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/ticket.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
// CSRF Token for AJAX requests
|
||||||
|
window.CSRF_TOKEN = '<?php
|
||||||
|
require_once __DIR__ . '/../middleware/CsrfMiddleware.php';
|
||||||
|
echo CsrfMiddleware::getToken();
|
||||||
|
?>';
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
// Store ticket data in a global variable
|
// Store ticket data in a global variable
|
||||||
window.ticketData = {
|
window.ticketData = {
|
||||||
ticket_id: "<?php echo $ticket['ticket_id']; ?>",
|
ticket_id: "<?php echo $ticket['ticket_id']; ?>",
|
||||||
|
|||||||
Reference in New Issue
Block a user