Skip to content
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

refactor: Add dynamic secure link #7133

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
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
10 changes: 0 additions & 10 deletions api-reference-v2/openapi_spec.json
Original file line number Diff line number Diff line change
@@ -13076,11 +13076,6 @@
"type": "string",
"description": "URL for rendering the open payment link"
},
"secure_link": {
"type": "string",
"description": "URL for rendering the secure payment link",
"nullable": true
},
"payment_link_id": {
"type": "string",
"description": "Identifier for the payment link"
@@ -19297,11 +19292,6 @@
}
],
"nullable": true
},
"secure_link": {
"type": "string",
"description": "Secure payment link (with security checks and listing saved payment methods)",
"nullable": true
}
}
},
10 changes: 0 additions & 10 deletions api-reference/openapi_spec.json
Original file line number Diff line number Diff line change
@@ -16192,11 +16192,6 @@
"type": "string",
"description": "URL for rendering the open payment link"
},
"secure_link": {
"type": "string",
"description": "URL for rendering the secure payment link",
"nullable": true
},
"payment_link_id": {
"type": "string",
"description": "Identifier for the payment link"
@@ -23847,11 +23842,6 @@
}
],
"nullable": true
},
"secure_link": {
"type": "string",
"description": "Secure payment link (with security checks and listing saved payment methods)",
"nullable": true
}
}
},
4 changes: 0 additions & 4 deletions crates/api_models/src/payments.rs
Original file line number Diff line number Diff line change
@@ -6967,8 +6967,6 @@ pub struct RetrievePaymentLinkRequest {
pub struct PaymentLinkResponse {
/// URL for rendering the open payment link
pub link: String,
/// URL for rendering the secure payment link
pub secure_link: Option<String>,
/// Identifier for the payment link
pub payment_link_id: String,
}
@@ -6997,8 +6995,6 @@ pub struct RetrievePaymentLinkResponse {
pub status: PaymentLinkStatus,
#[schema(value_type = Option<Currency>)]
pub currency: Option<api_enums::Currency>,
/// Secure payment link (with security checks and listing saved payment methods)
pub secure_link: Option<String>,
}

#[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)]
214 changes: 114 additions & 100 deletions crates/router/src/core/payment_link.rs
Original file line number Diff line number Diff line change
@@ -285,98 +285,113 @@ pub async fn form_payment_link_data(
))
}

