reallink-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/bin/reallink.cjs +19 -0
- package/package.json +30 -0
- package/rust/Cargo.lock +1651 -0
- package/rust/Cargo.toml +15 -0
- package/rust/src/main.rs +492 -0
package/rust/Cargo.toml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "reallink-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
description = "CLI for Reallink auth and token workflows"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
|
|
8
|
+
[dependencies]
|
|
9
|
+
anyhow = "1.0"
|
|
10
|
+
clap = { version = "4.5", features = ["derive"] }
|
|
11
|
+
dirs = "5.0"
|
|
12
|
+
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
|
13
|
+
serde = { version = "1.0", features = ["derive"] }
|
|
14
|
+
serde_json = "1.0"
|
|
15
|
+
tokio = { version = "1.42", features = ["macros", "rt-multi-thread", "time"] }
|
package/rust/src/main.rs
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
use anyhow::{anyhow, Context, Result};
|
|
2
|
+
use clap::{ArgAction, Args, Parser, Subcommand};
|
|
3
|
+
use reqwest::{Method, StatusCode};
|
|
4
|
+
use serde::{Deserialize, Serialize};
|
|
5
|
+
use std::fs;
|
|
6
|
+
use std::path::PathBuf;
|
|
7
|
+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
8
|
+
use tokio::time::sleep;
|
|
9
|
+
|
|
10
|
+
const DEFAULT_BASE_URL: &str = "https://api.real-agent.link";
|
|
11
|
+
|
|
12
|
+
#[derive(Parser)]
|
|
13
|
+
#[command(name = "reallink", version, about = "Reallink CLI")]
|
|
14
|
+
struct Cli {
|
|
15
|
+
#[command(subcommand)]
|
|
16
|
+
command: Commands,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[derive(Subcommand)]
|
|
20
|
+
enum Commands {
|
|
21
|
+
Login(LoginArgs),
|
|
22
|
+
Whoami(BaseArgs),
|
|
23
|
+
Logout,
|
|
24
|
+
Token {
|
|
25
|
+
#[command(subcommand)]
|
|
26
|
+
command: TokenCommands,
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#[derive(Args)]
|
|
31
|
+
struct BaseArgs {
|
|
32
|
+
#[arg(long)]
|
|
33
|
+
base_url: Option<String>,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#[derive(Args)]
|
|
37
|
+
struct LoginArgs {
|
|
38
|
+
#[arg(long, default_value = DEFAULT_BASE_URL)]
|
|
39
|
+
base_url: String,
|
|
40
|
+
#[arg(long, default_value = "reallink-cli")]
|
|
41
|
+
client_id: String,
|
|
42
|
+
#[arg(long = "scope", action = ArgAction::Append)]
|
|
43
|
+
scope: Vec<String>,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#[derive(Subcommand)]
|
|
47
|
+
enum TokenCommands {
|
|
48
|
+
List(BaseArgs),
|
|
49
|
+
Create(TokenCreateArgs),
|
|
50
|
+
Revoke(TokenRevokeArgs),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#[derive(Args)]
|
|
54
|
+
struct TokenCreateArgs {
|
|
55
|
+
#[arg(long)]
|
|
56
|
+
name: String,
|
|
57
|
+
#[arg(long = "scope", action = ArgAction::Append)]
|
|
58
|
+
scope: Vec<String>,
|
|
59
|
+
#[arg(long)]
|
|
60
|
+
org_id: Option<String>,
|
|
61
|
+
#[arg(long)]
|
|
62
|
+
project_id: Option<String>,
|
|
63
|
+
#[arg(long)]
|
|
64
|
+
expires_in_days: Option<u32>,
|
|
65
|
+
#[arg(long)]
|
|
66
|
+
base_url: Option<String>,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[derive(Args)]
|
|
70
|
+
struct TokenRevokeArgs {
|
|
71
|
+
#[arg(long)]
|
|
72
|
+
token_id: String,
|
|
73
|
+
#[arg(long)]
|
|
74
|
+
base_url: Option<String>,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
78
|
+
struct SessionConfig {
|
|
79
|
+
base_url: String,
|
|
80
|
+
access_token: String,
|
|
81
|
+
refresh_token: String,
|
|
82
|
+
session_id: String,
|
|
83
|
+
updated_at_epoch_ms: u128,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
87
|
+
#[serde(rename_all = "camelCase")]
|
|
88
|
+
struct DeviceCodeRequest {
|
|
89
|
+
client_id: String,
|
|
90
|
+
scope: Vec<String>,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
94
|
+
#[serde(rename_all = "camelCase")]
|
|
95
|
+
struct DeviceCodeResponse {
|
|
96
|
+
device_code: String,
|
|
97
|
+
user_code: String,
|
|
98
|
+
verification_uri: String,
|
|
99
|
+
verification_uri_complete: String,
|
|
100
|
+
expires_in: u64,
|
|
101
|
+
interval: u64,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
105
|
+
#[serde(rename_all = "camelCase")]
|
|
106
|
+
struct DeviceTokenRequest {
|
|
107
|
+
grant_type: String,
|
|
108
|
+
device_code: String,
|
|
109
|
+
client_id: String,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
113
|
+
#[serde(rename_all = "camelCase")]
|
|
114
|
+
struct TokenPairResponse {
|
|
115
|
+
access_token: String,
|
|
116
|
+
refresh_token: String,
|
|
117
|
+
session_id: String,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
121
|
+
#[serde(rename_all = "camelCase")]
|
|
122
|
+
struct RefreshRequest {
|
|
123
|
+
refresh_token: String,
|
|
124
|
+
session_id: String,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
128
|
+
#[serde(rename_all = "camelCase")]
|
|
129
|
+
struct RefreshResponse {
|
|
130
|
+
tokens: TokenPairResponse,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
134
|
+
#[serde(rename_all = "camelCase")]
|
|
135
|
+
struct DeviceTokenSuccess {
|
|
136
|
+
access_token: String,
|
|
137
|
+
refresh_token: String,
|
|
138
|
+
session_id: String,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
142
|
+
struct DeviceTokenError {
|
|
143
|
+
error: String,
|
|
144
|
+
error_description: Option<String>,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
148
|
+
#[serde(rename_all = "camelCase")]
|
|
149
|
+
struct ApiTokenRecord {
|
|
150
|
+
id: String,
|
|
151
|
+
user_id: String,
|
|
152
|
+
org_id: Option<String>,
|
|
153
|
+
project_id: Option<String>,
|
|
154
|
+
name: String,
|
|
155
|
+
token_prefix: String,
|
|
156
|
+
scopes: Vec<String>,
|
|
157
|
+
created_at: String,
|
|
158
|
+
last_used_at: Option<String>,
|
|
159
|
+
expires_at: Option<String>,
|
|
160
|
+
revoked_at: Option<String>,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
164
|
+
struct ListApiTokensResponse {
|
|
165
|
+
tokens: Vec<ApiTokenRecord>,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#[derive(Debug, Serialize)]
|
|
169
|
+
#[serde(rename_all = "camelCase")]
|
|
170
|
+
struct CreateApiTokenRequest {
|
|
171
|
+
name: String,
|
|
172
|
+
org_id: Option<String>,
|
|
173
|
+
project_id: Option<String>,
|
|
174
|
+
scopes: Vec<String>,
|
|
175
|
+
expires_in_days: Option<u32>,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
179
|
+
#[serde(rename_all = "camelCase")]
|
|
180
|
+
struct CreateApiTokenResponse {
|
|
181
|
+
token: String,
|
|
182
|
+
token_id: String,
|
|
183
|
+
token_prefix: String,
|
|
184
|
+
scopes: Vec<String>,
|
|
185
|
+
expires_at: Option<String>,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
fn normalize_base_url(input: &str) -> String {
|
|
189
|
+
input.trim().trim_end_matches('/').to_string()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
fn now_epoch_ms() -> u128 {
|
|
193
|
+
SystemTime::now()
|
|
194
|
+
.duration_since(UNIX_EPOCH)
|
|
195
|
+
.unwrap_or_else(|_| Duration::from_secs(0))
|
|
196
|
+
.as_millis()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
fn config_path() -> Result<PathBuf> {
|
|
200
|
+
let base = dirs::config_dir().ok_or_else(|| anyhow!("Could not resolve config directory"))?;
|
|
201
|
+
Ok(base.join("reallink").join("session.json"))
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
fn save_session(session: &SessionConfig) -> Result<()> {
|
|
205
|
+
let path = config_path()?;
|
|
206
|
+
if let Some(parent) = path.parent() {
|
|
207
|
+
fs::create_dir_all(parent).with_context(|| format!("Failed to create {}", parent.display()))?;
|
|
208
|
+
}
|
|
209
|
+
let payload = serde_json::to_vec_pretty(session)?;
|
|
210
|
+
fs::write(&path, payload).with_context(|| format!("Failed to write {}", path.display()))?;
|
|
211
|
+
Ok(())
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
fn load_session() -> Result<SessionConfig> {
|
|
215
|
+
let path = config_path()?;
|
|
216
|
+
let raw = fs::read(&path).with_context(|| {
|
|
217
|
+
format!(
|
|
218
|
+
"No active session at {}. Run `reallink login` first.",
|
|
219
|
+
path.display()
|
|
220
|
+
)
|
|
221
|
+
})?;
|
|
222
|
+
let session: SessionConfig = serde_json::from_slice(&raw)
|
|
223
|
+
.with_context(|| format!("Invalid session format in {}", path.display()))?;
|
|
224
|
+
Ok(session)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
fn clear_session() -> Result<()> {
|
|
228
|
+
let path = config_path()?;
|
|
229
|
+
if path.exists() {
|
|
230
|
+
fs::remove_file(&path).with_context(|| format!("Failed to remove {}", path.display()))?;
|
|
231
|
+
}
|
|
232
|
+
Ok(())
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async fn read_error_body(response: reqwest::Response) -> String {
|
|
236
|
+
match response.text().await {
|
|
237
|
+
Ok(text) if !text.trim().is_empty() => text,
|
|
238
|
+
_ => "No response body".to_string(),
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async fn authed_request(
|
|
243
|
+
client: &reqwest::Client,
|
|
244
|
+
session: &mut SessionConfig,
|
|
245
|
+
method: Method,
|
|
246
|
+
path: &str,
|
|
247
|
+
body: Option<serde_json::Value>,
|
|
248
|
+
) -> Result<reqwest::Response> {
|
|
249
|
+
let url = format!("{}{}", normalize_base_url(&session.base_url), path);
|
|
250
|
+
let mut request = client
|
|
251
|
+
.request(method.clone(), &url)
|
|
252
|
+
.bearer_auth(&session.access_token);
|
|
253
|
+
if let Some(ref body_value) = body {
|
|
254
|
+
request = request.json(body_value);
|
|
255
|
+
}
|
|
256
|
+
let response = request.send().await?;
|
|
257
|
+
if response.status() != StatusCode::UNAUTHORIZED {
|
|
258
|
+
return Ok(response);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
refresh_session(client, session).await?;
|
|
262
|
+
|
|
263
|
+
let mut retry = client
|
|
264
|
+
.request(method, &url)
|
|
265
|
+
.bearer_auth(&session.access_token);
|
|
266
|
+
if let Some(ref body_value) = body {
|
|
267
|
+
retry = retry.json(body_value);
|
|
268
|
+
}
|
|
269
|
+
Ok(retry.send().await?)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async fn refresh_session(client: &reqwest::Client, session: &mut SessionConfig) -> Result<()> {
|
|
273
|
+
let url = format!("{}/auth/refresh", normalize_base_url(&session.base_url));
|
|
274
|
+
let payload = RefreshRequest {
|
|
275
|
+
refresh_token: session.refresh_token.clone(),
|
|
276
|
+
session_id: session.session_id.clone(),
|
|
277
|
+
};
|
|
278
|
+
let response = client.post(url).json(&payload).send().await?;
|
|
279
|
+
if !response.status().is_success() {
|
|
280
|
+
let body = read_error_body(response).await;
|
|
281
|
+
return Err(anyhow!("Refresh failed: {}", body));
|
|
282
|
+
}
|
|
283
|
+
let refreshed: RefreshResponse = response.json().await?;
|
|
284
|
+
session.access_token = refreshed.tokens.access_token;
|
|
285
|
+
session.refresh_token = refreshed.tokens.refresh_token;
|
|
286
|
+
session.session_id = refreshed.tokens.session_id;
|
|
287
|
+
session.updated_at_epoch_ms = now_epoch_ms();
|
|
288
|
+
save_session(session)?;
|
|
289
|
+
Ok(())
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async fn login_command(client: &reqwest::Client, args: LoginArgs) -> Result<()> {
|
|
293
|
+
let base_url = normalize_base_url(&args.base_url);
|
|
294
|
+
let scope = if args.scope.is_empty() {
|
|
295
|
+
vec![
|
|
296
|
+
"core:read".to_string(),
|
|
297
|
+
"core:write".to_string(),
|
|
298
|
+
"assets:read".to_string(),
|
|
299
|
+
"assets:write".to_string(),
|
|
300
|
+
"trace:read".to_string(),
|
|
301
|
+
"trace:write".to_string(),
|
|
302
|
+
"org:admin".to_string(),
|
|
303
|
+
"project:admin".to_string(),
|
|
304
|
+
]
|
|
305
|
+
} else {
|
|
306
|
+
args.scope
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
let device_code_response = client
|
|
310
|
+
.post(format!("{}/auth/device/code", base_url))
|
|
311
|
+
.json(&DeviceCodeRequest {
|
|
312
|
+
client_id: args.client_id.clone(),
|
|
313
|
+
scope,
|
|
314
|
+
})
|
|
315
|
+
.send()
|
|
316
|
+
.await?;
|
|
317
|
+
|
|
318
|
+
if !device_code_response.status().is_success() {
|
|
319
|
+
let body = read_error_body(device_code_response).await;
|
|
320
|
+
return Err(anyhow!("Failed to start device flow: {}", body));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
let device_code: DeviceCodeResponse = device_code_response.json().await?;
|
|
324
|
+
println!("Open this URL in your browser and approve the login:");
|
|
325
|
+
println!("{}", device_code.verification_uri_complete);
|
|
326
|
+
println!("User code: {}", device_code.user_code);
|
|
327
|
+
|
|
328
|
+
let expires_at = std::time::Instant::now() + Duration::from_secs(device_code.expires_in);
|
|
329
|
+
let mut poll_interval = Duration::from_secs(device_code.interval.max(1));
|
|
330
|
+
|
|
331
|
+
loop {
|
|
332
|
+
if std::time::Instant::now() >= expires_at {
|
|
333
|
+
return Err(anyhow!("Device code expired before approval"));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
sleep(poll_interval).await;
|
|
337
|
+
|
|
338
|
+
let token_response = client
|
|
339
|
+
.post(format!("{}/auth/device/token", base_url))
|
|
340
|
+
.json(&DeviceTokenRequest {
|
|
341
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code".to_string(),
|
|
342
|
+
device_code: device_code.device_code.clone(),
|
|
343
|
+
client_id: args.client_id.clone(),
|
|
344
|
+
})
|
|
345
|
+
.send()
|
|
346
|
+
.await?;
|
|
347
|
+
|
|
348
|
+
if token_response.status().is_success() {
|
|
349
|
+
let tokens: DeviceTokenSuccess = token_response.json().await?;
|
|
350
|
+
let session = SessionConfig {
|
|
351
|
+
base_url: base_url.clone(),
|
|
352
|
+
access_token: tokens.access_token,
|
|
353
|
+
refresh_token: tokens.refresh_token,
|
|
354
|
+
session_id: tokens.session_id,
|
|
355
|
+
updated_at_epoch_ms: now_epoch_ms(),
|
|
356
|
+
};
|
|
357
|
+
save_session(&session)?;
|
|
358
|
+
println!("Login successful.");
|
|
359
|
+
return Ok(());
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
let error_payload: DeviceTokenError = token_response
|
|
363
|
+
.json()
|
|
364
|
+
.await
|
|
365
|
+
.unwrap_or(DeviceTokenError {
|
|
366
|
+
error: "unknown_error".to_string(),
|
|
367
|
+
error_description: Some("Could not parse auth error".to_string()),
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
match error_payload.error.as_str() {
|
|
371
|
+
"authorization_pending" => continue,
|
|
372
|
+
"slow_down" => {
|
|
373
|
+
poll_interval += Duration::from_secs(1);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
_ => {
|
|
377
|
+
return Err(anyhow!(
|
|
378
|
+
"Device login failed: {} ({})",
|
|
379
|
+
error_payload.error,
|
|
380
|
+
error_payload.error_description.unwrap_or_default()
|
|
381
|
+
))
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async fn whoami_command(client: &reqwest::Client, args: BaseArgs) -> Result<()> {
|
|
388
|
+
let mut session = load_session()?;
|
|
389
|
+
if let Some(base_url) = args.base_url {
|
|
390
|
+
session.base_url = normalize_base_url(&base_url);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
let response = authed_request(client, &mut session, Method::GET, "/auth/me", None).await?;
|
|
394
|
+
if !response.status().is_success() {
|
|
395
|
+
let body = read_error_body(response).await;
|
|
396
|
+
return Err(anyhow!("whoami failed: {}", body));
|
|
397
|
+
}
|
|
398
|
+
let payload: serde_json::Value = response.json().await?;
|
|
399
|
+
println!("{}", serde_json::to_string_pretty(&payload)?);
|
|
400
|
+
save_session(&session)?;
|
|
401
|
+
Ok(())
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async fn token_list_command(client: &reqwest::Client, args: BaseArgs) -> Result<()> {
|
|
405
|
+
let mut session = load_session()?;
|
|
406
|
+
if let Some(base_url) = args.base_url {
|
|
407
|
+
session.base_url = normalize_base_url(&base_url);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
let response = authed_request(client, &mut session, Method::GET, "/auth/tokens", None).await?;
|
|
411
|
+
if !response.status().is_success() {
|
|
412
|
+
let body = read_error_body(response).await;
|
|
413
|
+
return Err(anyhow!("token list failed: {}", body));
|
|
414
|
+
}
|
|
415
|
+
let payload: ListApiTokensResponse = response.json().await?;
|
|
416
|
+
println!("{}", serde_json::to_string_pretty(&payload.tokens)?);
|
|
417
|
+
save_session(&session)?;
|
|
418
|
+
Ok(())
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async fn token_create_command(client: &reqwest::Client, args: TokenCreateArgs) -> Result<()> {
|
|
422
|
+
let mut session = load_session()?;
|
|
423
|
+
if let Some(base_url) = args.base_url {
|
|
424
|
+
session.base_url = normalize_base_url(&base_url);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
let scopes = if args.scope.is_empty() {
|
|
428
|
+
return Err(anyhow!("At least one --scope must be provided"));
|
|
429
|
+
} else {
|
|
430
|
+
args.scope
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
let body = serde_json::to_value(CreateApiTokenRequest {
|
|
434
|
+
name: args.name,
|
|
435
|
+
org_id: args.org_id,
|
|
436
|
+
project_id: args.project_id,
|
|
437
|
+
scopes,
|
|
438
|
+
expires_in_days: args.expires_in_days,
|
|
439
|
+
})?;
|
|
440
|
+
|
|
441
|
+
let response = authed_request(client, &mut session, Method::POST, "/auth/tokens", Some(body)).await?;
|
|
442
|
+
if !response.status().is_success() {
|
|
443
|
+
let body_text = read_error_body(response).await;
|
|
444
|
+
return Err(anyhow!("token create failed: {}", body_text));
|
|
445
|
+
}
|
|
446
|
+
let payload: CreateApiTokenResponse = response.json().await?;
|
|
447
|
+
println!("{}", serde_json::to_string_pretty(&payload)?);
|
|
448
|
+
save_session(&session)?;
|
|
449
|
+
Ok(())
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async fn token_revoke_command(client: &reqwest::Client, args: TokenRevokeArgs) -> Result<()> {
|
|
453
|
+
let mut session = load_session()?;
|
|
454
|
+
if let Some(base_url) = args.base_url {
|
|
455
|
+
session.base_url = normalize_base_url(&base_url);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
let path = format!("/auth/tokens/{}", args.token_id);
|
|
459
|
+
let response = authed_request(client, &mut session, Method::DELETE, &path, None).await?;
|
|
460
|
+
if !response.status().is_success() {
|
|
461
|
+
let body = read_error_body(response).await;
|
|
462
|
+
return Err(anyhow!("token revoke failed: {}", body));
|
|
463
|
+
}
|
|
464
|
+
let payload: serde_json::Value = response.json().await?;
|
|
465
|
+
println!("{}", serde_json::to_string_pretty(&payload)?);
|
|
466
|
+
save_session(&session)?;
|
|
467
|
+
Ok(())
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
#[tokio::main]
|
|
471
|
+
async fn main() -> Result<()> {
|
|
472
|
+
let cli = Cli::parse();
|
|
473
|
+
let client = reqwest::Client::builder()
|
|
474
|
+
.user_agent("reallink-cli/0.1.0")
|
|
475
|
+
.build()?;
|
|
476
|
+
|
|
477
|
+
match cli.command {
|
|
478
|
+
Commands::Login(args) => login_command(&client, args).await?,
|
|
479
|
+
Commands::Whoami(args) => whoami_command(&client, args).await?,
|
|
480
|
+
Commands::Logout => {
|
|
481
|
+
clear_session()?;
|
|
482
|
+
println!("Logged out.");
|
|
483
|
+
}
|
|
484
|
+
Commands::Token { command } => match command {
|
|
485
|
+
TokenCommands::List(args) => token_list_command(&client, args).await?,
|
|
486
|
+
TokenCommands::Create(args) => token_create_command(&client, args).await?,
|
|
487
|
+
TokenCommands::Revoke(args) => token_revoke_command(&client, args).await?,
|
|
488
|
+
},
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
Ok(())
|
|
492
|
+
}
|