-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement setup for bitbucket #3
Changes from all commits
4555f87
419cdde
3312b0d
393b8e2
e436bcb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
use std::env; | ||
use std::str; | ||
use std::process::Command; | ||
use std::time::{SystemTime, UNIX_EPOCH}; | ||
use reqwest::Client; | ||
use crate::db::auth::{save_auth_info_to_db, auth_info}; | ||
use crate::utils::auth::AuthInfo; | ||
|
||
pub async fn get_access_token_from_bitbucket(code: &str) -> Option<AuthInfo> { | ||
let client = Client::new(); | ||
let bitbucket_client_id = env::var("BITBUCKET_CLIENT_ID").unwrap(); | ||
let bitbucket_client_secret = env::var("BITBUCKET_CLIENT_SECRET").unwrap(); | ||
let mut params = std::collections::HashMap::new(); | ||
let redirect_uri = format!("{}/api/bitbucket/callbacks/install", | ||
env::var("SERVER_URL").expect("SERVER_URL must be set")); | ||
params.insert("client_id", bitbucket_client_id); | ||
params.insert("client_secret", bitbucket_client_secret); | ||
params.insert("code", code.to_owned()); | ||
params.insert("grant_type", "authorization_code".to_owned()); | ||
params.insert("redirect_uri", redirect_uri); | ||
println!("params = {:?}", ¶ms); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid using - println!("params = {:?}", ¶ms);
+ debug!("params = {:?}", ¶ms); |
||
let response = client | ||
.post("https://bitbucket.org/site/oauth2/access_token") | ||
.form(¶ms) | ||
.send() | ||
.await; | ||
if response.is_err() { | ||
let response_err = response.expect_err("No error in access token response"); | ||
eprintln!("error in calling api : {:?}", &response_err); | ||
return None; | ||
} | ||
let res = response.expect("Uncaught error in response"); | ||
if !res.status().is_success() { | ||
eprintln!( | ||
"Failed to exchange code for access token. Status code: {}, Response content: {}", | ||
res.status(), | ||
res.text().await.expect("No text in response") | ||
); | ||
return None; | ||
} | ||
let json_res = res.json::<AuthInfo>().await; | ||
if json_res.is_err() { | ||
let json_error = json_res.expect_err("Error not found in json"); | ||
eprintln!("error deserializing : {:?}", json_error); | ||
return None; | ||
} | ||
Comment on lines
+41
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Error handling could be improved by using the - if response.is_err() {
- let response_err = response.expect_err("No error in access token response");
- eprintln!("error in calling api : {:?}", &response_err);
- return None;
- }
- let res = response.expect("Uncaught error in response");
- if !res.status().is_success() {
- eprintln!(
- "Failed to exchange code for access token. Status code: {}, Response content: {}",
- res.status(),
- res.text().await.expect("No text in response")
- );
- return None;
- }
- let json_res = res.json::<AuthInfo>().await;
- if json_res.is_err() {
- let json_error = json_res.expect_err("Error not found in json");
- eprintln!("error deserializing : {:?}", json_error);
- return None;
- }
+ let res = response?;
+ if !res.status().is_success() {
+ return Err(anyhow::anyhow!(
+ "Failed to exchange code for access token. Status code: {}, Response content: {}",
+ res.status(),
+ res.text().await?
+ ));
+ }
+ let response_json = res.json::<AuthInfo>().await?; |
||
let mut response_json = json_res.expect("Uncaught error in deserializing response json"); | ||
save_auth_info_to_db(&mut response_json); | ||
return Some(response_json); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
use std::{env, collections::HashMap}; | ||
|
||
use reqwest::{Response, header::{HeaderMap, HeaderValue}}; | ||
use serde_json::Value; | ||
|
||
pub fn bitbucket_base_url() -> String { | ||
env::var("BITBUCKET_BASE_URL").expect("BITBUCKET_BASE_URL must be set") | ||
} | ||
|
||
pub async fn get_api(url: &str, access_token: &str, params: Option<HashMap<&str, &str>> ) -> Vec<Value> { | ||
let response_opt = call_get_api(url, access_token, ¶ms).await; | ||
println!("response of get_api = {:?}", &response_opt); | ||
let (mut response_values, next_url) = deserialize_response(response_opt).await; | ||
if next_url.is_some() { | ||
let mut page_values = get_all_pages(next_url, access_token, ¶ms).await; | ||
response_values.append(&mut page_values); | ||
} | ||
return response_values; | ||
} | ||
Comment on lines
+10
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function |
||
|
||
pub async fn call_get_api(url: &str, access_token: &str, params: &Option<HashMap<&str, &str>> ) -> Option<Response>{ | ||
println!("GET api url = {}", url); | ||
let client = reqwest::Client::new(); | ||
let mut headers = reqwest::header::HeaderMap::new(); | ||
headers.insert( reqwest::header::AUTHORIZATION, | ||
format!("Bearer {}", access_token).parse().expect("Invalid auth header"), ); | ||
headers.insert("Accept", | ||
"application/json".parse().expect("Invalid Accept header")); | ||
match params { | ||
Some(params) => { | ||
match client.get(url).headers(headers).query(params).send().await { | ||
Ok(response) => { | ||
if response.status().is_success() { | ||
return Some(response); | ||
} | ||
else { eprintln!("Failed to call API {}, status: {}", url, response.status()); } | ||
}, | ||
Err(e) => { eprintln!("Error sending GET request to {}, error: {}", url, e); }, | ||
}; | ||
}, | ||
None => { | ||
match client.get(url).headers(headers).send().await { | ||
Ok(response) => { | ||
if response.status().is_success() { | ||
return Some(response); | ||
} | ||
else { eprintln!("Failed to call API {}, status: {}", url, response.status()); } | ||
}, | ||
Err(e) => { eprintln!("Error sending GET request to {}, error: {}", url, e); }, | ||
}; | ||
} | ||
}; | ||
return None; | ||
} | ||
Comment on lines
+21
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function
Here's a suggested refactor: pub async fn call_get_api(url: &str, access_token: &str, params: &Option<HashMap<&str, &str>> ) -> Result<Response, reqwest::Error> {
let client = reqwest::Client::new();
let mut headers = HeaderMap::new();
headers.insert( reqwest::header::AUTHORIZATION, format!("Bearer {}", access_token).parse().expect("Invalid auth header"));
headers.insert("Accept", "application/json".parse().expect("Invalid Accept header"));
let request_builder = client.get(url).headers(headers);
let request_builder = match params {
Some(params) => request_builder.query(params),
None => request_builder,
};
let response = request_builder.send().await?;
if response.status().is_success() {
Ok(response)
} else {
Err(reqwest::Error::new(reqwest::StatusCode::from_u16(response.status().as_u16()).unwrap(), "Failed to call API"))
}
} |
||
|
||
async fn deserialize_response(response_opt: Option<Response>) -> (Vec<Value>, Option<String>) { | ||
let values_vec = Vec::new(); | ||
match response_opt { | ||
Some(response) => { | ||
match response.json::<serde_json::Value>().await { | ||
Ok(response_json) => { | ||
let mut values_vec = Vec::new(); | ||
if let Some(values) = response_json["values"].as_array() { | ||
for value in values { | ||
values_vec.push(value.to_owned()); | ||
} | ||
return (values_vec, Some(response_json["next"].to_string())); | ||
} | ||
} | ||
Err(e) => { eprintln!("Unable to deserialize response: {}", e); } | ||
}; | ||
}, | ||
None => { eprintln!("Response is None");} | ||
}; | ||
return (values_vec, None); | ||
} | ||
Comment on lines
+56
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function
|
||
|
||
async fn get_all_pages(next_url: Option<String>, access_token: &str, params: &Option<HashMap<&str, &str>>) -> Vec<Value>{ | ||
let mut values_vec = Vec::new(); | ||
let mut next_url = next_url; | ||
while next_url.is_some() { | ||
let url = next_url.as_ref().expect("next_url is none").trim_matches('"'); | ||
if url != "null" { | ||
let response_opt = call_get_api(url, access_token, params).await; | ||
let (mut response_values, url_opt) = deserialize_response(response_opt).await; | ||
next_url = url_opt.clone(); | ||
values_vec.append(&mut response_values); | ||
} else { | ||
break; | ||
} | ||
} | ||
return values_vec; | ||
} | ||
Comment on lines
+78
to
+93
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function
|
||
|
||
pub fn prepare_auth_headers(access_token: &str) -> HeaderMap{ | ||
let mut headers_map = HeaderMap::new(); | ||
let auth_header = format!("Bearer {}", access_token); | ||
let headervalres = HeaderValue::from_str(&auth_header); | ||
match headervalres { | ||
Ok(headerval) => { | ||
headers_map.insert("Authorization", headerval); | ||
}, | ||
Err(e) => panic!("Could not parse header value: {}", e), | ||
}; | ||
return headers_map; | ||
} | ||
Comment on lines
+95
to
+106
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function
Here's a suggested refactor: pub fn prepare_auth_headers(access_token: &str) -> Result<HeaderMap, reqwest::header::InvalidHeaderValue> {
let mut headers_map = HeaderMap::new();
let auth_header = format!("Bearer {}", access_token);
let headerval = HeaderValue::from_str(&auth_header)?;
headers_map.insert("Authorization", headerval);
Ok(headers_map)
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
pub mod auth; | ||
pub mod workspace; | ||
pub mod repo; | ||
mod config; | ||
pub mod webhook; | ||
pub mod user; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
use serde_json::Value; | ||
|
||
use crate::db::repo::save_repo_to_db; | ||
use crate::utils::repo::Repository; | ||
use super::config::{bitbucket_base_url, get_api}; | ||
|
||
pub async fn get_workspace_repos(workspace: &str, access_token: &str) -> Option<Vec<Repository>> { | ||
let repos_url = format!("{}/repositories/{}", bitbucket_base_url(), workspace); | ||
let response_json = get_api(&repos_url, access_token, None).await; | ||
let mut repos_data = Vec::new(); | ||
for repo_json in response_json { | ||
let val = Repository::new( | ||
repo_json["name"].to_string().trim_matches('"').to_string(), | ||
repo_json["uuid"].to_string().trim_matches('"').to_string(), | ||
repo_json["owner"]["username"].to_string().trim_matches('"').to_string(), | ||
repo_json["is_private"].as_bool().unwrap_or(false), | ||
repo_json["links"]["clone"].as_array() | ||
.expect("Unable to convert clone to array").iter().filter(|clone_val| { | ||
clone_val["name".to_string()].as_str() == Some("ssh") | ||
}).collect::<Vec<&Value>>()[0]["href"].to_string().replace('\"',""), | ||
repo_json["project"]["name"].to_string().trim_matches('"').to_string(), | ||
repo_json["workspace"]["slug"].to_string().trim_matches('"').to_string(), | ||
None, | ||
"bitbucket".to_string(), | ||
); | ||
save_repo_to_db(&val); | ||
repos_data.push(val); | ||
} | ||
Some(repos_data) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
use chrono::{DateTime, Utc, FixedOffset}; | ||
use crate::db::auth::auth_info; | ||
use crate::db::user::{save_user_to_db, user_from_db}; | ||
use crate::utils::auth::AuthInfo; | ||
use crate::utils::user::{User, Provider, ProviderEnum}; | ||
use super::config::{bitbucket_base_url, get_api, call_get_api}; | ||
|
||
pub async fn get_and_save_workspace_users(workspace_id: &str, access_token: &str) { | ||
let base_url = bitbucket_base_url(); | ||
let members_url = format!("{}/workspaces/{}/members", &base_url, workspace_id); | ||
let response_json = get_api(&members_url, access_token, None).await; | ||
Comment on lines
+9
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
for user_json in response_json { | ||
let provider_id = user_json["user"]["uuid"].to_string().replace('"', ""); | ||
let user = User::new( | ||
Provider::new( | ||
provider_id, | ||
ProviderEnum::Bitbucket), | ||
user_json["user"]["display_name"].to_string().replace('"', ""), | ||
user_json["workspace"]["slug"].to_string().replace('"', ""), | ||
None); | ||
save_user_to_db(&user); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
} | ||
Comment on lines
+12
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code assumes that the JSON response will always have the expected structure and fields. If the API response changes or if there's an error in the response, this could lead to panics due to accessing non-existent fields. It would be better to use a library like |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
use std::env; | ||
|
||
use reqwest::{header::HeaderValue, Response, Error}; | ||
use serde_json::json; | ||
|
||
use crate::{db::webhook::save_webhook_to_db, utils::webhook::{Webhook, WebhookResponse}, bitbucket::config::{bitbucket_base_url, get_api}}; | ||
|
||
use super::config::prepare_auth_headers; | ||
|
||
|
||
pub async fn get_webhooks_in_repo(workspace_slug: &str, repo_slug: &str, access_token: &str) -> Vec<Webhook> { | ||
let url = format!("{}/repositories/{}/{}/hooks", bitbucket_base_url(), workspace_slug, repo_slug); | ||
println!("Getting webhooks from {}", url); | ||
let response_json = get_api(&url, access_token, None).await; | ||
let mut webhooks = Vec::new(); | ||
for webhook_json in response_json { | ||
let active = matches!(webhook_json["active"].to_string().trim_matches('"'), "true" | "false"); | ||
let webhook = Webhook::new( | ||
webhook_json["uuid"].to_string(), | ||
active, | ||
webhook_json["created_at"].to_string().replace('"', ""), | ||
webhook_json["events"].as_array().expect("Unable to deserialize events").into_iter() | ||
.map(|events| events.as_str().expect("Unable to convert event").to_string()).collect(), | ||
webhook_json["links"]["self"]["href"].to_string().replace('"', ""), | ||
webhook_json["url"].to_string().replace('"', ""), | ||
); | ||
Comment on lines
+17
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code assumes that all fields in the webhook JSON are present and correctly formatted. If any of these assumptions are not met (for example, if a field is missing or is not the expected type), the program will panic. It would be better to handle these cases gracefully, perhaps by skipping over malformed webhooks or returning an error. |
||
webhooks.push(webhook); | ||
} | ||
return webhooks; | ||
} | ||
Comment on lines
+11
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function |
||
|
||
pub async fn add_webhook(workspace_slug: &str, repo_slug: &str, access_token: &str) { | ||
let url = format!( | ||
"{}/repositories/{}/{}/hooks", | ||
bitbucket_base_url(), workspace_slug, repo_slug | ||
); | ||
|
||
let mut headers_map = prepare_auth_headers(&access_token); | ||
headers_map.insert("Accept", HeaderValue::from_static("application/vnd.github+json")); | ||
let callback_url = format!("{}/api/bitbucket/callbacks/webhook", | ||
env::var("SERVER_URL").expect("SERVER_URL must be set")); | ||
let payload = json!({ | ||
"description": "Webhook for PRs when raised and when something is pushed to the open PRs", | ||
"url": callback_url, | ||
"active": true, | ||
"events": ["pullrequest:created", "pullrequest:updated"] | ||
}); | ||
|
||
let response = reqwest::Client::new() | ||
.post(&url) | ||
.headers(headers_map) | ||
.json(&payload) | ||
.send() | ||
.await; | ||
process_add_webhook_response(response).await; | ||
} | ||
Comment on lines
+32
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function |
||
|
||
async fn process_add_webhook_response(response: Result<Response, Error>){ | ||
if response.is_err() { | ||
let err = response.expect_err("No error in response"); | ||
eprintln!("Error in api call: {:?}", err); | ||
return; | ||
} | ||
let res = response.expect("Uncaught error in response"); | ||
if !res.status().is_success() { | ||
println!("Failed to add webhook. Status code: {}, Text: {:?}", | ||
res.status(), res.text().await); | ||
return; | ||
} | ||
let webhook_res = res.json::<WebhookResponse>().await; | ||
if webhook_res.is_err() { | ||
let err = webhook_res.expect_err("No error in webhook response"); | ||
return; | ||
} | ||
|
||
let webhook = webhook_res.expect("Uncaught error in webhook response"); | ||
let webhook_data = Webhook::new( | ||
webhook.uuid().to_string(), | ||
webhook.active(), | ||
webhook.created_at().to_string(), | ||
webhook.events().to_owned(), | ||
webhook.links()["self"]["href"].clone(), | ||
webhook.url().to_string(), | ||
); | ||
save_webhook_to_db(&webhook_data); | ||
} | ||
Comment on lines
+58
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
use super::config::{bitbucket_base_url, get_api}; | ||
use crate::db::owner::save_workspace_to_db; | ||
use crate::utils::owner::Workspace; | ||
pub async fn get_bitbucket_workspaces(access_token: &str) -> Vec<Workspace> { | ||
let user_url = format!("{}/workspaces", bitbucket_base_url()); | ||
let response = get_api(&user_url, access_token, None).await; | ||
let mut workspace_vec = Vec::new(); | ||
for workspace_json in response { | ||
let val = serde_json::from_value::<Workspace>(workspace_json.clone()).expect("Unable to deserialize workspace"); | ||
save_workspace_to_db(&val); | ||
workspace_vec.push(val); | ||
} | ||
return workspace_vec; | ||
} | ||
Comment on lines
+1
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function let user_url = format!("{}/workspaces", bitbucket_base_url());
let response = match get_api(&user_url, access_token, None).await {
Ok(res) => res,
Err(e) => {
eprintln!("Failed to fetch workspaces: {}", e);
return Vec::new();
}
};
let mut workspace_vec = Vec::new();
for workspace_json in response {
let val = serde_json::from_value::<Workspace>(workspace_json.clone()).expect("Unable to deserialize workspace");
if let Err(e) = save_workspace_to_db(&val) {
eprintln!("Failed to save workspace to DB: {}", e);
continue;
}
workspace_vec.push(val);
}
return workspace_vec;
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
pub mod setup; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of
unwrap()
on environment variables can lead to a panic if the variables are not set. It's better to handle these errors gracefully.