use axum::extract::{Extension, Path, Query}; use axum::http::StatusCode; use axum::Json; use mongodb::bson::doc; use super::dto::*; use compliance_core::models::Finding; #[tracing::instrument(skip_all, fields(repo_id = ?filter.repo_id, severity = ?filter.severity, scan_type = ?filter.scan_type))] pub async fn list_findings( Extension(agent): AgentExt, Query(filter): Query, ) -> ApiResult> { let db = &agent.db; let mut query = doc! {}; if let Some(repo_id) = &filter.repo_id { query.insert("repo_id", repo_id); } if let Some(severity) = &filter.severity { query.insert("severity", severity); } if let Some(scan_type) = &filter.scan_type { query.insert("scan_type", scan_type); } if let Some(status) = &filter.status { query.insert("status", status); } // Text search across title, description, file_path, rule_id if let Some(q) = &filter.q { if !q.is_empty() { let regex = doc! { "$regex": q, "$options": "i" }; query.insert( "$or", mongodb::bson::bson!([ { "title": regex.clone() }, { "description": regex.clone() }, { "file_path": regex.clone() }, { "rule_id": regex }, ]), ); } } // Dynamic sort let sort_field = filter.sort_by.as_deref().unwrap_or("created_at"); let sort_dir: i32 = match filter.sort_order.as_deref() { Some("asc") => 1, _ => -1, }; let sort_doc = doc! { sort_field: sort_dir }; let skip = (filter.page.saturating_sub(1)) * filter.limit as u64; let total = db .findings() .count_documents(query.clone()) .await .unwrap_or(0); let findings = match db .findings() .find(query) .sort(sort_doc) .skip(skip) .limit(filter.limit) .await { Ok(cursor) => collect_cursor_async(cursor).await, Err(e) => { tracing::warn!("Failed to fetch findings: {e}"); Vec::new() } }; Ok(Json(ApiResponse { data: findings, total: Some(total), page: Some(filter.page), })) } #[tracing::instrument(skip_all, fields(finding_id = %id))] pub async fn get_finding( Extension(agent): AgentExt, Path(id): Path, ) -> Result>, StatusCode> { let oid = mongodb::bson::oid::ObjectId::parse_str(&id).map_err(|_| StatusCode::BAD_REQUEST)?; let finding = agent .db .findings() .find_one(doc! { "_id": oid }) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? .ok_or(StatusCode::NOT_FOUND)?; Ok(Json(ApiResponse { data: finding, total: None, page: None, })) } #[tracing::instrument(skip_all, fields(finding_id = %id))] pub async fn update_finding_status( Extension(agent): AgentExt, Path(id): Path, Json(req): Json, ) -> Result, StatusCode> { let oid = mongodb::bson::oid::ObjectId::parse_str(&id).map_err(|_| StatusCode::BAD_REQUEST)?; agent .db .findings() .update_one( doc! { "_id": oid }, doc! { "$set": { "status": &req.status, "updated_at": mongodb::bson::DateTime::now() } }, ) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(serde_json::json!({ "status": "updated" }))) } #[tracing::instrument(skip_all)] pub async fn bulk_update_finding_status( Extension(agent): AgentExt, Json(req): Json, ) -> Result, StatusCode> { let oids: Vec = req .ids .iter() .filter_map(|id| mongodb::bson::oid::ObjectId::parse_str(id).ok()) .collect(); if oids.is_empty() { return Err(StatusCode::BAD_REQUEST); } let result = agent .db .findings() .update_many( doc! { "_id": { "$in": oids } }, doc! { "$set": { "status": &req.status, "updated_at": mongodb::bson::DateTime::now() } }, ) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json( serde_json::json!({ "status": "updated", "modified_count": result.modified_count }), )) } #[tracing::instrument(skip_all)] pub async fn update_finding_feedback( Extension(agent): AgentExt, Path(id): Path, Json(req): Json, ) -> Result, StatusCode> { let oid = mongodb::bson::oid::ObjectId::parse_str(&id).map_err(|_| StatusCode::BAD_REQUEST)?; agent .db .findings() .update_one( doc! { "_id": oid }, doc! { "$set": { "developer_feedback": &req.feedback, "updated_at": mongodb::bson::DateTime::now() } }, ) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(serde_json::json!({ "status": "updated" }))) }