fix: cleanup StravaConfig on user delete (FK violation); equalize login timing (user enum); scope upload task-status to owner; SQL-side data-point downsampling; drop unused clsx/react-leaflet deps
Build and push images / validate (push) Successful in 3s
Build and push images / build-backend (push) Successful in 6s
Build and push images / build-worker (push) Successful in 5s
Build and push images / build-frontend (push) Successful in 19s

This commit is contained in:
2026-06-25 12:21:25 +01:00
parent 84eb1c46bd
commit 64caaad4e7
7 changed files with 79 additions and 42 deletions
+41 -1
View File
@@ -17,6 +17,34 @@ MAX_FILE_SIZE = 500 * 1024 * 1024 # 500 MB upload cap
MAX_EXTRACT_SIZE = 4 * 1024 * 1024 * 1024 # 4 GB total uncompressed cap (zip-bomb guard)
_CHUNK = 1024 * 1024
_TASK_OWNER_TTL = 86400 # 24h — long enough to outlive any upload's polling
def _remember_task_owner(task_id: str, user_id: int) -> None:
"""Record which user a pollable task belongs to, so the status endpoint can
refuse to surface another user's task result (the Celery task id is the only
thing the client presents). Best-effort: Redis hiccups must not fail uploads."""
if not task_id:
return
try:
import redis as redis_lib
redis_lib.Redis.from_url(settings.redis_url).set(
f"upload_task_owner:{task_id}", user_id, ex=_TASK_OWNER_TTL
)
except Exception:
pass
def _task_owner(task_id: str) -> int | None:
try:
import redis as redis_lib
v = redis_lib.Redis.from_url(settings.redis_url).get(f"upload_task_owner:{task_id}")
if v is None:
return None
return int(v.decode() if isinstance(v, (bytes, bytearray)) else v)
except Exception:
return None
def _safe_name(filename: str) -> str:
"""Reduce an uploaded filename to a safe basename — no path traversal."""
@@ -119,6 +147,7 @@ async def upload_activity(
# Queue processing
task = process_activity_file.delay(str(dest), current_user.id, suffix[1:])
_remember_task_owner(task.id, current_user.id)
return {"task_id": task.id, "status": "queued", "filename": file.filename}
@@ -180,6 +209,7 @@ async def upload_garmin_export(
# Queue health/wellness data extraction
health_task = process_garmin_health_zip.delay(str(dest), current_user.id)
_remember_task_owner(health_task.id, current_user.id)
return {
"status": "queued",
@@ -247,6 +277,7 @@ async def upload_strava_export(
# Preview only — classify new/duplicate without writing. The extracted
# files are kept so the confirm step can import them without re-uploading.
task = analyze_strava_export.delay([str(p) for p in files], current_user.id)
_remember_task_owner(task.id, current_user.id)
return {"status": "analyzing", "task_id": task.id, "token": dest.stem,
"activity_files": len(files)}
@@ -255,10 +286,12 @@ async def upload_strava_export(
prefer_existing=True).id
for p in files
]
polled = task_ids[-1] if task_ids else None
_remember_task_owner(polled, current_user.id)
return {
"status": "queued",
"activity_tasks": len(task_ids),
"task_id": task_ids[-1] if task_ids else None,
"task_id": polled,
}
@@ -294,6 +327,13 @@ async def check_task_status(
current_user: User = Depends(get_current_user),
):
"""Check the status of an upload processing task."""
# A task result can carry the owner's activity data, so don't surface another
# user's task. We fail closed only on a positive owner mismatch; a missing
# record (Redis down / TTL expired) stays permissive so polling never breaks.
owner = _task_owner(task_id)
if owner is not None and owner != current_user.id:
raise HTTPException(status_code=404, detail="Task not found")
from app.workers.celery_app import celery_app
result = celery_app.AsyncResult(task_id)
return {