feat(core): add hypersense integration api (#7218)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Uzair Khan
2025-02-19 13:23:26 +05:30
committed by GitHub
parent d6e13dd0c8
commit 22633be55c
14 changed files with 326 additions and 6 deletions

View File

@ -2,6 +2,7 @@ pub mod apple_pay_certificates_migration;
pub mod connector_onboarding; pub mod connector_onboarding;
pub mod customer; pub mod customer;
pub mod dispute; pub mod dispute;
pub mod external_service_auth;
pub mod gsm; pub mod gsm;
mod locker_migration; mod locker_migration;
pub mod payment; pub mod payment;

View File

@ -0,0 +1,30 @@
use common_utils::events::{ApiEventMetric, ApiEventsType};
use crate::external_service_auth::{
ExternalSignoutTokenRequest, ExternalTokenResponse, ExternalVerifyTokenRequest,
ExternalVerifyTokenResponse,
};
impl ApiEventMetric for ExternalTokenResponse {
fn get_api_event_type(&self) -> Option<ApiEventsType> {
Some(ApiEventsType::ExternalServiceAuth)
}
}
impl ApiEventMetric for ExternalVerifyTokenRequest {
fn get_api_event_type(&self) -> Option<ApiEventsType> {
Some(ApiEventsType::ExternalServiceAuth)
}
}
impl ApiEventMetric for ExternalVerifyTokenResponse {
fn get_api_event_type(&self) -> Option<ApiEventsType> {
Some(ApiEventsType::ExternalServiceAuth)
}
}
impl ApiEventMetric for ExternalSignoutTokenRequest {
fn get_api_event_type(&self) -> Option<ApiEventsType> {
Some(ApiEventsType::ExternalServiceAuth)
}
}

View File

@ -0,0 +1,35 @@
use common_utils::{id_type, pii};
use masking::Secret;
#[derive(Debug, serde::Serialize)]
pub struct ExternalTokenResponse {
pub token: Secret<String>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct ExternalVerifyTokenRequest {
pub token: Secret<String>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct ExternalSignoutTokenRequest {
pub token: Secret<String>,
}
#[derive(serde::Serialize, Debug)]
#[serde(untagged)]
pub enum ExternalVerifyTokenResponse {
Hypersense {
user_id: String,
merchant_id: id_type::MerchantId,
name: Secret<String>,
email: pii::Email,
},
}
impl ExternalVerifyTokenResponse {
pub fn get_user_id(&self) -> &str {
match self {
Self::Hypersense { user_id, .. } => user_id,
}
}
}

View File

@ -16,6 +16,7 @@ pub mod ephemeral_key;
#[cfg(feature = "errors")] #[cfg(feature = "errors")]
pub mod errors; pub mod errors;
pub mod events; pub mod events;
pub mod external_service_auth;
pub mod feature_matrix; pub mod feature_matrix;
pub mod files; pub mod files;
pub mod gsm; pub mod gsm;

View File

@ -99,6 +99,7 @@ pub enum ApiEventsType {
ApplePayCertificatesMigration, ApplePayCertificatesMigration,
FraudCheck, FraudCheck,
Recon, Recon,
ExternalServiceAuth,
Dispute { Dispute {
dispute_id: String, dispute_id: String,
}, },

View File

@ -18,6 +18,7 @@ pub mod customers;
pub mod disputes; pub mod disputes;
pub mod encryption; pub mod encryption;
pub mod errors; pub mod errors;
pub mod external_service_auth;
pub mod files; pub mod files;
#[cfg(feature = "frm")] #[cfg(feature = "frm")]
pub mod fraud_check; pub mod fraud_check;

View File

@ -0,0 +1,94 @@
use api_models::external_service_auth as external_service_auth_api;
use common_utils::fp_utils;
use error_stack::ResultExt;
use masking::ExposeInterface;
use crate::{
core::errors::{self, RouterResponse},
services::{
api as service_api,
authentication::{self, ExternalServiceType, ExternalToken},
},
SessionState,
};
pub async fn generate_external_token(
state: SessionState,
user: authentication::UserFromToken,
external_service_type: ExternalServiceType,
) -> RouterResponse<external_service_auth_api::ExternalTokenResponse> {
let token = ExternalToken::new_token(
user.user_id.clone(),
user.merchant_id.clone(),
&state.conf,
external_service_type.clone(),
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable_lazy(|| {
format!(
"Failed to create external token for params [user_id, mid, external_service_type] [{}, {:?}, {:?}]",
user.user_id, user.merchant_id, external_service_type,
)
})?;
Ok(service_api::ApplicationResponse::Json(
external_service_auth_api::ExternalTokenResponse {
token: token.into(),
},
))
}
pub async fn signout_external_token(
state: SessionState,
json_payload: external_service_auth_api::ExternalSignoutTokenRequest,
) -> RouterResponse<()> {
let token = authentication::decode_jwt::<ExternalToken>(&json_payload.token.expose(), &state)
.await
.change_context(errors::ApiErrorResponse::Unauthorized)?;
authentication::blacklist::insert_user_in_blacklist(&state, &token.user_id)
.await
.change_context(errors::ApiErrorResponse::InvalidJwtToken)?;
Ok(service_api::ApplicationResponse::StatusOk)
}
pub async fn verify_external_token(
state: SessionState,
json_payload: external_service_auth_api::ExternalVerifyTokenRequest,
external_service_type: ExternalServiceType,
) -> RouterResponse<external_service_auth_api::ExternalVerifyTokenResponse> {
let token_from_payload = json_payload.token.expose();
let token = authentication::decode_jwt::<ExternalToken>(&token_from_payload, &state)
.await
.change_context(errors::ApiErrorResponse::Unauthorized)?;
fp_utils::when(
authentication::blacklist::check_user_in_blacklist(&state, &token.user_id, token.exp)
.await?,
|| Err(errors::ApiErrorResponse::InvalidJwtToken),
)?;
token.check_service_type(&external_service_type)?;
let user_in_db = state
.global_store
.find_user_by_id(&token.user_id)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("User not found in database")?;
let email = user_in_db.email.clone();
let name = user_in_db.name;
Ok(service_api::ApplicationResponse::Json(
external_service_auth_api::ExternalVerifyTokenResponse::Hypersense {
user_id: user_in_db.user_id,
merchant_id: token.merchant_id,
name,
email,
},
))
}

View File

@ -144,6 +144,7 @@ pub fn mk_app(
.service(routes::MerchantConnectorAccount::server(state.clone())) .service(routes::MerchantConnectorAccount::server(state.clone()))
.service(routes::RelayWebhooks::server(state.clone())) .service(routes::RelayWebhooks::server(state.clone()))
.service(routes::Webhooks::server(state.clone())) .service(routes::Webhooks::server(state.clone()))
.service(routes::Hypersense::server(state.clone()))
.service(routes::Relay::server(state.clone())); .service(routes::Relay::server(state.clone()));
#[cfg(feature = "oltp")] #[cfg(feature = "oltp")]

View File

@ -23,6 +23,7 @@ pub mod files;
pub mod fraud_check; pub mod fraud_check;
pub mod gsm; pub mod gsm;
pub mod health; pub mod health;
pub mod hypersense;
pub mod lock_utils; pub mod lock_utils;
#[cfg(feature = "v1")] #[cfg(feature = "v1")]
pub mod locker_migration; pub mod locker_migration;
@ -69,9 +70,9 @@ pub use self::app::PaymentMethodsSession;
pub use self::app::Recon; pub use self::app::Recon;
pub use self::app::{ pub use self::app::{
ApiKeys, AppState, ApplePayCertificatesMigration, Cache, Cards, Configs, ConnectorOnboarding, ApiKeys, AppState, ApplePayCertificatesMigration, Cache, Cards, Configs, ConnectorOnboarding,
Customers, Disputes, EphemeralKey, FeatureMatrix, Files, Forex, Gsm, Health, Mandates, Customers, Disputes, EphemeralKey, FeatureMatrix, Files, Forex, Gsm, Health, Hypersense,
MerchantAccount, MerchantConnectorAccount, PaymentLink, PaymentMethods, Payments, Poll, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, PaymentMethods, Payments,
Profile, ProfileNew, Refunds, Relay, RelayWebhooks, SessionState, User, Webhooks, Poll, Profile, ProfileNew, Refunds, Relay, RelayWebhooks, SessionState, User, Webhooks,
}; };
#[cfg(feature = "olap")] #[cfg(feature = "olap")]
pub use self::app::{Blocklist, Organization, Routing, Verify, WebhookEvents}; pub use self::app::{Blocklist, Organization, Routing, Verify, WebhookEvents};

View File

@ -88,6 +88,7 @@ pub use crate::{
use crate::{ use crate::{
configs::{secrets_transformers, Settings}, configs::{secrets_transformers, Settings},
db::kafka_store::{KafkaStore, TenantID}, db::kafka_store::{KafkaStore, TenantID},
routes::hypersense as hypersense_routes,
}; };
#[derive(Clone)] #[derive(Clone)]
@ -1330,6 +1331,27 @@ impl Recon {
} }
} }
pub struct Hypersense;
impl Hypersense {
pub fn server(state: AppState) -> Scope {
web::scope("/hypersense")
.app_data(web::Data::new(state))
.service(
web::resource("/token")
.route(web::get().to(hypersense_routes::get_hypersense_token)),
)
.service(
web::resource("/verify_token")
.route(web::post().to(hypersense_routes::verify_hypersense_token)),
)
.service(
web::resource("/signout")
.route(web::post().to(hypersense_routes::signout_hypersense_token)),
)
}
}
#[cfg(feature = "olap")] #[cfg(feature = "olap")]
pub struct Blocklist; pub struct Blocklist;

View File

@ -0,0 +1,76 @@
use actix_web::{web, HttpRequest, HttpResponse};
use api_models::external_service_auth as external_service_auth_api;
use router_env::Flow;
use super::AppState;
use crate::{
core::{api_locking, external_service_auth},
services::{
api,
authentication::{self, ExternalServiceType},
},
};
pub async fn get_hypersense_token(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
let flow = Flow::HypersenseTokenRequest;
Box::pin(api::server_wrap(
flow,
state,
&req,
(),
|state, user, _, _| {
external_service_auth::generate_external_token(
state,
user,
ExternalServiceType::Hypersense,
)
},
&authentication::DashboardNoPermissionAuth,
api_locking::LockAction::NotApplicable,
))
.await
}
pub async fn signout_hypersense_token(
state: web::Data<AppState>,
http_req: HttpRequest,
json_payload: web::Json<external_service_auth_api::ExternalSignoutTokenRequest>,
) -> HttpResponse {
let flow = Flow::HypersenseSignoutToken;
Box::pin(api::server_wrap(
flow,
state.clone(),
&http_req,
json_payload.into_inner(),
|state, _: (), json_payload, _| {
external_service_auth::signout_external_token(state, json_payload)
},
&authentication::NoAuth,
api_locking::LockAction::NotApplicable,
))
.await
}
pub async fn verify_hypersense_token(
state: web::Data<AppState>,
http_req: HttpRequest,
json_payload: web::Json<external_service_auth_api::ExternalVerifyTokenRequest>,
) -> HttpResponse {
let flow = Flow::HypersenseVerifyToken;
Box::pin(api::server_wrap(
flow,
state.clone(),
&http_req,
json_payload.into_inner(),
|state, _: (), json_payload, _| {
external_service_auth::verify_external_token(
state,
json_payload,
ExternalServiceType::Hypersense,
)
},
&authentication::NoAuth,
api_locking::LockAction::NotApplicable,
))
.await
}

View File

@ -39,6 +39,7 @@ pub enum ApiIdentifier {
ApplePayCertificatesMigration, ApplePayCertificatesMigration,
Relay, Relay,
Documentation, Documentation,
Hypersense,
PaymentMethodsSession, PaymentMethodsSession,
} }
@ -311,6 +312,10 @@ impl From<Flow> for ApiIdentifier {
Flow::FeatureMatrix => Self::Documentation, Flow::FeatureMatrix => Self::Documentation,
Flow::HypersenseTokenRequest
| Flow::HypersenseVerifyToken
| Flow::HypersenseSignoutToken => Self::Hypersense,
Flow::PaymentMethodSessionCreate Flow::PaymentMethodSessionCreate
| Flow::PaymentMethodSessionRetrieve | Flow::PaymentMethodSessionRetrieve
| Flow::PaymentMethodSessionUpdateSavedPaymentMethod => Self::PaymentMethodsSession, | Flow::PaymentMethodSessionUpdateSavedPaymentMethod => Self::PaymentMethodsSession,

View File

@ -13,9 +13,7 @@ use api_models::payouts;
use api_models::{payment_methods::PaymentMethodListRequest, payments}; use api_models::{payment_methods::PaymentMethodListRequest, payments};
use async_trait::async_trait; use async_trait::async_trait;
use common_enums::TokenPurpose; use common_enums::TokenPurpose;
#[cfg(feature = "v2")] use common_utils::{date_time, fp_utils, id_type};
use common_utils::fp_utils;
use common_utils::{date_time, id_type};
#[cfg(feature = "v2")] #[cfg(feature = "v2")]
use diesel_models::ephemeral_key; use diesel_models::ephemeral_key;
use error_stack::{report, ResultExt}; use error_stack::{report, ResultExt};
@ -195,6 +193,13 @@ impl AuthenticationType {
} }
} }
#[derive(Clone, Debug, Eq, PartialEq, Serialize, serde::Deserialize, strum::Display)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum ExternalServiceType {
Hypersense,
}
#[cfg(feature = "olap")] #[cfg(feature = "olap")]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct UserFromSinglePurposeToken { pub struct UserFromSinglePurposeToken {
@ -3857,3 +3862,44 @@ impl ReconToken {
jwt::generate_jwt(&token_payload, settings).await jwt::generate_jwt(&token_payload, settings).await
} }
} }
#[derive(serde::Serialize, serde::Deserialize)]
pub struct ExternalToken {
pub user_id: String,
pub merchant_id: id_type::MerchantId,
pub exp: u64,
pub external_service_type: ExternalServiceType,
}
impl ExternalToken {
pub async fn new_token(
user_id: String,
merchant_id: id_type::MerchantId,
settings: &Settings,
external_service_type: ExternalServiceType,
) -> UserResult<String> {
let exp_duration = std::time::Duration::from_secs(consts::JWT_TOKEN_TIME_IN_SECS);
let exp = jwt::generate_exp(exp_duration)?.as_secs();
let token_payload = Self {
user_id,
merchant_id,
exp,
external_service_type,
};
jwt::generate_jwt(&token_payload, settings).await
}
pub fn check_service_type(
&self,
required_service_type: &ExternalServiceType,
) -> RouterResult<()> {
Ok(fp_utils::when(
&self.external_service_type != required_service_type,
|| {
Err(errors::ApiErrorResponse::AccessForbidden {
resource: required_service_type.to_string(),
})
},
)?)
}
}

View File

@ -543,6 +543,12 @@ pub enum Flow {
RelayRetrieve, RelayRetrieve,
/// Incoming Relay Webhook Receive /// Incoming Relay Webhook Receive
IncomingRelayWebhookReceive, IncomingRelayWebhookReceive,
/// Generate Hypersense Token
HypersenseTokenRequest,
/// Verify Hypersense Token
HypersenseVerifyToken,
/// Signout Hypersense Token
HypersenseSignoutToken,
/// Payment Method Session Create /// Payment Method Session Create
PaymentMethodSessionCreate, PaymentMethodSessionCreate,
/// Payment Method Session Retrieve /// Payment Method Session Retrieve