cellstate/entity/
api_key.rs1use 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}