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 App 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: http://127.0.0.1:*/callback");
87                            println!("3. Select required permissions (repository, pullrequest, issue, pipeline, account)");
88                            println!("4. Copy the Client ID and Secret");
89                            println!();
90                            
91                            Input::<String>::new()
92                                .with_prompt("OAuth Client ID")
93                                .interact_text()
94                                .ok()
95                        })
96                        .ok_or_else(|| anyhow::anyhow!("OAuth Client ID is required"))?;
97
98                    let client_secret = client_secret
99                        .or_else(|| std::env::var("BITBUCKET_CLIENT_SECRET").ok())
100                        .or_else(|| {
101                            Input::<String>::new()
102                                .with_prompt("OAuth Client Secret")
103                                .interact_text()
104                                .ok()
105                        })
106                        .ok_or_else(|| anyhow::anyhow!("OAuth Client Secret is required"))?;
107
108                    let oauth = OAuthFlow::new(client_id, client_secret);
109                    oauth.authenticate(&auth_manager).await?
110                } else {
111                    // API Key flow
112                    ApiKeyAuth::authenticate(&auth_manager).await?
113                };
114
115                // Save username to config if available
116                if let Some(username) = credential.username() {
117                    let mut config = Config::load()?;
118                    config.set_username(username);
119                    config.save()?;
120                }
121
122                Ok(())
123            }
124
125            AuthCommands::Logout => {
126                let auth_manager = AuthManager::new()?;
127                auth_manager.clear_credentials()?;
128
129                let mut config = Config::load()?;
130                config.clear_auth();
131                config.save()?;
132
133                println!("{} Logged out successfully", "āœ“".green());
134                Ok(())
135            }
136
137            AuthCommands::Status => {
138                let auth_manager = AuthManager::new()?;
139                let config = Config::load()?;
140
141                if auth_manager.is_authenticated() {
142                    println!("{} Authenticated", "āœ“".green());
143
144                    // Show credential type
145                    if let Ok(Some(credential)) = auth_manager.get_credentials() {
146                        println!("  {} {}", "Method:".dimmed(), credential.type_name());
147                        
148                        if credential.is_oauth() && credential.needs_refresh() {
149                            println!("  {} {}", "Status:".dimmed(), "Token needs refresh".yellow());
150                        }
151                    }
152
153                    if let Some(username) = config.username() {
154                        println!("  {} {}", "Username:".dimmed(), username);
155                    }
156
157                    if let Some(workspace) = config.default_workspace() {
158                        println!("  {} {}", "Workspace:".dimmed(), workspace);
159                    }
160
161                    // Test the credentials
162                    match crate::api::BitbucketClient::from_stored() {
163                        Ok(client) => match client.get::<serde_json::Value>("/user").await {
164                            Ok(user) => {
165                                if let Some(display_name) = user.get("display_name") {
166                                    println!(
167                                        "  {} {}",
168                                        "Display name:".dimmed(),
169                                        display_name.as_str().unwrap_or("Unknown")
170                                    );
171                                }
172                            }
173                            Err(e) => {
174                                println!("{} Credentials may be invalid: {}", "⚠".yellow(), e);
175                            }
176                        },
177                        Err(e) => {
178                            println!("{} Failed to create client: {}", "āœ—".red(), e);
179                        }
180                    }
181                } else {
182                    println!("{} Not authenticated", "āœ—".red());
183                    println!();
184                    println!("Run {} to authenticate", "bitbucket auth login".cyan());
185                    println!();
186                    println!("šŸ’” Tip: Use {} for the best experience", "--oauth".green());
187                }
188
189                Ok(())
190            }
191        }
192    }
193}