Add backend API, running status and 30-day run history
- FastAPI + SQLite backend: tenants, jobs and runs, with an idempotent /api/v1/ingest endpoint authenticated by per-collector API keys - Runs store started_at and finished_at; "Last Run" is the most recent finish, and in-progress runs show as Running - Job modal shows a 30-day daily strip, success rate, and a filterable run list with per-run messages - nginx: HTTPS plus basic auth, ingest endpoint exempt; systemd unit - manage.py for API keys, job cleanup and demo data; pytest suite Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
"""Collector (machine) authentication.
|
||||
|
||||
Dashboard *users* are authenticated by nginx in front of the app (basic auth today,
|
||||
oauth2-proxy + Microsoft Entra ID later), so nothing here deals with people.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import secrets
|
||||
import sqlite3
|
||||
|
||||
from fastapi import Depends, Header, HTTPException
|
||||
|
||||
from .db import get_db
|
||||
from .timeutil import to_db, utcnow
|
||||
|
||||
KEY_PREFIX = "bdk_"
|
||||
|
||||
|
||||
def generate_key() -> str:
|
||||
return KEY_PREFIX + secrets.token_urlsafe(32)
|
||||
|
||||
|
||||
def hash_key(key: str) -> str:
|
||||
# Keys are 256-bit random, so a fast unsalted hash is fine (no dictionary to attack).
|
||||
return hashlib.sha256(key.encode()).hexdigest()
|
||||
|
||||
|
||||
def require_ingest_key(
|
||||
authorization: str | None = Header(None),
|
||||
conn: sqlite3.Connection = Depends(get_db),
|
||||
) -> str:
|
||||
scheme, _, token = (authorization or "").partition(" ")
|
||||
token = token.strip()
|
||||
if scheme.lower() != "bearer" or not token:
|
||||
raise HTTPException(401, "Missing bearer token", headers={"WWW-Authenticate": "Bearer"})
|
||||
row = conn.execute(
|
||||
"SELECT id, name FROM api_keys WHERE key_hash = ? AND revoked_at IS NULL", (hash_key(token),)
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise HTTPException(401, "Invalid or revoked API key", headers={"WWW-Authenticate": "Bearer"})
|
||||
conn.execute("UPDATE api_keys SET last_used_at = ? WHERE id = ?", (to_db(utcnow()), row["id"]))
|
||||
return row["name"]
|
||||
Reference in New Issue
Block a user