cellstate/entity/
api_key.rs

1//! `cellstate api-key` — Manage API keys for programmatic access.
2//!
3//! Subcommands: create, list, revoke.
4
5use anyhow::Result;
6use clap::{Arg, ArgMatches, Command};
7
8use cellstate_core::api_types::{CreateApiKeyRequest, CreateApiKeyResponse, ListApiKeysResponse};
9
10use super::client::ApiClient;
11use super::output::OutputConfig;
12
13const BASE: &str = "/api/v1/api-keys";
14
15pub fn build_command() -> Command {
16    Command::new("api-key")
17        .about("Manage API keys for programmatic access")
18        .subcommand_required(true)
19        .subcommand(
20            Command::new("create")
21                .about("Create a new API key")
22                .arg(Arg::new("name").required(true).help("API key name"))
23                .arg(
24                    Arg::new("scopes")
25                        .long("scopes")
26                        .value_delimiter(',')
27                        .help("Comma-separated list of scopes"),
28                )
29                .arg(
30                    Arg::new("expires-at")
31                        .long("expires-at")
32                        .help("Expiration timestamp (RFC 3339)"),
33                ),
34        )
35        .subcommand(
36            Command::new("list")
37                .about("List API keys")
38                .arg(
39                    Arg::new("active")
40                        .long("active")
41                        .help("Filter by active status (true/false)"),
42                )
43                .arg(Arg::new("name").long("name").help("Filter by name"))
44                .arg(
45                    Arg::new("limit")
46                        .long("limit")
47                        .default_value("20")
48                        .help("Max results"),
49                )
50                .arg(
51                    Arg::new("offset")
52                        .long("offset")
53                        .default_value("0")
54                        .help("Offset"),
55                ),
56        )
57        .subcommand(
58            Command::new("revoke")
59                .about("Revoke an API key")
60                .arg(Arg::new("id").required(true).help("API key ID")),
61        )
62}
63
64pub async fn dispatch(
65    matches: &ArgMatches,
66    client: &ApiClient,
67    output: &OutputConfig,
68    _session: &crate::session::CliSession,
69) -> Result<()> {
70    match matches.subcommand() {
71        Some(("create", sub)) => {
72            let name = sub.get_one::<String>("name").unwrap().clone();
73            let scopes: Option<Vec<String>> = sub
74                .get_many::<String>("scopes")
75                .map(|vals| vals.cloned().collect());
76            let expires_at = sub
77                .get_one::<String>("expires-at")
78                .map(|s| s.parse())
79                .transpose()?;
80            let req = CreateApiKeyRequest {
81                name,
82                scopes,
83                expires_at,
84            };
85            let resp: CreateApiKeyResponse = client.post(BASE, &req).await?;
86            output.print(&resp);
87        }
88        Some(("list", sub)) => {
89            let limit: i64 = sub.get_one::<String>("limit").unwrap().parse()?;
90            let offset: i64 = sub.get_one::<String>("offset").unwrap().parse()?;
91            let mut path = format!("{BASE}?limit={limit}&offset={offset}");
92            if let Some(active) = sub.get_one::<String>("active") {
93                path.push_str(&format!("&is_active={active}"));
94            }
95            if let Some(name) = sub.get_one::<String>("name") {
96                path.push_str(&format!("&name={name}"));
97            }
98            let resp: ListApiKeysResponse = client.get(&path).await?;
99            output.print_list(&resp.api_keys, resp.total);
100        }
101        Some(("revoke", sub)) => {
102            let id = sub.get_one::<String>("id").unwrap();
103            let resp: serde_json::Value = client
104                .post_no_body_raw(&format!("{BASE}/{id}/revoke"))
105                .await?;
106            output.print_value(&resp);
107        }
108        _ => unreachable!("subcommand_required(true) prevents this"),
109    }
110    Ok(())
111}