Fix visibility enforcement and register missing API routes
Security fixes: - add_comment.php: verify canUserAccessTicket() before allowing comment creation - assign_ticket.php: use canUserAccessTicket() to prevent info leakage via 403 vs 404 - check_duplicates.php: apply getVisibilityFilter() so confidential ticket titles are not exposed in duplicate search results - ticket_dependencies.php: verify ticket access on GET before returning dependency data Route registration: - Register 7 previously missing API endpoints in index.php: custom_fields, saved_filters, audit_log, user_preferences, download_attachment, clone_ticket, health Frontend: - ticket.js: fill empty catch block and empty else block in addComment() with proper error toasts Documentation: - README.md: document all API endpoints and update project structure listing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
22
README.md
22
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/update_ticket.php` | POST | Update ticket with workflow validation |
|
||||||
| `/api/assign_ticket.php` | POST | Assign ticket to user |
|
| `/api/assign_ticket.php` | POST | Assign ticket to user |
|
||||||
| `/api/add_comment.php` | POST | Add comment to ticket |
|
| `/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_template.php` | GET | Fetch ticket template |
|
||||||
| `/api/get_users.php` | GET | Get user list for assignments |
|
| `/api/get_users.php` | GET | Get user list for assignments |
|
||||||
| `/api/bulk_operation.php` | POST | Perform bulk operations |
|
| `/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_recurring.php` | CRUD | Recurring tickets (admin) |
|
||||||
| `/api/manage_templates.php` | CRUD | Templates (admin) |
|
| `/api/manage_templates.php` | CRUD | Templates (admin) |
|
||||||
| `/api/manage_workflows.php` | CRUD | Workflow rules (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
|
## Project Structure
|
||||||
|
|
||||||
@@ -228,8 +235,12 @@ tinker_tickets/
|
|||||||
├── api/
|
├── api/
|
||||||
│ ├── add_comment.php # POST: Add comment
|
│ ├── add_comment.php # POST: Add comment
|
||||||
│ ├── assign_ticket.php # POST: Assign ticket to user
|
│ ├── 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)
|
│ ├── bulk_operation.php # POST: Bulk operations (admin only)
|
||||||
│ ├── check_duplicates.php # GET: Check for duplicate tickets
|
│ ├── 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_attachment.php # POST/DELETE: Delete attachment
|
||||||
│ ├── delete_comment.php # POST: Delete comment (owner/admin)
|
│ ├── delete_comment.php # POST: Delete comment (owner/admin)
|
||||||
│ ├── download_attachment.php # GET: Download with visibility check
|
│ ├── download_attachment.php # GET: Download with visibility check
|
||||||
@@ -237,23 +248,26 @@ tinker_tickets/
|
|||||||
│ ├── generate_api_key.php # POST: Generate API key (admin)
|
│ ├── generate_api_key.php # POST: Generate API key (admin)
|
||||||
│ ├── get_template.php # GET: Fetch ticket template
|
│ ├── get_template.php # GET: Fetch ticket template
|
||||||
│ ├── get_users.php # GET: Get user list
|
│ ├── get_users.php # GET: Get user list
|
||||||
|
│ ├── health.php # GET: Health check endpoint
|
||||||
│ ├── manage_recurring.php # CRUD: Recurring tickets (admin)
|
│ ├── manage_recurring.php # CRUD: Recurring tickets (admin)
|
||||||
│ ├── manage_templates.php # CRUD: Templates (admin)
|
│ ├── manage_templates.php # CRUD: Templates (admin)
|
||||||
│ ├── manage_workflows.php # CRUD: Workflow rules (admin)
|
│ ├── manage_workflows.php # CRUD: Workflow rules (admin)
|
||||||
│ ├── revoke_api_key.php # POST: Revoke API key (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
|
│ ├── ticket_dependencies.php # GET/POST/DELETE: Ticket dependencies
|
||||||
│ ├── update_comment.php # POST: Update comment (owner/admin)
|
│ ├── update_comment.php # POST: Update comment (owner/admin)
|
||||||
│ ├── update_ticket.php # POST: Update ticket (workflow validation)
|
│ ├── 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/
|
├── assets/
|
||||||
│ ├── css/
|
│ ├── 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
|
│ │ ├── dashboard.css # Dashboard + terminal styling
|
||||||
│ │ └── ticket.css # Ticket view styling
|
│ │ └── ticket.css # Ticket view styling
|
||||||
│ ├── js/
|
│ ├── js/
|
||||||
│ │ ├── advanced-search.js # Advanced search modal
|
│ │ ├── advanced-search.js # Advanced search modal
|
||||||
│ │ ├── ascii-banner.js # ASCII art banner (rendered in boot overlay on first visit)
|
│ │ ├── 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
|
│ │ ├── dashboard.js # Dashboard + bulk actions + kanban + sidebar
|
||||||
│ │ ├── keyboard-shortcuts.js # Keyboard shortcuts (uses lt.keys)
|
│ │ ├── keyboard-shortcuts.js # Keyboard shortcuts (uses lt.keys)
|
||||||
│ │ ├── markdown.js # Markdown rendering + ticket linking (XSS-safe)
|
│ │ ├── markdown.js # Markdown rendering + ticket linking (XSS-safe)
|
||||||
@@ -292,6 +306,8 @@ tinker_tickets/
|
|||||||
│ ├── UserPreferencesModel.php # User preferences
|
│ ├── UserPreferencesModel.php # User preferences
|
||||||
│ └── WorkflowModel.php # Status transition workflows
|
│ └── WorkflowModel.php # Status transition workflows
|
||||||
├── scripts/
|
├── 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
|
│ ├── cleanup_orphan_uploads.php # Clean orphaned uploads
|
||||||
│ └── create_dependencies_table.php # Create ticket_dependencies table
|
│ └── create_dependencies_table.php # Create ticket_dependencies table
|
||||||
├── uploads/ # File attachment storage
|
├── uploads/ # File attachment storage
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ try {
|
|||||||
require_once $commentModelPath;
|
require_once $commentModelPath;
|
||||||
require_once $auditLogModelPath;
|
require_once $auditLogModelPath;
|
||||||
require_once dirname(__DIR__) . '/helpers/Database.php';
|
require_once dirname(__DIR__) . '/helpers/Database.php';
|
||||||
|
require_once dirname(__DIR__) . '/models/TicketModel.php';
|
||||||
|
|
||||||
// Check authentication via session
|
// Check authentication via session
|
||||||
if (session_status() === PHP_SESSION_NONE) {
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
@@ -71,6 +72,24 @@ try {
|
|||||||
exit;
|
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
|
// Initialize models
|
||||||
$commentModel = new CommentModel($conn);
|
$commentModel = new CommentModel($conn);
|
||||||
$auditLog = new AuditLogModel($conn);
|
$auditLog = new AuditLogModel($conn);
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ $ticketModel = new TicketModel($conn);
|
|||||||
$auditLogModel = new AuditLogModel($conn);
|
$auditLogModel = new AuditLogModel($conn);
|
||||||
$userModel = new UserModel($conn);
|
$userModel = new UserModel($conn);
|
||||||
|
|
||||||
// Verify ticket exists
|
// Verify ticket exists and user can access it
|
||||||
$ticket = $ticketModel->getTicketById($ticketId);
|
$ticket = $ticketModel->getTicketById($ticketId);
|
||||||
if (!$ticket) {
|
if (!$ticket || !$ticketModel->canUserAccessTicket($ticket, $currentUser)) {
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
echo json_encode(['success' => false, 'error' => 'Ticket not found']);
|
echo json_encode(['success' => false, 'error' => 'Ticket not found']);
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
require_once __DIR__ . '/bootstrap.php';
|
require_once __DIR__ . '/bootstrap.php';
|
||||||
require_once dirname(__DIR__) . '/helpers/ResponseHelper.php';
|
require_once dirname(__DIR__) . '/helpers/ResponseHelper.php';
|
||||||
|
require_once dirname(__DIR__) . '/models/TicketModel.php';
|
||||||
|
|
||||||
// Only accept GET requests
|
// Only accept GET requests
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||||
@@ -30,6 +31,10 @@ $searchTerm = '%' . $title . '%';
|
|||||||
// Get SOUNDEX of title
|
// Get SOUNDEX of title
|
||||||
$soundexTitle = soundex($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)
|
// First, search for exact substring matches (case-insensitive)
|
||||||
$sql = "SELECT ticket_id, title, status, priority, created_at
|
$sql = "SELECT ticket_id, title, status, priority, created_at
|
||||||
FROM tickets
|
FROM tickets
|
||||||
@@ -38,11 +43,16 @@ $sql = "SELECT ticket_id, title, status, priority, created_at
|
|||||||
OR SOUNDEX(title) = ?
|
OR SOUNDEX(title) = ?
|
||||||
)
|
)
|
||||||
AND status != 'Closed'
|
AND status != 'Closed'
|
||||||
|
AND ({$visFilter['sql']})
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT 10";
|
LIMIT 10";
|
||||||
|
|
||||||
|
$types = "ss" . $visFilter['types'];
|
||||||
|
$params = array_merge([$searchTerm, $soundexTitle], $visFilter['params']);
|
||||||
$stmt = $conn->prepare($sql);
|
$stmt = $conn->prepare($sql);
|
||||||
$stmt->bind_param("ss", $searchTerm, $soundexTitle);
|
if (!empty($params)) {
|
||||||
|
$stmt->bind_param($types, ...$params);
|
||||||
|
}
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
$result = $stmt->get_result();
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ require_once dirname(__DIR__) . '/config/config.php';
|
|||||||
require_once dirname(__DIR__) . '/helpers/Database.php';
|
require_once dirname(__DIR__) . '/helpers/Database.php';
|
||||||
require_once dirname(__DIR__) . '/models/DependencyModel.php';
|
require_once dirname(__DIR__) . '/models/DependencyModel.php';
|
||||||
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
|
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
|
||||||
|
require_once dirname(__DIR__) . '/models/TicketModel.php';
|
||||||
require_once dirname(__DIR__) . '/helpers/ResponseHelper.php';
|
require_once dirname(__DIR__) . '/helpers/ResponseHelper.php';
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
@@ -77,6 +78,7 @@ if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$userId = $_SESSION['user']['user_id'];
|
$userId = $_SESSION['user']['user_id'];
|
||||||
|
$currentUser = $_SESSION['user'];
|
||||||
|
|
||||||
// CSRF Protection for POST/DELETE
|
// CSRF Protection for POST/DELETE
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' || $_SERVER['REQUEST_METHOD'] === 'DELETE') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' || $_SERVER['REQUEST_METHOD'] === 'DELETE') {
|
||||||
@@ -99,6 +101,7 @@ if ($tableCheck->num_rows === 0) {
|
|||||||
try {
|
try {
|
||||||
$dependencyModel = new DependencyModel($conn);
|
$dependencyModel = new DependencyModel($conn);
|
||||||
$auditLog = new AuditLogModel($conn);
|
$auditLog = new AuditLogModel($conn);
|
||||||
|
$ticketModel = new TicketModel($conn);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
error_log('Failed to initialize models in ticket_dependencies.php: ' . $e->getMessage());
|
error_log('Failed to initialize models in ticket_dependencies.php: ' . $e->getMessage());
|
||||||
ResponseHelper::serverError('Failed to initialize required components');
|
ResponseHelper::serverError('Failed to initialize required components');
|
||||||
@@ -116,6 +119,12 @@ switch ($method) {
|
|||||||
ResponseHelper::error('Ticket ID required');
|
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 {
|
try {
|
||||||
$dependencies = $dependencyModel->getDependencies($ticketId);
|
$dependencies = $dependencyModel->getDependencies($ticketId);
|
||||||
$dependents = $dependencyModel->getDependentTickets($ticketId);
|
$dependents = $dependencyModel->getDependentTickets($ticketId);
|
||||||
|
|||||||
@@ -188,9 +188,11 @@ function addComment() {
|
|||||||
|
|
||||||
commentsList.insertBefore(commentDiv, commentsList.firstChild);
|
commentsList.insertBefore(commentDiv, commentsList.firstChild);
|
||||||
} else {
|
} else {
|
||||||
|
lt.toast.error(data.error || 'Failed to add comment');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
lt.toast.error('Error adding comment: ' + error.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
28
index.php
28
index.php
@@ -146,6 +146,34 @@ switch (true) {
|
|||||||
require_once 'api/check_duplicates.php';
|
require_once 'api/check_duplicates.php';
|
||||||
break;
|
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
|
// Admin Routes - require admin privileges
|
||||||
case $requestPath == '/admin/recurring-tickets':
|
case $requestPath == '/admin/recurring-tickets':
|
||||||
if (!$currentUser || !$currentUser['is_admin']) {
|
if (!$currentUser || !$currentUser['is_admin']) {
|
||||||
|
|||||||
Reference in New Issue
Block a user