use axum::extract::Extension; use axum::http::StatusCode; use axum::Json; use mongodb::bson::doc; use serde::Deserialize; use compliance_core::models::notification::CveNotification; use super::dto::{AgentExt, ApiResponse}; /// GET /api/v1/notifications — List CVE notifications (newest first) #[tracing::instrument(skip_all)] pub async fn list_notifications( Extension(agent): AgentExt, axum::extract::Query(params): axum::extract::Query, ) -> Result>>, StatusCode> { let mut filter = doc! {}; // Filter by status (default: show new + read, exclude dismissed) match params.status.as_deref() { Some("all") => {} Some(s) => { filter.insert("status", s); } None => { filter.insert("status", doc! { "$in": ["new", "read"] }); } } // Filter by severity if let Some(ref sev) = params.severity { filter.insert("severity", sev.as_str()); } // Filter by repo if let Some(ref repo_id) = params.repo_id { filter.insert("repo_id", repo_id.as_str()); } let page = params.page.unwrap_or(1).max(1); let limit = params.limit.unwrap_or(50).min(200); let skip = (page - 1) * limit as u64; let total = agent .db .cve_notifications() .count_documents(filter.clone()) .await .unwrap_or(0); let notifications: Vec = match agent .db .cve_notifications() .find(filter) .sort(doc! { "created_at": -1 }) .skip(skip) .limit(limit) .await { Ok(cursor) => { use futures_util::StreamExt; let mut items = Vec::new(); let mut cursor = cursor; while let Some(Ok(n)) = cursor.next().await { items.push(n); } items } Err(e) => { tracing::error!("Failed to list notifications: {e}"); return Err(StatusCode::INTERNAL_SERVER_ERROR); } }; Ok(Json(ApiResponse { data: notifications, total: Some(total), page: Some(page), })) } /// GET /api/v1/notifications/count — Count of unread notifications #[tracing::instrument(skip_all)] pub async fn notification_count( Extension(agent): AgentExt, ) -> Result, StatusCode> { let count = agent .db .cve_notifications() .count_documents(doc! { "status": "new" }) .await .unwrap_or(0); Ok(Json(serde_json::json!({ "count": count }))) } /// PATCH /api/v1/notifications/:id/read — Mark a notification as read #[tracing::instrument(skip_all, fields(id = %id))] pub async fn mark_read( Extension(agent): AgentExt, axum::extract::Path(id): axum::extract::Path, ) -> Result, StatusCode> { let oid = mongodb::bson::oid::ObjectId::parse_str(&id).map_err(|_| StatusCode::BAD_REQUEST)?; let result = agent .db .cve_notifications() .update_one( doc! { "_id": oid }, doc! { "$set": { "status": "read", "read_at": mongodb::bson::DateTime::now(), }}, ) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; if result.matched_count == 0 { return Err(StatusCode::NOT_FOUND); } Ok(Json(serde_json::json!({ "status": "read" }))) } /// PATCH /api/v1/notifications/:id/dismiss — Dismiss a notification #[tracing::instrument(skip_all, fields(id = %id))] pub async fn dismiss_notification( Extension(agent): AgentExt, axum::extract::Path(id): axum::extract::Path, ) -> Result, StatusCode> { let oid = mongodb::bson::oid::ObjectId::parse_str(&id).map_err(|_| StatusCode::BAD_REQUEST)?; let result = agent .db .cve_notifications() .update_one( doc! { "_id": oid }, doc! { "$set": { "status": "dismissed" } }, ) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; if result.matched_count == 0 { return Err(StatusCode::NOT_FOUND); } Ok(Json(serde_json::json!({ "status": "dismissed" }))) } /// POST /api/v1/notifications/read-all — Mark all new notifications as read #[tracing::instrument(skip_all)] pub async fn mark_all_read( Extension(agent): AgentExt, ) -> Result, StatusCode> { let result = agent .db .cve_notifications() .update_many( doc! { "status": "new" }, doc! { "$set": { "status": "read", "read_at": mongodb::bson::DateTime::now(), }}, ) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json( serde_json::json!({ "updated": result.modified_count }), )) } #[derive(Debug, Deserialize)] pub struct NotificationFilter { pub status: Option, pub severity: Option, pub repo_id: Option, pub page: Option, pub limit: Option, }