mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 20:23:43 +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
|
||||
|
||||
[features]
|
||||
default = ["payouts", "frm"]
|
||||
default = ["payouts", "frm", "recon"]
|
||||
business_profile_routing = []
|
||||
connector_choice_bcompat = []
|
||||
errors = ["dep:actix-web", "dep:reqwest"]
|
||||
@ -18,6 +18,7 @@ dummy_connector = ["euclid/dummy_connector", "common_enums/dummy_connector"]
|
||||
detailed_errors = []
|
||||
payouts = []
|
||||
frm = []
|
||||
recon = []
|
||||
|
||||
[dependencies]
|
||||
actix-web = { version = "4.3.1", optional = true }
|
||||
|
||||
@ -5,6 +5,8 @@ mod locker_migration;
|
||||
pub mod payment;
|
||||
#[cfg(feature = "payouts")]
|
||||
pub mod payouts;
|
||||
#[cfg(feature = "recon")]
|
||||
pub mod recon;
|
||||
pub mod refund;
|
||||
pub mod routing;
|
||||
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};
|
||||
#[cfg(feature = "recon")]
|
||||
use masking::PeekInterface;
|
||||
|
||||
#[cfg(feature = "dummy_connector")]
|
||||
use crate::user::sample_data::SampleDataRequest;
|
||||
#[cfg(feature = "recon")]
|
||||
use crate::user::VerifyTokenResponse;
|
||||
use crate::user::{
|
||||
dashboard_metadata::{
|
||||
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!(
|
||||
SignUpRequest,
|
||||
SignUpWithMerchantIdRequest,
|
||||
|
||||
@ -26,6 +26,8 @@ pub mod payments;
|
||||
#[cfg(feature = "payouts")]
|
||||
pub mod payouts;
|
||||
pub mod pm_auth;
|
||||
#[cfg(feature = "recon")]
|
||||
pub mod recon;
|
||||
pub mod refunds;
|
||||
pub mod routing;
|
||||
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_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,
|
||||
RustLocker,
|
||||
FraudCheck,
|
||||
Recon,
|
||||
}
|
||||
|
||||
impl ApiEventMetric for serde_json::Value {}
|
||||
|
||||
@ -9,7 +9,7 @@ readme = "README.md"
|
||||
license.workspace = true
|
||||
|
||||
[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"]
|
||||
kms = ["external_services/kms", "dep:aws-config"]
|
||||
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"]
|
||||
detailed_errors = ["api_models/detailed_errors", "error-stack/serde"]
|
||||
payouts = []
|
||||
recon = ["email"]
|
||||
retry = []
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -757,3 +757,32 @@ pub async fn send_verification_mail(
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
#[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::Cache::server(state.clone()));
|
||||
server_app = server_app.service(routes::Health::server(state));
|
||||
|
||||
@ -28,6 +28,8 @@ pub mod payment_methods;
|
||||
pub mod payments;
|
||||
#[cfg(feature = "payouts")]
|
||||
pub mod payouts;
|
||||
#[cfg(feature = "recon")]
|
||||
pub mod recon;
|
||||
pub mod refunds;
|
||||
#[cfg(feature = "olap")]
|
||||
pub mod routing;
|
||||
@ -53,6 +55,8 @@ pub use self::app::DummyConnector;
|
||||
pub use self::app::Forex;
|
||||
#[cfg(feature = "payouts")]
|
||||
pub use self::app::Payouts;
|
||||
#[cfg(all(feature = "olap", feature = "recon"))]
|
||||
pub use self::app::Recon;
|
||||
#[cfg(all(feature = "olap", feature = "kms"))]
|
||||
pub use self::app::Verify;
|
||||
pub use self::app::{
|
||||
|
||||
@ -40,6 +40,8 @@ use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*};
|
||||
use super::{ephemeral_key::*, payment_methods::*, webhooks::*};
|
||||
#[cfg(all(feature = "frm", feature = "oltp"))]
|
||||
use crate::routes::fraud_check as frm_routes;
|
||||
#[cfg(all(feature = "recon", feature = "olap"))]
|
||||
use crate::routes::recon as recon_routes;
|
||||
#[cfg(feature = "olap")]
|
||||
use crate::routes::verify_connector::payment_connector_verify;
|
||||
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")]
|
||||
pub struct Blocklist;
|
||||
|
||||
|
||||
@ -31,6 +31,7 @@ pub enum ApiIdentifier {
|
||||
User,
|
||||
UserRole,
|
||||
ConnectorOnboarding,
|
||||
Recon,
|
||||
}
|
||||
|
||||
impl From<Flow> for ApiIdentifier {
|
||||
@ -186,6 +187,11 @@ impl From<Flow> for ApiIdentifier {
|
||||
Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => {
|
||||
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
|
||||
}
|
||||
|
||||
#[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 logger;
|
||||
pub mod pm_auth;
|
||||
#[cfg(feature = "recon")]
|
||||
pub mod recon;
|
||||
|
||||
#[cfg(feature = "email")]
|
||||
pub mod email;
|
||||
|
||||
@ -12,10 +12,14 @@ use serde::Serialize;
|
||||
use super::authorization::{self, permissions::Permission};
|
||||
#[cfg(feature = "olap")]
|
||||
use super::jwt;
|
||||
#[cfg(feature = "recon")]
|
||||
use super::recon::ReconToken;
|
||||
#[cfg(feature = "olap")]
|
||||
use crate::consts;
|
||||
#[cfg(feature = "olap")]
|
||||
use crate::core::errors::UserResult;
|
||||
#[cfg(feature = "recon")]
|
||||
use crate::routes::AppState;
|
||||
use crate::{
|
||||
configs::settings,
|
||||
core::{
|
||||
@ -822,3 +826,95 @@ where
|
||||
}
|
||||
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 error_stack::ResultExt;
|
||||
use external_services::email::{EmailContents, EmailData, EmailError};
|
||||
use masking::ExposeInterface;
|
||||
use masking::{ExposeInterface, PeekInterface};
|
||||
|
||||
use crate::{configs, consts};
|
||||
#[cfg(feature = "olap")]
|
||||
use crate::{core::errors::UserErrors, services::jwt, types::domain};
|
||||
|
||||
pub enum EmailBody {
|
||||
Verify { link: String },
|
||||
Reset { link: String, user_name: String },
|
||||
MagicLink { link: String, user_name: String },
|
||||
InviteUser { link: String, user_name: String },
|
||||
Verify {
|
||||
link: 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 {
|
||||
@ -43,6 +63,30 @@ pub mod html {
|
||||
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,
|
||||
// Retrieve forex flow.
|
||||
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,
|
||||
RoutingCreateConfig,
|
||||
/// Routing link config
|
||||
|
||||
Reference in New Issue
Block a user