From 0e8c84b0e799ca65511a21075c951e5e2ba4c6bd Mon Sep 17 00:00:00 2001 From: type-two Date: Sun, 21 Jun 2026 15:03:18 +1000 Subject: [PATCH] =?UTF-8?q?feat(dash):=20connection=20testing=20=E2=80=94?= =?UTF-8?q?=20Discogs=20/=20Woo=20/=20DealGod?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit POST /settings/test/{service} validates stored creds against the live service (Discogs identity, Woo orders, DealGod /api/me) so "connected" means connected. Dash gets per-service Test buttons with green/amber status. Co-Authored-By: Claude Opus 4.8 --- app/settings_routes.py | 45 ++++++++++++++++++++++++++++++++++++++++++ site/dash.html | 24 +++++++++++++++++----- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/app/settings_routes.py b/app/settings_routes.py index 2c1c98f..707dce8 100644 --- a/app/settings_routes.py +++ b/app/settings_routes.py @@ -1,3 +1,6 @@ +from urllib.parse import urlparse + +import httpx from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel @@ -46,3 +49,45 @@ async def save_secret(body: SecretIn, ident=Depends(require_token), db=Depends(g 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_token), 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") diff --git a/site/dash.html b/site/dash.html index 423f50d..39ca17b 100644 --- a/site/dash.html +++ b/site/dash.html @@ -48,13 +48,17 @@
-

Orders (WooCommerce)

-

Save your Woo store URL + consumer key/secret above, then live orders land here — - managed without wp-admin. (Coming next.)

+

Connections — save the keys above, then test

+
+
+
+
+
+
-

Discogs

-

Save your Discogs OAuth/token above to enrich intake from the live API and your wantlist.

+

Orders (WooCommerce)

+

Once WooCommerce tests green, live orders land here — managed without wp-admin. (Next build.)

@@ -100,6 +104,16 @@ async function save(name, btn){ btn.previousElementSibling; setTimeout(()=>btn.textContent='Save', 1500); } } +async function testConn(svc){ + const el = document.getElementById('c-'+svc); el.textContent='testing…'; el.style.color='#9a9aa6'; + let r, d; + try { r = await fetch('/settings/test/'+svc, {method:'POST', headers:hdr()}); d = await r.json(); } + catch(e){ el.textContent='✕ network error'; el.style.color='#e66'; return; } + if(!r.ok){ el.textContent='✕ '+(d.detail||r.status); el.style.color='#e6a046'; return; } + if(d.connected){ el.textContent='✓ connected'+(d.as?(' as '+d.as):''); el.style.color='#46d18a'; } + else { el.textContent='✕ '+(d.detail||('HTTP '+d.status)); el.style.color='#e6a046'; } +} + // auto-resume if a token is already stored if(TOKEN){ $('#tok').value = TOKEN; signin(); }