(opus assessment) Fix auth wall flash, broken multi-user write API, and single-user redirect loop
Auth wall (Base.astro): set data-auth-pending on <body> at SSG time and hide
it with inline CSS before any JS runs; remove the attribute after /api/me
resolves. Eliminates the flash of protected content on private instances.
Multi-user write API (serve/server.py): the previous _apply_sidecar_edit and
strava_sync imports from bincio.edit.server were broken (those names don't
exist as module-level exports) and the Strava sync mutated a global data_dir,
making concurrent requests from different users racy. Fix: extract both
operations into bincio/edit/ops.py as pure functions that take data_dir
explicitly. Both edit/server.py and serve/server.py now import from there.
Security: add rate limiting to POST /api/register (5 attempts / 15 min / IP,
separate bucket from login). Add _check_id() activity ID validation to both
GET and POST /api/activity/{id} in serve/server.py.
Single-user mode: _write_root_manifest now forces instance.private=false when
no instance.db exists, even if a previous run wrote true. Prevents the auth
wall from firing and redirecting to /login/ when bincio serve isn't running.
ActivityFeed: skip filterHandle when profileIndexUrl is set (per-user profile
pages load the right shard directly; activities have no handle tag at that
point, so the filter was producing an empty feed). Fix handle links to point
to /u/{handle}/ instead of /{handle}/. Fix <a>-inside-<a> Svelte warning by
converting the inner handle link to a <button>.
This commit is contained in:
+7
-89
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -405,37 +404,10 @@ async def save_activity(activity_id: str, payload: dict[str, Any]) -> JSONRespon
|
||||
if not (dd / "activities" / f"{activity_id}.json").exists():
|
||||
raise HTTPException(404, f"Activity {activity_id!r} not found")
|
||||
|
||||
edits_dir = dd / "edits"
|
||||
edits_dir.mkdir(exist_ok=True)
|
||||
sidecar_path = edits_dir / f"{activity_id}.md"
|
||||
|
||||
lines: list[str] = []
|
||||
if payload.get("title"):
|
||||
lines.append(f"title: {json.dumps(payload['title'])}")
|
||||
if payload.get("sport") and payload["sport"] in SPORTS and payload["sport"] != "other":
|
||||
lines.append(f"sport: {payload['sport']}")
|
||||
if payload.get("gear"):
|
||||
lines.append(f"gear: {json.dumps(payload['gear'])}")
|
||||
if payload.get("highlight"):
|
||||
lines.append("highlight: true")
|
||||
if payload.get("private"):
|
||||
lines.append("private: true")
|
||||
hide = [s for s in (payload.get("hide_stats") or []) if s in STAT_PANELS]
|
||||
if hide:
|
||||
lines.append(f"hide_stats: [{', '.join(hide)}]")
|
||||
|
||||
description = (payload.get("description") or "").strip()
|
||||
|
||||
content = "---\n" + "\n".join(lines) + "\n---\n"
|
||||
if description:
|
||||
content += "\n" + description + "\n"
|
||||
|
||||
sidecar_path.write_text(content, encoding="utf-8")
|
||||
|
||||
# Re-merge so the Astro dev server immediately serves updated data
|
||||
from bincio.render.merge import merge_all
|
||||
merge_all(dd)
|
||||
from bincio.edit.ops import apply_sidecar_edit
|
||||
apply_sidecar_edit(activity_id, payload, dd)
|
||||
|
||||
sidecar_path = dd / "edits" / f"{activity_id}.md"
|
||||
return JSONResponse({"ok": True, "sidecar": str(sidecar_path)})
|
||||
|
||||
|
||||
@@ -726,66 +698,12 @@ async def strava_sync() -> JSONResponse:
|
||||
if not strava_client_id or not strava_client_secret:
|
||||
raise HTTPException(400, "Strava not configured. Pass --strava-client-id and --strava-client-secret to bincio edit.")
|
||||
dd = _get_data_dir()
|
||||
|
||||
from bincio.extract.strava_api import (
|
||||
StravaError, ensure_fresh, fetch_activities, fetch_streams,
|
||||
save_token, strava_to_parsed,
|
||||
)
|
||||
from bincio.edit.ops import run_strava_sync
|
||||
try:
|
||||
token = ensure_fresh(dd, strava_client_id, strava_client_secret)
|
||||
except StravaError as e:
|
||||
result = run_strava_sync(dd, strava_client_id, strava_client_secret)
|
||||
except RuntimeError as e:
|
||||
raise HTTPException(502, str(e))
|
||||
|
||||
after: int | None = token.get("last_sync_at")
|
||||
try:
|
||||
activities = fetch_activities(token["access_token"], after=after)
|
||||
except StravaError as e:
|
||||
raise HTTPException(502, str(e))
|
||||
|
||||
from bincio.extract.metrics import compute
|
||||
from bincio.extract.writer import build_summary, make_activity_id, write_activity, write_index
|
||||
from bincio.extract.strava_api import strava_meta_to_partial
|
||||
from bincio.render.merge import merge_all
|
||||
|
||||
# Load existing index once
|
||||
index_path = dd / "index.json"
|
||||
if index_path.exists():
|
||||
index_data = json.loads(index_path.read_text(encoding="utf-8"))
|
||||
else:
|
||||
index_data = {"owner": {"handle": "unknown"}, "activities": []}
|
||||
owner = index_data.get("owner", {})
|
||||
summaries: dict[str, dict] = {s["id"]: s for s in index_data.get("activities", [])}
|
||||
|
||||
imported = 0
|
||||
skipped = 0
|
||||
errors: list[str] = []
|
||||
|
||||
for meta in activities:
|
||||
try:
|
||||
# Compute ID from meta alone (no API call) to skip already-known activities
|
||||
activity_id = make_activity_id(strava_meta_to_partial(meta))
|
||||
if (dd / "activities" / f"{activity_id}.json").exists():
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
# Only fetch streams for genuinely new activities
|
||||
streams = fetch_streams(token["access_token"], meta["id"])
|
||||
parsed = strava_to_parsed(meta, streams)
|
||||
metrics = compute(parsed)
|
||||
write_activity(parsed, metrics, dd, privacy="public", rdp_epsilon=0.0001)
|
||||
summaries[activity_id] = build_summary(parsed, metrics, activity_id, "public")
|
||||
imported += 1
|
||||
except Exception as exc:
|
||||
errors.append(f"{meta.get('id')}: {type(exc).__name__}")
|
||||
|
||||
if imported:
|
||||
write_index(list(summaries.values()), dd, owner)
|
||||
merge_all(dd)
|
||||
|
||||
token["last_sync_at"] = int(time.time())
|
||||
save_token(dd, token)
|
||||
|
||||
return JSONResponse({"ok": True, "imported": imported, "skipped": skipped, "error_count": len(errors), "errors": errors[:5]})
|
||||
return JSONResponse(result)
|
||||
|
||||
|
||||
@app.post("/api/strava/reset")
|
||||
|
||||
Reference in New Issue
Block a user