Skip to main content

codeberg_cli/actions/auth/
login.rs

1use std::fs::Permissions;
2use std::os::unix::fs::PermissionsExt;
3use std::path::PathBuf;
4
5use crate::actions::GlobalArgs;
6use crate::client::BergClient;
7use crate::paths::token_path;
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 inquire::CustomUserError;
13use inquire::validator::Validation;
14use miette::{Context, IntoDiagnostic};
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, _global_args: GlobalArgs) -> miette::Result<()> {
30        let config = BergConfig::new()?;
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(config.base_url)?;
42
43        // save the token
44        std::fs::write(token_path.as_path(), token.as_str())
45            .into_diagnostic()
46            .context("Failed to save token")?;
47        // make the token secret
48        std::fs::set_permissions(token_path.as_path(), Permissions::from_mode(0o0600))
49            .into_diagnostic()?;
50
51        Ok(())
52    }
53}
54
55fn prompt_for_token() -> miette::Result<Token> {
56    let config = BergConfig::new()?;
57    let url = config.url()?;
58    let token_generation_url = url.join("/user/settings/applications").into_diagnostic()?;
59    let token_generation_url = token_generation_url.as_str();
60    // ask for usage of browser
61    if confirm_with_prompt("Authenticating. Open Browser to generate token for `berg`?")? {
62        println!(
63            "\nOpening {token_generation_url:?} in the browser.\n\nPlease log in, generate a token and provide it after the following prompt:\n\n"
64        );
65        webbrowser::open(token_generation_url).into_diagnostic()?;
66    } else {
67        println!(
68            "\nYou chose not to authenticate via browser. Visit\n\n\t{}\n\nto generate a token.\n",
69            token_generation_url
70        );
71    }
72
73    // get token from user
74    let token = ask_for_token()?;
75
76    Ok(token)
77}
78
79async fn verify_setup(token: &Token) -> miette::Result<()> {
80    let config = BergConfig::new()?;
81    let base_url = config.url()?;
82    tracing::debug!("URL: {base_url}");
83    tracing::debug!("TOKEN: {token:?}");
84    let client = BergClient::new(token, base_url).context("Couldn't create `berg` client.")?;
85
86    _ = client.user_get_current().await.map_err(|e| {
87        miette::miette!("Verification API call didn't contain expected information.\n\n{e}")
88    })?;
89
90    println!("\nAuthentication success!");
91
92    Ok(())
93}
94
95fn create_token_storage_path(instance: impl AsRef<str>) -> miette::Result<PathBuf> {
96    let token_path = token_path(instance)?;
97    let token_dir = token_path
98        .parent()
99        .context("Parent directory of token path '{token_path}' should exist")?;
100    std::fs::create_dir_all(token_dir)
101        .into_diagnostic()
102        .context("Couldn't create directory for saving the token.")?;
103    Ok(token_path)
104}
105
106fn validate_token(input: &str) -> Result<Validation, CustomUserError> {
107    let v = validate_word_count(input);
108    if let Validation::Invalid(_) = v {
109        return Ok(v);
110    }
111    Ok(validate_token_length(input))
112}
113
114fn validate_word_count(input: &str) -> Validation {
115    let words = input.split_whitespace().collect::<Vec<_>>();
116    if words.len() != 1 {
117        Validation::Invalid(
118            format!(
119                "Token is just one word. Your input words were\n{}",
120                words
121                    .iter()
122                    .map(|word| format!("  - {word}"))
123                    .collect::<Vec<_>>()
124                    .join("\n")
125            )
126            .into(),
127        )
128    } else {
129        Validation::Valid
130    }
131}
132
133fn validate_token_length(token: &str) -> Validation {
134    if token.len() != 40 {
135        Validation::Invalid(
136            format!(
137                "Usual token length is 40. Token\n\n\t{token:?}\n\nhas length {}",
138                token.len()
139            )
140            .into(),
141        )
142    } else {
143        Validation::Valid
144    }
145}
146
147fn ask_for_token() -> miette::Result<Token> {
148    inquire::Text::new(input_prompt_for("Token").as_str())
149        .with_validator(validate_token)
150        .prompt()
151        .map(Token::new)
152        .into_diagnostic()
153}