use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use crate::client::{build_client, get_json};
use crate::config::canonical_base_url;
use crate::keyring_store;
use crate::output::print_json;
#[derive(Debug, Parser)]
pub struct AuthCmd {
#[command(subcommand)]
sub: AuthSub,
}
#[derive(Debug, Subcommand)]
enum AuthSub {
Login,
Whoami,
Logout,
Key(KeyCmd),
}
#[derive(Debug, Parser)]
struct KeyCmd {
#[command(subcommand)]
sub: KeySub,
}
#[derive(Debug, Subcommand)]
enum KeySub {
Create {
#[arg(long, short)]
name: String,
},
List,
Revoke {
id: String,
},
}
impl AuthCmd {
pub async fn run(self, api_url: &str, pretty: bool) -> Result<()> {
let base = canonical_base_url(api_url);
match self.sub {
AuthSub::Login => run_login(&base).await,
AuthSub::Whoami => run_whoami(&base, pretty).await,
AuthSub::Logout => run_logout(),
AuthSub::Key(k) => match k.sub {
KeySub::Create { name } => run_key_create(&base, &name, pretty).await,
KeySub::List => run_key_list(&base, pretty).await,
KeySub::Revoke { id } => run_key_revoke(&base, &id, pretty).await,
},
}
}
}
async fn run_login(_base: &str) -> Result<()> {
println!("Opening the Tranc dashboard to get an API key…");
println!();
println!(" https://app.tranc.ai/keys");
println!();
let _ = open::that("https://app.tranc.ai/keys");
println!("Paste your API key below and press Enter:");
print!("> ");
use std::io::Write;
std::io::stdout().flush().ok();
let mut key = String::new();
std::io::stdin()
.read_line(&mut key)
.context("failed to read API key from stdin")?;
let key = key.trim().to_string();
if key.is_empty() {
anyhow::bail!("no API key provided");
}
keyring_store::set_token(&key).context("failed to store API key in OS keyring")?;
println!("Token stored in OS keyring. You're logged in.");
Ok(())
}
async fn run_whoami(base: &str, pretty: bool) -> Result<()> {
let (client, token) = build_client()?;
let url = format!("{base}/v1/auth/whoami");
let rb = client.get(&url).bearer_auth(&token);
let json = get_json(rb).await?;
print_json(&json, pretty)
}
fn run_logout() -> Result<()> {
keyring_store::delete_token().context("failed to remove token from OS keyring")?;
println!("Logged out. Token removed from OS keyring.");
Ok(())
}
async fn run_key_create(base: &str, name: &str, pretty: bool) -> Result<()> {
let (client, token) = build_client()?;
let url = format!("{base}/v1/keys");
let body = serde_json::json!({ "name": name });
let rb = client.post(&url).bearer_auth(&token).json(&body);
let json = crate::client::post_json(rb).await?;
print_json(&json, pretty)
}
async fn run_key_list(base: &str, pretty: bool) -> Result<()> {
let (client, token) = build_client()?;
let url = format!("{base}/v1/keys");
let rb = client.get(&url).bearer_auth(&token);
let json = get_json(rb).await?;
print_json(&json, pretty)
}
async fn run_key_revoke(base: &str, id: &str, pretty: bool) -> Result<()> {
let (client, token) = build_client()?;
let url = format!("{base}/v1/keys/{id}");
let rb = client.delete(&url).bearer_auth(&token);
let json = get_json(rb).await?;
print_json(&json, pretty)
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn parse_auth_login() {
let cli = AuthCmd::try_parse_from(["auth", "login"]).unwrap();
assert!(matches!(cli.sub, AuthSub::Login));
}
#[test]
fn parse_auth_whoami() {
let cli = AuthCmd::try_parse_from(["auth", "whoami"]).unwrap();
assert!(matches!(cli.sub, AuthSub::Whoami));
}
#[test]
fn parse_auth_logout() {
let cli = AuthCmd::try_parse_from(["auth", "logout"]).unwrap();
assert!(matches!(cli.sub, AuthSub::Logout));
}
#[test]
fn parse_key_create() {
let cli = AuthCmd::try_parse_from(["auth", "key", "create", "--name", "my-agent"]).unwrap();
assert!(matches!(
cli.sub,
AuthSub::Key(KeyCmd {
sub: KeySub::Create { name: _ }
})
));
}
#[test]
fn parse_key_list() {
let cli = AuthCmd::try_parse_from(["auth", "key", "list"]).unwrap();
assert!(matches!(
cli.sub,
AuthSub::Key(KeyCmd { sub: KeySub::List })
));
}
#[test]
fn parse_key_revoke() {
let cli = AuthCmd::try_parse_from(["auth", "key", "revoke", "abc-123"]).unwrap();
assert!(matches!(
cli.sub,
AuthSub::Key(KeyCmd {
sub: KeySub::Revoke { .. }
})
));
}
}