codeberg_cli/actions/auth/
login.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
use 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;

/// Login via generating authentication token
#[derive(Parser, Debug)]
pub struct LoginArgs {
    /// Access Token
    #[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)?;

        // verify
        spin_until_ready(verify_setup(&token)).await?;

        // this is where the token gets stored
        let token_path = create_token_storage_path()?;

        // save the token
        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();
    // ask for usage of browser
    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
        );
    }

    // get token from user
    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)
}