chore(users): add hubspot tracking to prod intent (#7798)

Co-authored-by: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com>
This commit is contained in:
Riddhiagrawal001
2025-05-13 17:25:27 +05:30
committed by GitHub
parent 1dabfe3e2c
commit 67f38f864e
36 changed files with 923 additions and 420 deletions

View File

@ -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 {

View File

@ -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

View File

@ -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()?;

View File

@ -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;

View File

@ -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::{

View File

@ -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(

View File

@ -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(),
)

View File

@ -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(),
)

View File

@ -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) => {

View File

@ -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(),
})
}
}

View File

@ -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);

View File

@ -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 servers 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 cant 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>,

View File

@ -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) {

View File

@ -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
}
}

View File

@ -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

View File

@ -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)

View File

@ -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
}
}

View File

@ -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(),
)