From 695dc9fdce807fe86931f46640b1cb33b876eb1d Mon Sep 17 00:00:00 2001 From: Davide Scaini Date: Sun, 10 May 2026 16:33:52 +0200 Subject: [PATCH] Fix Strava re-auth when credentials change; add disconnect button When a user saves new Strava credentials with a different client_id, auto-delete the existing token (it belongs to a different OAuth app and will always fail on refresh). Add POST /api/strava/disconnect endpoint and a "Disconnect from Strava" button in settings, visible only when connected. Immediate: deleted diego_p's stale token so he can reconnect. --- bincio/serve/server.py | 21 ++++++++++++++++ site/src/pages/settings/index.astro | 38 +++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/bincio/serve/server.py b/bincio/serve/server.py index 5c680c1..77612ea 100644 --- a/bincio/serve/server.py +++ b/bincio/serve/server.py @@ -1760,6 +1760,18 @@ async def me_set_strava_credentials( pass if not csec: raise HTTPException(400, "client_secret is required (no existing secret to preserve)") + + # If the client_id changed, the existing token belongs to a different OAuth + # app and will fail on refresh — delete it so the user must re-authenticate. + token_path = _get_data_dir() / user.handle / "strava_token.json" + if creds_path.exists() and token_path.exists(): + try: + old_cid = str(json.loads(creds_path.read_text(encoding="utf-8")).get("client_id", "")).strip() + if old_cid and old_cid != cid: + token_path.unlink(missing_ok=True) + except Exception: + pass + creds_path.write_text( json.dumps({"client_id": cid, "client_secret": csec}, indent=2), encoding="utf-8", @@ -2470,6 +2482,15 @@ async def strava_status(bincio_session: Optional[str] = Cookie(default=None)) -> }) +@app.post("/api/strava/disconnect") +async def strava_disconnect(bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse: + """Remove the stored Strava token, forcing a fresh OAuth on next connect.""" + user = _require_user(bincio_session) + token_path = _get_data_dir() / user.handle / "strava_token.json" + token_path.unlink(missing_ok=True) + return JSONResponse({"ok": True}) + + @app.post("/api/strava/reset") async def strava_reset(request: Request, bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse: """Reset last_sync_at so the next sync re-fetches from a chosen point. diff --git a/site/src/pages/settings/index.astro b/site/src/pages/settings/index.astro index 7f039b9..264be5b 100644 --- a/site/src/pages/settings/index.astro +++ b/site/src/pages/settings/index.astro @@ -124,6 +124,14 @@ import Base from '../../layouts/Base.astro'; + @@ -509,10 +517,40 @@ import Base from '../../layouts/Base.astro'; } }); + // ── Strava disconnect ───────────────────────────────────────────────────────── + + async function loadStravaConnection() { + try { + const r = await fetch('/api/strava/status', { credentials: 'include' }); + if (!r.ok) return; + const d = await r.json(); + document.getElementById('strava-disconnect-row')! + .classList.toggle('hidden', !d.connected); + } catch { /* ignore */ } + } + + document.getElementById('strava-disconnect-btn')?.addEventListener('click', async () => { + const statusEl = document.getElementById('strava-disconnect-status')!; + if (!confirm('Disconnect from Strava? You will need to reconnect via OAuth to re-enable sync.')) return; + try { + const r = await fetch('/api/strava/disconnect', { method: 'POST', credentials: 'include' }); + if (r.ok) { + setStatus(statusEl, 'Disconnected.', true); + loadStravaConnection(); + } else { + const d = await r.json(); + setStatus(statusEl, d.detail ?? 'Failed', false); + } + } catch { + setStatus(statusEl, 'Could not reach server', false); + } + }); + // ── Init ───────────────────────────────────────────────────────────────────── loadMe(); loadStorage(); loadNavPrefs(); loadStravaCreds(); + loadStravaConnection();