mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-11-01 02:57:02 +08:00 
			
		
		
		
	 20b52f11c3
			
		
	
	20b52f11c3
	
	
	
		
			
			Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
		
			
				
	
	
		
			354 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			354 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| //! This module has common utilities for links in HyperSwitch
 | |
| 
 | |
| use std::{collections::HashSet, primitive::i64};
 | |
| 
 | |
| use common_enums::{enums, UIWidgetFormLayout};
 | |
| use diesel::{
 | |
|     backend::Backend,
 | |
|     deserialize,
 | |
|     deserialize::FromSql,
 | |
|     serialize::{Output, ToSql},
 | |
|     sql_types::Jsonb,
 | |
|     AsExpression, FromSqlRow,
 | |
| };
 | |
| use error_stack::{report, ResultExt};
 | |
| use masking::Secret;
 | |
| use regex::Regex;
 | |
| #[cfg(feature = "logs")]
 | |
| use router_env::logger;
 | |
| use serde::Serialize;
 | |
| use utoipa::ToSchema;
 | |
| 
 | |
| use crate::{consts, errors::ParsingError, id_type, types::MinorUnit};
 | |
| 
 | |
| #[derive(
 | |
|     Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq, FromSqlRow, AsExpression, ToSchema,
 | |
| )]
 | |
| #[serde(rename_all = "snake_case", tag = "type", content = "value")]
 | |
| #[diesel(sql_type = Jsonb)]
 | |
| /// Link status enum
 | |
| pub enum GenericLinkStatus {
 | |
|     /// Status variants for payment method collect link
 | |
|     PaymentMethodCollect(PaymentMethodCollectStatus),
 | |
|     /// Status variants for payout link
 | |
|     PayoutLink(PayoutLinkStatus),
 | |
| }
 | |
| 
 | |
| impl Default for GenericLinkStatus {
 | |
|     fn default() -> Self {
 | |
|         Self::PaymentMethodCollect(PaymentMethodCollectStatus::Initiated)
 | |
|     }
 | |
| }
 | |
| 
 | |
| crate::impl_to_sql_from_sql_json!(GenericLinkStatus);
 | |
| 
 | |
| #[derive(
 | |
|     Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq, FromSqlRow, AsExpression, ToSchema,
 | |
| )]
 | |
| #[serde(rename_all = "snake_case")]
 | |
| #[diesel(sql_type = Jsonb)]
 | |
| /// Status variants for payment method collect links
 | |
| pub enum PaymentMethodCollectStatus {
 | |
|     /// Link was initialized
 | |
|     Initiated,
 | |
|     /// Link was expired or invalidated
 | |
|     Invalidated,
 | |
|     /// Payment method details were submitted
 | |
|     Submitted,
 | |
| }
 | |
| 
 | |
| impl<DB: Backend> FromSql<Jsonb, DB> for PaymentMethodCollectStatus
 | |
| where
 | |
|     serde_json::Value: FromSql<Jsonb, DB>,
 | |
