Skip to main content

clickup_cli/commands/
audit_log.rs

1use crate::client::ClickUpClient;
2use crate::commands::auth::resolve_token;
3use crate::commands::workspace::resolve_workspace;
4use crate::error::CliError;
5use crate::output::OutputConfig;
6use crate::Cli;
7use clap::Subcommand;
8
9#[derive(Subcommand)]
10pub enum AuditLogCommands {
11    /// Query audit logs (Enterprise only, v3)
12    Query {
13        /// Required scope of the query. ClickUp's documented values: WORKSPACE, TEAMS, USERS.
14        #[arg(long)]
15        applicability: String,
16        /// Filter by event type (e.g. AUTH, HIERARCHY, USER, CUSTOM_FIELDS, AGENT, OTHER)
17        #[arg(long = "event-type")]
18        event_type: Option<String>,
19        /// Filter by event status (e.g. SUCCESS, FAILURE)
20        #[arg(long = "event-status")]
21        event_status: Option<String>,
22        /// Filter by user ID (repeat for multiple)
23        #[arg(long = "user-id")]
24        user_id: Vec<String>,
25        /// Filter by user email (repeat for multiple)
26        #[arg(long = "user-email")]
27        user_email: Vec<String>,
28        /// Start time (Unix timestamp in milliseconds), maps to filter.startTime
29        #[arg(long)]
30        start_time: Option<i64>,
31        /// End time (Unix timestamp in milliseconds), maps to filter.endTime
32        #[arg(long)]
33        end_time: Option<i64>,
34        /// Max rows per page (pagination.pageRows)
35        #[arg(long)]
36        page_rows: Option<i64>,
37        /// Cursor timestamp (pagination.pageTimestamp)
38        #[arg(long)]
39        page_timestamp: Option<i64>,
40        /// Page direction (pagination.pageDirection): NEXT or PREVIOUS
41        #[arg(long)]
42        page_direction: Option<String>,
43    },
44}
45
46const AUDIT_LOG_FIELDS: &[&str] = &["id", "eventType", "userId", "createdAt"];
47
48pub async fn execute(command: AuditLogCommands, cli: &Cli) -> Result<(), CliError> {
49    let token = resolve_token(cli)?;
50    let client = ClickUpClient::new(&token, cli.timeout)?;
51    let output = OutputConfig::from_cli(&cli.output, &cli.fields, cli.no_header, cli.quiet);
52
53    match command {
54        AuditLogCommands::Query {
55            applicability,
56            event_type,
57            event_status,
58            user_id,
59            user_email,
60            start_time,
61            end_time,
62            page_rows,
63            page_timestamp,
64            page_direction,
65        } => {
66            let team_id = resolve_workspace(cli)?;
67
68            // ClickUp's audit-log request body shape per the v3 OpenAPI spec:
69            //   { applicability, filter?: {...}, pagination?: {...} }
70            // The previous implementation invented `{type, user_id, date_filter}`,
71            // which ClickUp's endpoint does not recognise.
72            let mut body = serde_json::json!({ "applicability": applicability });
73
74            let mut filter = serde_json::Map::new();
75            if let Some(t) = event_type {
76                filter.insert("eventType".into(), serde_json::Value::String(t));
77            }
78            if let Some(s) = event_status {
79                filter.insert("eventStatus".into(), serde_json::Value::String(s));
80            }
81            if !user_id.is_empty() {
82                filter.insert(
83                    "userId".into(),
84                    serde_json::Value::Array(
85                        user_id.into_iter().map(serde_json::Value::String).collect(),
86                    ),
87                );
88            }
89            if !user_email.is_empty() {
90                filter.insert(
91                    "userEmail".into(),
92                    serde_json::Value::Array(
93                        user_email
94                            .into_iter()
95                            .map(serde_json::Value::String)
96                            .collect(),
97                    ),
98                );
99            }
100            if let Some(s) = start_time {
101                filter.insert("startTime".into(), serde_json::Value::Number(s.into()));
102            }
103            if let Some(e) = end_time {
104                filter.insert("endTime".into(), serde_json::Value::Number(e.into()));
105            }
106            if !filter.is_empty() {
107                body["filter"] = serde_json::Value::Object(filter);
108            }
109
110            let mut pagination = serde_json::Map::new();
111            if let Some(n) = page_rows {
112                pagination.insert("pageRows".into(), serde_json::Value::Number(n.into()));
113            }
114            if let Some(t) = page_timestamp {
115                pagination.insert("pageTimestamp".into(), serde_json::Value::Number(t.into()));
116            }
117            if let Some(d) = page_direction {
118                pagination.insert("pageDirection".into(), serde_json::Value::String(d));
119            }
120            if !pagination.is_empty() {
121                body["pagination"] = serde_json::Value::Object(pagination);
122            }
123
124            let resp = client
125                .post(&format!("/v3/workspaces/{}/auditlogs", team_id), &body)
126                .await?;
127
128            if cli.output == "json" {
129                println!("{}", serde_json::to_string_pretty(&resp).unwrap());
130                return Ok(());
131            }
132
133            let logs = resp
134                .get("data")
135                .and_then(|d| d.as_array())
136                .or_else(|| resp.get("audit_logs").and_then(|d| d.as_array()))
137                .cloned()
138                .unwrap_or_default();
139            output.print_items(&logs, AUDIT_LOG_FIELDS, "id");
140            Ok(())
141        }
142    }
143}