feat(dash): connection testing — Discogs / Woo / DealGod

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 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-06-21 15:03:18 +10:00
parent 834c549d5e
commit 0e8c84b0e7
2 changed files with 64 additions and 5 deletions

View File

@ -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")

View File

@ -48,13 +48,17 @@
<div id="secrets"></div>
</div>
<div class="card">
<h2>Orders (WooCommerce)</h2>
<p class="soon">Save your Woo store URL + consumer key/secret above, then live orders land here —
managed without wp-admin. (Coming next.)</p>
<h2>Connections <span class="soon">— save the keys above, then test</span></h2>
<div style="display:flex;gap:10px;align-items:center;margin:9px 0">
<button class="ghost" onclick="testConn('discogs')">Test Discogs</button><span id="c-discogs" class="soon"></span></div>
<div style="display:flex;gap:10px;align-items:center;margin:9px 0">
<button class="ghost" onclick="testConn('woo')">Test WooCommerce</button><span id="c-woo" class="soon"></span></div>
<div style="display:flex;gap:10px;align-items:center;margin:9px 0">
<button class="ghost" onclick="testConn('dealgod')">Test DealGod</button><span id="c-dealgod" class="soon"></span></div>
</div>
<div class="card">
<h2>Discogs</h2>
<p class="soon">Save your Discogs OAuth/token above to enrich intake from the live API and your wantlist.</p>
<h2>Orders (WooCommerce)</h2>
<p class="soon">Once WooCommerce tests green, live orders land here — managed without wp-admin. (Next build.)</p>
</div>
</div>
</div>
@ -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(); }
</script>