The intake sheet is read straight from the stored Google service account (discotag@…
inventory-sheets, migrated out of WowPlatter's wp_rmp_disc_google_service into the vault) — a
signed JWT (cryptography, no new dep) → Sheets API → private sheet, no public-share needed. Uses
the saved column_mappings (timestamp/release_id/media/sleeve/price/comment, 1-indexed). SKU from
the sheet's locale timestamp '2025-01-30, 14:01:02' → 20250130140102 (matches migrated stock).
Bulk import (both connected + paste-URL paths) now SKIPS SKUs already in inventory, so importing
the 34,752-row form log only stages the genuinely-new rows (live: 2,006 new / 32,746 skipped).
Preview shows new-of-total. Endpoints: GET/POST /admin/intake/gsheet/{preview,import}.
Also: .dockerignore now excludes disc_images/ (volume-mounted, was 916MB) — build context
918MB→1MB, deploy 13min(hung)→24s. The COPY . . was baking the image store into every image.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
110 lines
5.3 KiB
Python
110 lines
5.3 KiB
Python
from urllib.parse import urlparse
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel
|
|
|
|
from . import vault
|
|
from .auth import require_admin
|
|
from .db import get_db
|
|
|
|
# ADMIN-ONLY (require_admin): API keys / connections. Staff tokens get 403 here.
|
|
# 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)",
|
|
"smtp_host": "Receipt email — SMTP host (e.g. mail.monsterrobot.party)",
|
|
"smtp_port": "Receipt email — SMTP port (587 STARTTLS / 465 SSL)",
|
|
"smtp_user": "Receipt email — SMTP username (e.g. shop@monsterrobot.party)",
|
|
"smtp_pass": "Receipt email — SMTP password",
|
|
"smtp_from": "Receipt email — From address (defaults to username)",
|
|
"smtp_from_name": "Receipt email — From name (e.g. Monster Robot Records)",
|
|
"square_access_token": "Square access token (EAAA…) — drives the payment terminal",
|
|
"square_location_id": "Square location ID (e.g. S5D3EN3YM2AN8)",
|
|
"square_environment": "Square environment — production or sandbox",
|
|
"bridge_key": "WordPress bridge shared secret — the WP plugin sends this to post back orders",
|
|
"google_sa_email": "Google service-account email (…@…iam.gserviceaccount.com) — reads the intake sheet",
|
|
"google_sa_private_key": "Google service-account private key (PEM) — signs the Sheets API token",
|
|
"google_sheet_id": "Intake Google Sheet ID (from its URL)",
|
|
"google_sheet_name": "Intake sheet/tab name (e.g. Sheet1)",
|
|
"google_column_map": "Intake sheet column map JSON (Google-Form positions)",
|
|
}
|
|
|
|
|
|
class SecretIn(BaseModel):
|
|
name: str
|
|
value: str
|
|
|
|
|
|
@router.get("/secrets")
|
|
async def list_secrets(ident=Depends(require_admin), 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_admin), 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}
|
|
|
|
|
|
@router.post("/test/{service}")
|
|
async def test_connection(service: str, ident=Depends(require_admin), db=Depends(get_db)):
|
|
"""Validate stored credentials against the live service — so 'connected' means connected."""
|
|
async with httpx.AsyncClient(timeout=15, follow_redirects=True) as c:
|
|
if service == "discogs":
|
|
tok = await vault.get_secret(db, "discogs_token")
|
|
if not tok:
|
|
raise HTTPException(400, "save a Discogs personal access token first")
|
|
r = await c.get("https://api.discogs.com/oauth/identity",
|
|
headers={"Authorization": f"Discogs token={tok}",
|
|
"User-Agent": "RecordGod/0.1"})
|
|
if r.status_code == 200:
|
|
return {"ok": True, "connected": True, "as": r.json().get("username")}
|
|
return {"ok": True, "connected": False, "detail": f"HTTP {r.status_code}"}
|
|
|
|
if service == "woo":
|
|
base = await vault.get_secret(db, "woo_base_url")
|
|
ck = await vault.get_secret(db, "woo_consumer_key")
|
|
cs = await vault.get_secret(db, "woo_consumer_secret")
|
|
if not (base and ck and cs):
|
|
raise HTTPException(400, "save Woo store URL + consumer key + secret first")
|
|
r = await c.get(base.rstrip("/") + "/wp-json/wc/v3/orders",
|
|
params={"per_page": 1}, auth=(ck, cs))
|
|
if r.status_code == 200:
|
|
return {"ok": True, "connected": True, "as": urlparse(base).netloc}
|
|
return {"ok": True, "connected": False, "detail": f"HTTP {r.status_code}"}
|
|
|
|
if service == "dealgod":
|
|
key = await vault.get_secret(db, "dealgod_api_key")
|
|
if not key:
|
|
raise HTTPException(400, "save a DealGod API key first")
|
|
r = await c.get("https://api.dealgod.pro/api/me", headers={"X-Api-Key": key})
|
|
if r.status_code == 200:
|
|
j = r.json()
|
|
return {"ok": True, "connected": True, "as": f"{j.get('user')} · {j.get('plan')}"}
|
|
if r.status_code == 404:
|
|
return {"ok": True, "connected": False, "detail": "DealGod /api/me not deployed yet"}
|
|
return {"ok": True, "connected": False, "detail": f"HTTP {r.status_code}"}
|
|
|
|
raise HTTPException(400, "unknown service")
|