diff --git a/.env.example b/.env.example index b9c24e4..5d02e12 100644 --- a/.env.example +++ b/.env.example @@ -10,5 +10,13 @@ DB_NAME=ticketing_system # Discord Webhook (optional - for notifications) DISCORD_WEBHOOK_URL= +# Application Domain (required for Discord webhook links) +# Set this to your public domain (e.g., t.lotusguild.org) +APP_DOMAIN= + +# Allowed Hosts for HTTP_HOST validation (comma-separated) +# Include all domains that can access this application +ALLOWED_HOSTS=localhost,127.0.0.1 + # Timezone (default: America/New_York) TIMEZONE=America/New_York diff --git a/Claude.md b/Claude.md index 120d324..db31fc4 100644 --- a/Claude.md +++ b/Claude.md @@ -241,6 +241,7 @@ All admin pages are accessible via the **Admin dropdown** in the dashboard heade 10. **API routing**: All API endpoints must be added to `index.php` router 11. **Session in APIs**: RateLimitMiddleware starts session; don't call session_start() again 12. **Database collation**: Use `utf8mb4_general_ci` (not unicode_ci) for new tables +13. **Discord webhook URLs**: Set `APP_DOMAIN` in `.env` for correct ticket URLs in Discord notifications 13. **Ticket ID extraction**: Use `getTicketIdFromUrl()` helper in JS files 14. **CSP Nonces**: All inline scripts require `nonce=""` attribute 15. **Visibility validation**: Internal visibility requires groups; code validates this diff --git a/README.md b/README.md index 55f9105..f4b0d0f 100644 --- a/README.md +++ b/README.md @@ -187,9 +187,12 @@ DB_USER=tinkertickets DB_PASS=your_password DB_NAME=ticketing_system DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/... +APP_DOMAIN=t.lotusguild.org TIMEZONE=America/New_York ``` +**Note**: `APP_DOMAIN` is required for Discord webhook ticket links to work correctly. Without it, links will default to localhost. + ### 2. Cron Jobs Add to crontab for recurring tickets: diff --git a/assets/js/dashboard.js b/assets/js/dashboard.js index 669eb08..dcbaf64 100644 --- a/assets/js/dashboard.js +++ b/assets/js/dashboard.js @@ -643,6 +643,10 @@ function loadTemplate() { document.getElementById('priority').value = '4'; document.getElementById('category').value = 'General'; document.getElementById('type').value = 'Issue'; + const assignedToSelect = document.getElementById('assigned_to'); + if (assignedToSelect) { + assignedToSelect.value = ''; + } return; } diff --git a/controllers/TicketController.php b/controllers/TicketController.php index 904aa3b..90c5626 100644 --- a/controllers/TicketController.php +++ b/controllers/TicketController.php @@ -108,13 +108,15 @@ class TicketController { 'category' => $_POST['category'] ?? 'General', 'type' => $_POST['type'] ?? 'Issue', 'visibility' => $_POST['visibility'] ?? 'public', - 'visibility_groups' => $visibilityGroups + 'visibility_groups' => $visibilityGroups, + 'assigned_to' => !empty($_POST['assigned_to']) ? $_POST['assigned_to'] : null ]; // Validate input if (empty($ticketData['title'])) { $error = "Title is required"; $templates = $this->templateModel->getAllTemplates(); + $allUsers = $this->userModel->getAllUsers(); $conn = $this->conn; // Make $conn available to view include dirname(__DIR__) . '/views/CreateTicketView.php'; return; @@ -138,6 +140,7 @@ class TicketController { } else { $error = $result['error']; $templates = $this->templateModel->getAllTemplates(); + $allUsers = $this->userModel->getAllUsers(); $conn = $this->conn; // Make $conn available to view include dirname(__DIR__) . '/views/CreateTicketView.php'; return; @@ -145,6 +148,8 @@ class TicketController { } else { // Get all templates for the template selector $templates = $this->templateModel->getAllTemplates(); + // Get all users for assignment dropdown + $allUsers = $this->userModel->getAllUsers(); $conn = $this->conn; // Make $conn available to view // Display the create ticket form diff --git a/models/TicketModel.php b/models/TicketModel.php index 70a55c5..7935ed5 100644 --- a/models/TicketModel.php +++ b/models/TicketModel.php @@ -376,8 +376,8 @@ class TicketModel { error_log("Ticket ID generation used fallback after {$maxAttempts} random attempts"); } - $sql = "INSERT INTO tickets (ticket_id, title, description, status, priority, category, type, created_by, visibility, visibility_groups) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + $sql = "INSERT INTO tickets (ticket_id, title, description, status, priority, category, type, created_by, assigned_to, visibility, visibility_groups) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; $stmt = $this->conn->prepare($sql); @@ -388,6 +388,7 @@ class TicketModel { $type = $ticketData['type'] ?? 'Issue'; $visibility = $ticketData['visibility'] ?? 'public'; $visibilityGroups = $ticketData['visibility_groups'] ?? null; + $assignedTo = !empty($ticketData['assigned_to']) ? (int)$ticketData['assigned_to'] : null; // Validate visibility $allowedVisibilities = ['public', 'internal', 'confidential']; @@ -409,7 +410,7 @@ class TicketModel { } $stmt->bind_param( - "sssssssiss", + "sssssssiiss", $ticket_id, $ticketData['title'], $ticketData['description'], @@ -418,6 +419,7 @@ class TicketModel { $category, $type, $createdBy, + $assignedTo, $visibility, $visibilityGroups ); diff --git a/views/CreateTicketView.php b/views/CreateTicketView.php index e8bac56..41690e2 100644 --- a/views/CreateTicketView.php +++ b/views/CreateTicketView.php @@ -13,7 +13,7 @@ $nonce = SecurityHeadersMiddleware::getNonce(); - + - + - +