use ratatui::layout::Constraint;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::Span;
use ratatui::widgets::{Cell, Row, Table};
use vta_sdk::prelude::*;
use vta_sdk::protocols::audit_management::list::{AuditEnvelope, ListAuditLogsResultBody};
use crate::display::{NameBook, book_from_acl, named_did_cell};
use crate::render::{is_full_display, print_full_entry_owned, print_full_list_title, print_widget};
pub async fn cmd_list_audit_logs(
client: &VtaClient,
params: &ListAuditLogsBody,
) -> Result<(), Box<dyn std::error::Error>> {
let result = client.list_audit_logs(params).await?;
if result.entries.is_empty() {
println!(" No audit log entries found.");
return Ok(());
}
let mut book = NameBook::new();
if let Ok(acl) = client.list_acl(None).await {
book_from_acl(&mut book, &acl.entries);
}
if is_full_display() {
print_full_list_title("Audit Log", result.entries.len());
for entry in &result.entries {
let ts = format_recorded_at(&entry.recorded_at);
let actor = entry.actor.as_deref().unwrap_or("—");
let target = entry.target.as_deref().unwrap_or("—");
let channel = detail_str(entry, "channel").unwrap_or_else(|| "—".to_string());
let context = entry.context_id.as_deref().unwrap_or("—");
let mut fields = vec![
("ID", entry.event_id.clone()),
("Timestamp", ts),
("Action", entry.action.clone()),
];
if let Some(name) = book.name_of(actor) {
fields.push(("Actor", name));
}
fields.push(("Actor DID", actor.to_string()));
fields.push(("Resource", target.to_string()));
fields.push(("Channel", channel));
fields.push(("Context", context.to_string()));
fields.push((
"Outcome",
entry.outcome.clone().unwrap_or_else(|| "—".to_string()),
));
if let Some(reason) = detail_str(entry, "reason") {
fields.push(("Reason", reason));
}
print_full_entry_owned(&fields);
}
print_cursor_footer(&result);
return Ok(());
}
println!("\n \x1b[1mAudit Log\x1b[0m\n");
let rows: Vec<Row> = result
.entries
.iter()
.map(|entry| {
let ts = format_recorded_at(&entry.recorded_at);
let outcome = entry.outcome.as_deref().unwrap_or("—");
let outcome_style = if outcome == "success" {
Style::default().fg(Color::Green)
} else if outcome.starts_with("denied") {
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Yellow)
};
let action_style = if entry.action.starts_with("auth.") {
Style::default().fg(Color::Cyan)
} else if entry.action.starts_with("key.") || entry.action.starts_with("seed.") {
Style::default().fg(Color::Magenta)
} else if entry.action.starts_with("acl.") {
Style::default().fg(Color::Yellow)
} else if entry.action.starts_with("session.") {
Style::default().fg(Color::Blue)
} else {
Style::default()
};
let resource_display = entry.target.as_deref().unwrap_or("\u{2014}");
Row::new(vec![
Cell::from(Span::styled(ts, Style::default().fg(Color::DarkGray))),
Cell::from(Span::styled(entry.action.clone(), action_style)),
named_did_cell(&book, entry.actor.as_deref().unwrap_or("\u{2014}")),
Cell::from(resource_display.to_string()),
Cell::from(Span::styled(outcome.to_string(), outcome_style)),
])
})
.collect();
let header = Row::new(vec![
Cell::from(Span::styled(
"Timestamp",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)),
Cell::from(Span::styled(
"Action",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)),
Cell::from(Span::styled(
"Actor",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)),
Cell::from(Span::styled(
"Resource",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)),
Cell::from(Span::styled(
"Outcome",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)),
]);
let row_count = result.entries.len();
let table = Table::new(
rows,
[
Constraint::Length(25), Constraint::Length(22), Constraint::Min(30), Constraint::Min(16), Constraint::Length(20), ],
)
.header(header)
.column_spacing(2);
let height = row_count as u16 + 2; print_widget(table, height);
print_cursor_footer(&result);
Ok(())
}
fn format_recorded_at(recorded_at: &str) -> String {
match chrono::DateTime::parse_from_rfc3339(recorded_at) {
Ok(dt) => crate::duration::format_local_datetime(dt.with_timezone(&chrono::Utc)),
Err(_) => recorded_at.to_string(),
}
}
fn detail_str(entry: &AuditEnvelope, key: &str) -> Option<String> {
entry
.detail
.get(key)
.and_then(|v| v.as_str())
.map(str::to_owned)
}
fn print_cursor_footer(result: &ListAuditLogsResultBody) {
if let Some(cursor) = &result.cursor {
println!(
"\n \x1b[2mMore entries \u{2014} fetch the next page with --cursor {cursor}\x1b[0m"
);
println!(" \x1b[2m(keep the same filters; changing them invalidates the cursor)\x1b[0m");
}
}
pub async fn cmd_get_retention(client: &VtaClient) -> Result<(), Box<dyn std::error::Error>> {
let result = client.get_audit_retention().await?;
println!("\n \x1b[1mAudit Retention\x1b[0m");
println!(
" Retention period: \x1b[36m{}\x1b[0m days",
result.retention_days
);
println!();
Ok(())
}
pub async fn cmd_update_retention(
client: &VtaClient,
days: u32,
) -> Result<(), Box<dyn std::error::Error>> {
let result = client.update_audit_retention(days).await?;
println!(
"\n \x1b[32m\u{2713}\x1b[0m Audit retention updated to \x1b[36m{}\x1b[0m days",
result.retention_days
);
println!();
Ok(())
}