From 92c8a982b8c6089bc5581cff3a7f867b2ad36a5c Mon Sep 17 00:00:00 2001 From: Trezy Date: Thu, 14 May 2026 10:32:54 -0500 Subject: [PATCH] fix: harden backfill cancel idempotency, SQL injection guard, DID retry cap, and row a11y Signed-off-by: Trezy Signed-off-by: Trezy --- src/admin/backfill.rs | 20 ++++++++--- src/profile.rs | 44 +++++++++++++++++-------- web/src/app/dashboard/backfill/page.tsx | 10 +++++- 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/src/admin/backfill.rs b/src/admin/backfill.rs index eaa4e79..d0fc378 100644 --- a/src/admin/backfill.rs +++ b/src/admin/backfill.rs @@ -90,10 +90,19 @@ async fn set_stage(state: &AppState, job_id: &str, stage: &str) { } async fn update_job_counter(state: &AppState, job_id: &str, column: &str, value: i32) { - let sql = adapt_sql( - &format!("UPDATE backfill_jobs SET {column} = ? WHERE id = ?"), - state.db_backend, - ); + let query = match column { + "total_repos" => "UPDATE backfill_jobs SET total_repos = ? WHERE id = ?", + "processed_repos" => "UPDATE backfill_jobs SET processed_repos = ? WHERE id = ?", + "total_records" => "UPDATE backfill_jobs SET total_records = ? WHERE id = ?", + other => { + tracing::error!( + column = other, + "update_job_counter called with unknown column" + ); + return; + } + }; + let sql = adapt_sql(query, state.db_backend); let _ = sqlx::query(&sql) .bind(value) .bind(job_id) @@ -809,6 +818,9 @@ pub(super) async fn cancel_backfill( match row { None => Err(AppError::NotFound("backfill job not found".into())), + Some((ref status,)) if status == "cancelling" || status == "cancelled" => { + Ok(Json(serde_json::json!({ "id": job_id, "status": status }))) + } Some((status,)) if status != "running" => Err(AppError::BadRequest(format!( "job is not running (status: {status})" ))), diff --git a/src/profile.rs b/src/profile.rs index 25bf03b..298ea01 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -161,21 +161,37 @@ pub async fn resolve_did_document( format!("{}/{did}", plc_url.trim_end_matches('/')) }; - let resp = loop { - let r = http - .get(&url) - .send() - .await - .map_err(|e| AppError::Internal(format!("DID resolution failed: {e}")))?; - - if r.status() == reqwest::StatusCode::TOO_MANY_REQUESTS { - let wait = parse_retry_after(r.headers()); - tracing::warn!(did, wait, "rate limited during DID resolution, sleeping"); - tokio::time::sleep(tokio::time::Duration::from_secs(wait)).await; - continue; + let resp = { + let max_retries = 5; + let mut attempts = 0; + loop { + let r = http + .get(&url) + .send() + .await + .map_err(|e| AppError::Internal(format!("DID resolution failed: {e}")))?; + + if r.status() == reqwest::StatusCode::TOO_MANY_REQUESTS { + attempts += 1; + if attempts >= max_retries { + return Err(AppError::Internal(format!( + "DID resolution for {did} rate-limited after {max_retries} retries" + ))); + } + let wait = parse_retry_after(r.headers()); + tracing::warn!( + did, + wait, + attempts, + max_retries, + "rate limited during DID resolution, sleeping" + ); + tokio::time::sleep(tokio::time::Duration::from_secs(wait)).await; + continue; + } + + break r; } - - break r; }; if !resp.status().is_success() { diff --git a/web/src/app/dashboard/backfill/page.tsx b/web/src/app/dashboard/backfill/page.tsx index e602e03..f2ed0b6 100644 --- a/web/src/app/dashboard/backfill/page.tsx +++ b/web/src/app/dashboard/backfill/page.tsx @@ -157,8 +157,16 @@ export default function BackfillPage() { {jobs.map((job) => ( setSelectedJobId(job.id)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setSelectedJobId(job.id); + } + }} > {job.id.slice(0, 8)} -- 2.51.2