use http::{Response, StatusCode};
use serde::Deserialize;
use serde_json::json;
use crate::app_state::AppState;
use crate::hateoas::{api_root_links, collection_links, csaf_links, self_link};
use crate::router::{
Body, json_response, parse_json_body, parse_query, path_param, problem_response,
};
const DEFAULT_PER_PAGE: usize = 20;
const MAX_PER_PAGE: usize = 100;
#[derive(Debug, Deserialize)]
struct PaginationQuery {
#[serde(default = "default_page")]
page: String,
#[serde(default = "default_per_page")]
per_page: String,
}
fn default_page() -> String {
"1".to_owned()
}
fn default_per_page() -> String {
DEFAULT_PER_PAGE.to_string()
}
fn parse_pagination(query: &PaginationQuery) -> (usize, usize) {
let page = query.page.parse::<usize>().unwrap_or(1).max(1);
let per_page = query
.per_page
.parse::<usize>()
.unwrap_or(DEFAULT_PER_PAGE)
.clamp(1, MAX_PER_PAGE);
(page, per_page)
}
pub async fn api_root(
_state: AppState,
_parts: http::request::Parts,
_params: Vec<(String, String)>,
) -> Response<Body> {
let body = json!({
"data": {
"name": "CSAF API",
"version": env!("CARGO_PKG_VERSION"),
},
"_links": api_root_links(),
});
json_response(StatusCode::OK, &body)
}
pub async fn list_csaf(
state: AppState,
parts: http::request::Parts,
_params: Vec<(String, String)>,
) -> Response<Body> {
let query = match parse_query::<PaginationQuery>(&parts.uri) {
Ok(q) => q,
Err(e) => {
return problem_response(
StatusCode::BAD_REQUEST,
"https://ndaal.eu/csaf/errors/bad-request",
"Bad Request",
&e,
);
},
};
let (page, per_page) = parse_pagination(&query);
let offset = (page - 1) * per_page;
let total = match state.csaf_storage().count_documents() {
Ok(c) => c,
Err(e) => {
tracing::error!("Failed to count documents: {e}");
return problem_response(
StatusCode::INTERNAL_SERVER_ERROR,
"https://ndaal.eu/csaf/errors/storage",
"Storage Error",
"Failed to count documents",
);
},
};
let items = match state.csaf_storage().list_meta(per_page, offset) {
Ok(list) => list,
Err(e) => {
tracing::error!("Failed to list documents: {e}");
return problem_response(
StatusCode::INTERNAL_SERVER_ERROR,
"https://ndaal.eu/csaf/errors/storage",
"Storage Error",
"Failed to list documents",
);
},
};
let items_json: Vec<serde_json::Value> = items
.iter()
.map(|meta| {
let mut val = serde_json::to_value(meta).unwrap_or_default();
val["_links"] = csaf_links(&meta.tracking_id);
val
})
.collect();
let body = json!({
"data": items_json,
"total": total,
"page": page,
"per_page": per_page,
"_links": collection_links("/api/v1/csaf", page, per_page, total),
});
json_response(StatusCode::OK, &body)
}
pub async fn create_csaf(
state: AppState,
parts: http::request::Parts,
_params: Vec<(String, String)>,
) -> Response<Body> {
let doc: csaf_models::csaf_document::CsafDocument = match parse_json_body(&parts) {
Ok(d) => d,
Err(e) => {
return problem_response(
StatusCode::BAD_REQUEST,
"https://ndaal.eu/csaf/errors/invalid-json",
"Invalid JSON",
&e,
);
},
};
let validation_errors = csaf_core::validation::validate(&doc);
let hard_errors: Vec<_> = validation_errors
.iter()
.filter(|e| e.severity == csaf_core::validation::Severity::Error)
.collect();
if !hard_errors.is_empty() {
let details: Vec<serde_json::Value> = hard_errors
.iter()
.map(|e| json!({"path": e.path, "message": e.message}))
.collect();
let body = json!({
"type": "https://ndaal.eu/csaf/errors/validation",
"title": "Validation Failed",
"status": 422,
"detail": format!("{} validation error(s) found", details.len()),
"errors": details,
});
return json_response(StatusCode::UNPROCESSABLE_ENTITY, &body);
}
let tracking_id = doc.tracking_id().to_owned();
match state.csaf_storage().document_exists(&tracking_id) {
Ok(true) => {
return problem_response(
StatusCode::CONFLICT,
"https://ndaal.eu/csaf/errors/duplicate",
"Duplicate Document",
&format!("Document '{tracking_id}' already exists"),
);
},
Err(e) => {
tracing::error!("Failed to check document existence: {e}");
return problem_response(
StatusCode::INTERNAL_SERVER_ERROR,
"https://ndaal.eu/csaf/errors/storage",
"Storage Error",
"Failed to check document existence",
);
},
Ok(false) => {},
}
if let Err(e) = state.csaf_storage().put_document(&doc) {
tracing::error!("Failed to store document: {e}");
return problem_response(
StatusCode::INTERNAL_SERVER_ERROR,
"https://ndaal.eu/csaf/errors/storage",
"Storage Error",
"Failed to store document",
);
}
let audit_result = state
.db_pool()
.with_conn(|conn| csaf_models::audit_log::record(conn, "create", &tracking_id, None, None));
if let Err(e) = audit_result {
tracing::warn!("Failed to record audit log: {e}");
}
let body = json!({
"tracking_id": tracking_id,
"message": "Document created successfully",
"_links": csaf_links(&tracking_id),
});
Response::builder()
.status(StatusCode::CREATED)
.header("content-type", "application/json; charset=utf-8")
.header("location", format!("/api/v1/csaf/{tracking_id}"))
.body(Body::from(serde_json::to_string(&body).unwrap_or_default()))
.unwrap_or_default()
}
pub async fn get_csaf(
state: AppState,
_parts: http::request::Parts,
params: Vec<(String, String)>,
) -> Response<Body> {
let tracking_id = match path_param(¶ms, "id") {
Some(id) => id,
None => {
return problem_response(
StatusCode::BAD_REQUEST,
"https://ndaal.eu/csaf/errors/bad-request",
"Bad Request",
"Missing document ID",
);
},
};
match state.csaf_storage().get_document_json(&tracking_id) {
Ok(Some(doc_json)) => {
let body = json!({
"document": doc_json,
"_links": csaf_links(&tracking_id),
});
json_response(StatusCode::OK, &body)
},
Ok(None) => problem_response(
StatusCode::NOT_FOUND,
"https://ndaal.eu/csaf/errors/not-found",
"Not Found",
&format!("Document '{tracking_id}' not found"),
),
Err(e) => {
tracing::error!("Failed to get document: {e}");
problem_response(
StatusCode::INTERNAL_SERVER_ERROR,
"https://ndaal.eu/csaf/errors/storage",
"Storage Error",
"Failed to retrieve document",
)
},
}
}
pub async fn update_csaf(
state: AppState,
parts: http::request::Parts,
params: Vec<(String, String)>,
) -> Response<Body> {
let tracking_id = match path_param(¶ms, "id") {
Some(id) => id,
None => {
return problem_response(
StatusCode::BAD_REQUEST,
"https://ndaal.eu/csaf/errors/bad-request",
"Bad Request",
"Missing document ID",
);
},
};
let doc: csaf_models::csaf_document::CsafDocument = match parse_json_body(&parts) {
Ok(d) => d,
Err(e) => {
return problem_response(
StatusCode::BAD_REQUEST,
"https://ndaal.eu/csaf/errors/invalid-json",
"Invalid JSON",
&e,
);
},
};
if doc.tracking_id() != tracking_id {
return problem_response(
StatusCode::BAD_REQUEST,
"https://ndaal.eu/csaf/errors/id-mismatch",
"ID Mismatch",
"URL tracking ID does not match document tracking ID",
);
}
let validation_errors = csaf_core::validation::validate(&doc);
let hard_errors: Vec<_> = validation_errors
.iter()
.filter(|e| e.severity == csaf_core::validation::Severity::Error)
.collect();
if !hard_errors.is_empty() {
let details: Vec<serde_json::Value> = hard_errors
.iter()
.map(|e| json!({"path": e.path, "message": e.message}))
.collect();
let body = json!({
"type": "https://ndaal.eu/csaf/errors/validation",
"title": "Validation Failed",
"status": 422,
"detail": format!("{} validation error(s) found", details.len()),
"errors": details,
});
return json_response(StatusCode::UNPROCESSABLE_ENTITY, &body);
}
match state.csaf_storage().document_exists(&tracking_id) {
Ok(false) => {
return problem_response(
StatusCode::NOT_FOUND,
"https://ndaal.eu/csaf/errors/not-found",
"Not Found",
&format!("Document '{tracking_id}' not found"),
);
},
Err(e) => {
tracing::error!("Failed to check document existence: {e}");
return problem_response(
StatusCode::INTERNAL_SERVER_ERROR,
"https://ndaal.eu/csaf/errors/storage",
"Storage Error",
"Failed to check document existence",
);
},
Ok(true) => {},
}
if let Err(e) = state.csaf_storage().put_document(&doc) {
tracing::error!("Failed to update document: {e}");
return problem_response(
StatusCode::INTERNAL_SERVER_ERROR,
"https://ndaal.eu/csaf/errors/storage",
"Storage Error",
"Failed to update document",
);
}
let audit_result = state
.db_pool()
.with_conn(|conn| csaf_models::audit_log::record(conn, "update", &tracking_id, None, None));
if let Err(e) = audit_result {
tracing::warn!("Failed to record audit log: {e}");
}
let body = json!({
"tracking_id": tracking_id,
"message": "Document updated successfully",
"_links": csaf_links(&tracking_id),
});
json_response(StatusCode::OK, &body)
}
pub async fn delete_csaf(
state: AppState,
_parts: http::request::Parts,
params: Vec<(String, String)>,
) -> Response<Body> {
let tracking_id = match path_param(¶ms, "id") {
Some(id) => id,
None => {
return problem_response(
StatusCode::BAD_REQUEST,
"https://ndaal.eu/csaf/errors/bad-request",
"Bad Request",
"Missing document ID",
);
},
};
match state.csaf_storage().delete_document(&tracking_id) {
Ok(true) => {
let audit_result = state.db_pool().with_conn(|conn| {
csaf_models::audit_log::record(conn, "delete", &tracking_id, None, None)
});
if let Err(e) = audit_result {
tracing::warn!("Failed to record audit log: {e}");
}
Response::builder()
.status(StatusCode::NO_CONTENT)
.body(Body::default())
.unwrap_or_default()
},
Ok(false) => problem_response(
StatusCode::NOT_FOUND,
"https://ndaal.eu/csaf/errors/not-found",
"Not Found",
&format!("Document '{tracking_id}' not found"),
),
Err(e) => {
tracing::error!("Failed to delete document: {e}");
problem_response(
StatusCode::INTERNAL_SERVER_ERROR,
"https://ndaal.eu/csaf/errors/storage",
"Storage Error",
"Failed to delete document",
)
},
}
}
pub async fn validate_csaf(
state: AppState,
_parts: http::request::Parts,
params: Vec<(String, String)>,
) -> Response<Body> {
let tracking_id = match path_param(¶ms, "id") {
Some(id) => id,
None => {
return problem_response(
StatusCode::BAD_REQUEST,
"https://ndaal.eu/csaf/errors/bad-request",
"Bad Request",
"Missing document ID",
);
},
};
let doc = match state.csaf_storage().get_document(&tracking_id) {
Ok(Some(d)) => d,
Ok(None) => {
return problem_response(
StatusCode::NOT_FOUND,
"https://ndaal.eu/csaf/errors/not-found",
"Not Found",
&format!("Document '{tracking_id}' not found"),
);
},
Err(e) => {
tracing::error!("Failed to get document for validation: {e}");
return problem_response(
StatusCode::INTERNAL_SERVER_ERROR,
"https://ndaal.eu/csaf/errors/storage",
"Storage Error",
"Failed to retrieve document",
);
},
};
let errors = csaf_core::validation::validate(&doc);
let is_valid = errors
.iter()
.all(|e| e.severity != csaf_core::validation::Severity::Error);
let findings: Vec<serde_json::Value> = errors
.iter()
.map(|e| {
json!({
"path": e.path,
"severity": match e.severity {
csaf_core::validation::Severity::Error => "error",
csaf_core::validation::Severity::Warning => "warning",
},
"message": e.message,
})
})
.collect();
let body = json!({
"tracking_id": tracking_id,
"valid": is_valid,
"error_count": errors.iter().filter(|e| e.severity == csaf_core::validation::Severity::Error).count(),
"warning_count": errors.iter().filter(|e| e.severity == csaf_core::validation::Severity::Warning).count(),
"findings": findings,
"_links": json!({
"self": self_link(&format!("/api/v1/csaf/{tracking_id}/validate")),
"document": self_link(&format!("/api/v1/csaf/{tracking_id}")),
}),
});
json_response(StatusCode::OK, &body)
}