codeberg_cli/actions/auth/
login.rs1use 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#[derive(Parser, Debug)]
20pub struct LoginArgs {
21 #[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 spin_until_ready(verify_setup(&token)).await?;
37
38 let token_path = create_token_storage_path()?;
40
41 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 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 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}