pub async fn initiate_secure_payment_link_flow(
state: SessionState,
merchant_account: domain::MerchantAccount,
key_store: domain::MerchantKeyStore,
merchant_id: common_utils::id_type::MerchantId,
payment_id: common_utils::id_type::PaymentId,
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PaymentLinkType {
SecurePaymentLink,
OpenPaymentLink,
}

impl PaymentLinkType {
fn get_requested_payment_link_type(request_headers: &header::HeaderMap) -> Self {
match request_headers
.get("sec-fetch-dest")
.and_then(|v| v.to_str().ok())
{
Some("iframe") => Self::SecurePaymentLink,
_ => Self::OpenPaymentLink,
}
}
}

fn generate_secure_payment_link(
state: &SessionState,
request_headers: &header::HeaderMap,
payment_link: &PaymentLink,
payment_link_config: PaymentLinkConfig,
payment_details: api_models::payments::PaymentLinkDetails,
css_script: String,
) -> RouterResponse<services::PaymentLinkFormData> {
let (payment_link, payment_link_details, payment_link_config) =
form_payment_link_data(&state, merchant_account, key_store, merchant_id, payment_id)
.await?;

validator::validate_secure_payment_link_render_request(
request_headers,
&payment_link,
payment_link,
&payment_link_config,
)?;
let secure_payment_link_details = api_models::payments::SecurePaymentLinkDetails {
enabled_saved_payment_method: payment_link_config.enabled_saved_payment_method,
hide_card_nickname_field: payment_link_config.hide_card_nickname_field,
show_card_form_by_default: payment_link_config.show_card_form_by_default,
payment_link_details: payment_details.to_owned(),
payment_button_text: payment_link_config.payment_button_text.clone(),
};
let js_script = format!(
"window.__PAYMENT_DETAILS = {}",
serde_json::to_string(&secure_payment_link_details)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to serialize PaymentLinkData")?
);
let html_meta_tags = get_meta_tags_html(&payment_details);
let payment_link_data = services::PaymentLinkFormData {
js_script,
sdk_url: state.conf.payment_link.sdk_url.clone(),
css_script: css_script.clone(),
html_meta_tags,
};
let allowed_domains = payment_link_config
.allowed_domains
.clone()
.ok_or(report!(errors::ApiErrorResponse::InternalServerError))
.attach_printable_lazy(|| {
format!(
"Invalid list of allowed_domains found - {:?}",
payment_link_config.allowed_domains.clone()
)
})?;

let css_script = get_color_scheme_css(&payment_link_config);

match payment_link_details {
PaymentLinkData::PaymentLinkStatusDetails(ref status_details) => {
let js_script = get_js_script(&payment_link_details)?;
let payment_link_error_data = services::PaymentLinkStatusData {
js_script,
css_script,
};
logger::info!(
"payment link data, for building payment link status page {:?}",
status_details
);
Ok(services::ApplicationResponse::PaymentLinkForm(Box::new(
services::api::PaymentLinkAction::PaymentLinkStatus(payment_link_error_data),
)))
}
PaymentLinkData::PaymentLinkDetails(link_details) => {
let secure_payment_link_details = api_models::payments::SecurePaymentLinkDetails {
enabled_saved_payment_method: payment_link_config.enabled_saved_payment_method,
hide_card_nickname_field: payment_link_config.hide_card_nickname_field,
show_card_form_by_default: payment_link_config.show_card_form_by_default,
payment_link_details: *link_details.to_owned(),
payment_button_text: payment_link_config.payment_button_text,
};
let js_script = format!(
"window.__PAYMENT_DETAILS = {}",
serde_json::to_string(&secure_payment_link_details)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to serialize PaymentLinkData")?
);
let html_meta_tags = get_meta_tags_html(&link_details);
let payment_link_data = services::PaymentLinkFormData {
js_script,
sdk_url: state.conf.payment_link.sdk_url.clone(),
css_script,
html_meta_tags,
};
let allowed_domains = payment_link_config
.allowed_domains
.clone()
.ok_or(report!(errors::ApiErrorResponse::InternalServerError))
.attach_printable_lazy(|| {
format!(
"Invalid list of allowed_domains found - {:?}",
payment_link_config.allowed_domains.clone()
)
})?;

if allowed_domains.is_empty() {
return Err(report!(errors::ApiErrorResponse::InternalServerError))
.attach_printable_lazy(|| {
format!(
"Invalid list of allowed_domains found - {:?}",
payment_link_config.allowed_domains.clone()
)
});
}
if allowed_domains.is_empty() {
return Err(report!(errors::ApiErrorResponse::InternalServerError)).attach_printable_lazy(
|| {
format!(
"Invalid list of allowed_domains found - {:?}",
payment_link_config.allowed_domains.clone()
)
},
);
}
let link_data = GenericLinks {
allowed_domains,
data: GenericLinksData::SecurePaymentLink(payment_link_data),
locale: DEFAULT_LOCALE.to_string(),
};
logger::info!(
"payment link data, for building secure payment link {:?}",
link_data
);

let link_data = GenericLinks {
allowed_domains,
data: GenericLinksData::SecurePaymentLink(payment_link_data),
locale: DEFAULT_LOCALE.to_string(),
};
logger::info!(
"payment link data, for building secure payment link {:?}",
link_data
);
Ok(services::ApplicationResponse::GenericLinkForm(Box::new(
link_data,
)))
}

Ok(services::ApplicationResponse::GenericLinkForm(Box::new(
link_data,
)))
}
}
fn generate_open_payment_link(
state: &SessionState,
payment_details: api_models::payments::PaymentLinkDetails,
js_script: String,
css_script: String,
) -> RouterResponse<services::PaymentLinkFormData> {
let html_meta_tags = get_meta_tags_html(&payment_details);
let payment_link_data = services::PaymentLinkFormData {
js_script,
sdk_url: state.conf.payment_link.sdk_url.clone(),
css_script,
html_meta_tags,
};
logger::info!(
"payment link data, for building open payment link {:?}",
payment_link_data
);
Ok(services::ApplicationResponse::PaymentLinkForm(Box::new(
services::api::PaymentLinkAction::PaymentLinkFormData(payment_link_data),
)))
}

pub async fn initiate_payment_link_flow(
@@ -385,13 +400,15 @@ pub async fn initiate_payment_link_flow(
key_store: domain::MerchantKeyStore,
merchant_id: common_utils::id_type::MerchantId,
payment_id: common_utils::id_type::PaymentId,
request_headers: &header::HeaderMap,
) -> RouterResponse<services::PaymentLinkFormData> {
let (_, payment_details, payment_link_config) =
let (payment_link, payment_details, payment_link_config) =
form_payment_link_data(&state, merchant_account, key_store, merchant_id, payment_id)
.await?;

let css_script = get_color_scheme_css(&payment_link_config);
let js_script = get_js_script(&payment_details)?;
let requested_payment_link_type =
PaymentLinkType::get_requested_payment_link_type(request_headers);

match payment_details {
PaymentLinkData::PaymentLinkStatusDetails(status_details) => {
@@ -407,22 +424,19 @@ pub async fn initiate_payment_link_flow(
services::api::PaymentLinkAction::PaymentLinkStatus(payment_link_error_data),
)))
}
PaymentLinkData::PaymentLinkDetails(payment_details) => {
let html_meta_tags = get_meta_tags_html(&payment_details);
let payment_link_data = services::PaymentLinkFormData {
js_script,
sdk_url: state.conf.payment_link.sdk_url.clone(),
PaymentLinkData::PaymentLinkDetails(payment_details) => match requested_payment_link_type {
PaymentLinkType::SecurePaymentLink => generate_secure_payment_link(
&state,
request_headers,
&payment_link,
payment_link_config,
*payment_details,
css_script,
html_meta_tags,
};
logger::info!(
"payment link data, for building open payment link {:?}",
payment_link_data
);
Ok(services::ApplicationResponse::PaymentLinkForm(Box::new(
services::api::PaymentLinkAction::PaymentLinkFormData(payment_link_data),
)))
}
),
PaymentLinkType::OpenPaymentLink => {
generate_open_payment_link(&state, *payment_details, js_script, css_script)
}
},
}
}

