Implement comprehensive improvement plan (Phases 1-6)
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>
This commit is contained in:
@@ -1,3 +1,10 @@
|
||||
// XSS prevention helper
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Main initialization
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('DOM loaded, initializing dashboard...');
|
||||
@@ -999,6 +1006,10 @@ function showConfirmModal(title, message, type = 'warning', onConfirm, onCancel
|
||||
};
|
||||
const icon = icons[type] || icons.warning;
|
||||
|
||||
// Escape user-provided content to prevent XSS
|
||||
const safeTitle = escapeHtml(title);
|
||||
const safeMessage = escapeHtml(message);
|
||||
|
||||
const modalHtml = `
|
||||
<div class="modal-overlay" id="${modalId}">
|
||||
<div class="modal-content ascii-frame-outer" style="max-width: 500px;">
|
||||
@@ -1006,14 +1017,14 @@ function showConfirmModal(title, message, type = 'warning', onConfirm, onCancel
|
||||
<span class="bottom-right-corner">╝</span>
|
||||
|
||||
<div class="ascii-section-header" style="color: ${color};">
|
||||
${icon} ${title}
|
||||
${icon} ${safeTitle}
|
||||
</div>
|
||||
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<div class="modal-body" style="padding: 1.5rem; text-align: center;">
|
||||
<p style="color: var(--terminal-green); white-space: pre-line;">
|
||||
${message}
|
||||
${safeMessage}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1070,6 +1081,11 @@ function showInputModal(title, label, placeholder = '', onSubmit, onCancel = nul
|
||||
const modalId = 'inputModal' + Date.now();
|
||||
const inputId = modalId + '_input';
|
||||
|
||||
// Escape user-provided content to prevent XSS
|
||||
const safeTitle = escapeHtml(title);
|
||||
const safeLabel = escapeHtml(label);
|
||||
const safePlaceholder = escapeHtml(placeholder);
|
||||
|
||||
const modalHtml = `
|
||||
<div class="modal-overlay" id="${modalId}">
|
||||
<div class="modal-content ascii-frame-outer" style="max-width: 500px;">
|
||||
@@ -1077,20 +1093,20 @@ function showInputModal(title, label, placeholder = '', onSubmit, onCancel = nul
|
||||
<span class="bottom-right-corner">╝</span>
|
||||
|
||||
<div class="ascii-section-header">
|
||||
${title}
|
||||
${safeTitle}
|
||||
</div>
|
||||
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<div class="modal-body" style="padding: 1.5rem;">
|
||||
<label for="${inputId}" style="display: block; margin-bottom: 0.5rem; color: var(--terminal-green);">
|
||||
${label}
|
||||
${safeLabel}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="${inputId}"
|
||||
class="terminal-input"
|
||||
placeholder="${placeholder}"
|
||||
placeholder="${safePlaceholder}"
|
||||
style="width: 100%; padding: 0.5rem; background: var(--bg-primary); border: 1px solid var(--terminal-green); color: var(--terminal-green); font-family: var(--font-mono);"
|
||||
/>
|
||||
</div>
|
||||
@@ -1149,3 +1165,177 @@ function showInputModal(title, label, placeholder = '', onSubmit, onCancel = nul
|
||||
};
|
||||
document.addEventListener('keydown', escHandler);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// QUICK ACTIONS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Quick status change from dashboard
|
||||
*/
|
||||
function quickStatusChange(ticketId, currentStatus) {
|
||||
const statuses = ['Open', 'Pending', 'In Progress', 'Closed'];
|
||||
const otherStatuses = statuses.filter(s => s !== currentStatus);
|
||||
|
||||
const modalHtml = `
|
||||
<div class="modal-overlay" id="quickStatusModal">
|
||||
<div class="modal-content ascii-frame-outer" style="max-width: 400px;">
|
||||
<span class="bottom-left-corner">╚</span>
|
||||
<span class="bottom-right-corner">╝</span>
|
||||
|
||||
<div class="ascii-section-header">Quick Status Change</div>
|
||||
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<div class="modal-body" style="padding: 1rem;">
|
||||
<p style="margin-bottom: 1rem;">Ticket #${escapeHtml(ticketId)}</p>
|
||||
<p style="margin-bottom: 0.5rem; color: var(--terminal-amber);">Current: ${escapeHtml(currentStatus)}</p>
|
||||
<label for="quickStatusSelect">New Status:</label>
|
||||
<select id="quickStatusSelect" class="editable" style="width: 100%; margin-top: 0.5rem;">
|
||||
${otherStatuses.map(s => `<option value="${s}">${s}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ascii-divider"></div>
|
||||
|
||||
<div class="ascii-content">
|
||||
<div class="modal-footer">
|
||||
<button onclick="performQuickStatusChange('${ticketId}')" class="btn btn-primary">Update</button>
|
||||
<button onclick="closeQuickStatusModal()" class="btn btn-secondary">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||
}
|
||||
|
||||
function closeQuickStatusModal() {
|
||||
const modal = document.getElementById('quickStatusModal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
|
||||
function performQuickStatusChange(ticketId) {
|
||||
const newStatus = document.getElementById('quickStatusSelect').value;
|
||||
|
||||
fetch('/api/update_ticket.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify({
|
||||
ticket_id: ticketId,
|
||||
status: newStatus
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
closeQuickStatusModal();
|
||||
if (data.success) {
|
||||
toast.success(`Status updated to ${newStatus}`, 3000);
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
} else {
|
||||
toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
closeQuickStatusModal();
|
||||
console.error('Error:', error);
|
||||
toast.error('Error updating status', 4000);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick assign from dashboard
|
||||
*/
|
||||
function quickAssign(ticketId) {
|
||||
const modalHtml = `
|
||||
<div class="modal-overlay" id="quickAssignModal">
|
||||
<div class="modal-content ascii-frame-outer" style="max-width: 400px;">
|
||||
<span class="bottom-left-corner">╚</span>
|
||||
<span class="bottom-right-corner">╝</span>
|
||||
|
||||
<div class="ascii-section-header">Quick Assign</div>
|
||||
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<div class="modal-body" style="padding: 1rem;">
|
||||
<p style="margin-bottom: 1rem;">Ticket #${escapeHtml(ticketId)}</p>
|
||||
<label for="quickAssignSelect">Assign to:</label>
|
||||
<select id="quickAssignSelect" class="editable" style="width: 100%; margin-top: 0.5rem;">
|
||||
<option value="">Unassigned</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ascii-divider"></div>
|
||||
|
||||
<div class="ascii-content">
|
||||
<div class="modal-footer">
|
||||
<button onclick="performQuickAssign('${ticketId}')" class="btn btn-primary">Assign</button>
|
||||
<button onclick="closeQuickAssignModal()" class="btn btn-secondary">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||
|
||||
// Load users
|
||||
fetch('/api/get_users.php')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success && data.users) {
|
||||
const select = document.getElementById('quickAssignSelect');
|
||||
data.users.forEach(user => {
|
||||
const option = document.createElement('option');
|
||||
option.value = user.user_id;
|
||||
option.textContent = user.display_name || user.username;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error loading users:', error));
|
||||
}
|
||||
|
||||
function closeQuickAssignModal() {
|
||||
const modal = document.getElementById('quickAssignModal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
|
||||
function performQuickAssign(ticketId) {
|
||||
const assignedTo = document.getElementById('quickAssignSelect').value || null;
|
||||
|
||||
fetch('/api/assign_ticket.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify({
|
||||
ticket_id: ticketId,
|
||||
assigned_to: assignedTo
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
closeQuickAssignModal();
|
||||
if (data.success) {
|
||||
toast.success('Assignment updated', 3000);
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
} else {
|
||||
toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
closeQuickAssignModal();
|
||||
console.error('Error:', error);
|
||||
toast.error('Error updating assignment', 4000);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user