diff --git a/README.md b/README.md index 81bad77..b72c9d5 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,7 @@ Access all admin pages via the **Admin dropdown** in the dashboard header. | `/api/update_ticket.php` | POST | Update ticket with workflow validation | | `/api/assign_ticket.php` | POST | Assign ticket to user | | `/api/add_comment.php` | POST | Add comment to ticket | +| `/api/clone_ticket.php` | POST | Clone an existing ticket | | `/api/get_template.php` | GET | Fetch ticket template | | `/api/get_users.php` | GET | Get user list for assignments | | `/api/bulk_operation.php` | POST | Perform bulk operations | @@ -220,6 +221,12 @@ Access all admin pages via the **Admin dropdown** in the dashboard header. | `/api/manage_recurring.php` | CRUD | Recurring tickets (admin) | | `/api/manage_templates.php` | CRUD | Templates (admin) | | `/api/manage_workflows.php` | CRUD | Workflow rules (admin) | +| `/api/custom_fields.php` | CRUD | Custom field definitions/values (admin) | +| `/api/saved_filters.php` | CRUD | Saved filter combinations | +| `/api/user_preferences.php` | GET/POST | User preferences | +| `/api/audit_log.php` | GET | Audit log entries (admin) | +| `/api/bootstrap.php` | GET | Bootstrap config/user data for front-end | +| `/api/health.php` | GET | Health check | ## Project Structure @@ -228,8 +235,12 @@ tinker_tickets/ ├── api/ │ ├── add_comment.php # POST: Add comment │ ├── assign_ticket.php # POST: Assign ticket to user +│ ├── audit_log.php # GET: Audit log entries (admin) +│ ├── bootstrap.php # GET: Bootstrap data (config/user for front-end) │ ├── bulk_operation.php # POST: Bulk operations (admin only) │ ├── check_duplicates.php # GET: Check for duplicate tickets +│ ├── clone_ticket.php # POST: Clone an existing ticket +│ ├── custom_fields.php # CRUD: Custom field definitions/values (admin) │ ├── delete_attachment.php # POST/DELETE: Delete attachment │ ├── delete_comment.php # POST: Delete comment (owner/admin) │ ├── download_attachment.php # GET: Download with visibility check @@ -237,23 +248,26 @@ tinker_tickets/ │ ├── generate_api_key.php # POST: Generate API key (admin) │ ├── get_template.php # GET: Fetch ticket template │ ├── get_users.php # GET: Get user list +│ ├── health.php # GET: Health check endpoint │ ├── manage_recurring.php # CRUD: Recurring tickets (admin) │ ├── manage_templates.php # CRUD: Templates (admin) │ ├── manage_workflows.php # CRUD: Workflow rules (admin) │ ├── revoke_api_key.php # POST: Revoke API key (admin) +│ ├── saved_filters.php # CRUD: Saved filter combinations │ ├── ticket_dependencies.php # GET/POST/DELETE: Ticket dependencies │ ├── update_comment.php # POST: Update comment (owner/admin) │ ├── update_ticket.php # POST: Update ticket (workflow validation) -│ └── upload_attachment.php # GET/POST: List or upload attachments +│ ├── upload_attachment.php # GET/POST: List or upload attachments +│ └── user_preferences.php # GET/POST: User preferences ├── assets/ │ ├── css/ -│ │ ├── base.css # LotusGuild Terminal Design System (symlinked from web_template) +│ │ ├── base.css # LotusGuild Terminal Design System (copied from web_template) │ │ ├── dashboard.css # Dashboard + terminal styling │ │ └── ticket.css # Ticket view styling │ ├── js/ │ │ ├── advanced-search.js # Advanced search modal │ │ ├── ascii-banner.js # ASCII art banner (rendered in boot overlay on first visit) -│ │ ├── base.js # LotusGuild JS utilities — window.lt (symlinked from web_template) +│ │ ├── base.js # LotusGuild JS utilities — window.lt (copied from web_template) │ │ ├── dashboard.js # Dashboard + bulk actions + kanban + sidebar │ │ ├── keyboard-shortcuts.js # Keyboard shortcuts (uses lt.keys) │ │ ├── markdown.js # Markdown rendering + ticket linking (XSS-safe) @@ -292,6 +306,8 @@ tinker_tickets/ │ ├── UserPreferencesModel.php # User preferences │ └── WorkflowModel.php # Status transition workflows ├── scripts/ +│ ├── add_closed_at_column.php # Migration: add closed_at column to tickets +│ ├── add_comment_updated_at.php # Migration: add updated_at column to ticket_comments │ ├── cleanup_orphan_uploads.php # Clean orphaned uploads │ └── create_dependencies_table.php # Create ticket_dependencies table ├── uploads/ # File attachment storage diff --git a/api/add_comment.php b/api/add_comment.php index 937d2ab..d15396d 100644 --- a/api/add_comment.php +++ b/api/add_comment.php @@ -28,6 +28,7 @@ try { require_once $commentModelPath; require_once $auditLogModelPath; require_once dirname(__DIR__) . '/helpers/Database.php'; + require_once dirname(__DIR__) . '/models/TicketModel.php'; // Check authentication via session if (session_status() === PHP_SESSION_NONE) { @@ -71,6 +72,24 @@ try { exit; } + // Verify user can access the ticket before allowing a comment + $ticketModel = new TicketModel($conn); + $ticket = $ticketModel->getTicketById($ticketId); + if (!$ticket) { + http_response_code(404); + ob_end_clean(); + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'error' => 'Ticket not found']); + exit; + } + if (!$ticketModel->canUserAccessTicket($ticket, $currentUser)) { + http_response_code(403); + ob_end_clean(); + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'error' => 'Access denied']); + exit; + } + // Initialize models $commentModel = new CommentModel($conn); $auditLog = new AuditLogModel($conn); diff --git a/api/assign_ticket.php b/api/assign_ticket.php index 88ed712..351da55 100644 --- a/api/assign_ticket.php +++ b/api/assign_ticket.php @@ -25,9 +25,9 @@ $ticketModel = new TicketModel($conn); $auditLogModel = new AuditLogModel($conn); $userModel = new UserModel($conn); -// Verify ticket exists +// Verify ticket exists and user can access it $ticket = $ticketModel->getTicketById($ticketId); -if (!$ticket) { +if (!$ticket || !$ticketModel->canUserAccessTicket($ticket, $currentUser)) { http_response_code(404); echo json_encode(['success' => false, 'error' => 'Ticket not found']); exit; diff --git a/api/check_duplicates.php b/api/check_duplicates.php index efe14f2..91110d6 100644 --- a/api/check_duplicates.php +++ b/api/check_duplicates.php @@ -7,6 +7,7 @@ require_once __DIR__ . '/bootstrap.php'; require_once dirname(__DIR__) . '/helpers/ResponseHelper.php'; +require_once dirname(__DIR__) . '/models/TicketModel.php'; // Only accept GET requests if ($_SERVER['REQUEST_METHOD'] !== 'GET') { @@ -30,6 +31,10 @@ $searchTerm = '%' . $title . '%'; // Get SOUNDEX of title $soundexTitle = soundex($title); +// Build visibility filter so users only see titles they have access to +$ticketModel = new TicketModel($conn); +$visFilter = $ticketModel->getVisibilityFilter($currentUser); + // First, search for exact substring matches (case-insensitive) $sql = "SELECT ticket_id, title, status, priority, created_at FROM tickets @@ -38,11 +43,16 @@ $sql = "SELECT ticket_id, title, status, priority, created_at OR SOUNDEX(title) = ? ) AND status != 'Closed' + AND ({$visFilter['sql']}) ORDER BY created_at DESC LIMIT 10"; +$types = "ss" . $visFilter['types']; +$params = array_merge([$searchTerm, $soundexTitle], $visFilter['params']); $stmt = $conn->prepare($sql); -$stmt->bind_param("ss", $searchTerm, $soundexTitle); +if (!empty($params)) { + $stmt->bind_param($types, ...$params); +} $stmt->execute(); $result = $stmt->get_result(); diff --git a/api/ticket_dependencies.php b/api/ticket_dependencies.php index 743efd1..b3c781c 100644 --- a/api/ticket_dependencies.php +++ b/api/ticket_dependencies.php @@ -67,6 +67,7 @@ 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__) . '/models/TicketModel.php'; require_once dirname(__DIR__) . '/helpers/ResponseHelper.php'; header('Content-Type: application/json'); @@ -77,6 +78,7 @@ if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) { } $userId = $_SESSION['user']['user_id']; +$currentUser = $_SESSION['user']; // CSRF Protection for POST/DELETE if ($_SERVER['REQUEST_METHOD'] === 'POST' || $_SERVER['REQUEST_METHOD'] === 'DELETE') { @@ -99,6 +101,7 @@ if ($tableCheck->num_rows === 0) { try { $dependencyModel = new DependencyModel($conn); $auditLog = new AuditLogModel($conn); + $ticketModel = new TicketModel($conn); } catch (Exception $e) { error_log('Failed to initialize models in ticket_dependencies.php: ' . $e->getMessage()); ResponseHelper::serverError('Failed to initialize required components'); @@ -116,6 +119,12 @@ switch ($method) { ResponseHelper::error('Ticket ID required'); } + // Verify user can access this ticket + $ticket = $ticketModel->getTicketById((int)$ticketId); + if (!$ticket || !$ticketModel->canUserAccessTicket($ticket, $currentUser)) { + ResponseHelper::notFound('Ticket not found'); + } + try { $dependencies = $dependencyModel->getDependencies($ticketId); $dependents = $dependencyModel->getDependentTickets($ticketId); diff --git a/assets/js/ticket.js b/assets/js/ticket.js index 1e3ab6b..2124b5a 100644 --- a/assets/js/ticket.js +++ b/assets/js/ticket.js @@ -188,9 +188,11 @@ function addComment() { commentsList.insertBefore(commentDiv, commentsList.firstChild); } else { + lt.toast.error(data.error || 'Failed to add comment'); } }) .catch(error => { + lt.toast.error('Error adding comment: ' + error.message); }); } diff --git a/index.php b/index.php index 10ac07d..a6bebb5 100644 --- a/index.php +++ b/index.php @@ -146,6 +146,34 @@ switch (true) { require_once 'api/check_duplicates.php'; break; + case $requestPath == '/api/custom_fields.php': + require_once 'api/custom_fields.php'; + break; + + case $requestPath == '/api/saved_filters.php': + require_once 'api/saved_filters.php'; + break; + + case $requestPath == '/api/audit_log.php': + require_once 'api/audit_log.php'; + break; + + case $requestPath == '/api/user_preferences.php': + require_once 'api/user_preferences.php'; + break; + + case $requestPath == '/api/download_attachment.php': + require_once 'api/download_attachment.php'; + break; + + case $requestPath == '/api/clone_ticket.php': + require_once 'api/clone_ticket.php'; + break; + + case $requestPath == '/api/health.php': + require_once 'api/health.php'; + break; + // Admin Routes - require admin privileges case $requestPath == '/admin/recurring-tickets': if (!$currentUser || !$currentUser['is_admin']) {