codeberg_cli/actions/auth/
login.rsuse std::path::PathBuf;
use crate::actions::GeneralArgs;
use crate::client::BergClient;
use crate::paths::berg_data_dir;
use crate::render::spinner::spin_until_ready;
use crate::render::ui::confirm_with_prompt;
use crate::types::config::BergConfig;
use crate::types::token::Token;
use anyhow::Context;
use inquire::validator::Validation;
use inquire::CustomUserError;
use crate::actions::text_manipulation::input_prompt_for;
use clap::Parser;
#[derive(Parser, Debug)]
pub struct LoginArgs {
#[arg(short, long)]
pub token: Option<String>,
}
impl LoginArgs {
pub async fn run(self, general_args: GeneralArgs) -> anyhow::Result<()> {
let _ = general_args;
let token = self
.token
.map(Token)
.map(Ok)
.unwrap_or_else(prompt_for_token)?;
spin_until_ready(verify_setup(&token)).await?;
let token_path = create_token_storage_path()?;
std::fs::write(token_path.as_path(), token.as_str())?;
Ok(())
}
}
fn prompt_for_token() -> anyhow::Result<Token> {
let config = BergConfig::new()?;
let url = config.url()?;
let token_generation_url = url.join("/user/settings/applications")?;
let token_generation_url = token_generation_url.as_str();
if confirm_with_prompt("Authenticating. Open Browser to generate token for `berg`?")? {
println!("\nOpening {token_generation_url:?} in the browser.\n\nPlease log in, generate a token and provide it after the following prompt:\n\n");
webbrowser::open(token_generation_url)?;
} else {
println!(
"\nYou chose not to authenticate via browser. Visit\n\n\t{}\n\nto generate a token.\n",
token_generation_url
);
}
let token = ask_for_token()?;
Ok(token)
}
async fn verify_setup(token: &Token) -> anyhow::Result<()> {
let config = BergConfig::new()?;
let base_url = config.url()?;
let client = BergClient::new(token, base_url).context("Couldn't create `berg` client.")?;
_ = client.user_get_current().await.map_err(|e| {
anyhow::anyhow!("Verification API call didn't contain expected information.\n\n{e}")
})?;
println!("\nAuthentication success!");
Ok(())
}
fn create_token_storage_path() -> anyhow::Result<PathBuf> {
berg_data_dir().and_then(|token_dir| {
std::fs::create_dir_all(&token_dir)
.context("Couldn't create directory for saving the token.")?;
Ok(token_dir.join("TOKEN"))
})
}
fn validate_token(input: &str) -> Result<Validation, CustomUserError> {
let v = validate_word_count(input);
if let Validation::Invalid(_) = v {
return Ok(v);
}
Ok(validate_token_length(input))
}
fn validate_word_count(input: &str) -> Validation {
let words = input.split_whitespace().collect::<Vec<_>>();
if words.len() != 1 {
Validation::Invalid(
format!(
"Token is just one word. Your input words were\n{}",
words
.iter()
.map(|word| format!(" - {word}"))
.collect::<Vec<_>>()
.join("\n")
)
.into(),
)
} else {
Validation::Valid
}
}
fn validate_token_length(token: &str) -> Validation {
if token.len() != 40 {
Validation::Invalid(
format!(
"Usual token length is 40. Token\n\n\t{token:?}\n\nhas length {}",
token.len()
)
.into(),
)
} else {
Validation::Valid
}
}
fn ask_for_token() -> anyhow::Result<Token> {
inquire::Text::new(input_prompt_for("Token").as_str())
.with_validator(validate_token)
.prompt()
.map(Token::new)
.map_err(anyhow::Error::from)
}