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/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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
28
index.php
28
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']) {
|
||||
|
||||
Reference in New Issue
Block a user