Spaces:
Sleeping
Sleeping
| let adminKey = ''; | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const loginBtn = document.getElementById('login-btn'); | |
| const masterKeyInput = document.getElementById('master-key-input'); | |
| loginBtn.addEventListener('click', attemptLogin); | |
| masterKeyInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') attemptLogin(); | |
| }); | |
| document.getElementById('logout-btn').addEventListener('click', () => { | |
| adminKey = ''; | |
| document.getElementById('admin-section').style.display = 'none'; | |
| document.getElementById('login-section').style.display = 'block'; | |
| document.getElementById('logout-btn').style.display = 'none'; | |
| document.getElementById('master-key-input').value = ''; | |
| }); | |
| document.getElementById('generate-key-btn').addEventListener('click', generateKey); | |
| document.getElementById('refresh-btn').addEventListener('click', fetchKeys); | |
| document.getElementById('copy-btn').addEventListener('click', copyKey); | |
| document.getElementById('revoke-all-btn').addEventListener('click', revokeAllKeys); | |
| document.getElementById('refresh-logs-btn').addEventListener('click', fetchLogs); | |
| }); | |
| async function attemptLogin() { | |
| const key = document.getElementById('master-key-input').value.trim(); | |
| const errorEl = document.getElementById('login-error'); | |
| errorEl.style.display = 'none'; | |
| try { | |
| const res = await fetch('/api/admin/keys', { | |
| method: 'GET', | |
| headers: { 'admin-key': key } | |
| }); | |
| if (res.ok) { | |
| adminKey = key; | |
| document.getElementById('login-section').style.display = 'none'; | |
| document.getElementById('admin-section').style.display = 'block'; | |
| document.getElementById('logout-btn').style.display = 'block'; | |
| fetchKeys(); | |
| fetchLogs(); | |
| } else { | |
| errorEl.textContent = 'Invalid Master Key'; | |
| errorEl.style.display = 'block'; | |
| } | |
| } catch (e) { | |
| errorEl.textContent = 'Connection Error'; | |
| errorEl.style.display = 'block'; | |
| } | |
| } | |
| async function fetchKeys() { | |
| if (!adminKey) return; | |
| try { | |
| const res = await fetch('/api/admin/keys', { | |
| method: 'GET', | |
| headers: { 'admin-key': adminKey } | |
| }); | |
| if (res.ok) { | |
| const data = await res.json(); | |
| renderKeysTable(data.keys); | |
| } | |
| } catch (e) { | |
| console.error('Failed to fetch keys', e); | |
| } | |
| } | |
| function renderKeysTable(keysObj) { | |
| const tbody = document.getElementById('keys-table-body'); | |
| tbody.innerHTML = ''; | |
| const keysArray = Object.entries(keysObj).map(([key, data]) => ({ key, ...data })); | |
| // Sort by newest first | |
| keysArray.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); | |
| if (keysArray.length === 0) { | |
| tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; color: var(--text-muted);">No access keys generated yet.</td></tr>'; | |
| return; | |
| } | |
| keysArray.forEach(k => { | |
| const now = new Date(); | |
| const expiresAt = new Date(k.expires_at); | |
| let status = 'Active'; | |
| let badgeClass = 'badge-active'; | |
| if (k.revoked) { | |
| status = 'Revoked'; | |
| badgeClass = 'badge-revoked'; | |
| } else if (now > expiresAt) { | |
| status = 'Expired'; | |
| badgeClass = 'badge-expired'; | |
| } | |
| const tr = document.createElement('tr'); | |
| tr.innerHTML = ` | |
| <td><span class="key-text">${k.key}</span></td> | |
| <td><span class="badge ${badgeClass}">${status}</span></td> | |
| <td style="color: var(--text-muted); font-size: 0.9rem;">${new Date(k.created_at).toLocaleString()}</td> | |
| <td style="color: var(--text-muted); font-size: 0.9rem;">${expiresAt.toLocaleString()}</td> | |
| <td style="color: var(--text-muted); font-size: 0.9rem;">${k.used_by_ip || 'Never Used'}</td> | |
| <td> | |
| ${(status === 'Active') ? `<button class="action-btn" onclick="revokeKey('${k.key}')">Revoke</button>` : ''} | |
| </td> | |
| `; | |
| tbody.appendChild(tr); | |
| }); | |
| } | |
| async function generateKey() { | |
| if (!adminKey) return; | |
| const hours = parseInt(document.getElementById('key-hours').value); | |
| try { | |
| const res = await fetch('/api/admin/generate', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ admin_key: adminKey, hours: hours }) | |
| }); | |
| if (res.ok) { | |
| const data = await res.json(); | |
| document.getElementById('new-key-display').textContent = data.key; | |
| document.getElementById('new-key-result').style.display = 'block'; | |
| fetchKeys(); | |
| } else { | |
| alert('Failed to generate key'); | |
| } | |
| } catch (e) { | |
| console.error('Generation failed', e); | |
| } | |
| } | |
| async function revokeKey(targetKey) { | |
| if (!adminKey) return; | |
| if (!confirm(`Are you sure you want to revoke key: ${targetKey}?`)) return; | |
| try { | |
| const res = await fetch('/api/admin/revoke', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ admin_key: adminKey, target_key: targetKey }) | |
| }); | |
| if (res.ok) { | |
| fetchKeys(); | |
| } else { | |
| alert('Failed to revoke key'); | |
| } | |
| } catch (e) { | |
| console.error('Revoke failed', e); | |
| } | |
| } | |
| async function revokeAllKeys() { | |
| if (!adminKey) return; | |
| if (!confirm('Are you absolutely sure you want to revoke ALL active keys?')) return; | |
| try { | |
| const res = await fetch('/api/admin/revoke_all', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ admin_key: adminKey }) | |
| }); | |
| if (res.ok) { | |
| const data = await res.json(); | |
| alert(`Successfully revoked ${data.revoked_count} active keys.`); | |
| fetchKeys(); | |
| } else { | |
| alert('Failed to revoke keys'); | |
| } | |
| } catch (e) { | |
| console.error('Revoke all failed', e); | |
| } | |
| } | |
| async function fetchLogs() { | |
| if (!adminKey) return; | |
| try { | |
| const res = await fetch('/api/admin/logs', { | |
| method: 'GET', | |
| headers: { 'admin-key': adminKey } | |
| }); | |
| if (res.ok) { | |
| const data = await res.json(); | |
| const container = document.getElementById('logs-container'); | |
| if (data.logs && data.logs.length > 0) { | |
| // Reverse to show newest at top | |
| container.innerHTML = data.logs.reverse().map(l => { | |
| let color = '#a1a1aa'; | |
| if (l.includes('✅')) color = '#4ade80'; | |
| if (l.includes('⛔')) color = '#f87171'; | |
| return `<div style="color: ${color}; margin-bottom: 0.25rem;">${l}</div>`; | |
| }).join(''); | |
| } else { | |
| container.innerHTML = '<div style="text-align: center;">No logs found for the last 24 hours.</div>'; | |
| } | |
| } | |
| } catch (e) { | |
| console.error('Failed to fetch logs', e); | |
| } | |
| } | |
| function copyKey() { | |
| const key = document.getElementById('new-key-display').textContent; | |
| navigator.clipboard.writeText(key).then(() => { | |
| const btn = document.getElementById('copy-btn'); | |
| btn.textContent = 'Copied!'; | |
| setTimeout(() => { btn.textContent = 'Copy to Clipboard'; }, 2000); | |
| }); | |
| } | |