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            let path = format!("/v3/workspaces/{}/auditlogs", team_id);
68
69            let page_direction_owned = page_direction.clone();
70
71            // The static portion of the pagination block (pageRows + direction)
72            // is passed through unchanged on every iteration; pageTimestamp
73            // is what the walker advances.
74            let mut extra_pagination = serde_json::Map::new();
75            if let Some(n) = page_rows {
76                extra_pagination.insert("pageRows".into(), serde_json::Value::Number(n.into()));
77            }
78            if let Some(d) = page_direction_owned {
79                extra_pagination.insert("pageDirection".into(), serde_json::Value::String(d));
80            }
81
82            let logs = crate::commands::pagination::walk_body(
83                cli,
84                &client,
85                &path,
86                &["data", "audit_logs"],
87                || {
88                    let mut body = serde_json::json!({ "applicability": applicability });
89                    let mut filter = serde_json::Map::new();
90                    if let Some(t) = &event_type {
91                        filter.insert("eventType".into(), serde_json::json!(t));
92                    }
93                    if let Some(s) = &event_status {
94                        filter.insert("eventStatus".into(), serde_json::json!(s));
95                    }
96                    if !user_id.is_empty() {
97                        filter.insert(
98                            "userId".into(),
99                            serde_json::Value::Array(
100                                user_id
101                                    .iter()
102                                    .cloned()
103                                    .map(serde_json::Value::String)
104                                    .collect(),
105                            ),
106                        );
107                    }
108                    if !user_email.is_empty() {
109                        filter.insert(
110                            "userEmail".into(),
111                            serde_json::Value::Array(
112                                user_email
113                                    .iter()
114                                    .cloned()
115                                    .map(serde_json::Value::String)
116                                    .collect(),
117                            ),
118                        );
119                    }
120                    if let Some(s) = start_time {
121                        filter.insert("startTime".into(), serde_json::json!(s));
122                    }
123                    if let Some(e) = end_time {
124                        filter.insert("endTime".into(), serde_json::json!(e));
125                    }
126                    if !filter.is_empty() {
127                        body["filter"] = serde_json::Value::Object(filter);
128                    }
129                    body
130                },
131                extra_pagination,
132                page_timestamp,
133                |item| {
134                    for key in ["eventTime", "timestamp", "date"] {
135                        if let Some(v) = item.get(key) {
136                            if let Some(n) = v.as_i64() {
137                                return Some(n);
138                            }
139                            if let Some(s) = v.as_str() {
140                                if let Ok(n) = s.parse::<i64>() {
141                                    return Some(n);
142                                }
143                            }
144                        }
145                    }
146                    None
147                },
148            )
149            .await?;
150
151            if cli.output == "json" {
152                println!("{}", serde_json::to_string_pretty(&logs).unwrap());
153                return Ok(());
154            }
155
156            output.print_items(&logs, AUDIT_LOG_FIELDS, "id");
157            Ok(())
158        }
159    }
160}