use std::io::BufRead;
use anyhow::{Context, Result, anyhow, bail};
use boxlite::{BoxliteError, BoxliteRuntime, Principal};
use secrecy::SecretString;
use crate::credentials::{self, CredentialStore, Profile};
pub async fn run(
url: &str,
api_key_stdin: bool,
profile_name: &str,
store: &CredentialStore,
) -> Result<()> {
let api_key = if api_key_stdin {
read_stdin_line("API key")?
} else {
prompt_api_key()?
};
let mut profile = Profile {
url: url.to_string(),
api_key: Some(SecretString::from(api_key)),
..Profile::default()
};
let identity = validate(&profile).await?;
if let Some(p) = identity.as_ref() {
profile.path_prefix = p.path_prefix.clone();
}
store
.save_named(profile_name, &profile)
.context("saving credentials")?;
match identity {
Some(p) => {
let who = p.email.as_deref().unwrap_or(p.sub.as_str());
match p.path_prefix.as_deref() {
Some(path_prefix) => {
println!("Logged in as {} (path prefix: {})", who, path_prefix)
}
None => println!("Logged in as {} (the server returned no path prefix)", who),
}
}
None => println!("Logged in (API key)"),
}
Ok(())
}
fn prompt_api_key() -> Result<String> {
let key = rpassword::prompt_password("API key: ").context("reading API key from terminal")?;
let key = key.trim().to_string();
if key.is_empty() {
bail!("API key cannot be empty");
}
Ok(key)
}
fn read_stdin_line(label: &str) -> Result<String> {
let mut buf = String::new();
let n = std::io::stdin()
.lock()
.read_line(&mut buf)
.with_context(|| format!("reading {} from stdin", label))?;
if n == 0 {
bail!("{} not provided on stdin", label);
}
let trimmed = buf.trim_end_matches(['\n', '\r']).to_string();
if trimmed.is_empty() {
bail!("{} is empty", label);
}
Ok(trimmed)
}
async fn validate(profile: &Profile) -> Result<Option<Principal>> {
let opts = credentials::into_rest_options(profile.clone());
let runtime = BoxliteRuntime::rest(opts)
.map_err(|e| anyhow!("failed to construct REST runtime: {}", e))?;
let auth = runtime
.auth()
.map_err(|e| anyhow!("failed to construct REST runtime: {}", e))?;
match auth.whoami().await {
Ok(principal) => Ok(Some(principal)),
Err(BoxliteError::NotFound(_)) => match runtime.list_info().await {
Ok(_) => Ok(None),
Err(err) => Err(classify(profile, err)),
},
Err(err) => Err(classify(profile, err)),
}
}
fn classify(profile: &Profile, err: BoxliteError) -> anyhow::Error {
match err {
BoxliteError::Config(ref msg) if msg.starts_with("auth:") => anyhow!(
"authentication failed against {}: {} (credentials not saved)",
profile.url,
msg
),
ref e @ BoxliteError::Network(_) => anyhow!(
"could not reach {}: {} (credentials not saved). \
Check that the server is running, the URL is correct, \
and any HTTP_PROXY env vars don't shadow this destination.",
profile.url,
e
),
other => anyhow!(
"could not reach {}: {} (credentials not saved)",
profile.url,
other
),
}
}