Move admin page into API, switch to HTTP Basic Auth
This commit is contained in:
@@ -14,6 +14,8 @@ def get_db():
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
# your-very-long-random-admin-key
|
||||
|
||||
def init_db():
|
||||
conn = get_db()
|
||||
conn.execute("""
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
from fastapi import FastAPI, HTTPException, Depends, Header, Query, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from .auth import init_db, verify_api_key, get_db # make sure auth.py exports get_db
|
||||
from fastapi.responses import JSONResponse, FileResponse
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
from .auth import init_db, verify_api_key, get_db
|
||||
from .models import Event, BatchEvents
|
||||
from .enrichment import enrich_event
|
||||
from .influx import write_event, client, INFLUX_ORG, INFLUX_BUCKET
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
import datetime
|
||||
import pathlib
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# Environment variables
|
||||
ADMIN_API_KEY = os.getenv("ADMIN_API_KEY", "admin-secret-change-me")
|
||||
ADMIN_USERNAME = os.getenv("ADMIN_USERNAME", "admin")
|
||||
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin") # change in pod YAML!
|
||||
|
||||
# Initialise database (creates table if missing)
|
||||
init_db()
|
||||
|
||||
app = FastAPI(title="Signal - Roblox Telemetry")
|
||||
security = HTTPBasic()
|
||||
|
||||
# ---------- HTTP Basic auth dependency ----------
|
||||
def require_admin(credentials: HTTPBasicCredentials = Depends(security)):
|
||||
if credentials.username != ADMIN_USERNAME or credentials.password != ADMIN_PASSWORD:
|
||||
raise HTTPException(status_code=401, detail="Invalid credentials",
|
||||
headers={"WWW-Authenticate": "Basic"})
|
||||
return True
|
||||
|
||||
# ---------- Original endpoints ----------
|
||||
@app.get("/health")
|
||||
@@ -35,11 +43,12 @@ async def ingest_event(payload: Event | BatchEvents, game: str = Depends(verify_
|
||||
write_event(enriched)
|
||||
return {"success": True}
|
||||
|
||||
# ---------- Admin authentication helper ----------
|
||||
def require_admin(x_admin_key: str = Header(None)):
|
||||
if not x_admin_key or x_admin_key != ADMIN_API_KEY:
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
return True
|
||||
# ---------- Serve the admin HTML ----------
|
||||
STATIC_DIR = pathlib.Path(__file__).parent.parent / "static"
|
||||
|
||||
@app.get("/admin", include_in_schema=False)
|
||||
async def admin_page(admin: bool = Depends(require_admin)):
|
||||
return FileResponse(STATIC_DIR / "index.html")
|
||||
|
||||
# ---------- Admin: API Keys ----------
|
||||
@app.get("/admin/keys")
|
||||
|
||||
142
api/static/index.html
Normal file
142
api/static/index.html
Normal file
@@ -0,0 +1,142 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>RBXLogger Admin</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="container py-4">
|
||||
<h1>RBXLogger Admin</h1>
|
||||
|
||||
<!-- Tabs -->
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
<li class="nav-item"><a class="nav-link active" data-bs-toggle="tab" href="#keys">API Keys</a></li>
|
||||
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#events">Events</a></li>
|
||||
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#buckets">Buckets</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<!-- API Keys -->
|
||||
<div class="tab-pane fade show active" id="keys">
|
||||
<div class="mb-3">
|
||||
<h5>Create new key</h5>
|
||||
<input id="newGame" class="form-control w-25 d-inline" placeholder="Game name">
|
||||
<button class="btn btn-success" onclick="createKey()">Create</button>
|
||||
</div>
|
||||
<table class="table table-bordered" id="keysTable">
|
||||
<thead><tr><th>Key</th><th>Game</th><th>Active</th><th>Created</th><th>Actions</th></tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Events -->
|
||||
<div class="tab-pane fade" id="events">
|
||||
<div class="mb-3">
|
||||
<label>Hours back: <input id="hoursBack" value="24" class="form-control w-25 d-inline"></label>
|
||||
<button class="btn btn-info" onclick="loadEvents()">Load</button>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<h5>Delete events</h5>
|
||||
<input id="delStart" class="form-control w-25 d-inline" placeholder="Start (ISO)">
|
||||
<input id="delStop" class="form-control w-25 d-inline" placeholder="Stop (ISO)">
|
||||
<input id="delMeas" class="form-control w-25 d-inline" placeholder="Measurement (optional)">
|
||||
<button class="btn btn-danger" onclick="deleteEvents()">Delete</button>
|
||||
</div>
|
||||
<table class="table table-sm" id="eventsTable">
|
||||
<thead><tr><th>Time</th><th>Measurement</th><th>Fields</th><th>Tags</th></tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Buckets -->
|
||||
<div class="tab-pane fade" id="buckets">
|
||||
<button class="btn btn-secondary mb-2" onclick="loadBuckets()">Refresh</button>
|
||||
<table class="table" id="bucketsTable">
|
||||
<thead><tr><th>Name</th><th>ID</th></tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// All fetches automatically use the HTTP Basic Auth credentials
|
||||
// because the browser sends them with every request to this origin.
|
||||
|
||||
async function loadKeys() {
|
||||
const res = await fetch('/admin/keys');
|
||||
const keys = await res.json();
|
||||
const tbody = document.querySelector('#keysTable tbody');
|
||||
tbody.innerHTML = keys.map(k => `
|
||||
<tr>
|
||||
<td><code>${k.key}</code></td>
|
||||
<td>${k.game}</td>
|
||||
<td>${k.active ? '✅' : '❌'}</td>
|
||||
<td>${k.created_at}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-warning" onclick="toggleKey('${k.key}')">Toggle</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteKey('${k.key}')">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function createKey() {
|
||||
const game = document.getElementById('newGame').value;
|
||||
if (!game) return alert('Game name required');
|
||||
await fetch(`/admin/keys?game=${encodeURIComponent(game)}`, { method: 'POST' });
|
||||
loadKeys();
|
||||
}
|
||||
|
||||
async function toggleKey(key) {
|
||||
await fetch(`/admin/keys/${key}/toggle`, { method: 'PUT' });
|
||||
loadKeys();
|
||||
}
|
||||
|
||||
async function deleteKey(key) {
|
||||
if (!confirm('Delete key?')) return;
|
||||
await fetch(`/admin/keys/${key}`, { method: 'DELETE' });
|
||||
loadKeys();
|
||||
}
|
||||
|
||||
async function loadEvents() {
|
||||
const hrs = document.getElementById('hoursBack').value;
|
||||
const res = await fetch(`/admin/events?hours=${hrs}`);
|
||||
const events = await res.json();
|
||||
const tbody = document.querySelector('#eventsTable tbody');
|
||||
tbody.innerHTML = events.map(e => `
|
||||
<tr>
|
||||
<td>${e.time}</td>
|
||||
<td>${e.measurement}</td>
|
||||
<td>${JSON.stringify(e.fields)}</td>
|
||||
<td>${JSON.stringify(e.tags)}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function deleteEvents() {
|
||||
const start = document.getElementById('delStart').value;
|
||||
const stop = document.getElementById('delStop').value;
|
||||
const meas = document.getElementById('delMeas').value;
|
||||
if (!start || !stop) return alert('Start/stop required');
|
||||
let url = `/admin/events?start=${encodeURIComponent(start)}&stop=${encodeURIComponent(stop)}`;
|
||||
if (meas) url += `&measurement=${encodeURIComponent(meas)}`;
|
||||
if (!confirm('PERMANENTLY DELETE events in this range?')) return;
|
||||
await fetch(url, { method: 'DELETE' });
|
||||
alert('Done');
|
||||
loadEvents();
|
||||
}
|
||||
|
||||
async function loadBuckets() {
|
||||
const res = await fetch('/admin/buckets');
|
||||
const buckets = await res.json();
|
||||
const tbody = document.querySelector('#bucketsTable tbody');
|
||||
tbody.innerHTML = buckets.map(b => `<tr><td>${b.name}</td><td>${b.id}</td></tr>`).join('');
|
||||
}
|
||||
|
||||
// Initial loads
|
||||
loadKeys();
|
||||
loadBuckets();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user