codeberg_cli/actions/auth/
login.rs

1use std::path::PathBuf;
2
3use crate::actions::GeneralArgs;
4use crate::client::BergClient;
5use crate::paths::berg_data_dir;
6use crate::render::spinner::spin_until_ready;
7use crate::render::ui::confirm_with_prompt;
8use crate::types::config::BergConfig;
9use crate::types::token::Token;
10use anyhow::Context;
11use inquire::validator::Validation;
12use inquire::CustomUserError;
13
14use crate::actions::text_manipulation::input_prompt_for;
15
16use clap::Parser;
17
18/// Login via generating authentication token
19#[derive(Parser, Debug)]
20pub struct LoginArgs {
21    /// Access Token
22    #[arg(short, long)]
23    pub token: Option<String>,
24}
25
26impl LoginArgs {
27    pub async fn run(self, general_args: GeneralArgs) -> anyhow::Result<()> {
28        let _ = general_args;
29        let token = self
30            .token
31            .map(Token)
32            .map(Ok)
33            .unwrap_or_else(prompt_for_token)?;
34
35        // verify
36        spin_until_ready(verify_setup(&token)).await?;
37
38        // this is where the token gets stored
39        let token_path = create_token_storage_path()?;
40
41        // save the token
42        std::fs::write(token_path.as_path(), token.as_str())?;
43
44        Ok(())
45    }
46}
47
48fn prompt_for_token() -> anyhow::Result<Token> {
49    let config = BergConfig::new()?;
50    let url = config.url()?;
51    let token_generation_url = url.join("/user/settings/applications")?;
52    let token_generation_url = token_generation_url.as_str();
53    // ask for usage of browser
54    if confirm_with_prompt("Authenticating. Open Browser to generate token for `berg`?")? {
55        println!("\nOpening {token_generation_url:?} in the browser.\n\nPlease log in, generate a token and provide it after the following prompt:\n\n");
56        webbrowser::open(token_generation_url)?;
57    } else {
58        println!(
59            "\nYou chose not to authenticate via browser. Visit\n\n\t{}\n\nto generate a token.\n",
60            token_generation_url
61        );
62    }
63
64    // get token from user
65    let token = ask_for_token()?;
66
67    Ok(token)
68}
69
70async fn verify_setup(token: &Token) -> anyhow::Result<()> {
71    let config = BergConfig::new()?;
72    let base_url = config.url()?;
73    let client = BergClient::new(token, base_url).context("Couldn't create `berg` client.")?;
74
75    _ = client.user_get_current().await.map_err(|e| {
76        anyhow::anyhow!("Verification API call didn't contain expected information.\n\n{e}")
77    })?;
78
79    println!("\nAuthentication success!");
80
81    Ok(())
82}
83
84fn create_token_storage_path() -> anyhow::Result<PathBuf> {
85    berg_data_dir().and_then(|token_dir| {
86        std::fs::create_dir_all(&token_dir)
87            .context("Couldn't create directory for saving the token.")?;
88        Ok(token_dir.join("TOKEN"))
89    })
90}
91
92fn validate_token(input: &str) -> Result<Validation, CustomUserError> {
93    let v = validate_word_count(input);
94    if let Validation::Invalid(_) = v {
95        return Ok(v);
96    }
97    Ok(validate_token_length(input))
98}
99
100fn validate_word_count(input: &str) -> Validation {
101    let words = input.split_whitespace().collect::<Vec<_>>();
102    if words.len() != 1 {
103        Validation::Invalid(
104            format!(
105                "Token is just one word. Your input words were\n{}",
106                words
107                    .iter()
108                    .map(|word| format!("  - {word}"))
109                    .collect::<Vec<_>>()
110                    .join("\n")
111            )
112            .into(),
113        )
114    } else {
115        Validation::Valid
116    }
117}
118
119fn validate_token_length(token: &str) -> Validation {
120    if token.len() != 40 {
121        Validation::Invalid(
122            format!(
123                "Usual token length is 40. Token\n\n\t{token:?}\n\nhas length {}",
124                token.len()
125            )
126            .into(),
127        )
128    } else {
129        Validation::Valid
130    }
131}
132
133fn ask_for_token() -> anyhow::Result<Token> {
134    inquire::Text::new(input_prompt_for("Token").as_str())
135        .with_validator(validate_token)
136        .prompt()
137        .map(Token::new)
138        .map_err(anyhow::Error::from)
139}