feat: complete TDS v1.2 redesign across all views
Full application redesign using Terminal Design System v1.2 (lt-* class system). Introduces shared layout_header/footer partials, upgrades base.css/base.js to TDS v1.2, and rewrites all views (Dashboard, Ticket, CreateTicket, and all 7 admin views) with lt-frame, lt-table, lt-modal, lt-stats-grid, lt-kv-grid, and data-action event delegation patterns. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+152
-218
@@ -1,238 +1,172 @@
|
||||
<?php
|
||||
// Admin view for managing API keys
|
||||
// Receives $apiKeys from controller
|
||||
require_once __DIR__ . '/../../middleware/SecurityHeadersMiddleware.php';
|
||||
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
|
||||
$nonce = SecurityHeadersMiddleware::getNonce();
|
||||
$nonce = SecurityHeadersMiddleware::getNonce();
|
||||
$pageTitle = 'API Keys';
|
||||
$activeNav = 'admin-api-keys';
|
||||
$pageStyles = ['/assets/css/dashboard.css?v=20260327'];
|
||||
$pageScripts = [];
|
||||
include __DIR__ . '/../../views/layout_header.php';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>API Keys - Admin</title>
|
||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
||||
<link rel="stylesheet" href="/assets/css/base.css">
|
||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260320">
|
||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css?v=20260320">
|
||||
<script nonce="<?php echo $nonce; ?>" src="/assets/js/base.js"></script>
|
||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/utils.js?v=20260320"></script>
|
||||
<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 class="admin-page-title">Admin: API Keys</span>
|
||||
</div>
|
||||
<div class="user-header-right">
|
||||
<?php if (isset($GLOBALS['currentUser'])): ?>
|
||||
<span class="user-name">[ <?php echo htmlspecialchars(strtoupper($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username'])); ?> ]</span>
|
||||
<span class="admin-badge">[ ADMIN ]</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="lt-page-header">
|
||||
<div class="lt-flex lt-flex-gap-sm lt-flex-align-center">
|
||||
<a href="/" class="lt-btn lt-btn-ghost lt-btn-sm">← Dashboard</a>
|
||||
<span class="lt-text-muted lt-text-xs">/</span>
|
||||
<span class="lt-text-muted lt-text-xs">Admin: API Keys</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Generate new key -->
|
||||
<div class="lt-frame lt-mb-md">
|
||||
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
||||
<div class="lt-section-header">Generate New API Key</div>
|
||||
<div class="lt-section-body">
|
||||
<form id="generateKeyForm" class="lt-flex lt-flex-wrap lt-flex-gap-sm lt-flex-align-end">
|
||||
<div class="lt-form-group" style="flex:2;margin:0">
|
||||
<label class="lt-label" for="keyName">Key Name *</label>
|
||||
<input type="text" id="keyName" required class="lt-input" placeholder="e.g., CI/CD Pipeline">
|
||||
</div>
|
||||
<div class="lt-form-group" style="flex:1;margin:0">
|
||||
<label class="lt-label" for="expiresIn">Expires In</label>
|
||||
<select id="expiresIn" class="lt-select">
|
||||
<option value="">Never</option>
|
||||
<option value="30">30 days</option>
|
||||
<option value="90">90 days</option>
|
||||
<option value="180">180 days</option>
|
||||
<option value="365">1 year</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="lt-btn lt-btn-primary" style="margin-bottom:0">GENERATE KEY</button>
|
||||
</form>
|
||||
|
||||
<!-- New key display (hidden by default) -->
|
||||
<div id="newKeyDisplay" class="lt-frame-inner lt-mt-sm is-hidden">
|
||||
<div class="lt-subsection-header lt-text-amber">⚠ Copy this key now — you won't see it again!</div>
|
||||
<div class="lt-flex lt-flex-gap-sm lt-mt-sm">
|
||||
<input type="text" id="newKeyValue" readonly class="lt-input" style="flex:1;font-family:monospace">
|
||||
<button type="button" class="lt-btn lt-btn-sm" data-action="copy-api-key">COPY</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ascii-frame-outer admin-container">
|
||||
<span class="bottom-left-corner">╚</span>
|
||||
<span class="bottom-right-corner">╝</span>
|
||||
|
||||
<div class="ascii-section-header">API Key Management</div>
|
||||
<div class="ascii-content">
|
||||
<!-- Generate New Key Form -->
|
||||
<div class="ascii-frame-inner">
|
||||
<h3 class="admin-section-title">Generate New API Key</h3>
|
||||
<form id="generateKeyForm" class="admin-form-row">
|
||||
<div class="admin-form-field">
|
||||
<label class="admin-label" for="keyName">Key Name *</label>
|
||||
<input type="text" id="keyName" required placeholder="e.g., CI/CD Pipeline" class="admin-input">
|
||||
</div>
|
||||
<div class="admin-form-field">
|
||||
<label class="admin-label" for="expiresIn">Expires In</label>
|
||||
<select id="expiresIn" class="admin-input">
|
||||
<option value="">Never</option>
|
||||
<option value="30">30 days</option>
|
||||
<option value="90">90 days</option>
|
||||
<option value="180">180 days</option>
|
||||
<option value="365">1 year</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="btn">GENERATE KEY</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- New Key Display (hidden by default) -->
|
||||
<div id="newKeyDisplay" class="ascii-frame-inner key-generated-alert is-hidden">
|
||||
<h3 class="admin-section-title">New API Key Generated</h3>
|
||||
<p class="text-danger text-sm mb-1">
|
||||
Copy this key now. You won't be able to see it again!
|
||||
</p>
|
||||
<div class="admin-form-row">
|
||||
<input type="text" id="newKeyValue" readonly class="admin-input">
|
||||
<button data-action="copy-api-key" class="btn" title="Copy to clipboard">COPY</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Existing Keys Table -->
|
||||
<div class="ascii-frame-inner">
|
||||
<h3 class="admin-section-title">Existing API Keys</h3>
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Key Prefix</th>
|
||||
<th>Created By</th>
|
||||
<th>Created At</th>
|
||||
<th>Expires At</th>
|
||||
<th>Last Used</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($apiKeys)): ?>
|
||||
<tr>
|
||||
<td colspan="8" class="empty-state">No API keys found. Generate one above.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($apiKeys as $key): ?>
|
||||
<tr id="key-row-<?php echo $key['api_key_id']; ?>">
|
||||
<td><?php echo htmlspecialchars($key['key_name']); ?></td>
|
||||
<td class="mono">
|
||||
<code><?php echo htmlspecialchars($key['key_prefix']); ?>...</code>
|
||||
</td>
|
||||
<td><?php echo htmlspecialchars($key['display_name'] ?? $key['username'] ?? 'Unknown'); ?></td>
|
||||
<td class="nowrap"><?php echo date('Y-m-d H:i', strtotime($key['created_at'])); ?></td>
|
||||
<td class="nowrap">
|
||||
<?php if ($key['expires_at']): ?>
|
||||
<?php $expired = strtotime($key['expires_at']) < time(); ?>
|
||||
<span class="<?php echo $expired ? 'text-danger' : ''; ?>">
|
||||
<?php echo date('Y-m-d', strtotime($key['expires_at'])); ?>
|
||||
<?php if ($expired): ?> (Expired)<?php endif; ?>
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="text-cyan">Never</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="nowrap">
|
||||
<?php echo $key['last_used'] ? date('Y-m-d H:i', strtotime($key['last_used'])) : 'Never'; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($key['is_active']): ?>
|
||||
<span class="text-open">Active</span>
|
||||
<?php else: ?>
|
||||
<span class="text-closed">Revoked</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($key['is_active']): ?>
|
||||
<button data-action="revoke-key" data-id="<?php echo $key['api_key_id']; ?>" class="btn btn-secondary btn-small">
|
||||
REVOKE
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">-</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Usage Info -->
|
||||
<div class="ascii-frame-inner">
|
||||
<h3 class="admin-section-title">API Usage</h3>
|
||||
<p>Include the API key in your requests using the Authorization header:</p>
|
||||
<pre class="admin-code-block"><code>Authorization: Bearer YOUR_API_KEY</code></pre>
|
||||
<p class="text-muted text-sm">
|
||||
API keys provide programmatic access to create and manage tickets. Keep your keys secure and rotate them regularly.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Existing keys -->
|
||||
<div class="lt-frame lt-mb-md">
|
||||
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
||||
<div class="lt-section-header">Existing API Keys</div>
|
||||
<div class="lt-section-body">
|
||||
<div class="lt-table-wrap">
|
||||
<table class="lt-table lt-table-responsive" aria-label="API keys">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Key Prefix</th>
|
||||
<th scope="col">Created By</th>
|
||||
<th scope="col">Created</th>
|
||||
<th scope="col">Expires</th>
|
||||
<th scope="col">Last Used</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($apiKeys)): ?>
|
||||
<tr><td colspan="8" class="lt-empty">No API keys found. Generate one above.</td></tr>
|
||||
<?php else: foreach ($apiKeys as $key): ?>
|
||||
<?php $expired = $key['expires_at'] && strtotime($key['expires_at']) < time(); ?>
|
||||
<tr id="key-row-<?= $key['api_key_id'] ?>">
|
||||
<td data-label="Name"><strong><?= htmlspecialchars($key['key_name']) ?></strong></td>
|
||||
<td data-label="Prefix" class="lt-text-xs"><code><?= htmlspecialchars($key['key_prefix']) ?>…</code></td>
|
||||
<td data-label="Created By" class="lt-text-xs"><?= htmlspecialchars($key['display_name'] ?? $key['username'] ?? 'Unknown') ?></td>
|
||||
<td data-label="Created" class="lt-text-xs lt-text-muted"><?= date('Y-m-d H:i', strtotime($key['created_at'])) ?></td>
|
||||
<td data-label="Expires" class="lt-text-xs <?= $expired ? 'lt-text-danger' : 'lt-text-cyan' ?>">
|
||||
<?= $key['expires_at'] ? date('Y-m-d', strtotime($key['expires_at'])) . ($expired ? ' (Expired)' : '') : 'Never' ?>
|
||||
</td>
|
||||
<td data-label="Last Used" class="lt-text-xs lt-text-muted">
|
||||
<?= $key['last_used'] ? date('Y-m-d H:i', strtotime($key['last_used'])) : 'Never' ?>
|
||||
</td>
|
||||
<td data-label="Status">
|
||||
<?php if ($key['is_active']): ?>
|
||||
<span class="lt-status lt-status-open">Active</span>
|
||||
<?php else: ?>
|
||||
<span class="lt-status lt-status-closed">Revoked</span>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
<?php if ($key['is_active']): ?>
|
||||
<button type="button" class="lt-btn lt-btn-sm lt-btn-danger"
|
||||
data-action="revoke-key" data-id="<?= $key['api_key_id'] ?>">REVOKE</button>
|
||||
<?php else: ?>
|
||||
<span class="lt-text-muted lt-text-xs">—</span>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; endif ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nonce="<?php echo $nonce; ?>">
|
||||
// Event delegation for data-action handlers
|
||||
document.addEventListener('click', function(event) {
|
||||
const target = event.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
<!-- API usage -->
|
||||
<div class="lt-frame">
|
||||
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
||||
<div class="lt-section-header">API Usage</div>
|
||||
<div class="lt-section-body">
|
||||
<p class="lt-text-sm lt-text-muted">Include the API key in your requests using the Authorization header:</p>
|
||||
<pre class="lt-text-xs lt-text-cyan" style="border:1px solid rgba(0,255,65,0.2);padding:0.5rem;overflow-x:auto"><code>Authorization: Bearer YOUR_API_KEY</code></pre>
|
||||
<p class="lt-text-xs lt-text-muted">API keys provide programmatic access to create and manage tickets. Keep keys secure and rotate them regularly.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const action = target.dataset.action;
|
||||
switch (action) {
|
||||
case 'copy-api-key':
|
||||
copyApiKey();
|
||||
break;
|
||||
case 'revoke-key':
|
||||
revokeKey(target.dataset.id);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('generateKeyForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const keyName = document.getElementById('keyName').value.trim();
|
||||
const expiresIn = document.getElementById('expiresIn').value;
|
||||
|
||||
if (!keyName) {
|
||||
lt.toast.error('Please enter a key name');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await lt.api.post('/api/generate_api_key.php', {
|
||||
key_name: keyName,
|
||||
expires_in_days: expiresIn || null
|
||||
});
|
||||
<script nonce="<?= $nonce ?>">
|
||||
document.addEventListener('click', function (e) {
|
||||
var target = e.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
switch (target.getAttribute('data-action')) {
|
||||
case 'copy-api-key': copyApiKey(); break;
|
||||
case 'revoke-key': revokeKey(target.getAttribute('data-id')); break;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('generateKeyForm').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var keyName = document.getElementById('keyName').value.trim();
|
||||
var expiresIn = document.getElementById('expiresIn').value;
|
||||
if (!keyName) { lt.toast.error('Please enter a key name'); return; }
|
||||
lt.api.post('/api/generate_api_key.php', { key_name: keyName, expires_in_days: expiresIn || null })
|
||||
.then(function (data) {
|
||||
if (data.success) {
|
||||
// Show the new key
|
||||
document.getElementById('newKeyValue').value = data.api_key;
|
||||
document.getElementById('newKeyDisplay').classList.remove('is-hidden');
|
||||
document.getElementById('keyName').value = '';
|
||||
|
||||
lt.toast.success('API key generated successfully');
|
||||
|
||||
// Reload page after 5 seconds to show new key in table
|
||||
setTimeout(() => location.reload(), 5000);
|
||||
lt.toast.success('API key generated!');
|
||||
setTimeout(function () { location.reload(); }, 5000);
|
||||
} else {
|
||||
lt.toast.error(data.error || 'Failed to generate API key');
|
||||
}
|
||||
} catch (error) {
|
||||
lt.toast.error('Error generating API key: ' + error.message);
|
||||
}
|
||||
}).catch(function (err) { lt.toast.error('Error: ' + err.message); });
|
||||
});
|
||||
|
||||
function copyApiKey() {
|
||||
var input = document.getElementById('newKeyValue');
|
||||
input.select();
|
||||
document.execCommand('copy');
|
||||
lt.toast.success('Copied to clipboard!');
|
||||
}
|
||||
|
||||
function revokeKey(keyId) {
|
||||
showConfirmModal('Revoke API Key', 'Revoke this API key? This cannot be undone.', 'error', function () {
|
||||
lt.api.post('/api/revoke_api_key.php', { key_id: keyId })
|
||||
.then(function (data) {
|
||||
if (data.success) { lt.toast.success('API key revoked'); location.reload(); }
|
||||
else lt.toast.error(data.error || 'Failed to revoke');
|
||||
}).catch(function (err) { lt.toast.error('Error: ' + err.message); });
|
||||
});
|
||||
}
|
||||
|
||||
function copyApiKey() {
|
||||
const keyInput = document.getElementById('newKeyValue');
|
||||
keyInput.select();
|
||||
document.execCommand('copy');
|
||||
lt.toast.success('API key copied to clipboard');
|
||||
}
|
||||
if (window.lt) lt.keys.initDefaults();
|
||||
</script>
|
||||
|
||||
function revokeKey(keyId) {
|
||||
showConfirmModal('Revoke API Key', 'Are you sure you want to revoke this API key? This action cannot be undone.', 'error', function() {
|
||||
lt.api.post('/api/revoke_api_key.php', { key_id: keyId })
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
lt.toast.success('API key revoked successfully');
|
||||
location.reload();
|
||||
} else {
|
||||
lt.toast.error(data.error || 'Failed to revoke API key');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
lt.toast.error('Error revoking API key: ' + error.message);
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<?php include __DIR__ . '/../../views/layout_footer.php'; ?>
|
||||
|
||||
Reference in New Issue
Block a user