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");
|
||||
}
|
||||
|
||||
// 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'];
|
||||
$userId = $currentUser['user_id'];
|
||||
|
||||
|
||||
@@ -12,6 +12,17 @@ if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
|
||||
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'];
|
||||
|
||||
// Get request data
|
||||
|
||||
@@ -13,6 +13,17 @@ if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
|
||||
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
|
||||
$isAdmin = $_SESSION['user']['is_admin'] ?? false;
|
||||
if (!$isAdmin) {
|
||||
|
||||
@@ -17,6 +17,17 @@ if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
|
||||
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'];
|
||||
|
||||
// Create database connection
|
||||
|
||||
@@ -59,6 +59,19 @@ try {
|
||||
if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
|
||||
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'];
|
||||
$userId = $currentUser['user_id'];
|
||||
$isAdmin = $currentUser['is_admin'] ?? false;
|
||||
|
||||
@@ -17,6 +17,17 @@ if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
|
||||
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'];
|
||||
|
||||
// Create database connection
|
||||
|
||||
@@ -156,7 +156,10 @@ async function saveCurrentFilter() {
|
||||
try {
|
||||
const response = await fetch('/api/saved_filters.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify({
|
||||
filter_name: filterName.trim(),
|
||||
filter_criteria: filterCriteria
|
||||
@@ -319,7 +322,10 @@ async function deleteSavedFilter() {
|
||||
try {
|
||||
const response = await fetch('/api/saved_filters.php', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify({ filter_id: filterId })
|
||||
});
|
||||
|
||||
|
||||
@@ -276,7 +276,8 @@ function quickSave() {
|
||||
fetch('/api/update_ticket.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
@@ -355,7 +356,8 @@ function saveTicket() {
|
||||
fetch('/api/update_ticket.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify({
|
||||
ticket_id: ticketId,
|
||||
@@ -492,7 +494,10 @@ function bulkClose() {
|
||||
|
||||
fetch('/api/bulk_operation.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify({
|
||||
operation_type: 'bulk_close',
|
||||
ticket_ids: ticketIds
|
||||
@@ -593,7 +598,10 @@ function performBulkAssign() {
|
||||
|
||||
fetch('/api/bulk_operation.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify({
|
||||
operation_type: 'bulk_assign',
|
||||
ticket_ids: ticketIds,
|
||||
@@ -681,7 +689,10 @@ function performBulkPriority() {
|
||||
|
||||
fetch('/api/bulk_operation.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify({
|
||||
operation_type: 'bulk_priority',
|
||||
ticket_ids: ticketIds,
|
||||
@@ -800,15 +811,18 @@ function closeBulkStatusModal() {
|
||||
function performBulkStatusChange() {
|
||||
const status = document.getElementById('bulkStatus').value;
|
||||
const ticketIds = getSelectedTicketIds();
|
||||
|
||||
|
||||
if (!status) {
|
||||
alert('Please select a status');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
fetch('/api/bulk_operation.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify({
|
||||
operation_type: 'bulk_status',
|
||||
ticket_ids: ticketIds,
|
||||
@@ -885,10 +899,13 @@ function closeBulkDeleteModal() {
|
||||
|
||||
function performBulkDelete() {
|
||||
const ticketIds = getSelectedTicketIds();
|
||||
|
||||
|
||||
fetch('/api/bulk_operation.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify({
|
||||
operation_type: 'bulk_delete',
|
||||
ticket_ids: ticketIds
|
||||
|
||||
@@ -81,7 +81,10 @@ async function saveSettings() {
|
||||
for (const [key, value] of Object.entries(prefs)) {
|
||||
const response = await fetch('/api/user_preferences.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify({ key, value })
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,8 @@ function saveTicket() {
|
||||
fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify({
|
||||
ticket_id: ticketId,
|
||||
@@ -140,7 +141,8 @@ function addComment() {
|
||||
fetch('/api/add_comment.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify({
|
||||
ticket_id: ticketId,
|
||||
@@ -178,18 +180,33 @@ function addComment() {
|
||||
.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 newComment = `
|
||||
<div class="comment">
|
||||
<div class="comment-header">
|
||||
<span class="comment-user">${data.user_name}</span>
|
||||
<span class="comment-date">${data.created_at}</span>
|
||||
</div>
|
||||
<div class="comment-text">${displayText}</div>
|
||||
</div>
|
||||
`;
|
||||
commentsList.insertAdjacentHTML('afterbegin', newComment);
|
||||
|
||||
const commentDiv = document.createElement('div');
|
||||
commentDiv.className = 'comment';
|
||||
|
||||
const headerDiv = document.createElement('div');
|
||||
headerDiv.className = 'comment-header';
|
||||
|
||||
const userSpan = document.createElement('span');
|
||||
userSpan.className = 'comment-user';
|
||||
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 {
|
||||
console.error('Error adding comment:', data.error || 'Unknown error');
|
||||
}
|
||||
@@ -283,7 +300,10 @@ function handleAssignmentChange() {
|
||||
|
||||
fetch('/api/assign_ticket.php', {
|
||||
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 })
|
||||
})
|
||||
.then(response => response.json())
|
||||
@@ -316,7 +336,10 @@ function handleMetadataChanges() {
|
||||
|
||||
fetch('/api/update_ticket.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify({
|
||||
ticket_id: ticketId,
|
||||
[fieldName]: fieldName === 'priority' ? parseInt(newValue) : newValue
|
||||
@@ -418,7 +441,8 @@ function updateTicketStatus() {
|
||||
fetch('/api/update_ticket.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify({
|
||||
ticket_id: ticketId,
|
||||
|
||||
@@ -20,8 +20,14 @@ class AuthMiddleware {
|
||||
* @throws Exception if authentication fails
|
||||
*/
|
||||
public function authenticate() {
|
||||
// Start session if not already started
|
||||
// Start session if not already started with secure settings
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -66,10 +72,17 @@ class AuthMiddleware {
|
||||
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
|
||||
$_SESSION['user'] = $user;
|
||||
$_SESSION['last_activity'] = time();
|
||||
|
||||
// Generate new CSRF token on login
|
||||
require_once __DIR__ . '/CsrfMiddleware.php';
|
||||
CsrfMiddleware::generateToken();
|
||||
|
||||
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);
|
||||
$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) {
|
||||
$ticketId = trim($ticketId);
|
||||
$success = false;
|
||||
@@ -77,8 +80,8 @@ class BulkOperationsModel {
|
||||
try {
|
||||
switch ($operation['operation_type']) {
|
||||
case 'bulk_close':
|
||||
// Get current ticket to preserve other fields
|
||||
$currentTicket = $ticketModel->getTicketById($ticketId);
|
||||
// Get current ticket from pre-loaded batch
|
||||
$currentTicket = $ticketsById[$ticketId] ?? null;
|
||||
if ($currentTicket) {
|
||||
$success = $ticketModel->updateTicket([
|
||||
'ticket_id' => $ticketId,
|
||||
@@ -109,7 +112,7 @@ class BulkOperationsModel {
|
||||
|
||||
case 'bulk_priority':
|
||||
if (isset($parameters['priority'])) {
|
||||
$currentTicket = $ticketModel->getTicketById($ticketId);
|
||||
$currentTicket = $ticketsById[$ticketId] ?? null;
|
||||
if ($currentTicket) {
|
||||
$success = $ticketModel->updateTicket([
|
||||
'ticket_id' => $ticketId,
|
||||
@@ -131,7 +134,7 @@ class BulkOperationsModel {
|
||||
|
||||
case 'bulk_status':
|
||||
if (isset($parameters['status'])) {
|
||||
$currentTicket = $ticketModel->getTicketById($ticketId);
|
||||
$currentTicket = $ticketsById[$ticketId] ?? null;
|
||||
if ($currentTicket) {
|
||||
$success = $ticketModel->updateTicket([
|
||||
'ticket_id' => $ticketId,
|
||||
|
||||
@@ -377,4 +377,50 @@ class TicketModel {
|
||||
$stmt->close();
|
||||
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 {
|
||||
private $conn;
|
||||
private static $userCache = [];
|
||||
private static $userCache = []; // ['key' => ['data' => ..., 'expires' => timestamp]]
|
||||
private static $cacheTTL = 300; // 5 minutes
|
||||
|
||||
public function __construct($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)
|
||||
*
|
||||
@@ -22,8 +60,9 @@ class UserModel {
|
||||
public function syncUserFromAuthelia($username, $displayName = '', $email = '', $groups = '') {
|
||||
// Check cache first
|
||||
$cacheKey = "user_$username";
|
||||
if (isset(self::$userCache[$cacheKey])) {
|
||||
return self::$userCache[$cacheKey];
|
||||
$cached = self::getCached($cacheKey);
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
// Determine if user is admin based on groups
|
||||
@@ -72,8 +111,8 @@ class UserModel {
|
||||
|
||||
$stmt->close();
|
||||
|
||||
// Cache user
|
||||
self::$userCache[$cacheKey] = $user;
|
||||
// Cache user with TTL
|
||||
self::setCached($cacheKey, $user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
@@ -85,8 +124,9 @@ class UserModel {
|
||||
*/
|
||||
public function getSystemUser() {
|
||||
// Check cache first
|
||||
if (isset(self::$userCache['system'])) {
|
||||
return self::$userCache['system'];
|
||||
$cached = self::getCached('system');
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$stmt = $this->conn->prepare("SELECT * FROM users WHERE username = 'system'");
|
||||
@@ -95,7 +135,7 @@ class UserModel {
|
||||
|
||||
if ($result->num_rows > 0) {
|
||||
$user = $result->fetch_assoc();
|
||||
self::$userCache['system'] = $user;
|
||||
self::setCached('system', $user);
|
||||
$stmt->close();
|
||||
return $user;
|
||||
}
|
||||
@@ -113,8 +153,9 @@ class UserModel {
|
||||
public function getUserById($userId) {
|
||||
// Check cache first
|
||||
$cacheKey = "user_id_$userId";
|
||||
if (isset(self::$userCache[$cacheKey])) {
|
||||
return self::$userCache[$cacheKey];
|
||||
$cached = self::getCached($cacheKey);
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$stmt = $this->conn->prepare("SELECT * FROM users WHERE user_id = ?");
|
||||
@@ -124,7 +165,7 @@ class UserModel {
|
||||
|
||||
if ($result->num_rows > 0) {
|
||||
$user = $result->fetch_assoc();
|
||||
self::$userCache[$cacheKey] = $user;
|
||||
self::setCached($cacheKey, $user);
|
||||
$stmt->close();
|
||||
return $user;
|
||||
}
|
||||
@@ -142,8 +183,9 @@ class UserModel {
|
||||
public function getUserByUsername($username) {
|
||||
// Check cache first
|
||||
$cacheKey = "user_$username";
|
||||
if (isset(self::$userCache[$cacheKey])) {
|
||||
return self::$userCache[$cacheKey];
|
||||
$cached = self::getCached($cacheKey);
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$stmt = $this->conn->prepare("SELECT * FROM users WHERE username = ?");
|
||||
@@ -153,7 +195,7 @@ class UserModel {
|
||||
|
||||
if ($result->num_rows > 0) {
|
||||
$user = $result->fetch_assoc();
|
||||
self::$userCache[$cacheKey] = $user;
|
||||
self::setCached($cacheKey, $user);
|
||||
$stmt->close();
|
||||
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/ticket.css">
|
||||
<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>
|
||||
<body>
|
||||
<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/markdown.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>
|
||||
<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/ticket.js"></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
|
||||
window.ticketData = {
|
||||
ticket_id: "<?php echo $ticket['ticket_id']; ?>",
|
||||
|
||||
Reference in New Issue
Block a user