| {
 | |
|     fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result<Self> {
 | |
|         let value = <serde_json::Value as FromSql<Jsonb, DB>>::from_sql(bytes)?;
 | |
|         let generic_status: GenericLinkStatus = serde_json::from_value(value)?;
 | |
|         match generic_status {
 | |
|             GenericLinkStatus::PaymentMethodCollect(status) => Ok(status),
 | |
|             GenericLinkStatus::PayoutLink(_) => Err(report!(ParsingError::EnumParseFailure(
 | |
|                 "PaymentMethodCollectStatus"
 | |
|             )))
 | |
|             .attach_printable("Invalid status for PaymentMethodCollect")?,
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl ToSql<Jsonb, diesel::pg::Pg> for PaymentMethodCollectStatus
 | |
| where
 | |
|     serde_json::Value: ToSql<Jsonb, diesel::pg::Pg>,
 | |
| {
 | |
|     // This wraps PaymentMethodCollectStatus with GenericLinkStatus
 | |
|     // Required for storing the status in required format in DB (GenericLinkStatus)
 | |
|     // This type is used in PaymentMethodCollectLink (a variant of GenericLink, used in the application for avoiding conversion of data and status)
 | |
|     fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, diesel::pg::Pg>) -> diesel::serialize::Result {
 | |
|         let value = serde_json::to_value(GenericLinkStatus::PaymentMethodCollect(self.clone()))?;
 | |
| 
 | |
|         // the function `reborrow` only works in case of `Pg` backend. But, in case of other backends
 | |
|         // please refer to the diesel migration blog:
 | |
|         // https://github.com/Diesel-rs/Diesel/blob/master/guide_drafts/migration_guide.md#changed-tosql-implementations
 | |
|         <serde_json::Value as ToSql<Jsonb, diesel::pg::Pg>>::to_sql(&value, &mut out.reborrow())
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[derive(
 | |
|     Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq, FromSqlRow, AsExpression, ToSchema,
 | |
| )]
 | |
| #[serde(rename_all = "snake_case")]
 | |
| #[diesel(sql_type = Jsonb)]
 | |
| /// Status variants for payout links
 | |
| pub enum PayoutLinkStatus {
 | |
|     /// Link was initialized
 | |
|     Initiated,
 | |
|     /// Link was expired or invalidated
 | |
|     Invalidated,
 | |
|     /// Payout details were submitted
 | |
|     Submitted,
 | |
| }
 | |
| 
 | |
| impl<DB: Backend> FromSql<Jsonb, DB> for PayoutLinkStatus
 | |
| where
 | |
|     serde_json::Value: FromSql<Jsonb, DB>,
 | |
| {
 | |
|     fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result<Self> {
 | |
|         let value = <serde_json::Value as FromSql<Jsonb, DB>>::from_sql(bytes)?;
 | |
|         let generic_status: GenericLinkStatus = serde_json::from_value(value)?;
 | |
|         match generic_status {
 | |
|             GenericLinkStatus::PayoutLink(status) => Ok(status),
 | |
|             GenericLinkStatus::PaymentMethodCollect(_) => {
 | |
|                 Err(report!(ParsingError::EnumParseFailure("PayoutLinkStatus")))
 | |
|                     .attach_printable("Invalid status for PayoutLink")?
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl ToSql<Jsonb, diesel::pg::Pg> for PayoutLinkStatus
 | |
| where
 | |
|     serde_json::Value: ToSql<Jsonb, diesel::pg::Pg>,
 | |
| {
 | |
|     // This wraps PayoutLinkStatus with GenericLinkStatus
 | |
|     // Required for storing the status in required format in DB (GenericLinkStatus)
 | |
|     // This type is used in PayoutLink (a variant of GenericLink, used in the application for avoiding conversion of data and status)
 | |
|     fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, diesel::pg::Pg>) -> diesel::serialize::Result {
 | |
|         let value = serde_json::to_value(GenericLinkStatus::PayoutLink(self.clone()))?;
 | |
| 
 | |
|         // the function `reborrow` only works in case of `Pg` backend. But, in case of other backends
 | |
|         // please refer to the diesel migration blog:
 | |
|         // https://github.com/Diesel-rs/Diesel/blob/master/guide_drafts/migration_guide.md#changed-tosql-implementations
 | |
|         <serde_json::Value as ToSql<Jsonb, diesel::pg::Pg>>::to_sql(&value, &mut out.reborrow())
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[derive(Serialize, serde::Deserialize, Debug, Clone, FromSqlRow, AsExpression, ToSchema)]
 | |
| #[diesel(sql_type = Jsonb)]
 | |
| /// Payout link object
 | |
| pub struct PayoutLinkData {
 | |
|     /// Identifier for the payout link
 | |
|     pub payout_link_id: String,
 | |
|     /// Identifier for the customer
 | |
|     pub customer_id: id_type::CustomerId,
 | |
|     /// Identifier for the payouts resource
 | |
|     pub payout_id: id_type::PayoutId,
 | |
|     /// Link to render the payout link
 | |
|     pub link: url::Url,
 | |
|     /// Client secret generated for authenticating frontend APIs
 | |
|     pub client_secret: Secret<String>,
 | |
|     /// Expiry in seconds from the time it was created
 | |
|     pub session_expiry: u32,
 | |
|     #[serde(flatten)]
 | |
|     /// Payout link's UI configurations
 | |
|     pub ui_config: GenericLinkUiConfig,
 | |
|     /// List of enabled payment methods
 | |
|     pub enabled_payment_methods: Option<Vec<EnabledPaymentMethod>>,
 | |
|     /// Payout amount
 | |
|     pub amount: MinorUnit,
 | |
|     /// Payout currency
 | |
|     pub currency: enums::Currency,
 | |
|     /// A list of allowed domains (glob patterns) where this link can be embedded / opened from
 | |
|     pub allowed_domains: HashSet<String>,
 | |
|     /// Form layout of the payout link
 | |
|     pub form_layout: Option<UIWidgetFormLayout>,
 | |
|     /// `test_mode` can be used for testing payout links without any restrictions
 | |
|     pub test_mode: Option<bool>,
 | |
| }
 | |
| 
 | |
| crate::impl_to_sql_from_sql_json!(PayoutLinkData);
 | |
| 
 | |
| /// Object for GenericLinkUiConfig
 | |
| #[derive(Clone, Debug, serde::Deserialize, Serialize, ToSchema)]
 | |
| pub struct GenericLinkUiConfig {
 | |
|     /// Merchant's display logo
 | |
|     #[schema(value_type = Option<String>, max_length = 255, example = "https://hyperswitch.io/favicon.ico")]
 | |
|     pub logo: Option<url::Url>,
 | |
| 
 | |
|     /// Custom merchant name for the link
 | |
|     #[schema(value_type = Option<String>, max_length = 255, example = "Hyperswitch")]
 | |
|     pub merchant_name: Option<Secret<String>>,
 | |
| 
 | |
|     /// Primary color to be used in the form represented in hex format
 | |
|     #[schema(value_type = Option<String>, max_length = 255, example = "#4285F4")]
 | |
|     pub theme: Option<String>,
 | |
| }
 | |
| 
 | |
| /// Object for GenericLinkUiConfigFormData
 | |
| #[derive(Clone, Debug, serde::Deserialize, Serialize, ToSchema)]
 | |
| pub struct GenericLinkUiConfigFormData {
 | |
|     /// Merchant's display logo
 | |
|     #[schema(value_type = String, max_length = 255, example = "https://hyperswitch.io/favicon.ico")]
 | |
|     pub logo: url::Url,
 | |
| 
 | |
|     /// Custom merchant name for the link
 | |
|     #[schema(value_type = String, max_length = 255, example = "Hyperswitch")]
 | |
|     pub merchant_name: Secret<String>,
 | |
| 
 | |
|     /// Primary color to be used in the form represented in hex format
 | |
|     #[schema(value_type = String, max_length = 255, example = "#4285F4")]
 | |
|     pub theme: String,
 | |
| }
 | |
| 
 | |
| /// Object for EnabledPaymentMethod
 | |
| #[derive(Clone, Debug, Serialize, serde::Deserialize, ToSchema)]
 | |
| pub struct EnabledPaymentMethod {
 | |
|     /// Payment method (banks, cards, wallets) enabled for the operation
 | |
|     #[schema(value_type = PaymentMethod)]
 | |
|     pub payment_method: enums::PaymentMethod,
 | |
| 
 | |
|     /// An array of associated payment method types
 | |
|     #[schema(value_type = HashSet<PaymentMethodType>)]
 | |
|     pub payment_method_types: HashSet<enums::PaymentMethodType>,
 | |
| }
 | |
| 
 | |
| /// Util function for validating a domain without any wildcard characters.
 | |
| pub fn validate_strict_domain(domain: &str) -> bool {
 | |
|     Regex::new(consts::STRICT_DOMAIN_REGEX)
 | |
|         .map(|regex| regex.is_match(domain))
 | |
|         .map_err(|err| {
 | |
|             let err_msg = format!("Invalid strict domain regex: {err:?}");
 | |
|             #[cfg(feature = "logs")]
 | |
|             logger::error!(err_msg);
 | |
|             err_msg
 | |
|         })
 | |
|         .unwrap_or(false)
 | |
| }
 | |
| 
 | |
| /// Util function for validating a domain with "*" wildcard characters.
 | |
| pub fn validate_wildcard_domain(domain: &str) -> bool {
 | |
|     Regex::new(consts::WILDCARD_DOMAIN_REGEX)
 | |
|         .map(|regex| regex.is_match(domain))
 | |
|         .map_err(|err| {
 | |
|             let err_msg = format!("Invalid strict domain regex: {err:?}");
 | |
|             #[cfg(feature = "logs")]
 | |
|             logger::error!(err_msg);
 | |
|             err_msg
 | |
|         })
 | |
|         .unwrap_or(false)
 | |
| }
 | |
| 
 | |
| #[cfg(test)]
 | |
| mod domain_tests {
 | |
|     use regex::Regex;
 | |
| 
 | |
|     use super::*;
 | |
| 
 | |
|     #[test]
 | |
|     fn test_validate_strict_domain_regex() {
 | |
|         assert!(
 | |
|             Regex::new(consts::STRICT_DOMAIN_REGEX).is_ok(),
 | |
|             "Strict domain regex is invalid"
 | |
|         );
 | |
|     }
 | |
| 
 | |
|     #[test]
 | |
|     fn test_validate_wildcard_domain_regex() {
 | |
|         assert!(
 | |
|             Regex::new(consts::WILDCARD_DOMAIN_REGEX).is_ok(),
 | |
|             "Wildcard domain regex is invalid"
 | |
|         );
 | |
|     }
 | |
| 
 | |
|     #[test]
 | |
|     fn test_validate_strict_domain() {
 | |
|         let valid_domains = vec![
 | |
|             "example.com",
 | |
|             "example.subdomain.com",
 | |
|             "https://example.com:8080",
 | |
|             "http://example.com",
 | |
|             "example.com:8080",
 | |
|             "example.com:443",
 | |
|             "localhost:443",
 | |
|             "127.0.0.1:443",
 | |
|         ];
 | |
| 
 | |
|         for domain in valid_domains {
 | |
|             assert!(
 | |
|                 validate_strict_domain(domain),
 | |
|                 "Could not validate strict domain: {domain}",
 | |
|             );
 | |
|         }
 | |
| 
 | |
|         let invalid_domains = vec![
 | |
|             "",
 | |
|             "invalid.domain.",
 | |
|             "not_a_domain",
 | |
|             "http://example.com/path?query=1#fragment",
 | |
|             "127.0.0.1.2:443",
 | |
|         ];
 | |
| 
 | |
|         for domain in invalid_domains {
 | |
|             assert!(
 | |
|                 !validate_strict_domain(domain),
 | |
|                 "Could not validate invalid strict domain: {domain}",
 | |
|             );
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     #[test]
 | |
|     fn test_validate_wildcard_domain() {
 | |
|         let valid_domains = vec![
 | |
|             "example.com",
 | |
|             "example.subdomain.com",
 | |
|             "https://example.com:8080",
 | |
|             "http://example.com",
 | |
|             "example.com:8080",
 | |
|             "example.com:443",
 | |
|             "localhost:443",
 | |
|             "127.0.0.1:443",
 | |
|             "*.com",
 | |
|             "example.*.com",
 | |
|             "example.com:*",
 | |
|             "*:443",
 | |
|             "localhost:*",
 | |
|             "127.0.0.*:*",
 | |
|             "*:*",
 | |
|         ];
 | |
| 
 | |
|         for domain in valid_domains {
 | |
|             assert!(
 | |
|                 validate_wildcard_domain(domain),
 | |
|                 "Could not validate wildcard domain: {domain}",
 | |
|             );
 | |
|         }
 | |
| 
 | |
|         let invalid_domains = vec![
 | |
|             "",
 | |
|             "invalid.domain.",
 | |
|             "not_a_domain",
 | |
|             "http://example.com/path?query=1#fragment",
 | |
|             "*.",
 | |
|             ".*",
 | |
|             "example.com:*:",
 | |
|             "*:443:",
 | |
|             ":localhost:*",
 | |
|             "127.00.*:*",
 | |
|         ];
 | |
| 
 | |
|         for domain in invalid_domains {
 | |
|             assert!(
 | |
|                 !validate_wildcard_domain(domain),
 | |
|                 "Could not validate invalid wildcard domain: {domain}",
 | |
|             );
 | |
|         }
 | |
|     }
 | |
| }
 |