bitbucket_cli/cli/
auth.rs1use anyhow::Result;
2use clap::Subcommand;
3use colored::Colorize;
4use dialoguer::{Confirm, Input};
5
6use crate::auth::{ApiKeyAuth, AuthManager, OAuthFlow};
7use crate::config::Config;
8
9#[derive(Subcommand)]
10pub enum AuthCommands {
11 Login {
13 #[arg(long)]
15 oauth: bool,
16
17 #[arg(long)]
19 api_key: bool,
20
21 #[arg(long, env = "BITBUCKET_CLIENT_ID")]
23 client_id: Option<String>,
24
25 #[arg(long, env = "BITBUCKET_CLIENT_SECRET")]
27 client_secret: Option<String>,
28 },
29
30 Logout,
32
33 Status,
35}
36
37impl AuthCommands {
38 pub async fn run(self) -> Result<()> {
39 match self {
40 AuthCommands::Login {
41 oauth,
42 api_key,
43 client_id,
44 client_secret,
45 } => {
46 let auth_manager = AuthManager::new()?;
47
48 let use_oauth = if oauth || api_key {
50 oauth
52 } else {
53 println!("\nš Bitbucket CLI Authentication");
55 println!("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
56 println!();
57 println!("Choose authentication method:");
58 println!();
59 println!(" 1. {} (Recommended)", "OAuth 2.0".green().bold());
60 println!(" ⢠More secure with token refresh");
61 println!(" ⢠Better user experience");
62 println!(" ⢠Requires OAuth app setup");
63 println!();
64 println!(" 2. {} (Fallback)", "API Key".yellow());
65 println!(" ⢠For automation/CI pipelines");
66 println!(" ⢠Requires HTTP access token");
67 println!(" ⢠No automatic refresh");
68 println!();
69
70 Confirm::new()
71 .with_prompt("Use OAuth 2.0?")
72 .default(true)
73 .interact()?
74 };
75
76 let credential = if use_oauth {
77 let stored_consumer = auth_manager
83 .get_credentials()
84 .ok()
85 .flatten()
86 .and_then(|c| {
87 c.oauth_consumer_credentials()
88 .map(|(id, secret)| (id.to_owned(), secret.to_owned()))
89 });
90
91 let client_id = client_id
92 .or_else(|| std::env::var("BITBUCKET_CLIENT_ID").ok())
93 .or_else(|| stored_consumer.as_ref().map(|(id, _)| id.clone()))
94 .or_else(|| {
95 println!();
96 println!("š OAuth Consumer Setup Required");
97 println!();
98 println!("To use OAuth authentication, you need to create an OAuth consumer:");
99 println!("1. Go to: https://bitbucket.org/[workspace]/workspace/settings/oauth-consumers/new");
100 println!("2. Set callback URL to ONE of these (pick any available port):");
101 println!(" ⢠http://127.0.0.1:8080/callback");
102 println!(" ⢠http://127.0.0.1:3000/callback");
103 println!(" ⢠http://127.0.0.1:8888/callback");
104 println!(" ⢠http://127.0.0.1:9000/callback");
105 println!("3. Select required permissions:");
106 println!(" ā Account (Read)");
107 println!(" ā Repositories (Read)");
108 println!(" ā Pull requests (Read, Write)");
109 println!(" ā Issues (Read, Write)");
110 println!(" ā Pipelines (Read, Write)");
111 println!("4. Copy the Key (Client ID) and Secret");
112 println!();
113
114 Input::<String>::new()
115 .with_prompt("OAuth Client ID (Key)")
116 .interact_text()
117 .ok()
118 })
119 .ok_or_else(|| anyhow::anyhow!("OAuth Client ID is required"))?;
120
121 let client_secret = client_secret
122 .or_else(|| std::env::var("BITBUCKET_CLIENT_SECRET").ok())
123 .or_else(|| stored_consumer.map(|(_, secret)| secret))
124 .or_else(|| {
125 Input::<String>::new()
126 .with_prompt("OAuth Client Secret")
127 .interact_text()
128 .ok()
129 })
130 .ok_or_else(|| anyhow::anyhow!("OAuth Client Secret is required"))?;
131
132 let oauth = OAuthFlow::new(client_id, client_secret);
133 oauth.authenticate(&auth_manager).await?
134 } else {
135 ApiKeyAuth::authenticate(&auth_manager).await?
137 };
138
139 if let Some(username) = credential.username() {
141 let mut config = Config::load()?;
142 config.set_username(username);
143 config.save()?;
144 }
145
146 Ok(())
147 }
148
149 AuthCommands::Logout => {
150 let auth_manager = AuthManager::new()?;
151 auth_manager.clear_credentials()?;
152
153 let mut config = Config::load()?;
154 config.clear_auth();
155 config.save()?;
156
157 println!("{} Logged out successfully", "ā".green());
158 Ok(())
159 }
160
161 AuthCommands::Status => {
162 let auth_manager = AuthManager::new()?;
163 let config = Config::load()?;
164
165 if auth_manager.is_authenticated() {
166 println!("{} Authenticated", "ā".green());
167
168 if let Ok(Some(credential)) = auth_manager.get_credentials() {
170 println!(" {} {}", "Method:".dimmed(), credential.type_name());
171
172 if credential.is_oauth() && credential.needs_refresh() {
173 println!(
174 " {} {}",
175 "Status:".dimmed(),
176 "Token needs refresh".yellow()
177 );
178 }
179 }
180
181 if let Some(username) = config.username() {
182 println!(" {} {}", "Username:".dimmed(), username);
183 }
184
185 if let Some(workspace) = config.default_workspace() {
186 println!(" {} {}", "Workspace:".dimmed(), workspace);
187 }
188
189 match crate::api::BitbucketClient::from_stored() {
191 Ok(client) => match client.get::<serde_json::Value>("/user").await {
192 Ok(user) => {
193 if let Some(display_name) = user.get("display_name") {
194 println!(
195 " {} {}",
196 "Display name:".dimmed(),
197 display_name.as_str().unwrap_or("Unknown")
198 );
199 }
200 }
201 Err(e) => {
202 println!("{} Credentials may be invalid: {}", "ā ".yellow(), e);
203 }
204 },
205 Err(e) => {
206 println!("{} Failed to create client: {}", "ā".red(), e);
207 }
208 }
209 } else {
210 println!("{} Not authenticated", "ā".red());
211 println!();
212 println!("Run {} to authenticate", "bitbucket auth login".cyan());
213 println!();
214 println!("š” Tip: Use {} for the best experience", "--oauth".green());
215 }
216
217 Ok(())
218 }
219 }
220 }
221}