Skip to main content

bitbucket_cli/cli/
auth.rs

1use anyhow::Result;
2use clap::Subcommand;
3use colored::Colorize;
4use dialoguer::Input;
5
6use crate::auth::{AuthManager, OAuthFlow};
7use crate::config::Config;
8
9#[derive(Subcommand)]
10pub enum AuthCommands {
11    /// Authenticate with Bitbucket via OAuth 2.0
12    Login {
13        /// OAuth Client ID
14        #[arg(long, env = "BITBUCKET_CLIENT_ID")]
15        client_id: Option<String>,
16
17        /// OAuth Client Secret
18        #[arg(long, env = "BITBUCKET_CLIENT_SECRET")]
19        client_secret: Option<String>,
20    },
21
22    /// Remove stored credentials
23    Logout,
24
25    /// Show authentication status
26    Status,
27}
28
29impl AuthCommands {
30    pub async fn run(self) -> Result<()> {
31        match self {
32            AuthCommands::Login {
33                client_id,
34                client_secret,
35            } => {
36                let auth_manager = AuthManager::new()?;
37
38                // Resolve consumer credentials from (in priority):
39                // 1. CLI flags / env vars
40                // 2. Previously stored credentials
41                // 3. Interactive prompt (first-time only)
42                let stored_consumer = auth_manager
43                    .get_credentials()
44                    .ok()
45                    .flatten()
46                    .and_then(|c| {
47                        c.oauth_consumer_credentials()
48                            .map(|(id, secret)| (id.to_owned(), secret.to_owned()))
49                    });
50
51                let client_id = client_id
52                    .or_else(|| stored_consumer.as_ref().map(|(id, _)| id.clone()))
53                    .or_else(|| {
54                        println!();
55                        println!("📋 OAuth Consumer Setup Required");
56                        println!();
57                        println!("To use OAuth authentication, create an OAuth consumer in Bitbucket:");
58                        println!("1. Go to: https://bitbucket.org/[workspace]/workspace/settings/oauth-consumers/new");
59                        println!("2. Set callback URL to ONE of these (pick any available port):");
60                        println!("   • http://127.0.0.1:8080/callback");
61                        println!("   • http://127.0.0.1:3000/callback");
62                        println!("   • http://127.0.0.1:8888/callback");
63                        println!("   • http://127.0.0.1:9000/callback");
64                        println!("3. Select required permissions:");
65                        println!("   ✓ Account (Read)");
66                        println!("   ✓ Repositories (Read)");
67                        println!("   ✓ Pull requests (Read, Write)");
68                        println!("   ✓ Issues (Read, Write)");
69                        println!("   ✓ Pipelines (Read, Write)");
70                        println!("4. Copy the Key (Client ID) and Secret");
71                        println!();
72
73                        Input::<String>::new()
74                            .with_prompt("OAuth Client ID (Key)")
75                            .interact_text()
76                            .ok()
77                    })
78                    .ok_or_else(|| anyhow::anyhow!("OAuth Client ID is required"))?;
79
80                let client_secret = client_secret
81                    .or_else(|| stored_consumer.map(|(_, secret)| secret))
82                    .or_else(|| {
83                        Input::<String>::new()
84                            .with_prompt("OAuth Client Secret")
85                            .interact_text()
86                            .ok()
87                    })
88                    .ok_or_else(|| anyhow::anyhow!("OAuth Client Secret is required"))?;
89
90                let oauth = OAuthFlow::new(client_id, client_secret);
91                oauth.authenticate(&auth_manager).await?;
92
93                Ok(())
94            }
95
96            AuthCommands::Logout => {
97                let auth_manager = AuthManager::new()?;
98                auth_manager.clear_credentials()?;
99
100                let mut config = Config::load()?;
101                config.clear_auth();
102                config.save()?;
103
104                println!("{} Logged out successfully", "✓".green());
105                Ok(())
106            }
107
108            AuthCommands::Status => {
109                let auth_manager = AuthManager::new()?;
110                let config = Config::load()?;
111
112                if auth_manager.is_authenticated() {
113                    println!("{} Authenticated via OAuth 2.0", "✓".green());
114
115                    if let Ok(Some(credential)) = auth_manager.get_credentials() {
116                        if credential.needs_refresh() {
117                            println!(
118                                "  {} {}",
119                                "Status:".dimmed(),
120                                "Token needs refresh (will auto-refresh on next use)".yellow()
121                            );
122                        }
123                    }
124
125                    if let Some(username) = config.username() {
126                        println!("  {} {}", "Username:".dimmed(), username);
127                    }
128
129                    if let Some(workspace) = config.default_workspace() {
130                        println!("  {} {}", "Workspace:".dimmed(), workspace);
131                    }
132
133                    match crate::api::BitbucketClient::from_stored().await {
134                        Ok(client) => match client.get::<serde_json::Value>("/user").await {
135                            Ok(user) => {
136                                if let Some(display_name) = user.get("display_name") {
137                                    println!(
138                                        "  {} {}",
139                                        "Display name:".dimmed(),
140                                        display_name.as_str().unwrap_or("Unknown")
141                                    );
142                                }
143                            }
144                            Err(e) => {
145                                println!("{} Credentials may be invalid: {}", "âš ".yellow(), e);
146                            }
147                        },
148                        Err(e) => {
149                            println!("{} Failed to create client: {}", "✗".red(), e);
150                        }
151                    }
152                } else {
153                    println!("{} Not authenticated", "✗".red());
154                    println!();
155                    println!("Run {} to authenticate", "bitbucket auth login".cyan());
156                }
157
158                Ok(())
159            }
160        }
161    }
162}