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:
@@ -1760,6 +1760,18 @@ async def me_set_strava_credentials(
|
|||||||
pass
|
pass
|
||||||
if not csec:
|
if not csec:
|
||||||
raise HTTPException(400, "client_secret is required (no existing secret to preserve)")
|
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(
|
creds_path.write_text(
|
||||||
json.dumps({"client_id": cid, "client_secret": csec}, indent=2),
|
json.dumps({"client_id": cid, "client_secret": csec}, indent=2),
|
||||||
encoding="utf-8",
|
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")
|
@app.post("/api/strava/reset")
|
||||||
async def strava_reset(request: Request, bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse:
|
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.
|
"""Reset last_sync_at so the next sync re-fetches from a chosen point.
|
||||||
|
|||||||
@@ -124,6 +124,14 @@ import Base from '../../layouts/Base.astro';
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
</section>
|
||||||
|
|
||||||
<!-- Danger zone -->
|
<!-- 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 ─────────────────────────────────────────────────────────────────────
|
// ── Init ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
loadMe();
|
loadMe();
|
||||||
loadStorage();
|
loadStorage();
|
||||||
loadNavPrefs();
|
loadNavPrefs();
|
||||||
loadStravaCreds();
|
loadStravaCreds();
|
||||||
|
loadStravaConnection();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user