codeberg_cli/actions/auth/
login.rs1use 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#[derive(Parser, Debug)]
22pub struct LoginArgs {
23 #[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 spin_until_ready(verify_setup(&token)).await?;
39
40 let token_path = create_token_storage_path(config.base_url)?;
42
43 std::fs::write(token_path.as_path(), token.as_str())
45 .into_diagnostic()
46 .context("Failed to save token")?;
47 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 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 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 let client = BergClient::new(token, base_url).context("Couldn't create `berg` client.")?;
83
84 _ = client.user_get_current().await.map_err(|e| {
85 miette::miette!("Verification API call didn't contain expected information.\n\n{e}")
86 })?;
87
88 println!("\nAuthentication success!");
89
90 Ok(())
91}
92
93fn create_token_storage_path(instance: impl AsRef<str>) -> miette::Result<PathBuf> {
94 let token_path = token_path(instance)?;
95 let token_dir = token_path
96 .parent()
97 .context("Parent directory of token path '{token_path}' should exist")?;
98 std::fs::create_dir_all(token_dir)
99 .into_diagnostic()
100 .context("Couldn't create directory for saving the token.")?;
101 Ok(token_path)
102}
103
104fn validate_token(input: &str) -> Result<Validation, CustomUserError> {
105 let v = validate_word_count(input);
106 if let Validation::Invalid(_) = v {
107 return Ok(v);
108 }
109 Ok(validate_token_length(input))
110}
111
112fn validate_word_count(input: &str) -> Validation {
113 let words = input.split_whitespace().collect::<Vec<_>>();
114 if words.len() != 1 {
115 Validation::Invalid(
116 format!(
117 "Token is just one word. Your input words were\n{}",
118 words
119 .iter()
120 .map(|word| format!(" - {word}"))
121 .collect::<Vec<_>>()
122 .join("\n")
123 )
124 .into(),
125 )
126 } else {
127 Validation::Valid
128 }
129}
130
131fn validate_token_length(token: &str) -> Validation {
132 if token.len() != 40 {
133 Validation::Invalid(
134 format!(
135 "Usual token length is 40. Token\n\n\t{token:?}\n\nhas length {}",
136 token.len()
137 )
138 .into(),
139 )
140 } else {
141 Validation::Valid
142 }
143}
144
145fn ask_for_token() -> miette::Result<Token> {
146 inquire::Text::new(input_prompt_for("Token").as_str())
147 .with_validator(validate_token)
148 .prompt()
149 .map(Token::new)
150 .into_diagnostic()
151}