35 changes: 0 additions & 35 deletions crates/router/src/core/payment_link/validator.rs
Original file line number Diff line number Diff line change
@@ -26,41 +26,6 @@ pub fn validate_secure_payment_link_render_request(
)
})?;

// Validate secure_link was generated
if payment_link.secure_link.clone().is_none() {
return Err(report!(errors::ApiErrorResponse::InvalidRequestUrl)).attach_printable_lazy(
|| {
format!(
"Secure payment link was not generated for {}\nmissing secure_link",
link_id
)
},
);
}

// Fetch destination is "iframe"
match request_headers.get("sec-fetch-dest").and_then(|v| v.to_str().ok()) {
Some("iframe") => Ok(()),
Some(requestor) => Err(report!(errors::ApiErrorResponse::AccessForbidden {
resource: "payment_link".to_string(),
}))
.attach_printable_lazy(|| {
format!(
"Access to payment_link [{}] is forbidden when requested through {}",
link_id, requestor
)
}),
None => Err(report!(errors::ApiErrorResponse::AccessForbidden {
resource: "payment_link".to_string(),
}))
.attach_printable_lazy(|| {
format!(
"Access to payment_link [{}] is forbidden when sec-fetch-dest is not present in request headers",
link_id
)
}),
}?;

// Validate origin / referer
let domain_in_req = {
let origin_or_referer = request_headers
13 changes: 1 addition & 12 deletions crates/router/src/core/payments/operations/payment_create.rs
Original file line number Diff line number Diff line change
@@ -1571,16 +1571,6 @@ async fn create_payment_link(
locale_str.clone(),
);

let secure_link = payment_link_config.allowed_domains.as_ref().map(|_| {
format!(
"{}/payment_link/s/{}/{}?locale={}",
domain_name,
merchant_id.get_string_repr(),
payment_id.get_string_repr(),
locale_str,
)
});

let payment_link_config_encoded_value = payment_link_config.encode_to_value().change_context(
errors::ApiErrorResponse::InvalidDataValue {
field_name: "payment_link_config",
@@ -1601,7 +1591,7 @@ async fn create_payment_link(
description,
payment_link_config: Some(payment_link_config_encoded_value),
profile_id: Some(profile_id),
secure_link,
secure_link: None, //It is deprecated now.,
};
let payment_link_db = db
.insert_payment_link(payment_link_req)
@@ -1612,7 +1602,6 @@ async fn create_payment_link(

Ok(Some(api_models::payments::PaymentLinkResponse {
link: payment_link_db.link_to_pay.clone(),
secure_link: payment_link_db.secure_link,
payment_link_id: payment_link_db.payment_link_id,
}))
}
Loading