Skip to content

Commit 4332361

Browse files
fix(#477): api(bounties): aggregates ignore repo visibility, exposing private-repo activity
`bounty_stats` and `agent_bounty_stats` (`crates/gitlawb-node/src/api/bounties.rs:460-502`) run unfiltered aggregates over all bounties (`crates/gitla... Signed-off-by: chenzeyan54-commits <chenzeyan54-commits@users.noreply.github.com>
1 parent bfc44f9 commit 4332361

1 file changed

Lines changed: 131 additions & 17 deletions

File tree

‎crates/gitlawb-node/src/api/bounties.rs‎

Lines changed: 131 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -457,29 +457,88 @@ pub async fn dispute_bounty(
457457
}
458458

459459
/// GET /api/v1/bounties/stats
460-
pub async fn bounty_stats(State(state): State<AppState>) -> Result<Json<BountyStatsResponse>> {
461-
let open = state.db.count_bounties_by_status("open").await.unwrap_or(0);
462-
let claimed = state
463-
.db
464-
.count_bounties_by_status("claimed")
465-
.await
466-
.unwrap_or(0);
467-
let completed = state
468-
.db
469-
.count_bounties_by_status("completed")
470-
.await
471-
.unwrap_or(0);
460+
pub async fn bounty_stats(
461+
State(state): State<AppState>,
462+
auth: Option<Extension<AuthenticatedDid>>,
463+
) -> Result<Json<BountyStatsResponse>> {
464+
let caller = auth.as_ref().map(|e| e.0 .0.as_str());
465+
let mut memo = std::collections::HashMap::new();
466+
let mut open = 0i64;
467+
let mut claimed = 0i64;
468+
let mut completed = 0i64;
469+
let mut agent_earnings: std::collections::HashMap<String, (i64, i64)> = std::collections::HashMap::new();
470+
471+
let page_size = 200i64;
472+
let mut cursor: Option<(String, String)> = None;
473+
474+
loop {
475+
let after = cursor.as_ref().map(|(ts, id)| (ts.as_str(), id.as_str()));
476+
let bounties = state
477+
.db
478+
.list_bounties(
479+
None,
480+
None,
481+
None,
482+
page_size,
483+
after.map(|(ts, _)| ts),
484+
after.map(|(_, id)| id),
485+
)
486+
.await?;
487+
if bounties.is_empty() {
488+
break;
489+
}
472490

473-
let leaders = state.db.bounty_leaderboard(10).await.unwrap_or_default();
474-
let leaderboard = leaders
491+
let last = bounties.last().unwrap();
492+
for b in &bounties {
493+
let key = format!("{}/{}", b.repo_owner, b.repo_name);
494+
let admitted = if let Some(&result) = memo.get(&key) {
495+
result
496+
} else {
497+
let result = crate::api::authorize_repo_read(
498+
&state,
499+
&b.repo_owner,
500+
&b.repo_name,
501+
caller,
502+
"/",
503+
)
504+
.await
505+
.is_ok();
506+
memo.insert(key, result);
507+
result
508+
};
509+
510+
if admitted {
511+
match b.status.as_str() {
512+
"open" => open += 1,
513+
"claimed" => claimed += 1,
514+
"completed" => {
515+
completed += 1;
516+
if let Some(ref claimant) = b.claimant_did {
517+
let entry = agent_earnings.entry(claimant.clone()).or_insert((0, 0));
518+
entry.0 += 1;
519+
entry.1 += b.amount;
520+
}
521+
}
522+
_ => {}
523+
}
524+
}
525+
}
526+
527+
cursor = Some((last.created_at.clone(), last.id.clone()));
528+
}
529+
530+
let mut leaderboard: Vec<AgentBountyEntry> = agent_earnings
475531
.into_iter()
476-
.map(|(did, cnt, total)| AgentBountyEntry {
532+
.map(|(did, (cnt, total))| AgentBountyEntry {
477533
did,
478534
completed: cnt,
479535
total_earned: total,
480536
})
481537
.collect();
482538

539+
leaderboard.sort_by(|a, b| b.total_earned.cmp(&a.total_earned).then_with(|| b.completed.cmp(&a.completed)));
540+
leaderboard.truncate(10);
541+
483542
Ok(Json(BountyStatsResponse {
484543
open,
485544
claimed,
@@ -492,11 +551,66 @@ pub async fn bounty_stats(State(state): State<AppState>) -> Result<Json<BountySt
492551
pub async fn agent_bounty_stats(
493552
State(state): State<AppState>,
494553
Path(did): Path<String>,
554+
auth: Option<Extension<AuthenticatedDid>>,
495555
) -> Result<Json<serde_json::Value>> {
496-
let (count, total) = state.db.agent_bounty_stats(&did).await.unwrap_or((0, 0));
556+
let caller = auth.as_ref().map(|e| e.0 .0.as_str());
557+
let mut memo = std::collections::HashMap::new();
558+
let mut count = 0i64;
559+
let mut total = 0i64;
560+
561+
let page_size = 200i64;
562+
let mut cursor: Option<(String, String)> = None;
563+
564+
loop {
565+
let after = cursor.as_ref().map(|(ts, id)| (ts.as_str(), id.as_str()));
566+
let bounties = state
567+
.db
568+
.list_bounties(
569+
None,
570+
None,
571+
Some("completed"),
572+
page_size,
573+
after.map(|(ts, _)| ts),
574+
after.map(|(_, id)| id),
575+
)
576+
.await?;
577+
if bounties.is_empty() {
578+
break;
579+
}
580+
581+
let last = bounties.last().unwrap();
582+
for b in &bounties {
583+
if b.claimant_did.as_deref().is_some_and(|c| crate::api::did_matches(&did, c)) {
584+
let key = format!("{}/{}", b.repo_owner, b.repo_name);
585+
let admitted = if let Some(&result) = memo.get(&key) {
586+
result
587+
} else {
588+
let result = crate::api::authorize_repo_read(
589+
&state,
590+
&b.repo_owner,
591+
&b.repo_name,
592+
caller,
593+
"/",
594+
)
595+
.await
596+
.is_ok();
597+
memo.insert(key, result);
598+
result
599+
};
600+
601+
if admitted {
602+
count += 1;
603+
total += b.amount;
604+
}
605+
}
606+
}
607+
608+
cursor = Some((last.created_at.clone(), last.id.clone()));
609+
}
610+
497611
Ok(Json(serde_json::json!({
498612
"did": did,
499613
"completed_bounties": count,
500614
"total_earned": total,
501615
})))
502-
}
616+
}

0 commit comments

Comments
 (0)