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}