mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 09:07:09 +08:00
feat(recon): add recon APIs (#3345)
Co-authored-by: Kashif <mohammed.kashif@juspay.in> Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -8,7 +8,7 @@ readme = "README.md"
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["payouts", "frm"]
|
default = ["payouts", "frm", "recon"]
|
||||||
business_profile_routing = []
|
business_profile_routing = []
|
||||||
connector_choice_bcompat = []
|
connector_choice_bcompat = []
|
||||||
errors = ["dep:actix-web", "dep:reqwest"]
|
errors = ["dep:actix-web", "dep:reqwest"]
|
||||||
@ -18,6 +18,7 @@ dummy_connector = ["euclid/dummy_connector", "common_enums/dummy_connector"]
|
|||||||
detailed_errors = []
|
detailed_errors = []
|
||||||
payouts = []
|
payouts = []
|
||||||
frm = []
|
frm = []
|
||||||
|
recon = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = { version = "4.3.1", optional = true }
|
actix-web = { version = "4.3.1", optional = true }
|
||||||
|
|||||||
@ -5,6 +5,8 @@ mod locker_migration;
|
|||||||
pub mod payment;
|
pub mod payment;
|
||||||
#[cfg(feature = "payouts")]
|
#[cfg(feature = "payouts")]
|
||||||
pub mod payouts;
|
pub mod payouts;
|
||||||
|
#[cfg(feature = "recon")]
|
||||||
|
pub mod recon;
|
||||||
pub mod refund;
|
pub mod refund;
|
||||||
pub mod routing;
|
pub mod routing;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|||||||
21
crates/api_models/src/events/recon.rs
Normal file
21
crates/api_models/src/events/recon.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
use common_utils::events::{ApiEventMetric, ApiEventsType};
|
||||||
|
|
||||||
|
use crate::recon::{ReconStatusResponse, ReconTokenResponse, ReconUpdateMerchantRequest};
|
||||||
|
|
||||||
|
impl ApiEventMetric for ReconUpdateMerchantRequest {
|
||||||
|
fn get_api_event_type(&self) -> Option<ApiEventsType> {
|
||||||
|
Some(ApiEventsType::Recon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApiEventMetric for ReconTokenResponse {
|
||||||
|
fn get_api_event_type(&self) -> Option<ApiEventsType> {
|
||||||
|
Some(ApiEventsType::Recon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApiEventMetric for ReconStatusResponse {
|
||||||
|
fn get_api_event_type(&self) -> Option<ApiEventsType> {
|
||||||
|
Some(ApiEventsType::Recon)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,11 @@
|
|||||||
use common_utils::events::{ApiEventMetric, ApiEventsType};
|
use common_utils::events::{ApiEventMetric, ApiEventsType};
|
||||||
|
#[cfg(feature = "recon")]
|
||||||
|
use masking::PeekInterface;
|
||||||
|
|
||||||
#[cfg(feature = "dummy_connector")]
|
#[cfg(feature = "dummy_connector")]
|
||||||
use crate::user::sample_data::SampleDataRequest;
|
use crate::user::sample_data::SampleDataRequest;
|
||||||
|
#[cfg(feature = "recon")]
|
||||||
|
use crate::user::VerifyTokenResponse;
|
||||||
use crate::user::{
|
use crate::user::{
|
||||||
dashboard_metadata::{
|
dashboard_metadata::{
|
||||||
GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest,
|
GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest,
|
||||||
@ -21,6 +25,16 @@ impl ApiEventMetric for DashboardEntryResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "recon")]
|
||||||
|
impl ApiEventMetric for VerifyTokenResponse {
|
||||||
|
fn get_api_event_type(&self) -> Option<ApiEventsType> {
|
||||||
|
Some(ApiEventsType::User {
|
||||||
|
merchant_id: self.merchant_id.clone(),
|
||||||
|
user_id: self.user_email.peek().to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
common_utils::impl_misc_api_event_type!(
|
common_utils::impl_misc_api_event_type!(
|
||||||
SignUpRequest,
|
SignUpRequest,
|
||||||
SignUpWithMerchantIdRequest,
|
SignUpWithMerchantIdRequest,
|
||||||
|
|||||||
@ -26,6 +26,8 @@ pub mod payments;
|
|||||||
#[cfg(feature = "payouts")]
|
#[cfg(feature = "payouts")]
|
||||||
pub mod payouts;
|
pub mod payouts;
|
||||||
pub mod pm_auth;
|
pub mod pm_auth;
|
||||||
|
#[cfg(feature = "recon")]
|
||||||
|
pub mod recon;
|
||||||
pub mod refunds;
|
pub mod refunds;
|
||||||
pub mod routing;
|
pub mod routing;
|
||||||
pub mod surcharge_decision_configs;
|
pub mod surcharge_decision_configs;
|
||||||
|
|||||||
21
crates/api_models/src/recon.rs
Normal file
21
crates/api_models/src/recon.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
use common_utils::pii;
|
||||||
|
use masking::Secret;
|
||||||
|
|
||||||
|
use crate::enums;
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug, serde::Serialize)]
|
||||||
|
pub struct ReconUpdateMerchantRequest {
|
||||||
|
pub merchant_id: String,
|
||||||
|
pub recon_status: enums::ReconStatus,
|
||||||
|
pub user_email: pii::Email,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize)]
|
||||||
|
pub struct ReconTokenResponse {
|
||||||
|
pub token: Secret<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize)]
|
||||||
|
pub struct ReconStatusResponse {
|
||||||
|
pub recon_status: enums::ReconStatus,
|
||||||
|
}
|
||||||
@ -140,3 +140,10 @@ pub struct UserMerchantAccount {
|
|||||||
pub merchant_id: String,
|
pub merchant_id: String,
|
||||||
pub merchant_name: OptionalEncryptableName,
|
pub merchant_name: OptionalEncryptableName,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "recon")]
|
||||||
|
#[derive(serde::Serialize, Debug)]
|
||||||
|
pub struct VerifyTokenResponse {
|
||||||
|
pub merchant_id: String,
|
||||||
|
pub user_email: pii::Email,
|
||||||
|
}
|
||||||
|
|||||||
@ -49,6 +49,7 @@ pub enum ApiEventsType {
|
|||||||
Miscellaneous,
|
Miscellaneous,
|
||||||
RustLocker,
|
RustLocker,
|
||||||
FraudCheck,
|
FraudCheck,
|
||||||
|
Recon,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApiEventMetric for serde_json::Value {}
|
impl ApiEventMetric for serde_json::Value {}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ readme = "README.md"
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "business_profile_routing", "connector_choice_mca_id", "profile_specific_fallback_routing", "retry", "frm"]
|
default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "business_profile_routing", "connector_choice_mca_id", "profile_specific_fallback_routing", "retry", "frm", "recon"]
|
||||||
s3 = ["dep:aws-sdk-s3", "dep:aws-config"]
|
s3 = ["dep:aws-sdk-s3", "dep:aws-config"]
|
||||||
kms = ["external_services/kms", "dep:aws-config"]
|
kms = ["external_services/kms", "dep:aws-config"]
|
||||||
email = ["external_services/email", "dep:aws-config", "olap"]
|
email = ["external_services/email", "dep:aws-config", "olap"]
|
||||||
@ -30,6 +30,7 @@ connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connect
|
|||||||
external_access_dc = ["dummy_connector"]
|
external_access_dc = ["dummy_connector"]
|
||||||
detailed_errors = ["api_models/detailed_errors", "error-stack/serde"]
|
detailed_errors = ["api_models/detailed_errors", "error-stack/serde"]
|
||||||
payouts = []
|
payouts = []
|
||||||
|
recon = ["email"]
|
||||||
retry = []
|
retry = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -757,3 +757,32 @@ pub async fn send_verification_mail(
|
|||||||
|
|
||||||
Ok(ApplicationResponse::StatusOk)
|
Ok(ApplicationResponse::StatusOk)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "recon")]
|
||||||
|
pub async fn verify_token(
|
||||||
|
state: AppState,
|
||||||
|
req: auth::ReconUser,
|
||||||
|
) -> UserResponse<user_api::VerifyTokenResponse> {
|
||||||
|
let user = state
|
||||||
|
.store
|
||||||
|
.find_user_by_id(&req.user_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
if e.current_context().is_db_not_found() {
|
||||||
|
e.change_context(UserErrors::UserNotFound)
|
||||||
|
} else {
|
||||||
|
e.change_context(UserErrors::InternalServerError)
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
let merchant_id = state
|
||||||
|
.store
|
||||||
|
.find_user_role_by_user_id(&req.user_id)
|
||||||
|
.await
|
||||||
|
.change_context(UserErrors::InternalServerError)?
|
||||||
|
.merchant_id;
|
||||||
|
|
||||||
|
Ok(ApplicationResponse::Json(user_api::VerifyTokenResponse {
|
||||||
|
merchant_id: merchant_id.to_string(),
|
||||||
|
user_email: user.email,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|||||||
@ -165,6 +165,12 @@ pub fn mk_app(
|
|||||||
{
|
{
|
||||||
server_app = server_app.service(routes::StripeApis::server(state.clone()));
|
server_app = server_app.service(routes::StripeApis::server(state.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "recon")]
|
||||||
|
{
|
||||||
|
server_app = server_app.service(routes::Recon::server(state.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
server_app = server_app.service(routes::Cards::server(state.clone()));
|
server_app = server_app.service(routes::Cards::server(state.clone()));
|
||||||
server_app = server_app.service(routes::Cache::server(state.clone()));
|
server_app = server_app.service(routes::Cache::server(state.clone()));
|
||||||
server_app = server_app.service(routes::Health::server(state));
|
server_app = server_app.service(routes::Health::server(state));
|
||||||
|
|||||||
@ -28,6 +28,8 @@ pub mod payment_methods;
|
|||||||
pub mod payments;
|
pub mod payments;
|
||||||
#[cfg(feature = "payouts")]
|
#[cfg(feature = "payouts")]
|
||||||
pub mod payouts;
|
pub mod payouts;
|
||||||
|
#[cfg(feature = "recon")]
|
||||||
|
pub mod recon;
|
||||||
pub mod refunds;
|
pub mod refunds;
|
||||||
#[cfg(feature = "olap")]
|
#[cfg(feature = "olap")]
|
||||||
pub mod routing;
|
pub mod routing;
|
||||||
@ -53,6 +55,8 @@ pub use self::app::DummyConnector;
|
|||||||
pub use self::app::Forex;
|
pub use self::app::Forex;
|
||||||
#[cfg(feature = "payouts")]
|
#[cfg(feature = "payouts")]
|
||||||
pub use self::app::Payouts;
|
pub use self::app::Payouts;
|
||||||
|
#[cfg(all(feature = "olap", feature = "recon"))]
|
||||||
|
pub use self::app::Recon;
|
||||||
#[cfg(all(feature = "olap", feature = "kms"))]
|
#[cfg(all(feature = "olap", feature = "kms"))]
|
||||||
pub use self::app::Verify;
|
pub use self::app::Verify;
|
||||||
pub use self::app::{
|
pub use self::app::{
|
||||||
|
|||||||
@ -40,6 +40,8 @@ use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*};
|
|||||||
use super::{ephemeral_key::*, payment_methods::*, webhooks::*};
|
use super::{ephemeral_key::*, payment_methods::*, webhooks::*};
|
||||||
#[cfg(all(feature = "frm", feature = "oltp"))]
|
#[cfg(all(feature = "frm", feature = "oltp"))]
|
||||||
use crate::routes::fraud_check as frm_routes;
|
use crate::routes::fraud_check as frm_routes;
|
||||||
|
#[cfg(all(feature = "recon", feature = "olap"))]
|
||||||
|
use crate::routes::recon as recon_routes;
|
||||||
#[cfg(feature = "olap")]
|
#[cfg(feature = "olap")]
|
||||||
use crate::routes::verify_connector::payment_connector_verify;
|
use crate::routes::verify_connector::payment_connector_verify;
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
@ -568,6 +570,26 @@ impl PaymentMethods {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "olap", feature = "recon"))]
|
||||||
|
pub struct Recon;
|
||||||
|
|
||||||
|
#[cfg(all(feature = "olap", feature = "recon"))]
|
||||||
|
impl Recon {
|
||||||
|
pub fn server(state: AppState) -> Scope {
|
||||||
|
web::scope("/recon")
|
||||||
|
.app_data(web::Data::new(state))
|
||||||
|
.service(
|
||||||
|
web::resource("/update_merchant")
|
||||||
|
.route(web::post().to(recon_routes::update_merchant)),
|
||||||
|
)
|
||||||
|
.service(web::resource("/token").route(web::get().to(recon_routes::get_recon_token)))
|
||||||
|
.service(
|
||||||
|
web::resource("/request").route(web::post().to(recon_routes::request_for_recon)),
|
||||||
|
)
|
||||||
|
.service(web::resource("/verify_token").route(web::get().to(verify_recon_token)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "olap")]
|
#[cfg(feature = "olap")]
|
||||||
pub struct Blocklist;
|
pub struct Blocklist;
|
||||||
|
|
||||||
|
|||||||
@ -31,6 +31,7 @@ pub enum ApiIdentifier {
|
|||||||
User,
|
User,
|
||||||
UserRole,
|
UserRole,
|
||||||
ConnectorOnboarding,
|
ConnectorOnboarding,
|
||||||
|
Recon,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Flow> for ApiIdentifier {
|
impl From<Flow> for ApiIdentifier {
|
||||||
@ -186,6 +187,11 @@ impl From<Flow> for ApiIdentifier {
|
|||||||
Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => {
|
Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => {
|
||||||
Self::ConnectorOnboarding
|
Self::ConnectorOnboarding
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Flow::ReconMerchantUpdate
|
||||||
|
| Flow::ReconTokenRequest
|
||||||
|
| Flow::ReconServiceRequest
|
||||||
|
| Flow::ReconVerifyToken => Self::Recon,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
250
crates/router/src/routes/recon.rs
Normal file
250
crates/router/src/routes/recon.rs
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
use actix_web::{web, HttpRequest, HttpResponse};
|
||||||
|
use api_models::recon as recon_api;
|
||||||
|
use common_enums::ReconStatus;
|
||||||
|
use error_stack::ResultExt;
|
||||||
|
use masking::{ExposeInterface, PeekInterface, Secret};
|
||||||
|
use router_env::Flow;
|
||||||
|
|
||||||
|
use super::AppState;
|
||||||
|
use crate::{
|
||||||
|
core::{
|
||||||
|
api_locking,
|
||||||
|
errors::{self, RouterResponse, RouterResult, StorageErrorExt, UserErrors},
|
||||||
|
},
|
||||||
|
services::{
|
||||||
|
api as service_api, api,
|
||||||
|
authentication::{self as auth, ReconUser, UserFromToken},
|
||||||
|
email::types as email_types,
|
||||||
|
recon::ReconToken,
|
||||||
|
},
|
||||||
|
types::{
|
||||||
|
api::{self as api_types, enums},
|
||||||
|
domain::{UserEmail, UserFromStorage, UserName},
|
||||||
|
storage,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn update_merchant(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
req: HttpRequest,
|
||||||
|
json_payload: web::Json<recon_api::ReconUpdateMerchantRequest>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let flow = Flow::ReconMerchantUpdate;
|
||||||
|
Box::pin(api::server_wrap(
|
||||||
|
flow,
|
||||||
|
state,
|
||||||
|
&req,
|
||||||
|
json_payload.into_inner(),
|
||||||
|
|state, _user, req| recon_merchant_account_update(state, req),
|
||||||
|
&auth::ReconAdmin,
|
||||||
|
api_locking::LockAction::NotApplicable,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn request_for_recon(state: web::Data<AppState>, http_req: HttpRequest) -> HttpResponse {
|
||||||
|
let flow = Flow::ReconServiceRequest;
|
||||||
|
Box::pin(api::server_wrap(
|
||||||
|
flow,
|
||||||
|
state,
|
||||||
|
&http_req,
|
||||||
|
(),
|
||||||
|
|state, user: UserFromToken, _req| send_recon_request(state, user),
|
||||||
|
&auth::DashboardNoPermissionAuth,
|
||||||
|
api_locking::LockAction::NotApplicable,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_recon_token(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
|
||||||
|
let flow = Flow::ReconTokenRequest;
|
||||||
|
Box::pin(api::server_wrap(
|
||||||
|
flow,
|
||||||
|
state,
|
||||||
|
&req,
|
||||||
|
(),
|
||||||
|
|state, user: ReconUser, _| generate_recon_token(state, user),
|
||||||
|
&auth::ReconJWT,
|
||||||
|
api_locking::LockAction::NotApplicable,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_recon_request(
|
||||||
|
state: AppState,
|
||||||
|
user: UserFromToken,
|
||||||
|
) -> RouterResponse<recon_api::ReconStatusResponse> {
|
||||||
|
let db = &*state.store;
|
||||||
|
let user_from_db = db
|
||||||
|
.find_user_by_id(&user.user_id)
|
||||||
|
.await
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)?;
|
||||||
|
let merchant_id = db
|
||||||
|
.find_user_role_by_user_id(&user.user_id)
|
||||||
|
.await
|
||||||
|
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?
|
||||||
|
.merchant_id;
|
||||||
|
let key_store = db
|
||||||
|
.get_merchant_key_store_by_merchant_id(
|
||||||
|
merchant_id.as_str(),
|
||||||
|
&db.get_master_key().to_vec().into(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
|
||||||
|
let merchant_account = db
|
||||||
|
.find_merchant_account_by_merchant_id(merchant_id.as_str(), &key_store)
|
||||||
|
.await
|
||||||
|
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
|
||||||
|
|
||||||
|
let email_contents = email_types::ProFeatureRequest {
|
||||||
|
feature_name: "RECONCILIATION & SETTLEMENT".to_string(),
|
||||||
|
merchant_id: merchant_id.clone(),
|
||||||
|
user_name: UserName::new(user_from_db.name)
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("Failed to form username")?,
|
||||||
|
recipient_email: UserEmail::from_pii_email(user_from_db.email.clone())
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("Failed to convert to UserEmail from pii::Email")?,
|
||||||
|
settings: state.conf.clone(),
|
||||||
|
subject: format!(
|
||||||
|
"Dashboard Pro Feature Request by {}",
|
||||||
|
user_from_db.email.expose().peek()
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_email_sent = state
|
||||||
|
.email_client
|
||||||
|
.compose_and_send_email(
|
||||||
|
Box::new(email_contents),
|
||||||
|
state.conf.proxy.https_url.as_ref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.change_context(UserErrors::InternalServerError)
|
||||||
|
.attach_printable("Failed to compose and send email for ProFeatureRequest")
|
||||||
|
.is_ok();
|
||||||
|
|
||||||
|
if is_email_sent {
|
||||||
|
let updated_merchant_account = storage::MerchantAccountUpdate::ReconUpdate {
|
||||||
|
recon_status: enums::ReconStatus::Requested,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = db
|
||||||
|
.update_merchant(merchant_account, updated_merchant_account, &key_store)
|
||||||
|
.await
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable_lazy(|| {
|
||||||
|
format!("Failed while updating merchant's recon status: {merchant_id}")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(service_api::ApplicationResponse::Json(
|
||||||
|
recon_api::ReconStatusResponse {
|
||||||
|
recon_status: response.recon_status,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(service_api::ApplicationResponse::Json(
|
||||||
|
recon_api::ReconStatusResponse {
|
||||||
|
recon_status: enums::ReconStatus::NotRequested,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn recon_merchant_account_update(
|
||||||
|
state: AppState,
|
||||||
|
req: recon_api::ReconUpdateMerchantRequest,
|
||||||
|
) -> RouterResponse<api_types::MerchantAccountResponse> {
|
||||||
|
let merchant_id = &req.merchant_id.clone();
|
||||||
|
let user_email = &req.user_email.clone();
|
||||||
|
|
||||||
|
let db = &*state.store;
|
||||||
|
|
||||||
|
let key_store = db
|
||||||
|
.get_merchant_key_store_by_merchant_id(
|
||||||
|
&req.merchant_id,
|
||||||
|
&db.get_master_key().to_vec().into(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
|
||||||
|
|
||||||
|
let merchant_account = db
|
||||||
|
.find_merchant_account_by_merchant_id(merchant_id, &key_store)
|
||||||
|
.await
|
||||||
|
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
|
||||||
|
|
||||||
|
let updated_merchant_account = storage::MerchantAccountUpdate::ReconUpdate {
|
||||||
|
recon_status: req.recon_status,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = db
|
||||||
|
.update_merchant(merchant_account, updated_merchant_account, &key_store)
|
||||||
|
.await
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable_lazy(|| {
|
||||||
|
format!("Failed while updating merchant's recon status: {merchant_id}")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let email_contents = email_types::ReconActivation {
|
||||||
|
recipient_email: UserEmail::from_pii_email(user_email.clone())
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("Failed to convert to UserEmail from pii::Email")?,
|
||||||
|
user_name: UserName::new(Secret::new("HyperSwitch User".to_string()))
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("Failed to form username")?,
|
||||||
|
settings: state.conf.clone(),
|
||||||
|
subject: "Approval of Recon Request - Access Granted to Recon Dashboard",
|
||||||
|
};
|
||||||
|
|
||||||
|
if req.recon_status == ReconStatus::Active {
|
||||||
|
let _is_email_sent = state
|
||||||
|
.email_client
|
||||||
|
.compose_and_send_email(
|
||||||
|
Box::new(email_contents),
|
||||||
|
state.conf.proxy.https_url.as_ref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.change_context(UserErrors::InternalServerError)
|
||||||
|
.attach_printable("Failed to compose and send email for ReconActivation")
|
||||||
|
.is_ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(service_api::ApplicationResponse::Json(
|
||||||
|
response
|
||||||
|
.try_into()
|
||||||
|
.change_context(errors::ApiErrorResponse::InvalidDataValue {
|
||||||
|
field_name: "merchant_account",
|
||||||
|
})?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn generate_recon_token(
|
||||||
|
state: AppState,
|
||||||
|
req: ReconUser,
|
||||||
|
) -> RouterResponse<recon_api::ReconTokenResponse> {
|
||||||
|
let db = &*state.store;
|
||||||
|
let user = db
|
||||||
|
.find_user_by_id(&req.user_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
if e.current_context().is_db_not_found() {
|
||||||
|
e.change_context(errors::ApiErrorResponse::InvalidJwtToken)
|
||||||
|
} else {
|
||||||
|
e.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
}
|
||||||
|
})?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let token = Box::pin(get_recon_auth_token(user, state))
|
||||||
|
.await
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)?;
|
||||||
|
Ok(service_api::ApplicationResponse::Json(
|
||||||
|
recon_api::ReconTokenResponse { token },
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_recon_auth_token(
|
||||||
|
user: UserFromStorage,
|
||||||
|
state: AppState,
|
||||||
|
) -> RouterResult<Secret<String>> {
|
||||||
|
ReconToken::new_token(user.0.user_id.clone(), &state.conf).await
|
||||||
|
}
|
||||||
@ -388,3 +388,18 @@ pub async fn verify_email_request(
|
|||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "recon")]
|
||||||
|
pub async fn verify_recon_token(state: web::Data<AppState>, http_req: HttpRequest) -> HttpResponse {
|
||||||
|
let flow = Flow::ReconVerifyToken;
|
||||||
|
Box::pin(api::server_wrap(
|
||||||
|
flow,
|
||||||
|
state.clone(),
|
||||||
|
&http_req,
|
||||||
|
(),
|
||||||
|
|state, user, _req| user_core::verify_token(state, user),
|
||||||
|
&auth::ReconJWT,
|
||||||
|
api_locking::LockAction::NotApplicable,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|||||||
@ -7,6 +7,8 @@ pub mod jwt;
|
|||||||
pub mod kafka;
|
pub mod kafka;
|
||||||
pub mod logger;
|
pub mod logger;
|
||||||
pub mod pm_auth;
|
pub mod pm_auth;
|
||||||
|
#[cfg(feature = "recon")]
|
||||||
|
pub mod recon;
|
||||||
|
|
||||||
#[cfg(feature = "email")]
|
#[cfg(feature = "email")]
|
||||||
pub mod email;
|
pub mod email;
|
||||||
|
|||||||
@ -12,10 +12,14 @@ use serde::Serialize;
|
|||||||
use super::authorization::{self, permissions::Permission};
|
use super::authorization::{self, permissions::Permission};
|
||||||
#[cfg(feature = "olap")]
|
#[cfg(feature = "olap")]
|
||||||
use super::jwt;
|
use super::jwt;
|
||||||
|
#[cfg(feature = "recon")]
|
||||||
|
use super::recon::ReconToken;
|
||||||
#[cfg(feature = "olap")]
|
#[cfg(feature = "olap")]
|
||||||
use crate::consts;
|
use crate::consts;
|
||||||
#[cfg(feature = "olap")]
|
#[cfg(feature = "olap")]
|
||||||
use crate::core::errors::UserResult;
|
use crate::core::errors::UserResult;
|
||||||
|
#[cfg(feature = "recon")]
|
||||||
|
use crate::routes::AppState;
|
||||||
use crate::{
|
use crate::{
|
||||||
configs::settings,
|
configs::settings,
|
||||||
core::{
|
core::{
|
||||||
@ -822,3 +826,95 @@ where
|
|||||||
}
|
}
|
||||||
default_auth
|
default_auth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "recon")]
|
||||||
|
static RECON_API_KEY: tokio::sync::OnceCell<StrongSecret<String>> =
|
||||||
|
tokio::sync::OnceCell::const_new();
|
||||||
|
|
||||||
|
#[cfg(feature = "recon")]
|
||||||
|
pub async fn get_recon_admin_api_key(
|
||||||
|
secrets: &settings::Secrets,
|
||||||
|
#[cfg(feature = "kms")] kms_client: &kms::KmsClient,
|
||||||
|
) -> RouterResult<&'static StrongSecret<String>> {
|
||||||
|
RECON_API_KEY
|
||||||
|
.get_or_try_init(|| async {
|
||||||
|
#[cfg(feature = "kms")]
|
||||||
|
let recon_admin_api_key = secrets
|
||||||
|
.kms_encrypted_recon_admin_api_key
|
||||||
|
.decrypt_inner(kms_client)
|
||||||
|
.await
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("Failed to KMS decrypt recon admin API key")?;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "kms"))]
|
||||||
|
let recon_admin_api_key = secrets.recon_admin_api_key.clone();
|
||||||
|
|
||||||
|
Ok(StrongSecret::new(recon_admin_api_key))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "recon")]
|
||||||
|
pub struct ReconAdmin;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
#[cfg(feature = "recon")]
|
||||||
|
impl<A> AuthenticateAndFetch<(), A> for ReconAdmin
|
||||||
|
where
|
||||||
|
A: AppStateInfo + Sync,
|
||||||
|
{
|
||||||
|
async fn authenticate_and_fetch(
|
||||||
|
&self,
|
||||||
|
request_headers: &HeaderMap,
|
||||||
|
state: &A,
|
||||||
|
) -> RouterResult<((), AuthenticationType)> {
|
||||||
|
let request_admin_api_key =
|
||||||
|
get_api_key(request_headers).change_context(errors::ApiErrorResponse::Unauthorized)?;
|
||||||
|
let conf = state.conf();
|
||||||
|
|
||||||
|
let admin_api_key = get_recon_admin_api_key(
|
||||||
|
&conf.secrets,
|
||||||
|
#[cfg(feature = "kms")]
|
||||||
|
kms::get_kms_client(&conf.kms).await,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if request_admin_api_key != admin_api_key.peek() {
|
||||||
|
Err(report!(errors::ApiErrorResponse::Unauthorized)
|
||||||
|
.attach_printable("Recon Admin Authentication Failure"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(((), AuthenticationType::NoAuth))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "recon")]
|
||||||
|
pub struct ReconJWT;
|
||||||
|
#[cfg(feature = "recon")]
|
||||||
|
pub struct ReconUser {
|
||||||
|
pub user_id: String,
|
||||||
|
}
|
||||||
|
#[cfg(feature = "recon")]
|
||||||
|
impl AuthInfo for ReconUser {
|
||||||
|
fn get_merchant_id(&self) -> Option<&str> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(all(feature = "olap", feature = "recon"))]
|
||||||
|
#[async_trait]
|
||||||
|
impl AuthenticateAndFetch<ReconUser, AppState> for ReconJWT {
|
||||||
|
async fn authenticate_and_fetch(
|
||||||
|
&self,
|
||||||
|
request_headers: &HeaderMap,
|
||||||
|
state: &AppState,
|
||||||
|
) -> RouterResult<(ReconUser, AuthenticationType)> {
|
||||||
|
let payload = parse_jwt_payload::<AppState, ReconToken>(request_headers, state).await?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
ReconUser {
|
||||||
|
user_id: payload.user_id,
|
||||||
|
},
|
||||||
|
AuthenticationType::NoAuth,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,17 +1,37 @@
|
|||||||
use common_utils::errors::CustomResult;
|
use common_utils::errors::CustomResult;
|
||||||
use error_stack::ResultExt;
|
use error_stack::ResultExt;
|
||||||
use external_services::email::{EmailContents, EmailData, EmailError};
|
use external_services::email::{EmailContents, EmailData, EmailError};
|
||||||
use masking::ExposeInterface;
|
use masking::{ExposeInterface, PeekInterface};
|
||||||
|
|
||||||
use crate::{configs, consts};
|
use crate::{configs, consts};
|
||||||
#[cfg(feature = "olap")]
|
#[cfg(feature = "olap")]
|
||||||
use crate::{core::errors::UserErrors, services::jwt, types::domain};
|
use crate::{core::errors::UserErrors, services::jwt, types::domain};
|
||||||
|
|
||||||
pub enum EmailBody {
|
pub enum EmailBody {
|
||||||
Verify { link: String },
|
Verify {
|
||||||
Reset { link: String, user_name: String },
|
link: String,
|
||||||
MagicLink { link: String, user_name: String },
|
},
|
||||||
InviteUser { link: String, user_name: String },
|
Reset {
|
||||||
|
link: String,
|
||||||
|
user_name: String,
|
||||||
|
},
|
||||||
|
MagicLink {
|
||||||
|
link: String,
|
||||||
|
user_name: String,
|
||||||
|
},
|
||||||
|
InviteUser {
|
||||||
|
link: String,
|
||||||
|
user_name: String,
|
||||||
|
},
|
||||||
|
ReconActivation {
|
||||||
|
user_name: String,
|
||||||
|
},
|
||||||
|
ProFeatureRequest {
|
||||||
|
feature_name: String,
|
||||||
|
merchant_id: String,
|
||||||
|
user_name: String,
|
||||||
|
user_email: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod html {
|
pub mod html {
|
||||||
@ -43,6 +63,30 @@ pub mod html {
|
|||||||
link = link
|
link = link
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
EmailBody::ReconActivation { user_name } => {
|
||||||
|
format!(
|
||||||
|
include_str!("assets/recon_activation.html"),
|
||||||
|
username = user_name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
EmailBody::ProFeatureRequest {
|
||||||
|
feature_name,
|
||||||
|
merchant_id,
|
||||||
|
user_name,
|
||||||
|
user_email,
|
||||||
|
} => {
|
||||||
|
format!(
|
||||||
|
"Dear Hyperswitch Support Team,
|
||||||
|
|
||||||
|
Dashboard Pro Feature Request,
|
||||||
|
Feature name : {feature_name}
|
||||||
|
Merchant ID : {merchant_id}
|
||||||
|
Merchant Name : {user_name}
|
||||||
|
Email : {user_email}
|
||||||
|
|
||||||
|
(note: This is an auto generated email. use merchant email for any further comunications)",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -198,3 +242,54 @@ impl EmailData for InviteUser {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ReconActivation {
|
||||||
|
pub recipient_email: domain::UserEmail,
|
||||||
|
pub user_name: domain::UserName,
|
||||||
|
pub settings: std::sync::Arc<configs::settings::Settings>,
|
||||||
|
pub subject: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl EmailData for ReconActivation {
|
||||||
|
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
|
||||||
|
let body = html::get_html_body(EmailBody::ReconActivation {
|
||||||
|
user_name: self.user_name.clone().get_secret().expose(),
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(EmailContents {
|
||||||
|
subject: self.subject.to_string(),
|
||||||
|
body: external_services::email::IntermediateString::new(body),
|
||||||
|
recipient: self.recipient_email.clone().into_inner(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ProFeatureRequest {
|
||||||
|
pub recipient_email: domain::UserEmail,
|
||||||
|
pub feature_name: String,
|
||||||
|
pub merchant_id: String,
|
||||||
|
pub user_name: domain::UserName,
|
||||||
|
pub settings: std::sync::Arc<configs::settings::Settings>,
|
||||||
|
pub subject: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl EmailData for ProFeatureRequest {
|
||||||
|
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
|
||||||
|
let recipient = self.recipient_email.clone().into_inner();
|
||||||
|
|
||||||
|
let body = html::get_html_body(EmailBody::ProFeatureRequest {
|
||||||
|
user_name: self.user_name.clone().get_secret().expose(),
|
||||||
|
feature_name: self.feature_name.clone(),
|
||||||
|
merchant_id: self.merchant_id.clone(),
|
||||||
|
user_email: recipient.peek().to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(EmailContents {
|
||||||
|
subject: self.subject.clone(),
|
||||||
|
body: external_services::email::IntermediateString::new(body),
|
||||||
|
recipient,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
29
crates/router/src/services/recon.rs
Normal file
29
crates/router/src/services/recon.rs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
use error_stack::ResultExt;
|
||||||
|
use masking::Secret;
|
||||||
|
|
||||||
|
use super::jwt;
|
||||||
|
use crate::{
|
||||||
|
consts,
|
||||||
|
core::{self, errors::RouterResult},
|
||||||
|
routes::app::settings::Settings,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct ReconToken {
|
||||||
|
pub user_id: String,
|
||||||
|
pub exp: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReconToken {
|
||||||
|
pub async fn new_token(user_id: String, settings: &Settings) -> RouterResult<Secret<String>> {
|
||||||
|
let exp_duration = std::time::Duration::from_secs(consts::JWT_TOKEN_TIME_IN_SECS);
|
||||||
|
let exp = jwt::generate_exp(exp_duration)
|
||||||
|
.change_context(core::errors::ApiErrorResponse::InternalServerError)?
|
||||||
|
.as_secs();
|
||||||
|
let token_payload = Self { user_id, exp };
|
||||||
|
let token = jwt::generate_jwt(&token_payload, settings)
|
||||||
|
.await
|
||||||
|
.change_context(core::errors::ApiErrorResponse::InternalServerError)?;
|
||||||
|
Ok(Secret::new(token))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -165,6 +165,14 @@ pub enum Flow {
|
|||||||
RefundsList,
|
RefundsList,
|
||||||
// Retrieve forex flow.
|
// Retrieve forex flow.
|
||||||
RetrieveForexFlow,
|
RetrieveForexFlow,
|
||||||
|
/// Toggles recon service for a merchant.
|
||||||
|
ReconMerchantUpdate,
|
||||||
|
/// Recon token request flow.
|
||||||
|
ReconTokenRequest,
|
||||||
|
/// Initial request for recon service.
|
||||||
|
ReconServiceRequest,
|
||||||
|
/// Recon token verification flow
|
||||||
|
ReconVerifyToken,
|
||||||
/// Routing create flow,
|
/// Routing create flow,
|
||||||
RoutingCreateConfig,
|
RoutingCreateConfig,
|
||||||
/// Routing link config
|
/// Routing link config
|
||||||
|
|||||||
Reference in New Issue
Block a user