Security (Phase 1-2): - Add SecurityHeadersMiddleware with CSP, X-Frame-Options, etc. - Add RateLimitMiddleware for API rate limiting - Add security event logging to AuditLogModel - Add ResponseHelper for standardized API responses - Update config.php with security constants Database (Phase 3): - Add migration 014 for additional indexes - Add migration 015 for ticket dependencies - Add migration 016 for ticket attachments - Add migration 017 for recurring tickets - Add migration 018 for custom fields Features (Phase 4-5): - Add ticket dependencies with DependencyModel and API - Add duplicate detection with check_duplicates API - Add file attachments with AttachmentModel and upload/download APIs - Add @mentions with autocomplete and highlighting - Add quick actions on dashboard rows Collaboration (Phase 5): - Add mention extraction in CommentModel - Add mention autocomplete dropdown in ticket.js - Add mention highlighting CSS styles Admin & Export (Phase 6): - Add StatsModel for dashboard widgets - Add dashboard stats cards (open, critical, unassigned, etc.) - Add CSV/JSON export via export_tickets API - Add rich text editor toolbar in markdown.js - Add RecurringTicketModel with cron job - Add CustomFieldModel for per-category fields - Add admin views: RecurringTickets, CustomFields, Workflow, Templates, AuditLog, UserActivity - Add admin APIs: manage_workflows, manage_templates, manage_recurring, custom_fields, get_users - Add admin routes in index.php Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
269 lines
9.3 KiB
PHP
269 lines
9.3 KiB
PHP
<?php
|
|
// Main entry point for the application
|
|
require_once 'config/config.php';
|
|
require_once 'middleware/SecurityHeadersMiddleware.php';
|
|
require_once 'middleware/AuthMiddleware.php';
|
|
require_once 'models/AuditLogModel.php';
|
|
|
|
// Apply security headers early
|
|
SecurityHeadersMiddleware::apply();
|
|
|
|
// Parse the URL - no need to remove base path since we're at document root
|
|
$request = $_SERVER['REQUEST_URI'];
|
|
|
|
// Remove query string for routing (but keep it available)
|
|
$requestPath = strtok($request, '?');
|
|
|
|
// Create database connection for non-API routes
|
|
if (!str_starts_with($requestPath, '/api/')) {
|
|
$conn = new mysqli(
|
|
$GLOBALS['config']['DB_HOST'],
|
|
$GLOBALS['config']['DB_USER'],
|
|
$GLOBALS['config']['DB_PASS'],
|
|
$GLOBALS['config']['DB_NAME']
|
|
);
|
|
|
|
if ($conn->connect_error) {
|
|
die("Connection failed: " . $conn->connect_error);
|
|
}
|
|
|
|
// Authenticate user via Authelia forward auth
|
|
$authMiddleware = new AuthMiddleware($conn);
|
|
$currentUser = $authMiddleware->authenticate();
|
|
|
|
// Store current user in globals for controllers
|
|
$GLOBALS['currentUser'] = $currentUser;
|
|
|
|
// Initialize audit log model
|
|
$GLOBALS['auditLog'] = new AuditLogModel($conn);
|
|
}
|
|
|
|
// Simple router
|
|
switch (true) {
|
|
case $requestPath == '/' || $requestPath == '':
|
|
require_once 'controllers/DashboardController.php';
|
|
$controller = new DashboardController($conn);
|
|
$controller->index();
|
|
break;
|
|
|
|
case preg_match('/^\/ticket\/(\d+)$/', $requestPath, $matches):
|
|
require_once 'controllers/TicketController.php';
|
|
$controller = new TicketController($conn);
|
|
$controller->view($matches[1]);
|
|
break;
|
|
|
|
case $requestPath == '/ticket/create':
|
|
require_once 'controllers/TicketController.php';
|
|
$controller = new TicketController($conn);
|
|
$controller->create();
|
|
break;
|
|
|
|
// API Routes - these handle their own database connections
|
|
case $requestPath == '/api/update_ticket.php':
|
|
require_once 'api/update_ticket.php';
|
|
break;
|
|
|
|
case $requestPath == '/api/add_comment.php':
|
|
require_once 'api/add_comment.php';
|
|
break;
|
|
|
|
// Admin Routes - require admin privileges
|
|
case $requestPath == '/admin/recurring-tickets':
|
|
if (!$currentUser || !$currentUser['is_admin']) {
|
|
header("HTTP/1.0 403 Forbidden");
|
|
echo 'Admin access required';
|
|
break;
|
|
}
|
|
require_once 'models/RecurringTicketModel.php';
|
|
$recurringModel = new RecurringTicketModel($conn);
|
|
$recurringTickets = $recurringModel->getAll(true);
|
|
include 'views/admin/RecurringTicketsView.php';
|
|
break;
|
|
|
|
case $requestPath == '/admin/custom-fields':
|
|
if (!$currentUser || !$currentUser['is_admin']) {
|
|
header("HTTP/1.0 403 Forbidden");
|
|
echo 'Admin access required';
|
|
break;
|
|
}
|
|
require_once 'models/CustomFieldModel.php';
|
|
$fieldModel = new CustomFieldModel($conn);
|
|
$customFields = $fieldModel->getAllDefinitions(null, false);
|
|
include 'views/admin/CustomFieldsView.php';
|
|
break;
|
|
|
|
case $requestPath == '/admin/workflow':
|
|
if (!$currentUser || !$currentUser['is_admin']) {
|
|
header("HTTP/1.0 403 Forbidden");
|
|
echo 'Admin access required';
|
|
break;
|
|
}
|
|
$result = $conn->query("SELECT * FROM status_transitions ORDER BY from_status, to_status");
|
|
$workflows = [];
|
|
while ($row = $result->fetch_assoc()) {
|
|
$workflows[] = $row;
|
|
}
|
|
include 'views/admin/WorkflowDesignerView.php';
|
|
break;
|
|
|
|
case $requestPath == '/admin/templates':
|
|
if (!$currentUser || !$currentUser['is_admin']) {
|
|
header("HTTP/1.0 403 Forbidden");
|
|
echo 'Admin access required';
|
|
break;
|
|
}
|
|
$result = $conn->query("SELECT * FROM ticket_templates ORDER BY template_name");
|
|
$templates = [];
|
|
while ($row = $result->fetch_assoc()) {
|
|
$templates[] = $row;
|
|
}
|
|
include 'views/admin/TemplatesView.php';
|
|
break;
|
|
|
|
case $requestPath == '/admin/audit-log':
|
|
if (!$currentUser || !$currentUser['is_admin']) {
|
|
header("HTTP/1.0 403 Forbidden");
|
|
echo 'Admin access required';
|
|
break;
|
|
}
|
|
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
|
|
$perPage = 50;
|
|
$offset = ($page - 1) * $perPage;
|
|
|
|
$filters = [];
|
|
$whereConditions = [];
|
|
$params = [];
|
|
$types = '';
|
|
|
|
if (!empty($_GET['action_type'])) {
|
|
$whereConditions[] = "al.action_type = ?";
|
|
$params[] = $_GET['action_type'];
|
|
$types .= 's';
|
|
$filters['action_type'] = $_GET['action_type'];
|
|
}
|
|
if (!empty($_GET['user_id'])) {
|
|
$whereConditions[] = "al.user_id = ?";
|
|
$params[] = (int)$_GET['user_id'];
|
|
$types .= 'i';
|
|
$filters['user_id'] = $_GET['user_id'];
|
|
}
|
|
if (!empty($_GET['date_from'])) {
|
|
$whereConditions[] = "DATE(al.created_at) >= ?";
|
|
$params[] = $_GET['date_from'];
|
|
$types .= 's';
|
|
$filters['date_from'] = $_GET['date_from'];
|
|
}
|
|
if (!empty($_GET['date_to'])) {
|
|
$whereConditions[] = "DATE(al.created_at) <= ?";
|
|
$params[] = $_GET['date_to'];
|
|
$types .= 's';
|
|
$filters['date_to'] = $_GET['date_to'];
|
|
}
|
|
|
|
$where = !empty($whereConditions) ? 'WHERE ' . implode(' AND ', $whereConditions) : '';
|
|
|
|
$countSql = "SELECT COUNT(*) as total FROM audit_log al $where";
|
|
if (!empty($params)) {
|
|
$stmt = $conn->prepare($countSql);
|
|
$stmt->bind_param($types, ...$params);
|
|
$stmt->execute();
|
|
$countResult = $stmt->get_result();
|
|
} else {
|
|
$countResult = $conn->query($countSql);
|
|
}
|
|
$totalLogs = $countResult->fetch_assoc()['total'];
|
|
$totalPages = ceil($totalLogs / $perPage);
|
|
|
|
$sql = "SELECT al.*, u.display_name, u.username
|
|
FROM audit_log al
|
|
LEFT JOIN users u ON al.user_id = u.user_id
|
|
$where
|
|
ORDER BY al.created_at DESC
|
|
LIMIT $perPage OFFSET $offset";
|
|
|
|
if (!empty($params)) {
|
|
$stmt = $conn->prepare($sql);
|
|
$stmt->bind_param($types, ...$params);
|
|
$stmt->execute();
|
|
$result = $stmt->get_result();
|
|
} else {
|
|
$result = $conn->query($sql);
|
|
}
|
|
|
|
$auditLogs = [];
|
|
while ($row = $result->fetch_assoc()) {
|
|
$auditLogs[] = $row;
|
|
}
|
|
|
|
$usersResult = $conn->query("SELECT user_id, username, display_name FROM users ORDER BY display_name");
|
|
$users = [];
|
|
while ($row = $usersResult->fetch_assoc()) {
|
|
$users[] = $row;
|
|
}
|
|
|
|
include 'views/admin/AuditLogView.php';
|
|
break;
|
|
|
|
case $requestPath == '/admin/user-activity':
|
|
if (!$currentUser || !$currentUser['is_admin']) {
|
|
header("HTTP/1.0 403 Forbidden");
|
|
echo 'Admin access required';
|
|
break;
|
|
}
|
|
|
|
$dateRange = [
|
|
'from' => $_GET['date_from'] ?? date('Y-m-d', strtotime('-30 days')),
|
|
'to' => $_GET['date_to'] ?? date('Y-m-d')
|
|
];
|
|
|
|
$sql = "SELECT
|
|
u.user_id, u.username, u.display_name, u.is_admin,
|
|
(SELECT COUNT(*) FROM tickets t WHERE t.created_by = u.user_id AND DATE(t.created_at) BETWEEN ? AND ?) as tickets_created,
|
|
(SELECT COUNT(*) FROM tickets t WHERE t.assigned_to = u.user_id AND t.status = 'Closed' AND DATE(t.updated_at) BETWEEN ? AND ?) as tickets_resolved,
|
|
(SELECT COUNT(*) FROM ticket_comments tc WHERE tc.user_id = u.user_id AND DATE(tc.created_at) BETWEEN ? AND ?) as comments_added,
|
|
(SELECT COUNT(*) FROM tickets t WHERE t.assigned_to = u.user_id AND DATE(t.created_at) BETWEEN ? AND ?) as tickets_assigned,
|
|
(SELECT MAX(al.created_at) FROM audit_log al WHERE al.user_id = u.user_id) as last_activity
|
|
FROM users u
|
|
WHERE u.is_active = 1
|
|
ORDER BY tickets_created DESC, tickets_resolved DESC";
|
|
|
|
$stmt = $conn->prepare($sql);
|
|
$stmt->bind_param('ssssssss',
|
|
$dateRange['from'], $dateRange['to'],
|
|
$dateRange['from'], $dateRange['to'],
|
|
$dateRange['from'], $dateRange['to'],
|
|
$dateRange['from'], $dateRange['to']
|
|
);
|
|
$stmt->execute();
|
|
$result = $stmt->get_result();
|
|
|
|
$userStats = [];
|
|
while ($row = $result->fetch_assoc()) {
|
|
$userStats[] = $row;
|
|
}
|
|
$stmt->close();
|
|
|
|
include 'views/admin/UserActivityView.php';
|
|
break;
|
|
|
|
// Legacy support for old URLs
|
|
case $requestPath == '/dashboard.php':
|
|
header("Location: /");
|
|
exit;
|
|
|
|
case preg_match('/^\/ticket\.php/', $requestPath) && isset($_GET['id']):
|
|
header("Location: /ticket/" . $_GET['id']);
|
|
exit;
|
|
|
|
default:
|
|
// 404 Not Found
|
|
header("HTTP/1.0 404 Not Found");
|
|
echo '404 Page Not Found';
|
|
break;
|
|
}
|
|
|
|
// Close database connection if it was opened
|
|
if (isset($conn)) {
|
|
$conn->close();
|
|
}
|
|
?>
|