- PATCH /admin/api-keys/clients/{id} — update client name and is_active
- PATCH /admin/api-keys/{id} — update key name and permissions
- Edit Client modal with name field and active/inactive toggle
- Edit Key modal with name field and permissions JSON textarea (pre-filled)
- Fix JS syntax error: use data-* attributes instead of inline JSON in onclick
621 lines
27 KiB
HTML
621 lines
27 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>API Management - Admin</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
.container {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
background: white;
|
|
border-radius: 20px;
|
|
padding: 40px;
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
}
|
|
.header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 30px;
|
|
padding-bottom: 20px;
|
|
border-bottom: 2px solid #e0e0e0;
|
|
}
|
|
h1 { color: #333; font-size: 32px; }
|
|
.user-info { display: flex; align-items: center; gap: 15px; }
|
|
.role-badge {
|
|
padding: 5px 15px;
|
|
border-radius: 20px;
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
text-transform: uppercase;
|
|
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
|
|
color: white;
|
|
}
|
|
.back-link {
|
|
display: inline-block;
|
|
margin-bottom: 20px;
|
|
color: #667eea;
|
|
text-decoration: none;
|
|
font-weight: 600;
|
|
font-size: 16px;
|
|
}
|
|
.back-link:hover { text-decoration: underline; }
|
|
|
|
.alert {
|
|
padding: 15px 20px;
|
|
margin-bottom: 20px;
|
|
border-radius: 8px;
|
|
font-weight: 500;
|
|
}
|
|
.alert-success { background: #51cf66; color: white; }
|
|
.alert-error { background: #ff6b6b; color: white; }
|
|
|
|
.stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
.stat-card {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
padding: 20px;
|
|
border-radius: 12px;
|
|
text-align: center;
|
|
}
|
|
.stat-number { font-size: 36px; font-weight: bold; margin-bottom: 5px; }
|
|
.stat-label { font-size: 14px; opacity: 0.9; }
|
|
|
|
.section-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin: 30px 0 15px;
|
|
}
|
|
.section-header h2 { font-size: 22px; color: #333; }
|
|
|
|
.btn {
|
|
padding: 8px 16px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
transition: all 0.2s;
|
|
}
|
|
.btn-primary { background: #667eea; color: white; }
|
|
.btn-primary:hover { background: #5568d3; transform: translateY(-1px); }
|
|
.btn-success { background: #51cf66; color: white; }
|
|
.btn-success:hover { background: #40c057; transform: translateY(-1px); }
|
|
.btn-warning { background: #fcc419; color: #333; }
|
|
.btn-warning:hover { background: #fab005; transform: translateY(-1px); }
|
|
.btn-danger { background: #ff6b6b; color: white; }
|
|
.btn-danger:hover { background: #ee5a52; transform: translateY(-1px); }
|
|
.btn-sm { padding: 5px 10px; font-size: 12px; }
|
|
|
|
.client-card {
|
|
border: 1px solid #e0e0e0;
|
|
border-radius: 12px;
|
|
margin-bottom: 20px;
|
|
overflow: hidden;
|
|
}
|
|
.client-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 16px 20px;
|
|
background: #f8f9fa;
|
|
border-bottom: 1px solid #e0e0e0;
|
|
}
|
|
.client-name { font-size: 18px; font-weight: 600; color: #333; }
|
|
.client-meta { font-size: 13px; color: #666; margin-top: 3px; }
|
|
.client-actions { display: flex; gap: 8px; align-items: center; }
|
|
|
|
table { width: 100%; border-collapse: collapse; }
|
|
th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #f0f0f0; }
|
|
th { font-size: 12px; font-weight: 600; color: #666; text-transform: uppercase; letter-spacing: 0.5px; background: #fafafa; }
|
|
tr:last-child td { border-bottom: none; }
|
|
tr:hover td { background: #f8f9ff; }
|
|
|
|
.status-active { color: #51cf66; font-weight: 600; }
|
|
.status-inactive { color: #ff6b6b; font-weight: 600; }
|
|
|
|
.key-prefix {
|
|
font-family: monospace;
|
|
background: #f1f3f5;
|
|
padding: 3px 8px;
|
|
border-radius: 4px;
|
|
font-size: 13px;
|
|
}
|
|
.perm-tag {
|
|
display: inline-block;
|
|
background: #e7f5ff;
|
|
color: #1c7ed6;
|
|
border-radius: 4px;
|
|
padding: 2px 8px;
|
|
font-size: 12px;
|
|
margin: 2px 2px 2px 0;
|
|
}
|
|
|
|
.empty-keys {
|
|
padding: 20px;
|
|
text-align: center;
|
|
color: #aaa;
|
|
font-size: 14px;
|
|
}
|
|
|
|
/* Modal */
|
|
.modal-overlay {
|
|
display: none;
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0,0,0,0.5);
|
|
z-index: 1000;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.modal-overlay.active { display: flex; }
|
|
.modal {
|
|
background: white;
|
|
border-radius: 16px;
|
|
padding: 32px;
|
|
max-width: 500px;
|
|
width: 90%;
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
}
|
|
.modal h3 { font-size: 20px; margin-bottom: 20px; color: #333; }
|
|
.form-group { margin-bottom: 16px; }
|
|
.form-group label { display: block; font-size: 14px; font-weight: 600; color: #555; margin-bottom: 6px; }
|
|
.form-group input, .form-group textarea {
|
|
width: 100%;
|
|
padding: 10px 12px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
outline: none;
|
|
}
|
|
.form-group input:focus, .form-group textarea:focus { border-color: #667eea; }
|
|
.form-group small { font-size: 12px; color: #888; margin-top: 4px; display: block; }
|
|
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 24px; }
|
|
|
|
.key-result-box {
|
|
background: #f1f3f5;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 8px;
|
|
padding: 12px 16px;
|
|
font-family: monospace;
|
|
font-size: 14px;
|
|
word-break: break-all;
|
|
margin: 10px 0;
|
|
}
|
|
.key-warning {
|
|
background: #fff3cd;
|
|
border: 1px solid #ffc107;
|
|
border-radius: 8px;
|
|
padding: 10px 14px;
|
|
font-size: 13px;
|
|
color: #856404;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.loading { text-align: center; padding: 40px; color: #666; }
|
|
|
|
.toggle-row { display: flex; align-items: center; gap: 10px; }
|
|
.toggle-switch {
|
|
position: relative; width: 44px; height: 24px; cursor: pointer;
|
|
}
|
|
.toggle-switch input { opacity: 0; width: 0; height: 0; }
|
|
.toggle-slider {
|
|
position: absolute; inset: 0; background: #ccc; border-radius: 24px; transition: 0.3s;
|
|
}
|
|
.toggle-slider:before {
|
|
content: ''; position: absolute; width: 18px; height: 18px;
|
|
left: 3px; top: 3px; background: white; border-radius: 50%; transition: 0.3s;
|
|
}
|
|
.toggle-switch input:checked + .toggle-slider { background: #51cf66; }
|
|
.toggle-switch input:checked + .toggle-slider:before { transform: translateX(20px); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<a href="{{ root_path }}/" class="back-link">← Back to Dashboard</a>
|
|
|
|
<div class="header">
|
|
<h1>🔑 API Management</h1>
|
|
{% if user %}
|
|
<div class="user-info">
|
|
<span>{{ user.name or user.username }}</span>
|
|
{% if user.roles %}
|
|
{% for role in user.roles %}
|
|
<span class="role-badge">{{ role }}</span>
|
|
{% endfor %}
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div id="alertContainer"></div>
|
|
|
|
<div class="stats">
|
|
<div class="stat-card">
|
|
<div class="stat-number" id="totalClients">-</div>
|
|
<div class="stat-label">API Clients</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number" id="totalKeys">-</div>
|
|
<div class="stat-label">Total Keys</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number" id="activeKeys">-</div>
|
|
<div class="stat-label">Active Keys</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section-header">
|
|
<h2>API Clients</h2>
|
|
<button class="btn btn-primary" onclick="openNewClientModal()">+ New Client</button>
|
|
</div>
|
|
|
|
<div id="clientsContainer">
|
|
<div class="loading">Loading...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal: New Client -->
|
|
<div class="modal-overlay" id="newClientModal">
|
|
<div class="modal">
|
|
<h3>New API Client</h3>
|
|
<div class="form-group">
|
|
<label>Client Name</label>
|
|
<input type="text" id="newClientName" placeholder="e.g. hospital-erp" />
|
|
<small>Unique identifier for this client system</small>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button class="btn" onclick="closeModal('newClientModal')">Cancel</button>
|
|
<button class="btn btn-primary" onclick="createClient()">Create</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal: New Key -->
|
|
<div class="modal-overlay" id="newKeyModal">
|
|
<div class="modal">
|
|
<h3>Generate API Key</h3>
|
|
<input type="hidden" id="newKeyClientId" />
|
|
<div class="form-group">
|
|
<label>Key Name (optional)</label>
|
|
<input type="text" id="newKeyName" placeholder="e.g. production" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Permissions</label>
|
|
<textarea id="newKeyPermissions" rows="4" placeholder='["voc.data:write", "feed.checkpoint:write"]'></textarea>
|
|
<small>JSON array of permission strings</small>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button class="btn" onclick="closeModal('newKeyModal')">Cancel</button>
|
|
<button class="btn btn-success" onclick="generateKey()">Generate</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal: Edit Client -->
|
|
<div class="modal-overlay" id="editClientModal">
|
|
<div class="modal">
|
|
<h3>Edit API Client</h3>
|
|
<input type="hidden" id="editClientId" />
|
|
<div class="form-group">
|
|
<label>Client Name</label>
|
|
<input type="text" id="editClientName" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Status</label>
|
|
<div class="toggle-row">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="editClientActive" onchange="updateClientActiveLabel()" />
|
|
<span class="toggle-slider"></span>
|
|
</label>
|
|
<span id="editClientActiveLabel">Active</span>
|
|
</div>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button class="btn" onclick="closeModal('editClientModal')">Cancel</button>
|
|
<button class="btn btn-primary" onclick="saveEditClient()">Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal: Edit Key -->
|
|
<div class="modal-overlay" id="editKeyModal">
|
|
<div class="modal">
|
|
<h3>Edit API Key</h3>
|
|
<input type="hidden" id="editKeyId" />
|
|
<div class="form-group">
|
|
<label>Key Name (optional)</label>
|
|
<input type="text" id="editKeyName" placeholder="e.g. production" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Permissions</label>
|
|
<textarea id="editKeyPermissions" rows="4" placeholder='["voc.data:write"]'></textarea>
|
|
<small>JSON array of permission strings</small>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button class="btn" onclick="closeModal('editKeyModal')">Cancel</button>
|
|
<button class="btn btn-primary" onclick="saveEditKey()">Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal: Show Key -->
|
|
<div class="modal-overlay" id="keyResultModal">
|
|
<div class="modal">
|
|
<h3>🔑 API Key Generated</h3>
|
|
<div class="key-warning">⚠️ Copy this key now — it will NOT be shown again after closing this dialog.</div>
|
|
<div class="form-group">
|
|
<label>API Key</label>
|
|
<div class="key-result-box" id="keyResultValue"></div>
|
|
<button class="btn btn-primary btn-sm" onclick="copyKey()" style="margin-top:8px;">Copy to Clipboard</button>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Permissions</label>
|
|
<div id="keyResultPerms"></div>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button class="btn btn-primary" onclick="closeKeyResult()">Done</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const rootPath = "{{ root_path }}";
|
|
|
|
async function loadClients() {
|
|
try {
|
|
const res = await fetch(`${rootPath}/admin/api-keys/clients`);
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
const clients = await res.json();
|
|
|
|
const totalKeys = clients.reduce((s, c) => s + c.api_keys.length, 0);
|
|
const activeKeys = clients.reduce((s, c) => s + c.api_keys.filter(k => k.is_active).length, 0);
|
|
document.getElementById('totalClients').textContent = clients.length;
|
|
document.getElementById('totalKeys').textContent = totalKeys;
|
|
document.getElementById('activeKeys').textContent = activeKeys;
|
|
|
|
const container = document.getElementById('clientsContainer');
|
|
if (clients.length === 0) {
|
|
container.innerHTML = '<div class="empty-keys">No API clients yet. Create one to get started.</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = clients.map(client => `
|
|
<div class="client-card">
|
|
<div class="client-header">
|
|
<div>
|
|
<div class="client-name">${escapeHtml(client.name)}</div>
|
|
<div class="client-meta">ID: ${client.id} · ${client.api_keys.length} key(s)</div>
|
|
</div>
|
|
<div class="client-actions">
|
|
<span class="${client.is_active ? 'status-active' : 'status-inactive'}">${client.is_active ? '● Active' : '● Inactive'}</span>
|
|
<button class="btn btn-success btn-sm" onclick="openNewKeyModal(${client.id})">+ Add Key</button>
|
|
<button class="btn btn-warning btn-sm" data-id="${client.id}" data-name="${escapeHtml(client.name)}" data-active="${client.is_active}" onclick="openEditClientModal(this)">Edit</button>
|
|
</div>
|
|
</div>
|
|
${client.api_keys.length === 0
|
|
? '<div class="empty-keys">No API keys yet.</div>'
|
|
: `<table>
|
|
<thead><tr>
|
|
<th>ID</th><th>Name</th><th>Prefix</th><th>Permissions</th><th>Status</th><th>Created</th><th>Actions</th>
|
|
</tr></thead>
|
|
<tbody>
|
|
${client.api_keys.map(key => `
|
|
<tr>
|
|
<td>${key.id}</td>
|
|
<td>${key.name ? escapeHtml(key.name) : '-'}</td>
|
|
<td><span class="key-prefix">${escapeHtml(key.key_prefix)}...</span></td>
|
|
<td>${key.permissions.map(p => `<span class="perm-tag">${escapeHtml(p)}</span>`).join('') || '<span style="color:#aaa">none</span>'}</td>
|
|
<td class="${key.is_active ? 'status-active' : 'status-inactive'}">${key.is_active ? '● Active' : '● Inactive'}</td>
|
|
<td>${formatDate(key.created_at)}</td>
|
|
<td>
|
|
<button class="btn btn-primary btn-sm" data-id="${key.id}" data-name="${escapeHtml(key.name || '')}" data-perms="${JSON.stringify(key.permissions).replace(/"/g, '"')}" onclick="openEditKeyModal(this)">Edit</button>
|
|
<button class="btn btn-warning btn-sm" onclick="regenerateKey(${key.id})">Regenerate</button>
|
|
<button class="btn btn-sm" style="background:#dee2e6" onclick="toggleKey(${key.id})">${key.is_active ? 'Deactivate' : 'Activate'}</button>
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>`
|
|
}
|
|
</div>
|
|
`).join('');
|
|
} catch (err) {
|
|
document.getElementById('clientsContainer').innerHTML = `<div class="empty-keys" style="color:#ff6b6b">Error: ${escapeHtml(err.message)}</div>`;
|
|
showAlert('Failed to load clients: ' + err.message, 'error');
|
|
}
|
|
}
|
|
|
|
function updateClientActiveLabel() {
|
|
const cb = document.getElementById('editClientActive');
|
|
document.getElementById('editClientActiveLabel').textContent = cb.checked ? 'Active' : 'Inactive';
|
|
}
|
|
|
|
function openEditClientModal(btn) {
|
|
const clientId = btn.dataset.id;
|
|
const name = btn.dataset.name;
|
|
const isActive = btn.dataset.active === 'true';
|
|
document.getElementById('editClientId').value = clientId;
|
|
document.getElementById('editClientName').value = name;
|
|
document.getElementById('editClientActive').checked = isActive;
|
|
document.getElementById('editClientActiveLabel').textContent = isActive ? 'Active' : 'Inactive';
|
|
document.getElementById('editClientModal').classList.add('active');
|
|
}
|
|
|
|
async function saveEditClient() {
|
|
const clientId = document.getElementById('editClientId').value;
|
|
const name = document.getElementById('editClientName').value.trim();
|
|
const isActive = document.getElementById('editClientActive').checked;
|
|
if (!name) return showAlert('Client name is required', 'error');
|
|
try {
|
|
const res = await fetch(`${rootPath}/admin/api-keys/clients/${clientId}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name, is_active: isActive })
|
|
});
|
|
if (!res.ok) { const e = await res.json(); throw new Error(e.detail || res.statusText); }
|
|
closeModal('editClientModal');
|
|
showAlert('Client updated', 'success');
|
|
loadClients();
|
|
} catch (err) { showAlert('Failed: ' + err.message, 'error'); }
|
|
}
|
|
|
|
function openEditKeyModal(btn) {
|
|
const keyId = btn.dataset.id;
|
|
const name = btn.dataset.name;
|
|
let permissions = [];
|
|
try { permissions = JSON.parse(btn.dataset.perms); } catch {}
|
|
document.getElementById('editKeyId').value = keyId;
|
|
document.getElementById('editKeyName').value = name || '';
|
|
document.getElementById('editKeyPermissions').value = JSON.stringify(permissions, null, 2);
|
|
document.getElementById('editKeyModal').classList.add('active');
|
|
}
|
|
|
|
async function saveEditKey() {
|
|
const keyId = document.getElementById('editKeyId').value;
|
|
const name = document.getElementById('editKeyName').value.trim() || null;
|
|
const permsRaw = document.getElementById('editKeyPermissions').value.trim();
|
|
let permissions;
|
|
try { permissions = permsRaw ? JSON.parse(permsRaw) : []; }
|
|
catch { return showAlert('Permissions must be a valid JSON array', 'error'); }
|
|
try {
|
|
const res = await fetch(`${rootPath}/admin/api-keys/${keyId}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name, permissions })
|
|
});
|
|
if (!res.ok) { const e = await res.json(); throw new Error(e.detail || res.statusText); }
|
|
closeModal('editKeyModal');
|
|
showAlert('Key updated', 'success');
|
|
loadClients();
|
|
} catch (err) { showAlert('Failed: ' + err.message, 'error'); }
|
|
}
|
|
|
|
function openNewClientModal() {
|
|
document.getElementById('newClientName').value = '';
|
|
document.getElementById('newClientModal').classList.add('active');
|
|
}
|
|
|
|
async function createClient() {
|
|
const name = document.getElementById('newClientName').value.trim();
|
|
if (!name) return showAlert('Client name is required', 'error');
|
|
try {
|
|
const res = await fetch(`${rootPath}/admin/api-keys/clients`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name })
|
|
});
|
|
if (!res.ok) { const e = await res.json(); throw new Error(e.detail || res.statusText); }
|
|
closeModal('newClientModal');
|
|
showAlert(`Client "${name}" created`, 'success');
|
|
loadClients();
|
|
} catch (err) { showAlert('Failed: ' + err.message, 'error'); }
|
|
}
|
|
|
|
function openNewKeyModal(clientId) {
|
|
document.getElementById('newKeyClientId').value = clientId;
|
|
document.getElementById('newKeyName').value = '';
|
|
document.getElementById('newKeyPermissions').value = '';
|
|
document.getElementById('newKeyModal').classList.add('active');
|
|
}
|
|
|
|
async function generateKey() {
|
|
const clientId = parseInt(document.getElementById('newKeyClientId').value);
|
|
const name = document.getElementById('newKeyName').value.trim() || null;
|
|
const permsRaw = document.getElementById('newKeyPermissions').value.trim();
|
|
let permissions = [];
|
|
if (permsRaw) {
|
|
try { permissions = JSON.parse(permsRaw); }
|
|
catch { return showAlert('Permissions must be a valid JSON array', 'error'); }
|
|
}
|
|
try {
|
|
const res = await fetch(`${rootPath}/admin/api-keys/generate`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ client_id: clientId, name, permissions })
|
|
});
|
|
if (!res.ok) { const e = await res.json(); throw new Error(e.detail || res.statusText); }
|
|
const data = await res.json();
|
|
closeModal('newKeyModal');
|
|
showKeyResult(data.api_key, data.permissions);
|
|
loadClients();
|
|
} catch (err) { showAlert('Failed: ' + err.message, 'error'); }
|
|
}
|
|
|
|
async function regenerateKey(keyId) {
|
|
if (!confirm('Regenerate this key? The current key will stop working immediately.')) return;
|
|
try {
|
|
const res = await fetch(`${rootPath}/admin/api-keys/${keyId}/regenerate`, { method: 'POST' });
|
|
if (!res.ok) { const e = await res.json(); throw new Error(e.detail || res.statusText); }
|
|
const data = await res.json();
|
|
showKeyResult(data.api_key, data.permissions);
|
|
loadClients();
|
|
} catch (err) { showAlert('Failed: ' + err.message, 'error'); }
|
|
}
|
|
|
|
async function toggleKey(keyId) {
|
|
try {
|
|
const res = await fetch(`${rootPath}/admin/api-keys/${keyId}/toggle`, { method: 'PATCH' });
|
|
if (!res.ok) { const e = await res.json(); throw new Error(e.detail || res.statusText); }
|
|
const data = await res.json();
|
|
showAlert(`Key ${data.is_active ? 'activated' : 'deactivated'}`, 'success');
|
|
loadClients();
|
|
} catch (err) { showAlert('Failed: ' + err.message, 'error'); }
|
|
}
|
|
|
|
function showKeyResult(apiKey, permissions) {
|
|
document.getElementById('keyResultValue').textContent = apiKey;
|
|
document.getElementById('keyResultPerms').innerHTML = permissions.length
|
|
? permissions.map(p => `<span class="perm-tag">${escapeHtml(p)}</span>`).join('')
|
|
: '<span style="color:#aaa">none</span>';
|
|
document.getElementById('keyResultModal').classList.add('active');
|
|
}
|
|
|
|
function copyKey() {
|
|
const key = document.getElementById('keyResultValue').textContent;
|
|
navigator.clipboard.writeText(key).then(() => showAlert('Copied to clipboard', 'success'));
|
|
}
|
|
|
|
function closeKeyResult() {
|
|
closeModal('keyResultModal');
|
|
}
|
|
|
|
function closeModal(id) {
|
|
document.getElementById(id).classList.remove('active');
|
|
}
|
|
|
|
function showAlert(message, type) {
|
|
const el = document.getElementById('alertContainer');
|
|
el.innerHTML = `<div class="alert alert-${type}">${escapeHtml(message)}</div>`;
|
|
setTimeout(() => el.innerHTML = '', 5000);
|
|
}
|
|
|
|
function formatDate(s) {
|
|
return new Date(s).toLocaleString('th-TH', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const d = document.createElement('div');
|
|
d.textContent = String(text);
|
|
return d.innerHTML;
|
|
}
|
|
|
|
loadClients();
|
|
</script>
|
|
</body>
|
|
</html>
|