mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 01:27:31 +08:00
chore(users): add hubspot tracking to prod intent (#7798)
Co-authored-by: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com>
This commit is contained in:
@ -49,18 +49,6 @@ impl Default for super::settings::Database {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for super::settings::Proxy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
http_url: Default::default(),
|
||||
https_url: Default::default(),
|
||||
idle_pool_connection_timeout: Some(90),
|
||||
bypass_proxy_hosts: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for super::settings::Locker {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
|
||||
@ -525,6 +525,7 @@ pub(crate) async fn fetch_raw_secrets(
|
||||
decision: conf.decision,
|
||||
locker_based_open_banking_connectors: conf.locker_based_open_banking_connectors,
|
||||
grpc_client: conf.grpc_client,
|
||||
crm: conf.crm,
|
||||
#[cfg(feature = "v2")]
|
||||
cell_information: conf.cell_information,
|
||||
network_tokenization_supported_card_networks: conf
|
||||
|
||||
@ -13,6 +13,7 @@ use error_stack::ResultExt;
|
||||
#[cfg(feature = "email")]
|
||||
use external_services::email::EmailSettings;
|
||||
use external_services::{
|
||||
crm::CrmManagerConfig,
|
||||
file_storage::FileStorageConfig,
|
||||
grpc_client::GrpcClientSettings,
|
||||
managers::{
|
||||
@ -21,8 +22,11 @@ use external_services::{
|
||||
},
|
||||
};
|
||||
pub use hyperswitch_interfaces::configs::Connectors;
|
||||
use hyperswitch_interfaces::secrets_interface::secret_state::{
|
||||
RawSecret, SecretState, SecretStateContainer, SecuredSecret,
|
||||
use hyperswitch_interfaces::{
|
||||
secrets_interface::secret_state::{
|
||||
RawSecret, SecretState, SecretStateContainer, SecuredSecret,
|
||||
},
|
||||
types::Proxy,
|
||||
};
|
||||
use masking::Secret;
|
||||
pub use payment_methods::configs::settings::{
|
||||
@ -96,6 +100,7 @@ pub struct Settings<S: SecretState> {
|
||||
#[cfg(feature = "email")]
|
||||
pub email: EmailSettings,
|
||||
pub user: UserSettings,
|
||||
pub crm: CrmManagerConfig,
|
||||
pub cors: CorsSettings,
|
||||
pub mandates: Mandates,
|
||||
pub zero_mandates: ZeroMandates,
|
||||
@ -714,15 +719,6 @@ pub struct Jwekey {
|
||||
pub tunnel_private_key: Secret<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct Proxy {
|
||||
pub http_url: Option<String>,
|
||||
pub https_url: Option<String>,
|
||||
pub idle_pool_connection_timeout: Option<u64>,
|
||||
pub bypass_proxy_hosts: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct Server {
|
||||
@ -988,6 +984,10 @@ impl Settings<SecuredSecret> {
|
||||
.validate()
|
||||
.map_err(|err| ApplicationError::InvalidConfigurationValueError(err.to_string()))?;
|
||||
|
||||
self.crm
|
||||
.validate()
|
||||
.map_err(|err| ApplicationError::InvalidConfigurationValueError(err.to_string()))?;
|
||||
|
||||
self.lock_settings.validate()?;
|
||||
self.events.validate()?;
|
||||
|
||||
|
||||
@ -54,7 +54,6 @@ pub(crate) const API_KEY_LENGTH: usize = 64;
|
||||
// OID (Object Identifier) for the merchant ID field extension.
|
||||
pub(crate) const MERCHANT_ID_FIELD_EXTENSION_ID: &str = "1.2.840.113635.100.6.32";
|
||||
|
||||
pub(crate) const METRICS_HOST_TAG_NAME: &str = "host";
|
||||
pub const MAX_ROUTING_CONFIGS_PER_MERCHANT: usize = 100;
|
||||
pub const ROUTING_CONFIG_ID_LENGTH: usize = 10;
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ use diesel_models::configs;
|
||||
#[cfg(all(any(feature = "v1", feature = "v2"), feature = "olap"))]
|
||||
use diesel_models::{business_profile::CardTestingGuardConfig, organization::OrganizationBridge};
|
||||
use error_stack::{report, FutureExt, ResultExt};
|
||||
use external_services::http_client::client;
|
||||
use hyperswitch_domain_models::merchant_connector_account::{
|
||||
FromRequestEncryptableMerchantConnectorAccount, UpdateEncryptableMerchantConnectorAccount,
|
||||
};
|
||||
@ -39,7 +40,7 @@ use crate::{
|
||||
routes::{metrics, SessionState},
|
||||
services::{
|
||||
self,
|
||||
api::{self as service_api, client},
|
||||
api::{self as service_api},
|
||||
authentication, pm_auth as payment_initiation_service,
|
||||
},
|
||||
types::{
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use api_models::{admin::MerchantConnectorUpdate, connector_onboarding as api};
|
||||
use common_utils::ext_traits::Encode;
|
||||
use error_stack::ResultExt;
|
||||
pub use external_services::http_client;
|
||||
use masking::{ExposeInterface, PeekInterface, Secret};
|
||||
|
||||
use crate::{
|
||||
@ -8,7 +9,7 @@ use crate::{
|
||||
admin,
|
||||
errors::{ApiErrorResponse, RouterResult},
|
||||
},
|
||||
services::{send_request, ApplicationResponse, Request},
|
||||
services::{ApplicationResponse, Request},
|
||||
types::{self as oss_types, api as oss_api_types, api::connector_onboarding as types},
|
||||
utils::connector_onboarding as utils,
|
||||
SessionState,
|
||||
@ -47,7 +48,7 @@ pub async fn get_action_url_from_paypal(
|
||||
return_url,
|
||||
))
|
||||
.await?;
|
||||
let referral_response = send_request(&state, referral_request, None)
|
||||
let referral_response = http_client::send_request(&state.conf.proxy, referral_request, None)
|
||||
.await
|
||||
.change_context(ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to send request to paypal referrals")?;
|
||||
@ -97,10 +98,11 @@ pub async fn sync_merchant_onboarding_status(
|
||||
let merchant_details_request =
|
||||
utils::paypal::build_paypal_get_request(merchant_details_url, access_token.token.expose())?;
|
||||
|
||||
let merchant_details_response = send_request(&state, merchant_details_request, None)
|
||||
.await
|
||||
.change_context(ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to send request to paypal merchant details")?;
|
||||
let merchant_details_response =
|
||||
http_client::send_request(&state.conf.proxy, merchant_details_request, None)
|
||||
.await
|
||||
.change_context(ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to send request to paypal merchant details")?;
|
||||
|
||||
let parsed_response: types::paypal::SellerStatusDetailsResponse = merchant_details_response
|
||||
.json()
|
||||
@ -121,10 +123,11 @@ async fn find_paypal_merchant_by_tracking_id(
|
||||
merchant_onboarding_status_url(state.clone(), tracking_id),
|
||||
access_token.token.peek().to_string(),
|
||||
)?;
|
||||
let seller_status_response = send_request(&state, seller_status_request, None)
|
||||
.await
|
||||
.change_context(ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to send request to paypal onboarding status")?;
|
||||
let seller_status_response =
|
||||
http_client::send_request(&state.conf.proxy, seller_status_request, None)
|
||||
.await
|
||||
.change_context(ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to send request to paypal onboarding status")?;
|
||||
|
||||
if seller_status_response.status().is_success() {
|
||||
return Ok(Some(
|
||||
|
||||
@ -17,6 +17,7 @@ use crate::{
|
||||
storage,
|
||||
transformers::ForeignTryFrom,
|
||||
},
|
||||
utils::user as user_utils,
|
||||
SessionState,
|
||||
};
|
||||
|
||||
@ -80,7 +81,7 @@ pub async fn send_recon_request(
|
||||
state
|
||||
.email_client
|
||||
.compose_and_send_email(
|
||||
email_types::get_base_url(&state),
|
||||
user_utils::get_base_url(&state),
|
||||
Box::new(email_contents),
|
||||
state.conf.proxy.https_url.as_ref(),
|
||||
)
|
||||
@ -211,7 +212,7 @@ pub async fn recon_merchant_account_update(
|
||||
let _ = state
|
||||
.email_client
|
||||
.compose_and_send_email(
|
||||
email_types::get_base_url(&state),
|
||||
user_utils::get_base_url(&state),
|
||||
Box::new(email_contents),
|
||||
state.conf.proxy.https_url.as_ref(),
|
||||
)
|
||||
|
||||
@ -32,8 +32,6 @@ use user_api::dashboard_metadata::SetMetaDataRequest;
|
||||
#[cfg(feature = "v1")]
|
||||
use super::admin;
|
||||
use super::errors::{StorageErrorExt, UserErrors, UserResponse, UserResult};
|
||||
#[cfg(feature = "email")]
|
||||
use crate::services::email::types as email_types;
|
||||
#[cfg(feature = "v1")]
|
||||
use crate::types::transformers::ForeignFrom;
|
||||
use crate::{
|
||||
@ -51,6 +49,8 @@ use crate::{
|
||||
user::{theme as theme_utils, two_factor_auth as tfa_utils},
|
||||
},
|
||||
};
|
||||
#[cfg(feature = "email")]
|
||||
use crate::{services::email::types as email_types, utils::user as user_utils};
|
||||
|
||||
pub mod dashboard_metadata;
|
||||
#[cfg(feature = "dummy_connector")]
|
||||
@ -99,7 +99,7 @@ pub async fn signup_with_merchant_id(
|
||||
let send_email_result = state
|
||||
.email_client
|
||||
.compose_and_send_email(
|
||||
email_types::get_base_url(&state),
|
||||
user_utils::get_base_url(&state),
|
||||
Box::new(email_contents),
|
||||
state.conf.proxy.https_url.as_ref(),
|
||||
)
|
||||
@ -319,7 +319,7 @@ pub async fn connect_account(
|
||||
let send_email_result = state
|
||||
.email_client
|
||||
.compose_and_send_email(
|
||||
email_types::get_base_url(&state),
|
||||
user_utils::get_base_url(&state),
|
||||
Box::new(email_contents),
|
||||
state.conf.proxy.https_url.as_ref(),
|
||||
)
|
||||
@ -374,7 +374,7 @@ pub async fn connect_account(
|
||||
let magic_link_result = state
|
||||
.email_client
|
||||
.compose_and_send_email(
|
||||
email_types::get_base_url(&state),
|
||||
user_utils::get_base_url(&state),
|
||||
Box::new(magic_link_email),
|
||||
state.conf.proxy.https_url.as_ref(),
|
||||
)
|
||||
@ -390,7 +390,7 @@ pub async fn connect_account(
|
||||
let welcome_email_result = state
|
||||
.email_client
|
||||
.compose_and_send_email(
|
||||
email_types::get_base_url(&state),
|
||||
user_utils::get_base_url(&state),
|
||||
Box::new(welcome_to_community_email),
|
||||
state.conf.proxy.https_url.as_ref(),
|
||||
)
|
||||
@ -525,7 +525,7 @@ pub async fn forgot_password(
|
||||
state
|
||||
.email_client
|
||||
.compose_and_send_email(
|
||||
email_types::get_base_url(&state),
|
||||
user_utils::get_base_url(&state),
|
||||
Box::new(email_contents),
|
||||
state.conf.proxy.https_url.as_ref(),
|
||||
)
|
||||
@ -937,7 +937,7 @@ async fn handle_existing_user_invitation(
|
||||
is_email_sent = state
|
||||
.email_client
|
||||
.compose_and_send_email(
|
||||
email_types::get_base_url(state),
|
||||
user_utils::get_base_url(state),
|
||||
Box::new(email_contents),
|
||||
state.conf.proxy.https_url.as_ref(),
|
||||
)
|
||||
@ -1092,7 +1092,7 @@ async fn handle_new_user_invitation(
|
||||
let send_email_result = state
|
||||
.email_client
|
||||
.compose_and_send_email(
|
||||
email_types::get_base_url(state),
|
||||
user_utils::get_base_url(state),
|
||||
Box::new(email_contents),
|
||||
state.conf.proxy.https_url.as_ref(),
|
||||
)
|
||||
@ -1247,7 +1247,7 @@ pub async fn resend_invite(
|
||||
state
|
||||
.email_client
|
||||
.compose_and_send_email(
|
||||
email_types::get_base_url(&state),
|
||||
user_utils::get_base_url(&state),
|
||||
Box::new(email_contents),
|
||||
state.conf.proxy.https_url.as_ref(),
|
||||
)
|
||||
@ -1967,7 +1967,7 @@ pub async fn send_verification_mail(
|
||||
state
|
||||
.email_client
|
||||
.compose_and_send_email(
|
||||
email_types::get_base_url(&state),
|
||||
user_utils::get_base_url(&state),
|
||||
Box::new(email_contents),
|
||||
state.conf.proxy.https_url.as_ref(),
|
||||
)
|
||||
|
||||
@ -8,10 +8,10 @@ use diesel_models::{
|
||||
enums::DashboardMetadata as DBEnum, user::dashboard_metadata::DashboardMetadata,
|
||||
};
|
||||
use error_stack::{report, ResultExt};
|
||||
use hyperswitch_interfaces::crm::CrmPayload;
|
||||
#[cfg(feature = "email")]
|
||||
use masking::ExposeInterface;
|
||||
use masking::PeekInterface;
|
||||
#[cfg(feature = "email")]
|
||||
use router_env::logger;
|
||||
|
||||
use crate::{
|
||||
@ -19,7 +19,7 @@ use crate::{
|
||||
routes::{app::ReqState, SessionState},
|
||||
services::{authentication::UserFromToken, ApplicationResponse},
|
||||
types::domain::{self, user::dashboard_metadata as types, MerchantKeyStore},
|
||||
utils::user::dashboard_metadata as utils,
|
||||
utils::user::{self as user_utils, dashboard_metadata as utils},
|
||||
};
|
||||
#[cfg(feature = "email")]
|
||||
use crate::{services::email::types as email_types, utils::user::theme as theme_utils};
|
||||
@ -501,7 +501,7 @@ async fn insert_metadata(
|
||||
.await?;
|
||||
let email_contents = email_types::BizEmailProd::new(
|
||||
state,
|
||||
data,
|
||||
data.clone(),
|
||||
theme.as_ref().map(|theme| theme.theme_id.clone()),
|
||||
theme
|
||||
.map(|theme| theme.email_config())
|
||||
@ -510,7 +510,7 @@ async fn insert_metadata(
|
||||
let send_email_result = state
|
||||
.email_client
|
||||
.compose_and_send_email(
|
||||
email_types::get_base_url(state),
|
||||
user_utils::get_base_url(state),
|
||||
Box::new(email_contents),
|
||||
state.conf.proxy.https_url.as_ref(),
|
||||
)
|
||||
@ -519,6 +519,43 @@ async fn insert_metadata(
|
||||
}
|
||||
}
|
||||
|
||||
// Hubspot integration
|
||||
let hubspot_body = state
|
||||
.crm_client
|
||||
.make_body(CrmPayload {
|
||||
legal_business_name: data.legal_business_name,
|
||||
business_label: data.business_label,
|
||||
business_location: data.business_location,
|
||||
display_name: data.display_name,
|
||||
poc_email: data.poc_email,
|
||||
business_type: data.business_type,
|
||||
business_identifier: data.business_identifier,
|
||||
business_website: data.business_website,
|
||||
poc_name: data.poc_name,
|
||||
poc_contact: data.poc_contact,
|
||||
comments: data.comments,
|
||||
is_completed: data.is_completed,
|
||||
business_country_name: data.business_country_name,
|
||||
})
|
||||
.await;
|
||||
let base_url = user_utils::get_base_url(state);
|
||||
let hubspot_request = state
|
||||
.crm_client
|
||||
.make_request(hubspot_body, base_url.to_string())
|
||||
.await;
|
||||
|
||||
let _ = state
|
||||
.crm_client
|
||||
.send_request(&state.conf.proxy, hubspot_request)
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
logger::error!(
|
||||
"An error occurred while sending data to hubspot for user_id {}: {:?}",
|
||||
user.user_id,
|
||||
err
|
||||
);
|
||||
});
|
||||
|
||||
metadata
|
||||
}
|
||||
types::MetaData::SPTestPayment(data) => {
|
||||
|
||||
@ -17,6 +17,7 @@ use external_services::{
|
||||
grpc_client::{GrpcClients, GrpcHeaders},
|
||||
};
|
||||
use hyperswitch_interfaces::{
|
||||
crm::CrmInterface,
|
||||
encryption_interface::EncryptionManagementInterface,
|
||||
secrets_interface::secret_state::{RawSecret, SecuredSecret},
|
||||
};
|
||||
@ -124,6 +125,7 @@ pub struct SessionState {
|
||||
pub grpc_client: Arc<GrpcClients>,
|
||||
pub theme_storage_client: Arc<dyn FileStorageInterface>,
|
||||
pub locale: String,
|
||||
pub crm_client: Arc<dyn CrmInterface>,
|
||||
}
|
||||
impl scheduler::SchedulerSessionState for SessionState {
|
||||
fn get_db(&self) -> Box<dyn SchedulerInterface> {
|
||||
@ -235,6 +237,7 @@ pub struct AppState {
|
||||
pub encryption_client: Arc<dyn EncryptionManagementInterface>,
|
||||
pub grpc_client: Arc<GrpcClients>,
|
||||
pub theme_storage_client: Arc<dyn FileStorageInterface>,
|
||||
pub crm_client: Arc<dyn CrmInterface>,
|
||||
}
|
||||
impl scheduler::SchedulerAppState for AppState {
|
||||
fn get_tenants(&self) -> Vec<id_type::TenantId> {
|
||||
@ -396,6 +399,7 @@ impl AppState {
|
||||
|
||||
let file_storage_client = conf.file_storage.get_file_storage_client().await;
|
||||
let theme_storage_client = conf.theme.storage.get_file_storage_client().await;
|
||||
let crm_client = conf.crm.get_crm_client().await;
|
||||
|
||||
let grpc_client = conf.grpc_client.get_grpc_client_interface().await;
|
||||
|
||||
@ -418,6 +422,7 @@ impl AppState {
|
||||
encryption_client,
|
||||
grpc_client,
|
||||
theme_storage_client,
|
||||
crm_client,
|
||||
}
|
||||
})
|
||||
.await
|
||||
@ -511,6 +516,7 @@ impl AppState {
|
||||
grpc_client: Arc::clone(&self.grpc_client),
|
||||
theme_storage_client: self.theme_storage_client.clone(),
|
||||
locale: locale.unwrap_or(common_utils::consts::DEFAULT_LOCALE.to_string()),
|
||||
crm_client: self.crm_client.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,6 @@ counter_metric!(KV_MISS, GLOBAL_METER); // No. of KV misses
|
||||
// API Level Metrics
|
||||
counter_metric!(REQUESTS_RECEIVED, GLOBAL_METER);
|
||||
histogram_metric_f64!(REQUEST_TIME, GLOBAL_METER);
|
||||
histogram_metric_f64!(EXTERNAL_REQUEST_TIME, GLOBAL_METER);
|
||||
|
||||
// Operation Level Metrics
|
||||
counter_metric!(PAYMENT_OPS_COUNT, GLOBAL_METER);
|
||||
@ -99,7 +98,6 @@ counter_metric!(APPLE_PAY_MANUAL_FLOW_FAILED_PAYMENT, GLOBAL_METER);
|
||||
counter_metric!(APPLE_PAY_SIMPLIFIED_FLOW_FAILED_PAYMENT, GLOBAL_METER);
|
||||
|
||||
// Metrics for Payment Auto Retries
|
||||
counter_metric!(AUTO_RETRY_CONNECTION_CLOSED, GLOBAL_METER);
|
||||
counter_metric!(AUTO_RETRY_ELIGIBLE_REQUEST_COUNT, GLOBAL_METER);
|
||||
counter_metric!(AUTO_RETRY_GSM_MISS_COUNT, GLOBAL_METER);
|
||||
counter_metric!(AUTO_RETRY_GSM_FETCH_FAILURE_COUNT, GLOBAL_METER);
|
||||
|
||||
@ -3,7 +3,6 @@ pub mod generic_link_response;
|
||||
pub mod request;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
error::Error,
|
||||
fmt::Debug,
|
||||
future::Future,
|
||||
str,
|
||||
@ -52,7 +51,6 @@ use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use tera::{Context, Error as TeraError, Tera};
|
||||
|
||||
use self::request::{HeaderExt, RequestBuilderExt};
|
||||
use super::{
|
||||
authentication::AuthenticateAndFetch,
|
||||
connector_integration_interface::BoxedConnectorIntegrationInterface,
|
||||
@ -431,178 +429,6 @@ pub async fn call_connector_api(
|
||||
handle_response(response).await
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn send_request(
|
||||
state: &SessionState,
|
||||
request: Request,
|
||||
option_timeout_secs: Option<u64>,
|
||||
) -> CustomResult<reqwest::Response, errors::ApiClientError> {
|
||||
logger::info!(method=?request.method, headers=?request.headers, payload=?request.body, ?request);
|
||||
|
||||
let url =
|
||||
url::Url::parse(&request.url).change_context(errors::ApiClientError::UrlParsingFailed)?;
|
||||
|
||||
let client = client::create_client(
|
||||
&state.conf.proxy,
|
||||
request.certificate,
|
||||
request.certificate_key,
|
||||
)?;
|
||||
|
||||
let headers = request.headers.construct_header_map()?;
|
||||
let metrics_tag = router_env::metric_attributes!((
|
||||
consts::METRICS_HOST_TAG_NAME,
|
||||
url.host_str().unwrap_or_default().to_owned()
|
||||
));
|
||||
let request = {
|
||||
match request.method {
|
||||
Method::Get => client.get(url),
|
||||
Method::Post => {
|
||||
let client = client.post(url);
|
||||
match request.body {
|
||||
Some(RequestContent::Json(payload)) => client.json(&payload),
|
||||
Some(RequestContent::FormData(form)) => client.multipart(form),
|
||||
Some(RequestContent::FormUrlEncoded(payload)) => client.form(&payload),
|
||||
Some(RequestContent::Xml(payload)) => {
|
||||
let body = quick_xml::se::to_string(&payload)
|
||||
.change_context(errors::ApiClientError::BodySerializationFailed)?;
|
||||
client.body(body).header("Content-Type", "application/xml")
|
||||
}
|
||||
Some(RequestContent::RawBytes(payload)) => client.body(payload),
|
||||
None => client,
|
||||
}
|
||||
}
|
||||
Method::Put => {
|
||||
let client = client.put(url);
|
||||
match request.body {
|
||||
Some(RequestContent::Json(payload)) => client.json(&payload),
|
||||
Some(RequestContent::FormData(form)) => client.multipart(form),
|
||||
Some(RequestContent::FormUrlEncoded(payload)) => client.form(&payload),
|
||||
Some(RequestContent::Xml(payload)) => {
|
||||
let body = quick_xml::se::to_string(&payload)
|
||||
.change_context(errors::ApiClientError::BodySerializationFailed)?;
|
||||
client.body(body).header("Content-Type", "application/xml")
|
||||
}
|
||||
Some(RequestContent::RawBytes(payload)) => client.body(payload),
|
||||
None => client,
|
||||
}
|
||||
}
|
||||
Method::Patch => {
|
||||
let client = client.patch(url);
|
||||
match request.body {
|
||||
Some(RequestContent::Json(payload)) => client.json(&payload),
|
||||
Some(RequestContent::FormData(form)) => client.multipart(form),
|
||||
Some(RequestContent::FormUrlEncoded(payload)) => client.form(&payload),
|
||||
Some(RequestContent::Xml(payload)) => {
|
||||
let body = quick_xml::se::to_string(&payload)
|
||||
.change_context(errors::ApiClientError::BodySerializationFailed)?;
|
||||
client.body(body).header("Content-Type", "application/xml")
|
||||
}
|
||||
Some(RequestContent::RawBytes(payload)) => client.body(payload),
|
||||
None => client,
|
||||
}
|
||||
}
|
||||
Method::Delete => client.delete(url),
|
||||
}
|
||||
.add_headers(headers)
|
||||
.timeout(Duration::from_secs(
|
||||
option_timeout_secs.unwrap_or(consts::REQUEST_TIME_OUT),
|
||||
))
|
||||
};
|
||||
|
||||
// We cannot clone the request type, because it has Form trait which is not cloneable. So we are cloning the request builder here.
|
||||
let cloned_send_request = request.try_clone().map(|cloned_request| async {
|
||||
cloned_request
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| match error {
|
||||
error if error.is_timeout() => {
|
||||
metrics::REQUEST_BUILD_FAILURE.add(1, &[]);
|
||||
errors::ApiClientError::RequestTimeoutReceived
|
||||
}
|
||||
error if is_connection_closed_before_message_could_complete(&error) => {
|
||||
metrics::REQUEST_BUILD_FAILURE.add(1, &[]);
|
||||
errors::ApiClientError::ConnectionClosedIncompleteMessage
|
||||
}
|
||||
_ => errors::ApiClientError::RequestNotSent(error.to_string()),
|
||||
})
|
||||
.attach_printable("Unable to send request to connector")
|
||||
});
|
||||
|
||||
let send_request = async {
|
||||
request
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| match error {
|
||||
error if error.is_timeout() => {
|
||||
metrics::REQUEST_BUILD_FAILURE.add(1, &[]);
|
||||
errors::ApiClientError::RequestTimeoutReceived
|
||||
}
|
||||
error if is_connection_closed_before_message_could_complete(&error) => {
|
||||
metrics::REQUEST_BUILD_FAILURE.add(1, &[]);
|
||||
errors::ApiClientError::ConnectionClosedIncompleteMessage
|
||||
}
|
||||
_ => errors::ApiClientError::RequestNotSent(error.to_string()),
|
||||
})
|
||||
.attach_printable("Unable to send request to connector")
|
||||
};
|
||||
|
||||
let response = common_utils::metrics::utils::record_operation_time(
|
||||
send_request,
|
||||
&metrics::EXTERNAL_REQUEST_TIME,
|
||||
metrics_tag,
|
||||
)
|
||||
.await;
|
||||
// Retry once if the response is connection closed.
|
||||
//
|
||||
// This is just due to the racy nature of networking.
|
||||
// hyper has a connection pool of idle connections, and it selected one to send your request.
|
||||
// Most of the time, hyper will receive the server’s FIN and drop the dead connection from its pool.
|
||||
// But occasionally, a connection will be selected from the pool
|
||||
// and written to at the same time the server is deciding to close the connection.
|
||||
// Since hyper already wrote some of the request,
|
||||
// it can’t really retry it automatically on a new connection, since the server may have acted already
|
||||
match response {
|
||||
Ok(response) => Ok(response),
|
||||
Err(error)
|
||||
if error.current_context()
|
||||
== &errors::ApiClientError::ConnectionClosedIncompleteMessage =>
|
||||
{
|
||||
metrics::AUTO_RETRY_CONNECTION_CLOSED.add(1, &[]);
|
||||
match cloned_send_request {
|
||||
Some(cloned_request) => {
|
||||
logger::info!(
|
||||
"Retrying request due to connection closed before message could complete"
|
||||
);
|
||||
common_utils::metrics::utils::record_operation_time(
|
||||
cloned_request,
|
||||
&metrics::EXTERNAL_REQUEST_TIME,
|
||||
metrics_tag,
|
||||
)
|
||||
.await
|
||||
}
|
||||
None => {
|
||||
logger::info!("Retrying request due to connection closed before message could complete failed as request is not cloneable");
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
err @ Err(_) => err,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_connection_closed_before_message_could_complete(error: &reqwest::Error) -> bool {
|
||||
let mut source = error.source();
|
||||
while let Some(err) = source {
|
||||
if let Some(hyper_err) = err.downcast_ref::<hyper::Error>() {
|
||||
if hyper_err.is_incomplete_message() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
source = err.source();
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn handle_response(
|
||||
response: CustomResult<reqwest::Response, errors::ApiClientError>,
|
||||
|
||||
@ -1,139 +1,20 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use base64::Engine;
|
||||
use common_utils::errors::ReportSwitchExt;
|
||||
use error_stack::ResultExt;
|
||||
pub use external_services::http_client::{self, client};
|
||||
use http::{HeaderValue, Method};
|
||||
use masking::{ExposeInterface, PeekInterface};
|
||||
use once_cell::sync::OnceCell;
|
||||
use hyperswitch_interfaces::types::Proxy;
|
||||
use masking::PeekInterface;
|
||||
use reqwest::multipart::Form;
|
||||
use router_env::tracing_actix_web::RequestId;
|
||||
|
||||
use super::{request::Maskable, Request};
|
||||
use crate::{
|
||||
configs::settings::Proxy,
|
||||
consts::BASE64_ENGINE,
|
||||
core::errors::{ApiClientError, CustomResult},
|
||||
routes::SessionState,
|
||||
};
|
||||
|
||||
static DEFAULT_CLIENT: OnceCell<reqwest::Client> = OnceCell::new();
|
||||
|
||||
fn get_client_builder(
|
||||
proxy_config: &Proxy,
|
||||
) -> CustomResult<reqwest::ClientBuilder, ApiClientError> {
|
||||
let mut client_builder = reqwest::Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.pool_idle_timeout(Duration::from_secs(
|
||||
proxy_config
|
||||
.idle_pool_connection_timeout
|
||||
.unwrap_or_default(),
|
||||
));
|
||||
|
||||
let proxy_exclusion_config =
|
||||
reqwest::NoProxy::from_string(&proxy_config.bypass_proxy_hosts.clone().unwrap_or_default());
|
||||
|
||||
// Proxy all HTTPS traffic through the configured HTTPS proxy
|
||||
if let Some(url) = proxy_config.https_url.as_ref() {
|
||||
client_builder = client_builder.proxy(
|
||||
reqwest::Proxy::https(url)
|
||||
.change_context(ApiClientError::InvalidProxyConfiguration)
|
||||
.attach_printable("HTTPS proxy configuration error")?
|
||||
.no_proxy(proxy_exclusion_config.clone()),
|
||||
);
|
||||
}
|
||||
|
||||
// Proxy all HTTP traffic through the configured HTTP proxy
|
||||
if let Some(url) = proxy_config.http_url.as_ref() {
|
||||
client_builder = client_builder.proxy(
|
||||
reqwest::Proxy::http(url)
|
||||
.change_context(ApiClientError::InvalidProxyConfiguration)
|
||||
.attach_printable("HTTP proxy configuration error")?
|
||||
.no_proxy(proxy_exclusion_config),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(client_builder)
|
||||
}
|
||||
|
||||
fn get_base_client(proxy_config: &Proxy) -> CustomResult<reqwest::Client, ApiClientError> {
|
||||
Ok(DEFAULT_CLIENT
|
||||
.get_or_try_init(|| {
|
||||
get_client_builder(proxy_config)?
|
||||
.build()
|
||||
.change_context(ApiClientError::ClientConstructionFailed)
|
||||
.attach_printable("Failed to construct base client")
|
||||
})?
|
||||
.clone())
|
||||
}
|
||||
|
||||
// We may need to use outbound proxy to connect to external world.
|
||||
// Precedence will be the environment variables, followed by the config.
|
||||
pub fn create_client(
|
||||
proxy_config: &Proxy,
|
||||
client_certificate: Option<masking::Secret<String>>,
|
||||
client_certificate_key: Option<masking::Secret<String>>,
|
||||
) -> CustomResult<reqwest::Client, ApiClientError> {
|
||||
match (client_certificate, client_certificate_key) {
|
||||
(Some(encoded_certificate), Some(encoded_certificate_key)) => {
|
||||
let client_builder = get_client_builder(proxy_config)?;
|
||||
|
||||
let identity = create_identity_from_certificate_and_key(
|
||||
encoded_certificate.clone(),
|
||||
encoded_certificate_key,
|
||||
)?;
|
||||
let certificate_list = create_certificate(encoded_certificate)?;
|
||||
let client_builder = certificate_list
|
||||
.into_iter()
|
||||
.fold(client_builder, |client_builder, certificate| {
|
||||
client_builder.add_root_certificate(certificate)
|
||||
});
|
||||
client_builder
|
||||
.identity(identity)
|
||||
.use_rustls_tls()
|
||||
.build()
|
||||
.change_context(ApiClientError::ClientConstructionFailed)
|
||||
.attach_printable("Failed to construct client with certificate and certificate key")
|
||||
}
|
||||
_ => get_base_client(proxy_config),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_identity_from_certificate_and_key(
|
||||
encoded_certificate: masking::Secret<String>,
|
||||
encoded_certificate_key: masking::Secret<String>,
|
||||
) -> Result<reqwest::Identity, error_stack::Report<ApiClientError>> {
|
||||
let decoded_certificate = BASE64_ENGINE
|
||||
.decode(encoded_certificate.expose())
|
||||
.change_context(ApiClientError::CertificateDecodeFailed)?;
|
||||
|
||||
let decoded_certificate_key = BASE64_ENGINE
|
||||
.decode(encoded_certificate_key.expose())
|
||||
.change_context(ApiClientError::CertificateDecodeFailed)?;
|
||||
|
||||
let certificate = String::from_utf8(decoded_certificate)
|
||||
.change_context(ApiClientError::CertificateDecodeFailed)?;
|
||||
|
||||
let certificate_key = String::from_utf8(decoded_certificate_key)
|
||||
.change_context(ApiClientError::CertificateDecodeFailed)?;
|
||||
|
||||
let key_chain = format!("{}{}", certificate_key, certificate);
|
||||
reqwest::Identity::from_pem(key_chain.as_bytes())
|
||||
.change_context(ApiClientError::CertificateDecodeFailed)
|
||||
}
|
||||
|
||||
pub fn create_certificate(
|
||||
encoded_certificate: masking::Secret<String>,
|
||||
) -> Result<Vec<reqwest::Certificate>, error_stack::Report<ApiClientError>> {
|
||||
let decoded_certificate = BASE64_ENGINE
|
||||
.decode(encoded_certificate.expose())
|
||||
.change_context(ApiClientError::CertificateDecodeFailed)?;
|
||||
|
||||
let certificate = String::from_utf8(decoded_certificate)
|
||||
.change_context(ApiClientError::CertificateDecodeFailed)?;
|
||||
reqwest::Certificate::from_pem_bundle(certificate.as_bytes())
|
||||
.change_context(ApiClientError::CertificateDecodeFailed)
|
||||
}
|
||||
|
||||
pub trait RequestBuilder: Send + Sync {
|
||||
fn json(&mut self, body: serde_json::Value);
|
||||
fn url_encoded_form(&mut self, body: serde_json::Value);
|
||||
@ -196,7 +77,8 @@ pub struct ProxyClient {
|
||||
|
||||
impl ProxyClient {
|
||||
pub fn new(proxy_config: &Proxy) -> CustomResult<Self, ApiClientError> {
|
||||
let client = get_client_builder(proxy_config)?
|
||||
let client = client::get_client_builder(proxy_config)
|
||||
.switch()?
|
||||
.build()
|
||||
.change_context(ApiClientError::InvalidProxyConfiguration)?;
|
||||
Ok(Self {
|
||||
@ -213,9 +95,10 @@ impl ProxyClient {
|
||||
) -> CustomResult<reqwest::Client, ApiClientError> {
|
||||
match (client_certificate, client_certificate_key) {
|
||||
(Some(certificate), Some(certificate_key)) => {
|
||||
let client_builder = get_client_builder(&self.proxy_config)?;
|
||||
let client_builder = client::get_client_builder(&self.proxy_config).switch()?;
|
||||
let identity =
|
||||
create_identity_from_certificate_and_key(certificate, certificate_key)?;
|
||||
client::create_identity_from_certificate_and_key(certificate, certificate_key)
|
||||
.switch()?;
|
||||
Ok(client_builder
|
||||
.identity(identity)
|
||||
.build()
|
||||
@ -314,7 +197,9 @@ impl ApiClient for ProxyClient {
|
||||
option_timeout_secs: Option<u64>,
|
||||
_forward_to_kafka: bool,
|
||||
) -> CustomResult<reqwest::Response, ApiClientError> {
|
||||
crate::services::send_request(state, request, option_timeout_secs).await
|
||||
http_client::send_request(&state.conf.proxy, request, option_timeout_secs)
|
||||
.await
|
||||
.switch()
|
||||
}
|
||||
|
||||
fn add_request_id(&mut self, request_id: RequestId) {
|
||||
|
||||
@ -1,48 +1,2 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
pub use common_utils::request::ContentType;
|
||||
use common_utils::request::Headers;
|
||||
use error_stack::ResultExt;
|
||||
pub use masking::{Mask, Maskable};
|
||||
use router_env::{instrument, tracing};
|
||||
|
||||
use crate::core::errors::{self, CustomResult};
|
||||
|
||||
pub(super) trait HeaderExt {
|
||||
fn construct_header_map(
|
||||
self,
|
||||
) -> CustomResult<reqwest::header::HeaderMap, errors::ApiClientError>;
|
||||
}
|
||||
|
||||
impl HeaderExt for Headers {
|
||||
fn construct_header_map(
|
||||
self,
|
||||
) -> CustomResult<reqwest::header::HeaderMap, errors::ApiClientError> {
|
||||
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
||||
|
||||
self.into_iter().try_fold(
|
||||
HeaderMap::new(),
|
||||
|mut header_map, (header_name, header_value)| {
|
||||
let header_name = HeaderName::from_str(&header_name)
|
||||
.change_context(errors::ApiClientError::HeaderMapConstructionFailed)?;
|
||||
let header_value = header_value.into_inner();
|
||||
let header_value = HeaderValue::from_str(&header_value)
|
||||
.change_context(errors::ApiClientError::HeaderMapConstructionFailed)?;
|
||||
header_map.append(header_name, header_value);
|
||||
Ok(header_map)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) trait RequestBuilderExt {
|
||||
fn add_headers(self, headers: reqwest::header::HeaderMap) -> Self;
|
||||
}
|
||||
|
||||
impl RequestBuilderExt for reqwest::RequestBuilder {
|
||||
#[instrument(skip_all)]
|
||||
fn add_headers(mut self, headers: reqwest::header::HeaderMap) -> Self {
|
||||
self = self.headers(headers);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@ -320,15 +320,6 @@ pub fn get_link_with_token(
|
||||
|
||||
email_url
|
||||
}
|
||||
|
||||
pub fn get_base_url(state: &SessionState) -> &str {
|
||||
if !state.conf.multitenancy.enabled {
|
||||
&state.conf.user.base_url
|
||||
} else {
|
||||
&state.tenant.user.control_center_url
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VerifyEmail {
|
||||
pub recipient_email: domain::UserEmail,
|
||||
pub settings: std::sync::Arc<configs::Settings>,
|
||||
@ -576,7 +567,7 @@ impl BizEmailProd {
|
||||
state.conf.email.prod_intent_recipient_email.clone(),
|
||||
)?,
|
||||
settings: state.conf.clone(),
|
||||
user_name: data.poc_name.unwrap_or_default().into(),
|
||||
user_name: data.poc_name.unwrap_or_default(),
|
||||
poc_email: data.poc_email.unwrap_or_default(),
|
||||
legal_business_name: data.legal_business_name.unwrap_or_default(),
|
||||
business_location: data
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
use common_utils::errors::ErrorSwitch;
|
||||
use error_stack::ResultExt;
|
||||
use external_services::http_client::client;
|
||||
use masking::{ExposeInterface, Secret};
|
||||
use oidc::TokenResponse;
|
||||
use openidconnect::{self as oidc, core as oidc_core};
|
||||
@ -9,7 +11,6 @@ use crate::{
|
||||
consts,
|
||||
core::errors::{UserErrors, UserResult},
|
||||
routes::SessionState,
|
||||
services::api::client,
|
||||
types::domain::user::UserEmail,
|
||||
};
|
||||
|
||||
@ -156,7 +157,7 @@ async fn get_oidc_reqwest_client(
|
||||
request: oidc::HttpRequest,
|
||||
) -> Result<oidc::HttpResponse, ApiClientError> {
|
||||
let client = client::create_client(&state.conf.proxy, None, None)
|
||||
.map_err(|e| e.current_context().to_owned())?;
|
||||
.map_err(|e| e.current_context().switch())?;
|
||||
|
||||
let mut request_builder = client
|
||||
.request(request.method, request.url)
|
||||
|
||||
@ -380,3 +380,11 @@ pub fn generate_env_specific_merchant_id(value: String) -> UserResult<id_type::M
|
||||
Ok(id_type::MerchantId::new_from_unix_timestamp())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_base_url(state: &SessionState) -> &str {
|
||||
if !state.conf.multitenancy.enabled {
|
||||
&state.conf.user.base_url
|
||||
} else {
|
||||
&state.tenant.user.control_center_url
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,9 +9,12 @@ use crate::{
|
||||
consts, errors,
|
||||
logger::error,
|
||||
routes::{metrics, SessionState},
|
||||
services::email::types::{self as email_types, ApiKeyExpiryReminder},
|
||||
services::email::types::ApiKeyExpiryReminder,
|
||||
types::{api, domain::UserEmail, storage},
|
||||
utils::{user::theme as theme_utils, OptionExt},
|
||||
utils::{
|
||||
user::{self as user_utils, theme as theme_utils},
|
||||
OptionExt,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct ApiKeyExpiryWorkflow;
|
||||
@ -110,7 +113,7 @@ impl ProcessTrackerWorkflow<SessionState> for ApiKeyExpiryWorkflow {
|
||||
.email_client
|
||||
.clone()
|
||||
.compose_and_send_email(
|
||||
email_types::get_base_url(state),
|
||||
user_utils::get_base_url(state),
|
||||
Box::new(email_contents),
|
||||
state.conf.proxy.https_url.as_ref(),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user