codeberg_cli/actions/auth/
login.rs

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