RECORDGOD/app/settings_routes.py
type-two e1fcf94f93 feat: standalone RecordGod web UI — landing, control-room dash, encrypted secrets vault
- app/vault.py: Fernet-encrypted credential store (app_secret table); key lives only in
  RECORDGOD_SECRET_KEY (.env, gitignored) — refuses to store anything if the key is unset.
- app/settings_routes.py: admin-gated GET/POST /settings/secrets; values encrypted at rest,
  NEVER returned. Known fields: Woo / Discogs / Cloudflare / DealGod credentials.
- site/index.html: recordgod.com landing. site/dash.html: control room (token gate +
  paste-your-credentials form). main.py loads .env, mounts site at /, store at /store.
- Proven: 401 on bad token; DB holds ciphertext (gAAAA…), not plaintext.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 12:40:07 +10:00

49 lines
1.7 KiB
Python

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from . import vault
from .auth import require_token
from .db import get_db
# Admin-gated. The dash POSTs credentials in; values are encrypted at rest and NEVER returned.
router = APIRouter(prefix="/settings", tags=["settings"])
# The credentials the dash knows how to collect (name → human label).
KNOWN = {
"woo_base_url": "WooCommerce store URL (https://…)",
"woo_consumer_key": "WooCommerce REST consumer key (ck_…)",
"woo_consumer_secret": "WooCommerce REST consumer secret (cs_…)",
"discogs_consumer_key": "Discogs OAuth consumer key",
"discogs_consumer_secret": "Discogs OAuth consumer secret",
"discogs_token": "Discogs personal access token",
"cloudflare_api_token": "Cloudflare API token",
"dealgod_api_key": "DealGod API key (X-Api-Key)",
}
class SecretIn(BaseModel):
name: str
value: str
@router.get("/secrets")
async def list_secrets(ident=Depends(require_token), db=Depends(get_db)):
have = {s["name"]: s["updated_at"] for s in await vault.list_secret_names(db)}
return {"ok": True, "fields": [
{"name": n, "label": label, "set": n in have, "updated_at": have.get(n)}
for n, label in KNOWN.items()
]}
@router.post("/secrets")
async def save_secret(body: SecretIn, ident=Depends(require_token), db=Depends(get_db)):
if body.name not in KNOWN:
raise HTTPException(400, "unknown secret name")
if not body.value.strip():
raise HTTPException(400, "empty value")
try:
await vault.set_secret(db, body.name, body.value.strip())
except vault.SecretsUnconfigured as e:
raise HTTPException(503, str(e))
return {"ok": True, "name": body.name, "set": True}