Skip to main content

indodax_cli/commands/
auth.rs

1use crate::client::IndodaxClient;
2use crate::commands::helpers;
3use crate::config::{IndodaxConfig, SecretValue};
4use crate::output::CommandOutput;
5use anyhow::Result;
6
7#[derive(Debug, clap::Subcommand)]
8pub enum AuthCommand {
9    #[command(name = "set", about = "Set API key, secret and callback URL")]
10    Set {
11        #[arg(short = 'k', long = "api-key", help = "Your Indodax API key")]
12        api_key: Option<String>,
13        #[arg(short = 's', long = "api-secret", help = "Your Indodax API secret")]
14        api_secret: Option<String>,
15        #[arg(long = "api-secret-stdin", help = "Read API secret from stdin")]
16        api_secret_stdin: bool,
17        #[arg(long = "callback-url", help = "Your Indodax Callback URL")]
18        callback_url: Option<String>,
19    },
20
21    #[command(name = "show", about = "Show current API configuration")]
22    Show,
23
24    #[command(name = "test", about = "Test API credentials")]
25    Test,
26
27    #[command(name = "reset", about = "Remove stored API credentials")]
28    Reset,
29}
30
31pub async fn execute(
32    client: &IndodaxClient,
33    config: &mut IndodaxConfig,
34    cmd: &AuthCommand,
35) -> Result<CommandOutput> {
36    match cmd {
37        AuthCommand::Set { api_key, api_secret, api_secret_stdin, callback_url } => {
38            if let Some(key) = api_key {
39                config.api_key = Some(SecretValue::new(key));
40            }
41
42            if *api_secret_stdin {
43                let mut buf = String::new();
44                let mut stdin = tokio::io::BufReader::new(tokio::io::stdin());
45                use tokio::io::AsyncBufReadExt;
46                stdin.read_line(&mut buf).await?;
47                config.api_secret = Some(SecretValue::new(buf.trim().to_string()));
48            } else if let Some(s) = api_secret {
49                config.api_secret = Some(SecretValue::new(s.clone()));
50            }
51
52            if let Some(url) = callback_url {
53                config.callback_url = Some(url.clone());
54            }
55
56            config.save()?;
57
58            let data = serde_json::json!({
59                "status": "ok",
60                "message": "API configuration updated"
61            });
62            Ok(CommandOutput::json(data))
63        }
64
65        AuthCommand::Show => {
66            let key_status = config
67                .api_key
68                .as_ref()
69                .map_or("not set", |_| "set");
70            let secret_status = config
71                .api_secret
72                .as_ref()
73                .map_or("not set", |_| "set");
74            let callback_url = config
75                .callback_url
76                .as_deref()
77                .unwrap_or("not set");
78            let config_path = IndodaxConfig::config_path();
79
80            let headers = vec!["Field".into(), "Value".into()];
81            let rows = vec![
82                vec!["Config path".into(), config_path.display().to_string()],
83                vec!["API Key".into(), key_status.into()],
84                vec!["API Secret".into(), secret_status.into()],
85                vec!["Callback URL".into(), callback_url.into()],
86            ];
87
88            let masked_key = config.api_key.as_ref().map(|k| {
89                let s = k.as_str();
90                let visible_len = (s.len() / 4).min(4);
91                if visible_len > 0 {
92                    format!("{}****", &s[..visible_len])
93                } else {
94                    "****".to_string()
95                }
96            });
97
98            let data = serde_json::json!({
99                "config_path": config_path.to_string_lossy(),
100                "api_key_set": config.api_key.is_some(),
101                "api_secret_set": config.api_secret.is_some(),
102                "masked_key": masked_key,
103                "callback_url": config.callback_url,
104            });
105
106            Ok(CommandOutput::new(data, headers, rows))
107        }
108
109        AuthCommand::Test => {
110            if config.api_key.is_none() || config.api_secret.is_none() {
111                return Err(anyhow::anyhow!(
112                    "No API credentials configured. Use 'indodax auth set' first."
113                ));
114            }
115
116            let test_params = std::collections::HashMap::new();
117            let result: serde_json::Value = client.private_post_v1("getInfo", &test_params).await?;
118
119            let balance = &result["balance"];
120            let bal_summary = if balance.is_object() {
121                balance
122                    .as_object()
123                    .map(|obj| {
124                        obj.iter()
125                            .filter(|(_, v)| v.as_f64().unwrap_or(0.0) > 0.0)
126                            .map(|(k, v)| format!("{}: {}", k, v))
127                            .collect::<Vec<_>>()
128                            .join(", ")
129                    })
130                    .unwrap_or_default()
131            } else {
132                "N/A".into()
133            };
134
135            let headers = vec!["Field".into(), "Value".into()];
136            let rows = vec![
137                vec!["Status".into(), "OK - Credentials valid".into()],
138                vec!["Name".into(), helpers::value_to_string(result.get("name").unwrap_or(&serde_json::Value::Null))],
139                vec!["Server Time".into(), helpers::value_to_string(result.get("server_time").unwrap_or(&serde_json::Value::Null))],
140                vec!["Balances (non-zero)".into(), bal_summary],
141            ];
142
143            Ok(CommandOutput::new(result, headers, rows))
144        }
145
146        AuthCommand::Reset => {
147            config.api_key = None;
148            config.api_secret = None;
149            config.callback_url = None;
150            config.save()?;
151
152            let data = serde_json::json!({
153                "status": "ok",
154                "message": "API credentials removed"
155            });
156                Ok(CommandOutput::json(data))
157        }
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn test_auth_command_set() {
167        let cmd = AuthCommand::Set {
168            api_key: Some("key123".into()),
169            api_secret: Some("secret456".into()),
170            api_secret_stdin: false,
171            callback_url: Some("http://callback.test".into()),
172        };
173        match cmd {
174            AuthCommand::Set { api_key, api_secret, api_secret_stdin, callback_url } => {
175                assert_eq!(api_key, Some("key123".into()));
176                assert_eq!(api_secret, Some("secret456".into()));
177                assert!(!api_secret_stdin);
178                assert_eq!(callback_url, Some("http://callback.test".into()));
179            }
180            _ => assert!(false, "Expected Set command, got {:?}", cmd),
181        }
182    }
183
184    #[test]
185    fn test_auth_command_show() {
186        let cmd = AuthCommand::Show;
187        match cmd {
188            AuthCommand::Show => (),
189            _ => assert!(false, "Expected Show command, got {:?}", cmd),
190        }
191    }
192
193    #[test]
194    fn test_auth_command_test() {
195        let cmd = AuthCommand::Test;
196        match cmd {
197            AuthCommand::Test => (),
198            _ => assert!(false, "Expected Test command, got {:?}", cmd),
199        }
200    }
201
202    #[test]
203    fn test_auth_command_reset() {
204        let cmd = AuthCommand::Reset;
205        match cmd {
206            AuthCommand::Reset => (),
207            _ => assert!(false, "Expected Reset command, got {:?}", cmd),
208        }
209    }
210
211    #[test]
212    fn test_auth_command_set_minimal() {
213        let cmd = AuthCommand::Set {
214            api_key: None,
215            api_secret: None,
216            api_secret_stdin: true,
217            callback_url: None,
218        };
219        match cmd {
220            AuthCommand::Set { api_key, api_secret, api_secret_stdin, callback_url } => {
221                assert!(api_key.is_none());
222                assert!(api_secret.is_none());
223                assert!(api_secret_stdin);
224                assert!(callback_url.is_none());
225            }
226            _ => assert!(false, "Expected Set command, got {:?}", cmd),
227        }
228    }
229
230    #[test]
231    fn test_auth_command_variants() {
232        let _cmd1 = AuthCommand::Set { 
233            api_key: None, 
234            api_secret: None, 
235            api_secret_stdin: false, 
236            callback_url: None 
237        };
238        let _cmd2 = AuthCommand::Show;
239        let _cmd3 = AuthCommand::Test;
240        let _cmd4 = AuthCommand::Reset;
241    }
242}