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
use std::path::{Path, PathBuf};
use crate::cli::auth::login::LoginArgs;
use crate::client::BergClient;
use crate::endpoints::endpoint_generator::EndpointGenerator;
use crate::paths::token_directory;
use crate::render::spinner::spin_until_ready;
use crate::render::ui::confirm_with_prompt;
use crate::types::api::user::User;
use crate::types::token::Token;
use anyhow::Context;
use inquire::validator::Validation;
use inquire::CustomUserError;
use crate::actions::text_manipulation::input_prompt_for;
const TOKEN_GENERATION_URL: &str = "https://codeberg.org/user/settings/applications";
pub async fn login_user(_args: LoginArgs) -> anyhow::Result<()> {
    if confirm_with_prompt("Authenticating. Open Browser to generate token for `berg`?")? {
        println!("\nOpening Browser...\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()?;
    let token_path = create_token_storage_path()?;
    std::fs::write(token_path.as_path(), token.as_str())?;
    spin_until_ready(async {
        verify_setup(&token)
            .await
            .or_else(cleanup_token_failed_verification(token_path.as_path()))
    })
    .await
}
fn cleanup_token_failed_verification(
    token_path: &Path,
) -> impl FnOnce(anyhow::Error) -> anyhow::Result<()> + '_ {
    move |error: anyhow::Error| match std::fs::remove_file(token_path) {
        Err(e) if matches!(e.kind(), std::io::ErrorKind::PermissionDenied) => {
            anyhow::bail!("Couldn't delete saved broken token. Make sure to delete it to prevent malicious use from potential attackers.");
        }
        _ => Err(error),
    }
}
async fn verify_setup(token: &Token) -> anyhow::Result<()> {
    let client = BergClient::new(token).context("Couldn't create `berg` client.")?;
    let verification_api_endpoint = EndpointGenerator::verify()?;
    _ = client
        .get::<User>(verification_api_endpoint)
        .await
        .map_err(|_| {
            anyhow::anyhow!("Verification API call didn't contain expected information.")
        })?;
    println!("\nAuthentication success!");
    Ok(())
}
fn create_token_storage_path() -> anyhow::Result<PathBuf> {
    token_directory().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)
}