Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
861d6d5
add support for proxy api
prasunna09 Apr 15, 2025
ef4ca7a
code refactoring
prasunna09 Apr 22, 2025
77e6cdc
code optimization
prasunna09 Apr 25, 2025
dde789a
revert cargo.toml changes
prasunna09 Apr 25, 2025
d5638f5
code refactoring
prasunna09 May 5, 2025
7156896
code refactoring
prasunna09 May 14, 2025
6712bf8
code refactoring
prasunna09 May 15, 2025
5cf3510
add payment method id support for proxy api
prasunna09 May 16, 2025
51e1919
resolve pr comments
prasunna09 May 18, 2025
794ff40
Merge main
prasunna09 May 19, 2025
1d10061
resolve pr comments
prasunna09 May 19, 2025
3f995d8
merge main
prasunna09 May 19, 2025
2d1ade7
chore: run formatter
hyperswitch-bot[bot] May 19, 2025
3c73bad
fix clippy
prasunna09 May 19, 2025
6a78b23
Merge branch 'add-proxy-api' of github.com:juspay/hyperswitch into ad…
prasunna09 May 19, 2025
627e6b4
fix clippy
prasunna09 May 19, 2025
33467f7
resolve pr comments and generate open api spec
prasunna09 May 20, 2025
552f74c
chore: run formatter
hyperswitch-bot[bot] May 20, 2025
4d7f97c
fix openspec api_v2
prasunna09 May 20, 2025
9f16eb3
Merge branch 'add-proxy-api' of github.com:juspay/hyperswitch into ad…
prasunna09 May 20, 2025
8871c3a
chore: run formatter
hyperswitch-bot[bot] May 20, 2025
eef8450
docs(openapi): re-generate OpenAPI specification
hyperswitch-bot[bot] May 20, 2025
93f7242
fix open api spec
prasunna09 May 21, 2025
548bbdc
resolve pr comments
prasunna09 May 21, 2025
dd05e03
chore: run formatter
hyperswitch-bot[bot] May 21, 2025
7238311
add dependency to fix open api spec
prasunna09 May 22, 2025
2256117
Merge main
prasunna09 May 22, 2025
135ffbf
chore: run formatter
hyperswitch-bot[bot] May 22, 2025
5c33ec2
resolve conflicts
prasunna09 May 22, 2025
484cf3c
Merge branch 'add-proxy-api' of github.com:juspay/hyperswitch into ad…
prasunna09 May 22, 2025
1db2293
chore: run formatter
hyperswitch-bot[bot] May 22, 2025
d1f2cb7
Merge main
prasunna09 May 22, 2025
8943a24
docs(openapi): re-generate OpenAPI specification
hyperswitch-bot[bot] May 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/api_models/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ pub mod verifications;
pub mod verify_connector;
pub mod webhook_events;
pub mod webhooks;
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
pub mod proxy;

