use std::path::Path;
use std::time::Duration;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use super::error::CliError;
use super::pds::{PdsClient, PdsError};
use super::session::SessionFile;
const LIST_AUDIT_LOG_LXM: &str = "tools.cairn.admin.listAuditLog";
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AuditEntry {
pub id: i64,
#[serde(rename = "createdAt")]
pub created_at: String,
pub action: String,
#[serde(rename = "actorDid")]
pub actor_did: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub target: Option<String>,
#[serde(rename = "targetCid", skip_serializing_if = "Option::is_none", default)]
pub target_cid: Option<String>,
pub outcome: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub reason: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct AuditListResponse {
pub entries: Vec<AuditEntry>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub cursor: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct AuditListInput {
pub actor: Option<String>,
pub action: Option<String>,
pub outcome: Option<String>,
pub since: Option<String>,
pub until: Option<String>,
pub limit: Option<i64>,
pub cursor: Option<String>,
pub cairn_server_override: Option<String>,
}
pub async fn list(
session: &mut SessionFile,
session_path: &Path,
input: AuditListInput,
) -> Result<AuditListResponse, CliError> {
let cairn_server = input
.cairn_server_override
.as_deref()
.unwrap_or(&session.cairn_server_url)
.trim_end_matches('/')
.to_string();
let pds = PdsClient::new(&session.pds_url)?;
let token = acquire_service_auth(&pds, session, session_path, LIST_AUDIT_LOG_LXM).await?;
let url = format!("{cairn_server}/xrpc/{LIST_AUDIT_LOG_LXM}");
let limit_owned = input.limit.map(|n| n.to_string());
let mut query: Vec<(&str, &str)> = Vec::new();
if let Some(a) = &input.actor {
query.push(("actor", a.as_str()));
}
if let Some(a) = &input.action {
query.push(("action", a.as_str()));
}
if let Some(o) = &input.outcome {
query.push(("outcome", o.as_str()));
}
if let Some(s) = &input.since {
query.push(("since", s.as_str()));
}
if let Some(u) = &input.until {
query.push(("until", u.as_str()));
}
if let Some(n) = &limit_owned {
query.push(("limit", n.as_str()));
}
if let Some(c) = &input.cursor {
query.push(("cursor", c.as_str()));
}
let client = Client::builder()
.timeout(Duration::from_secs(30))
.build()
.expect("reqwest build");
let resp = client
.get(&url)
.bearer_auth(&token)
.query(&query)
.send()
.await
.map_err(|source| CliError::Http {
url: url.clone(),
source,
})?;
if !resp.status().is_success() {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
return Err(CliError::CairnStatus { url, status, body });
}
let bytes = resp.bytes().await.map_err(|source| CliError::Http {
url: url.clone(),
source,
})?;
serde_json::from_slice::<AuditListResponse>(&bytes)
.map_err(|source| CliError::MalformedResponse { url, source })
}
pub fn format_list_human(resp: &AuditListResponse) -> String {
use std::fmt::Write;
if resp.entries.is_empty() {
let mut s = "(no audit entries)".to_string();
if let Some(c) = &resp.cursor {
let _ = write!(s, "\nnext cursor: {c}");
}
return s;
}
let id_w = resp
.entries
.iter()
.map(|e| e.id.to_string().len())
.max()
.unwrap_or(2)
.max(2);
let action_w = resp
.entries
.iter()
.map(|e| e.action.len().min(28))
.max()
.unwrap_or(6)
.max(6);
let actor_w = resp
.entries
.iter()
.map(|e| e.actor_did.len().min(40))
.max()
.unwrap_or(8)
.max(8);
let mut s = String::new();
let _ = writeln!(
s,
"{:>id_w$} {:<24} {:<action_w$} {:<actor_w$} {:<7}",
"ID",
"CREATED_AT",
"ACTION",
"ACTOR_DID",
"OUTCOME",
id_w = id_w,
action_w = action_w,
actor_w = actor_w,
);
for e in &resp.entries {
let action = truncate(&e.action, 28);
let actor = truncate(&e.actor_did, 40);
let _ = writeln!(
s,
"{:>id_w$} {:<24} {:<action_w$} {:<actor_w$} {:<7}",
e.id,
e.created_at,
action,
actor,
e.outcome,
id_w = id_w,
action_w = action_w,
actor_w = actor_w,
);
}
if let Some(c) = &resp.cursor {
let _ = write!(s, "next cursor: {c}");
} else if s.ends_with('\n') {
s.pop();
}
s
}
pub fn format_list_json(resp: &AuditListResponse) -> String {
serde_json::to_string_pretty(resp).expect("AuditListResponse serializes")
}
fn truncate(s: &str, max: usize) -> String {
if s.chars().count() <= max {
return s.to_string();
}
let head: String = s.chars().take(max.saturating_sub(1)).collect();
format!("{head}…")
}
async fn acquire_service_auth(
pds: &PdsClient,
session: &mut SessionFile,
session_path: &Path,
lxm: &str,
) -> Result<String, CliError> {
match pds
.get_service_auth(&session.access_jwt, &session.cairn_service_did, lxm)
.await
{
Ok(t) => Ok(t),
Err(PdsError::Unauthorized {
context: "getServiceAuth",
..
}) => {
let refreshed = pds.refresh_session(&session.refresh_jwt).await?;
session.access_jwt = refreshed.access_jwt;
session.refresh_jwt = refreshed.refresh_jwt;
session.save(session_path)?;
Ok(pds
.get_service_auth(&session.access_jwt, &session.cairn_service_did, lxm)
.await?)
}
Err(other) => Err(other.into()),
}
}