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 client_id = client_id
79 .or_else(|| std::env::var("BITBUCKET_CLIENT_ID").ok())
80 .or_else(|| {
81 println!();
82 println!("š OAuth Consumer Setup Required");
83 println!();
84 println!("To use OAuth authentication, you need to create an OAuth consumer:");
85 println!("1. Go to: https://bitbucket.org/[workspace]/workspace/settings/oauth-consumers/new");
86 println!("2. Set callback URL to ONE of these (pick any available port):");
87 println!(" ⢠http://127.0.0.1:8080/callback");
88 println!(" ⢠http://127.0.0.1:3000/callback");
89 println!(" ⢠http://127.0.0.1:8888/callback");
90 println!(" ⢠http://127.0.0.1:9000/callback");
91 println!("3. Select required permissions:");
92 println!(" ā Account (Read)");
93 println!(" ā Repositories (Read)");
94 println!(" ā Pull requests (Read, Write)");
95 println!(" ā Issues (Read, Write)");
96 println!(" ā Pipelines (Read, Write)");
97 println!("4. Copy the Key (Client ID) and Secret");
98 println!();
99
100 Input::<String>::new()
101 .with_prompt("OAuth Client ID (Key)")
102 .interact_text()
103 .ok()
104 })
105 .ok_or_else(|| anyhow::anyhow!("OAuth Client ID is required"))?;
106
107 let client_secret = client_secret
108 .or_else(|| std::env::var("BITBUCKET_CLIENT_SECRET").ok())
109 .or_else(|| {
110 Input::<String>::new()
111 .with_prompt("OAuth Client Secret")
112 .interact_text()
113 .ok()
114 })
115 .ok_or_else(|| anyhow::anyhow!("OAuth Client Secret is required"))?;
116
117 let oauth = OAuthFlow::new(client_id, client_secret);
118 oauth.authenticate(&auth_manager).await?
119 } else {
120 ApiKeyAuth::authenticate(&auth_manager).await?
122 };
123
124 if let Some(username) = credential.username() {
126 let mut config = Config::load()?;
127 config.set_username(username);
128 config.save()?;
129 }
130
131 Ok(())
132 }
133
134 AuthCommands::Logout => {
135 let auth_manager = AuthManager::new()?;
136 auth_manager.clear_credentials()?;
137
138 let mut config = Config::load()?;
139 config.clear_auth();
140 config.save()?;
141
142 println!("{} Logged out successfully", "ā".green());
143 Ok(())
144 }
145
146 AuthCommands::Status => {
147 let auth_manager = AuthManager::new()?;
148 let config = Config::load()?;
149
150 if auth_manager.is_authenticated() {
151 println!("{} Authenticated", "ā".green());
152
153 if let Ok(Some(credential)) = auth_manager.get_credentials() {
155 println!(" {} {}", "Method:".dimmed(), credential.type_name());
156
157 if credential.is_oauth() && credential.needs_refresh() {
158 println!(" {} {}", "Status:".dimmed(), "Token needs refresh".yellow());
159 }
160 }
161
162 if let Some(username) = config.username() {
163 println!(" {} {}", "Username:".dimmed(), username);
164 }
165
166 if let Some(workspace) = config.default_workspace() {
167 println!(" {} {}", "Workspace:".dimmed(), workspace);
168 }
169
170 match crate::api::BitbucketClient::from_stored() {
172 Ok(client) => match client.get::<serde_json::Value>("/user").await {
173 Ok(user) => {
174 if let Some(display_name) = user.get("display_name") {
175 println!(
176 " {} {}",
177 "Display name:".dimmed(),
178 display_name.as_str().unwrap_or("Unknown")
179 );
180 }
181 }
182 Err(e) => {
183 println!("{} Credentials may be invalid: {}", "ā ".yellow(), e);
184 }
185 },
186 Err(e) => {
187 println!("{} Failed to create client: {}", "ā".red(), e);
188 }
189 }
190 } else {
191 println!("{} Not authenticated", "ā".red());
192 println!();
193 println!("Run {} to authenticate", "bitbucket auth login".cyan());
194 println!();
195 println!("š” Tip: Use {} for the best experience", "--oauth".green());
196 }
197
198 Ok(())
199 }
200 }
201 }
202}