Move admin page into API, switch to HTTP Basic Auth

This commit is contained in:
2026-04-27 19:07:23 +02:00
parent 5bb864f34f
commit 6479f74d50
4 changed files with 165 additions and 11 deletions

View File

@@ -3,6 +3,7 @@ WORKDIR /app
COPY api/requirements.txt . COPY api/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY api/app/ ./app/ COPY api/app/ ./app/
COPY api/static/ ./static/ # <-- Add this line
RUN mkdir -p /data RUN mkdir -p /data
EXPOSE 8000 EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -14,6 +14,8 @@ def get_db():
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
return conn return conn
# your-very-long-random-admin-key
def init_db(): def init_db():
conn = get_db() conn = get_db()
conn.execute(""" conn.execute("""

View File

@@ -1,23 +1,31 @@
from fastapi import FastAPI, HTTPException, Depends, Header, Query, Request from fastapi import FastAPI, HTTPException, Depends, Header, Query, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse, FileResponse
from .auth import init_db, verify_api_key, get_db # make sure auth.py exports get_db from fastapi.security import HTTPBasic, HTTPBasicCredentials
from .auth import init_db, verify_api_key, get_db
from .models import Event, BatchEvents from .models import Event, BatchEvents
from .enrichment import enrich_event from .enrichment import enrich_event
from .influx import write_event, client, INFLUX_ORG, INFLUX_BUCKET from .influx import write_event, client, INFLUX_ORG, INFLUX_BUCKET
import logging import logging
import os import os
import secrets import secrets
import datetime import pathlib
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
# Environment variables # 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() init_db()
app = FastAPI(title="Signal - Roblox Telemetry") 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 ---------- # ---------- Original endpoints ----------
@app.get("/health") @app.get("/health")
@@ -35,11 +43,12 @@ async def ingest_event(payload: Event | BatchEvents, game: str = Depends(verify_
write_event(enriched) write_event(enriched)
return {"success": True} return {"success": True}
# ---------- Admin authentication helper ---------- # ---------- Serve the admin HTML ----------
def require_admin(x_admin_key: str = Header(None)): STATIC_DIR = pathlib.Path(__file__).parent.parent / "static"
if not x_admin_key or x_admin_key != ADMIN_API_KEY:
raise HTTPException(status_code=403, detail="Forbidden") @app.get("/admin", include_in_schema=False)
return True async def admin_page(admin: bool = Depends(require_admin)):
return FileResponse(STATIC_DIR / "index.html")
# ---------- Admin: API Keys ---------- # ---------- Admin: API Keys ----------
@app.get("/admin/keys") @app.get("/admin/keys")

142
api/static/index.html Normal file
View 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>