csaf-crud 0.3.4

CSAF 2.0 / 2.1 advisory CRUD server with HATEOAS JSON API and HTML UI (TLS 1.3, HTTP/1.1 + HTTP/2 + HTTP/3)
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Pierre Gronau, ndaal in Cologne

//! Audit log API handler with paginated listing.

use http::{Response, StatusCode};
use serde::Deserialize;
use serde_json::json;

use crate::app_state::AppState;
use crate::hateoas::{collection_links, self_link};
use crate::router::{Body, json_response, parse_query, problem_response};

/// Default number of items per page.
const DEFAULT_PER_PAGE: usize = 20;

/// Maximum allowed items per page.
const MAX_PER_PAGE: usize = 100;

/// Query parameters for the audit log endpoint.
#[derive(Debug, Deserialize)]
struct AuditLogQuery {
    /// Filter by tracking ID.
    #[serde(default)]
    tracking_id: Option<String>,

    /// Page number (1-based).
    #[serde(default = "default_page")]
    page: String,

    /// Items per page.
    #[serde(default = "default_per_page")]
    per_page: String,
}

/// Return the default page number as a string.
fn default_page() -> String {
    "1".to_owned()
}

/// Return the default per-page count as a string.
fn default_per_page() -> String {
    DEFAULT_PER_PAGE.to_string()
}

/// `GET /api/v1/audit-log` -- List audit log entries with optional filtering.
pub async fn list_audit_log(
    state: AppState,
    parts: http::request::Parts,
    _params: Vec<(String, String)>,
) -> Response<Body> {
    let query = match parse_query::<AuditLogQuery>(&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 = 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);
    let offset = (page - 1) * per_page;

    let tracking_filter = query.tracking_id.as_deref();

    let result = state.db_pool().with_conn(|conn| {
        let total = csaf_models::audit_log::count(conn, tracking_filter)?;
        let entries = csaf_models::audit_log::list(conn, tracking_filter, per_page, offset)?;
        Ok((total, entries))
    });

    let (total, entries) = match result {
        Ok(data) => data,
        Err(e) => {
            tracing::error!("Failed to query audit log: {e}");
            return problem_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                "https://ndaal.eu/csaf/errors/database",
                "Database Error",
                "Failed to query audit log",
            );
        },
    };

    let items: Vec<serde_json::Value> = entries
        .iter()
        .map(|entry| {
            let mut val = serde_json::to_value(entry).unwrap_or_default();
            val["_links"] = json!({
                "document": self_link(&format!("/api/v1/csaf/{}", entry.tracking_id)),
            });
            val
        })
        .collect();

    let base_url = if let Some(tid) = tracking_filter {
        format!("/api/v1/audit-log?tracking_id={tid}")
    } else {
        "/api/v1/audit-log".to_owned()
    };

    let body = json!({
        "data": items,
        "total": total,
        "page": page,
        "per_page": per_page,
        "_links": collection_links(&base_url, page, per_page, total),
    });

    json_response(StatusCode::OK, &body)
}