bitbucket_cli/auth/
api_key.rs1use anyhow::{Context, Result};
2use dialoguer::{Input, Password};
3
4use super::{AuthManager, Credential};
5
6pub struct ApiKeyAuth;
9
10impl ApiKeyAuth {
11 pub async fn authenticate(auth_manager: &AuthManager) -> Result<Credential> {
13 println!("\nš Bitbucket API Key Authentication");
14 println!("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
15 println!();
16 println!("ā ļø Note: OAuth 2.0 is the preferred authentication method.");
17 println!(" API keys are provided for automation/CI scenarios.");
18 println!();
19 println!("To create an API key (HTTP access token):");
20 println!("1. Go to Bitbucket Settings ā Personal settings");
21 println!("2. Click 'HTTP access tokens' under 'Access management'");
22 println!("3. Click 'Create token'");
23 println!("4. Give it a label and select required permissions");
24 println!();
25
26 let username: String = Input::new()
27 .with_prompt("Bitbucket username")
28 .interact_text()
29 .context("Failed to read username")?;
30
31 let api_key: String = Password::new()
32 .with_prompt("API key (HTTP access token)")
33 .interact()
34 .context("Failed to read API key")?;
35
36 let api_key = api_key.trim().to_string();
38
39 if api_key.is_empty() {
41 anyhow::bail!("API key cannot be empty");
42 }
43
44 if !api_key.starts_with("ATATT") && !api_key.starts_with("ATCTT") {
46 println!("ā ļø Warning: Token doesn't start with expected prefix (ATATT or ATCTT)");
47 println!(" This might not be a valid Bitbucket API token.");
48 println!(" Token starts with: {}", &api_key.chars().take(5).collect::<String>());
49 }
50
51 let credential = Credential::ApiKey {
52 username: username.clone(),
53 api_key,
54 };
55
56 Self::validate_credentials(&credential).await?;
58
59 auth_manager.store_credentials(&credential)?;
61
62 println!("\nā
Successfully authenticated as {}", username);
63 println!("š” Tip: Use 'bitbucket auth login --oauth' for a better experience");
64
65 Ok(credential)
66 }
67
68 async fn validate_credentials(credential: &Credential) -> Result<()> {
70 let client = reqwest::Client::new();
71
72 println!("š Validating credentials with Bitbucket API...");
73
74 let response = client
75 .get("https://api.bitbucket.org/2.0/user")
76 .header("Authorization", credential.auth_header())
77 .header("User-Agent", "bitbucket-cli/0.3.0")
78 .send()
79 .await
80 .context("Failed to connect to Bitbucket API")?;
81
82 let status = response.status();
83
84 if status.is_success() {
85 Ok(())
86 } else if status == reqwest::StatusCode::UNAUTHORIZED {
87 anyhow::bail!(
88 "Authentication failed (401 Unauthorized).\n\n\
89 Possible causes:\n\
90 - Incorrect username\n\
91 - Invalid or expired API token\n\
92 - Token doesn't have required permissions\n\n\
93 Please verify:\n\
94 1. Your Bitbucket username is correct\n\
95 2. Your API token is copied completely (should start with 'ATATT' or 'ATCTT')\n\
96 3. Token has 'Read' permission at minimum"
97 )
98 } else {
99 let body = response.text().await.unwrap_or_else(|_| String::from("<unable to read response>"));
100 anyhow::bail!(
101 "API error ({}):\n{}\n\n\
102 This might indicate:\n\
103 - Network connectivity issues\n\
104 - Bitbucket API is unavailable\n\
105 - Rate limiting",
106 status, body
107 )
108 }
109 }
110}