summaryrefslogtreecommitdiff
path: root/admin_page.go
diff options
context:
space:
mode:
Diffstat (limited to 'admin_page.go')
-rw-r--r--admin_page.go152
1 files changed, 152 insertions, 0 deletions
diff --git a/admin_page.go b/admin_page.go
new file mode 100644
index 0000000..b132c1e
--- /dev/null
+++ b/admin_page.go
@@ -0,0 +1,152 @@
+// admin_page.go
+package main
+
+import "net/http"
+
+const adminHTML = `<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Bridge Admin</title>
+ <style>
+ body { font-family: system-ui, sans-serif; margin: 1rem; background:#111; color:#eee; }
+ h1 { font-size: 1.4rem; }
+ .section { margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid #333; }
+ label { display:block; margin-bottom:0.25rem; }
+ input, button { padding:0.4rem 0.6rem; margin:0.1rem 0; background:#222; border:1px solid #555; color:#eee; border-radius:4px; }
+ button { cursor:pointer; }
+ table { border-collapse: collapse; width: 100%; font-size:0.9rem; }
+ th, td { border:1px solid #333; padding:0.25rem 0.4rem; }
+ th { background:#222; }
+ #log { background:#000; border:1px solid #333; padding:0.5rem; height:300px; overflow-y:scroll; font-family:monospace; font-size:0.85rem; }
+ .revoked { opacity:0.5; text-decoration:line-through; }
+ </style>
+</head>
+<body>
+ <h1>Bridge Admin</h1>
+
+ <div class="section">
+ <h2>New Token</h2>
+ <form id="newTokenForm">
+ <label for="name">Internal name (e.g. jeff)</label>
+ <input id="name" name="name" required />
+ <button type="submit">Create token</button>
+ </form>
+ <div id="newTokenOutput"></div>
+ </div>
+
+ <div class="section">
+ <h2>Tokens</h2>
+ <button onclick="loadTokens()">Reload tokens</button>
+ <table>
+ <thead>
+ <tr><th>Name</th><th>Token</th><th>Revoked</th><th>Actions</th></tr>
+ </thead>
+ <tbody id="tokensBody"></tbody>
+ </table>
+ </div>
+
+ <div class="section">
+ <h2>Events (Chat + Location)</h2>
+ <div id="log"></div>
+ </div>
+
+<script>
+let lastEventId = 0;
+
+async function loadTokens() {
+ const res = await fetch('/admin/api/tokens');
+ if (!res.ok) return;
+ const tokens = await res.json();
+ const tbody = document.getElementById('tokensBody');
+ tbody.innerHTML = '';
+ tokens.forEach(t => {
+ const tr = document.createElement('tr');
+ if (t.revoked) tr.classList.add('revoked');
+ const tdName = document.createElement('td');
+ tdName.textContent = t.name;
+ const tdToken = document.createElement('td');
+ tdToken.textContent = t.token;
+ const tdRev = document.createElement('td');
+ tdRev.textContent = t.revoked ? 'yes' : 'no';
+ const tdAct = document.createElement('td');
+ if (!t.revoked) {
+ const btn = document.createElement('button');
+ btn.textContent = 'Revoke';
+ btn.onclick = () => revokeToken(t.token);
+ tdAct.appendChild(btn);
+ }
+ tr.appendChild(tdName);
+ tr.appendChild(tdToken);
+ tr.appendChild(tdRev);
+ tr.appendChild(tdAct);
+ tbody.appendChild(tr);
+ });
+}
+
+async function revokeToken(token) {
+ if (!confirm('Revoke this token?')) return;
+ await fetch('/admin/api/tokens/revoke', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ token })
+ });
+ loadTokens();
+}
+
+const form = document.getElementById('newTokenForm');
+form.addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const name = document.getElementById('name').value.trim();
+ if (!name) return;
+ const res = await fetch('/admin/api/tokens', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name })
+ });
+ if (!res.ok) {
+ alert('Failed to create token');
+ return;
+ }
+ const data = await res.json();
+ document.getElementById('newTokenOutput').textContent = 'New token for ' + data.name + ': ' + data.token;
+ form.reset();
+ loadTokens();
+});
+
+async function pollEvents() {
+ try {
+ const res = await fetch('/admin/api/events?since=' + lastEventId);
+ if (!res.ok) return;
+ const events = await res.json();
+ const logEl = document.getElementById('log');
+ events.forEach(ev => {
+ if (ev.id > lastEventId) lastEventId = ev.id;
+ const line = document.createElement('div');
+ const ts = new Date(ev.timestamp * 1000).toISOString();
+ if (ev.type === 'chat') {
+ line.textContent = '[' + ts + '] [CHAT] ' + ev.fromNick + '(' + ev.fromInternal + '): ' + ev.message;
+ } else if (ev.type === 'location') {
+ line.textContent = '[' + ts + '] [LOC][' + ev.serverId + '] ' + ev.fromNick + '(' + ev.fromInternal + '): ' +
+ ' x=' + ev.x + ' y=' + ev.y + ' z=' + ev.z + ' dim=' + ev.dimension;
+ } else {
+ line.textContent = '[' + ts + '] [?] ' + JSON.stringify(ev);
+ }
+ logEl.appendChild(line);
+ logEl.scrollTop = logEl.scrollHeight;
+ });
+ } catch (e) {
+ console.error('pollEvents error', e);
+ }
+}
+
+loadTokens();
+setInterval(pollEvents, 2000);
+</script>
+</body>
+</html>`
+
+func adminPageHandler(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ _, _ = w.Write([]byte(adminHTML))
+} \ No newline at end of file