Files
tinker_tickets/views/admin/WorkflowDesignerView.php
Jared Vititoe fa40010287 Security hardening and performance improvements
- Add visibility check to attachment downloads (prevents unauthorized access)
- Fix ticket ID collision with uniqueness verification loop
- Harden CSP: replace unsafe-inline with nonce-based script execution
- Add IP-based rate limiting (supplements session-based)
- Add visibility checks to bulk operations
- Validate internal visibility requires groups
- Optimize user activity query (JOINs vs subqueries)
- Update documentation with design decisions and security info

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 20:27:15 -05:00

270 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// Admin view for workflow/status transitions designer
// Receives $workflows from controller
require_once __DIR__ . '/../../middleware/SecurityHeadersMiddleware.php';
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
$nonce = SecurityHeadersMiddleware::getNonce();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Workflow Designer - Admin</title>
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
<script nonce="<?php echo $nonce; ?>">
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
</script>
</head>
<body>
<div class="user-header">
<div class="user-header-left">
<a href="/" class="back-link">← Dashboard</a>
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: Workflow Designer</span>
</div>
<div class="user-header-right">
<?php if (isset($GLOBALS['currentUser'])): ?>
<span class="user-name"><?php echo htmlspecialchars($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username']); ?></span>
<span class="admin-badge">Admin</span>
<?php endif; ?>
</div>
</div>
<div class="ascii-frame-outer" style="max-width: 1200px; margin: 2rem auto;">
<span class="bottom-left-corner">╚</span>
<span class="bottom-right-corner">╝</span>
<div class="ascii-section-header">Status Workflow Designer</div>
<div class="ascii-content">
<div class="ascii-frame-inner">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h2 style="margin: 0;">Status Transitions</h2>
<button onclick="showCreateModal()" class="btn">+ New Transition</button>
</div>
<p style="color: var(--terminal-green-dim); margin-bottom: 1rem;">
Define which status transitions are allowed. This controls what options appear in the status dropdown.
</p>
<!-- Visual Workflow Diagram -->
<div style="margin-bottom: 2rem; padding: 1rem; border: 1px solid var(--terminal-green); background: var(--bg-secondary);">
<h4 style="color: var(--terminal-amber); margin-bottom: 1rem;">Workflow Diagram</h4>
<div style="display: flex; justify-content: center; gap: 2rem; flex-wrap: wrap;">
<?php
$statuses = ['Open', 'Pending', 'In Progress', 'Closed'];
foreach ($statuses as $status):
$statusClass = 'status-' . str_replace(' ', '-', strtolower($status));
?>
<div style="text-align: center;">
<div class="<?php echo $statusClass; ?>" style="padding: 0.5rem 1rem; display: inline-block;">
<?php echo $status; ?>
</div>
<div style="font-size: 0.8rem; color: var(--terminal-green-dim); margin-top: 0.5rem;">
<?php
$toCount = 0;
if (isset($workflows)) {
foreach ($workflows as $w) {
if ($w['from_status'] === $status) $toCount++;
}
}
echo "→ $toCount transitions";
?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- Transitions Table -->
<table style="width: 100%;">
<thead>
<tr>
<th>From Status</th>
<th>→</th>
<th>To Status</th>
<th>Requires Comment</th>
<th>Requires Admin</th>
<th>Active</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($workflows)): ?>
<tr>
<td colspan="7" style="text-align: center; padding: 2rem; color: var(--terminal-green-dim);">
No transitions defined. Add transitions to enable status changes.
</td>
</tr>
<?php else: ?>
<?php foreach ($workflows as $wf): ?>
<tr>
<td>
<span class="status-<?php echo str_replace(' ', '-', strtolower($wf['from_status'])); ?>">
<?php echo htmlspecialchars($wf['from_status']); ?>
</span>
</td>
<td style="text-align: center; color: var(--terminal-amber);">→</td>
<td>
<span class="status-<?php echo str_replace(' ', '-', strtolower($wf['to_status'])); ?>">
<?php echo htmlspecialchars($wf['to_status']); ?>
</span>
</td>
<td style="text-align: center;"><?php echo $wf['requires_comment'] ? '✓' : ''; ?></td>
<td style="text-align: center;"><?php echo $wf['requires_admin'] ? '✓' : ''; ?></td>
<td style="text-align: center;">
<span style="color: <?php echo $wf['is_active'] ? 'var(--status-open)' : 'var(--status-closed)'; ?>;">
<?php echo $wf['is_active'] ? '✓' : '✗'; ?>
</span>
</td>
<td>
<button onclick="editTransition(<?php echo $wf['transition_id']; ?>)" class="btn btn-small">Edit</button>
<button onclick="deleteTransition(<?php echo $wf['transition_id']; ?>)" class="btn btn-small btn-danger">Delete</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Create/Edit Modal -->
<div class="settings-modal" id="workflowModal" style="display: none;">
<div class="settings-content" style="max-width: 450px;">
<div class="settings-header">
<h3 id="modalTitle">Create Transition</h3>
<button class="close-settings" onclick="closeModal()">×</button>
</div>
<form id="workflowForm" onsubmit="saveTransition(event)">
<input type="hidden" id="transition_id" name="transition_id">
<div class="settings-body">
<div class="setting-row">
<label for="from_status">From Status *</label>
<select id="from_status" name="from_status" required>
<option value="Open">Open</option>
<option value="Pending">Pending</option>
<option value="In Progress">In Progress</option>
<option value="Closed">Closed</option>
</select>
</div>
<div class="setting-row">
<label for="to_status">To Status *</label>
<select id="to_status" name="to_status" required>
<option value="Open">Open</option>
<option value="Pending">Pending</option>
<option value="In Progress">In Progress</option>
<option value="Closed">Closed</option>
</select>
</div>
<div class="setting-row">
<label><input type="checkbox" id="requires_comment" name="requires_comment"> Requires comment</label>
</div>
<div class="setting-row">
<label><input type="checkbox" id="requires_admin" name="requires_admin"> Requires admin privileges</label>
</div>
<div class="setting-row">
<label><input type="checkbox" id="is_active" name="is_active" checked> Active</label>
</div>
</div>
<div class="settings-footer">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
</div>
</form>
</div>
</div>
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
<script nonce="<?php echo $nonce; ?>">
const workflows = <?php echo json_encode($workflows ?? []); ?>;
function showCreateModal() {
document.getElementById('modalTitle').textContent = 'Create Transition';
document.getElementById('workflowForm').reset();
document.getElementById('transition_id').value = '';
document.getElementById('is_active').checked = true;
document.getElementById('workflowModal').style.display = 'flex';
}
function closeModal() {
document.getElementById('workflowModal').style.display = 'none';
}
// Close modal on ESC key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeModal();
}
});
// Close modal when clicking on backdrop (outside content)
document.getElementById('workflowModal').addEventListener('click', (e) => {
if (e.target.classList.contains('settings-modal')) {
closeModal();
}
});
function saveTransition(e) {
e.preventDefault();
const data = {
transition_id: document.getElementById('transition_id').value,
from_status: document.getElementById('from_status').value,
to_status: document.getElementById('to_status').value,
requires_comment: document.getElementById('requires_comment').checked ? 1 : 0,
requires_admin: document.getElementById('requires_admin').checked ? 1 : 0,
is_active: document.getElementById('is_active').checked ? 1 : 0
};
const method = data.transition_id ? 'PUT' : 'POST';
const url = '/api/manage_workflows.php' + (data.transition_id ? '?id=' + data.transition_id : '');
fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': window.CSRF_TOKEN
},
body: JSON.stringify(data)
})
.then(r => r.json())
.then(result => {
if (result.success) {
window.location.reload();
} else {
toast.error(result.error || 'Failed to save');
}
});
}
function editTransition(id) {
const wf = workflows.find(w => w.transition_id == id);
if (!wf) return;
document.getElementById('transition_id').value = wf.transition_id;
document.getElementById('from_status').value = wf.from_status;
document.getElementById('to_status').value = wf.to_status;
document.getElementById('requires_comment').checked = wf.requires_comment == 1;
document.getElementById('requires_admin').checked = wf.requires_admin == 1;
document.getElementById('is_active').checked = wf.is_active == 1;
document.getElementById('modalTitle').textContent = 'Edit Transition';
document.getElementById('workflowModal').style.display = 'flex';
}
function deleteTransition(id) {
if (!confirm('Delete this status transition?')) return;
fetch('/api/manage_workflows.php?id=' + id, {
method: 'DELETE',
headers: { 'X-CSRF-Token': window.CSRF_TOKEN }
})
.then(r => r.json())
.then(data => {
if (data.success) window.location.reload();
});
}
</script>
</body>
</html>