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.
This commit is contained in:
Davide Scaini
2026-05-10 16:33:52 +02:00
parent 8f028101c7
commit 695dc9fdce
2 changed files with 59 additions and 0 deletions
+21
View File
@@ -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.
+38
View File
@@ -124,6 +124,14 @@ import Base from '../../layouts/Base.astro';
</button>
</div>
</form>
<div id="strava-disconnect-row" class="hidden mt-4 pt-4 border-t border-zinc-800">
<p class="text-xs text-zinc-500 mb-2">Connected to Strava. Disconnect to re-authenticate with different credentials.</p>
<button id="strava-disconnect-btn"
class="px-4 py-2 rounded-lg text-sm bg-zinc-800 hover:bg-red-900 hover:text-red-300 text-zinc-400 transition-colors">
Disconnect from Strava
</button>
<p id="strava-disconnect-status" class="text-xs mt-2 hidden"></p>
</div>
</section>
<!-- Danger zone -->
@@ -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();
</script>