bitbucket_cli/cli/
auth.rs

1use 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    /// Authenticate with Bitbucket (OAuth 2.0 preferred)
12    Login {
13        /// Use OAuth 2.0 authentication (recommended)
14        #[arg(long)]
15        oauth: bool,
16
17        /// Use API key authentication (for automation/CI)
18        #[arg(long)]
19        api_key: bool,
20
21        /// OAuth Client ID (required for OAuth)
22        #[arg(long, env = "BITBUCKET_CLIENT_ID")]
23        client_id: Option<String>,
24
25        /// OAuth Client Secret (required for OAuth)
26        #[arg(long, env = "BITBUCKET_CLIENT_SECRET")]
27        client_secret: Option<String>,
28    },
29
30    /// Remove stored credentials
31    Logout,
32
33    /// Show authentication status
34    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                // Determine authentication method
49                let use_oauth = if oauth || api_key {
50                    // User explicitly chose a method
51                    oauth
52                } else {
53                    // Interactive prompt - prefer OAuth
54                    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                    // OAuth flow
78                    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                    // API Key flow
121                    ApiKeyAuth::authenticate(&auth_manager).await?
122                };
123
124                // Save username to config if available
125                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                    // Show credential type
154                    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                    // Test the credentials
171                    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}