179 lines
5.1 KiB
Rust
179 lines
5.1 KiB
Rust
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<NotificationFilter>,
|
|
) -> Result<Json<ApiResponse<Vec<CveNotification>>>, 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<CveNotification> = 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<Json<serde_json::Value>, 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<String>,
|
|
) -> Result<Json<serde_json::Value>, 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<String>,
|
|
) -> Result<Json<serde_json::Value>, 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<Json<serde_json::Value>, 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<String>,
|
|
pub severity: Option<String>,
|
|
pub repo_id: Option<String>,
|
|
pub page: Option<u64>,
|
|
pub limit: Option<i64>,
|
|
}
|