pub trait ValidateFieldAndGet<Request> {
fn validate_field_and_get(
Expand Down
35 changes: 35 additions & 0 deletions crates/api_models/src/proxy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use utoipa::ToSchema;

#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)]
pub struct ProxyRequest {
/// The request body that needs to be forwarded
pub req_body: Value,
/// The destination URL where the request needs to be forwarded
#[schema(example = "https://api.example.com/endpoint")]
pub destination_url: String,
/// The headers that need to be forwarded
pub headers: Value,
/// The vault token that is used to fetch sensitive data from the vault
pub token: String,
/// The type of token that is used to fetch sensitive data from the vault
pub token_type: TokenType
}

#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)]
pub enum TokenType {
TokenizationId,
PaymentMethodId
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When would we use PaymentMethodId here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we ll be using payment method sdk to collect payment method data from customer, then will be stored in payment method table, now merchant can use payment method id for proxy

}

#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)]
pub struct ProxyResponse {
/// The response received from the destination
pub response: Value,
pub status_code: u16,
pub response_headers: Value,
}

impl common_utils::events::ApiEventMetric for ProxyRequest {}
impl common_utils::events::ApiEventMetric for ProxyResponse {}
2 changes: 1 addition & 1 deletion crates/payment_methods/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ storage_impl = { version = "0.1.0", path = "../storage_impl", default-features =
workspace = true

[features]
default = ["v1"]
default = []
v1 = ["hyperswitch_domain_models/v1", "storage_impl/v1", "common_utils/v1"]
v2 = [ "payment_methods_v2"]
payment_methods_v2 = [ "hyperswitch_domain_models/payment_methods_v2", "storage_impl/payment_methods_v2", "common_utils/payment_methods_v2"]
2 changes: 1 addition & 1 deletion crates/payment_methods/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
pub mod state;
pub mod state;
1 change: 1 addition & 0 deletions crates/router/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,4 @@ pub mod unified_authentication_service;
pub mod relay;
#[cfg(feature = "v2")]
pub mod revenue_recovery;
pub mod proxy;
10 changes: 9 additions & 1 deletion crates/router/src/core/payment_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1956,8 +1956,16 @@ pub async fn update_payment_method_core(
},
)?;

let vault_id = payment_method
.locker_id
.clone()
.ok_or(errors::VaultError::MissingRequiredField {
field_name: "locker_id",
}).change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Missing locker_id for VaultRetrieveRequest")?;

let pmd: domain::PaymentMethodVaultingData =
vault::retrieve_payment_method_from_vault(state, merchant_account, &payment_method)
vault::retrieve_payment_method_from_vault(state, merchant_account, &vault_id)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to retrieve payment method from vault")?
Expand Down
10 changes: 2 additions & 8 deletions crates/router/src/core/payment_methods/vault.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1316,17 +1316,11 @@ pub async fn add_payment_method_to_vault(
pub async fn retrieve_payment_method_from_vault(
state: &routes::SessionState,
merchant_account: &domain::MerchantAccount,
pm: &domain::PaymentMethod,
vault_id: &domain::VaultId,
) -> CustomResult<pm_types::VaultRetrieveResponse, errors::VaultError> {
let payload = pm_types::VaultRetrieveRequest {
entity_id: merchant_account.get_id().to_owned(),
vault_id: pm
.locker_id
.clone()
.ok_or(errors::VaultError::MissingRequiredField {
field_name: "locker_id",
})
.attach_printable("Missing locker_id for VaultRetrieveRequest")?,
vault_id: vault_id.to_owned(),
}
.encode_to_vec()
.change_context(errors::VaultError::RequestEncodingFailed)
Expand Down
262 changes: 262 additions & 0 deletions crates/router/src/core/proxy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
use api_models::proxy as proxy_api_models;
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
use common_utils::{ext_traits::BytesExt, request};
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
use error_stack::ResultExt;
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
use x509_parser::nom::{
bytes::complete::{tag, take_while1},
character::complete::{char, multispace0},
sequence::{delimited, preceded, terminated},
IResult,
};

#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
use super::errors::{self, RouterResponse, RouterResult};
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
use crate::{
logger,
routes::SessionState,
services::{self, request::Mask},
types::domain,
};
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
use serde_json::Value;

#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
#[derive(Debug)]
struct TokenReference {
field: String,
}

#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
fn parse_token(input: &str) -> IResult<&str, TokenReference> {
let (input, field) = delimited(
tag("{{"),
preceded(
multispace0,
preceded(
char('$'),
terminated(
take_while1(|c: char| c.is_alphanumeric() || c == '_'),
multispace0,
),
),
),
tag("}}"),
)(input)?;

Ok((
input,
TokenReference {
field: field.to_string(),
},
))
}

#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
fn contains_token(s: &str) -> bool {
s.contains("{{") && s.contains("$") && s.contains("}}")
}

#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
async fn process_value(
state: &SessionState,
merchant_account: &domain::MerchantAccount,
value: Value,
token: &str,
vault_data: &Value,
) -> RouterResult<Value> {
match value {
Value::Object(obj) => {
let mut new_obj = serde_json::Map::new();

for (key, val) in obj {
let processed = Box::pin(process_value(
state,
merchant_account,
val,
token,
vault_data,
))
.await?;
new_obj.insert(key, processed);
}

Ok(Value::Object(new_obj))
}
Value::String(s) => {
if contains_token(&s) {
// Check if string contains multiple tokens
if s.matches("{{").count() > 1 {
let mut result = s.clone();
let mut tokens_processed = true;

while result.contains("{{") && result.contains("}}") {
let start = result.find("{{").unwrap();
let end = result[start..]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we try to use nom parser for multi token parsing logic as well somehow instead of manual parsing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, will be taking this multi token parsing in following pr, for now, reverted back these changes

.find("}}")
.map(|pos| start + pos + 2)
.unwrap_or(result.len());

if let Ok((_, token_ref)) = parse_token(&result[start..end]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of slicing, can use get methods to avoid any kind of panics?

if let Ok(field_value) =
extract_field_from_vault_data(vault_data, &token_ref.field)
{
let value_str = match field_value {
Value::String(s) => s,
_ => field_value.to_string(),
};
result = result[0..start].to_string() + &value_str + &result[end..];
} else {
tokens_processed = false;
break;
}
} else {
tokens_processed = false;
break;
}
}

if tokens_processed {
Ok(Value::String(result))
} else {
Ok(Value::String(s))
}
} else {
if let Ok((_, token_ref)) = parse_token(&s) {
extract_field_from_vault_data(vault_data, &token_ref.field)
} else {
Ok(Value::String(s))
}
}
} else {
Ok(Value::String(s))
}
}
_ => Ok(value),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should handle Array as well

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, will handle it in separate pr

}
}

#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
fn extract_field_from_vault_data(vault_data: &Value, field_name: &str) -> RouterResult<Value> {
let result = match vault_data {
Value::Object(obj) => obj.get(field_name).cloned().or_else(|| {
obj.values()
.filter_map(|val| {
if let Value::Object(inner_obj) = val {
inner_obj.get(field_name).cloned().or_else(|| {
inner_obj
.values()
.filter_map(|deeper_val| {
if let Value::Object(deepest_obj) = deeper_val {
deepest_obj.get(field_name).cloned()
} else {
None
}
})
.next()
})
} else {
None
}
})
.next()
}),
_ => None,
};

match result {
Some(value) => Ok(value),
None => {
logger::debug!(
"Field '{}' not found in vault data: {:?}",
field_name,
vault_data
);
Err(errors::ApiErrorResponse::InternalServerError)
.attach_printable(format!("Field '{}' not found", field_name))
}
}
}

#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
pub async fn proxy_core(
state: SessionState,
merchant_account: domain::MerchantAccount,
req: proxy_api_models::ProxyRequest,
) -> RouterResponse<proxy_api_models::ProxyResponse> {
let token = &req.token;
//TODO: match on token type,
//if token_type is tokenization id then fetch vault id from tokenization table
//else if token_type is payment method id then fetch vault id from payment method table
let vault_id = domain::VaultId::generate(token.clone());

let vault_response = super::payment_methods::vault::retrieve_payment_method_from_vault(
&state,
&merchant_account,
&vault_id,
)
.await
.map_err(|_| errors::ApiErrorResponse::InternalServerError)?;

let vault_data = serde_json::to_value(&vault_response.data)
.map_err(|_| errors::ApiErrorResponse::InternalServerError)?;

let processed_body =
process_value(&state, &merchant_account, req.req_body, token, &vault_data).await?;

let mut request = services::Request::new(services::Method::Post, &req.destination_url);
request.set_body(request::RequestContent::Json(Box::new(processed_body)));

if let Value::Object(headers) = req.headers {
headers.iter().for_each(|(key, value)| {
let header_value = match value {
Value::String(value_str) => value_str.clone(),
_ => value.to_string(),
}
.into_masked();
request.add_header(key, header_value);
});
}

let response = services::call_connector_api(&state, request, "proxy")
.await
.change_context(errors::ApiErrorResponse::InternalServerError);
let res = response
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error while receiving response")
.and_then(|inner| match inner {
Err(err_res) =>
{
logger::error!("Response Deserialization Failed: {err_res:?}");
Ok(err_res)
}
Ok(res) => Ok(res),
})
.inspect_err(|_| {})?;

let response_body: Value = res
.response
.parse_struct("ProxyResponse")
.change_context(errors::ApiErrorResponse::InternalServerError)?;

let status_code = res.status_code;
let response_headers = res.headers.as_ref()
.map(|h| {
let map: std::collections::BTreeMap<_, _> = h.iter()
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
.collect();
serde_json::to_value(map).unwrap_or_else(|_| serde_json::json!({}))
})
.unwrap_or_else(|| serde_json::json!({}));

Ok(services::ApplicationResponse::Json(
proxy_api_models::ProxyResponse {
response: response_body,
status_code,
response_headers,
},
))
}
5 changes: 5 additions & 0 deletions crates/router/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,11 @@ pub fn mk_app(
.service(routes::Cards::server(state.clone()));
}

#[cfg(all(feature = "oltp", feature = "v2", feature = "payment_methods_v2"))]
{
server_app = server_app.service(routes::Proxy::server(state.clone()));
}

#[cfg(all(feature = "recon", feature = "v1"))]
{
server_app = server_app.service(routes::Recon::server(state.clone()));
Expand Down
4 changes: 4 additions & 0 deletions crates/router/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ pub mod relay;

#[cfg(feature = "olap")]
pub mod process_tracker;
pub mod proxy;

#[cfg(feature = "dummy_connector")]
pub use self::app::DummyConnector;
Expand All @@ -93,3 +94,6 @@ pub use self::app::{PayoutLink, Payouts};
pub use super::compatibility::stripe::StripeApis;
#[cfg(feature = "olap")]
pub use crate::analytics::routes::{self as analytics, Analytics};

#[cfg(feature = "v2")]
pub use self::app::Proxy;
Loading
Loading