Skip to content

Commit

Permalink
feat(recon): add merchant and profile IDs in auth tokens (#5643)
Browse files Browse the repository at this point in the history
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
  • Loading branch information
kashif-m and hyperswitch-bot[bot] authored Sep 6, 2024
1 parent 36cd5c1 commit d9485a5
Show file tree
Hide file tree
Showing 30 changed files with 336 additions and 336 deletions.
6 changes: 4 additions & 2 deletions config/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ bg_metrics_collection_interval_in_secs = 15 # Interval for collecting
master_enc_key = "sample_key" # Master Encryption key used to encrypt merchant wise encryption key. Should be 32-byte long.
admin_api_key = "test_admin" # admin API key for admin authentication.
jwt_secret = "secret" # JWT secret used for user authentication.
recon_admin_api_key = "recon_test_admin" # recon_admin API key for recon authentication.

# Locker settings contain details for accessing a card locker, a
# PCI Compliant storage entity which stores payment method information
Expand Down Expand Up @@ -722,4 +721,7 @@ public = { name = "hyperswitch", base_url = "http://localhost:8080", schema = "p
encryption_key = "" # Encryption key used for encrypting data in user_authentication_methods table

[locker_based_open_banking_connectors]
connector_list = ""
connector_list = ""

[recipient_emails]
recon = "[email protected]"
4 changes: 3 additions & 1 deletion config/deployments/env_specific.toml
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,6 @@ url = "http://localhost:5000" # URL of the encryption service
master_enc_key = "sample_key" # Master Encryption key used to encrypt merchant wise encryption key. Should be 32-byte long.
admin_api_key = "test_admin" # admin API key for admin authentication.
jwt_secret = "secret" # JWT secret used for user authentication.
recon_admin_api_key = "recon_test_admin" # recon_admin API key for recon authentication.

# Server configuration
[server]
Expand Down Expand Up @@ -300,3 +299,6 @@ public = { name = "hyperswitch", base_url = "http://localhost:8080", schema = "p

[user_auth_methods]
encryption_key = "user_auth_table_encryption_key" # Encryption key used for encrypting data in user_authentication_methods table

[recipient_emails]
recon = "[email protected]"
2 changes: 1 addition & 1 deletion config/deployments/integration_test.toml
Original file line number Diff line number Diff line change
Expand Up @@ -369,4 +369,4 @@ keys = "accept-language,user-agent"
sdk_eligible_payment_methods = "card"

[locker_based_open_banking_connectors]
connector_list = ""
connector_list = ""
2 changes: 1 addition & 1 deletion config/deployments/production.toml
Original file line number Diff line number Diff line change
Expand Up @@ -382,4 +382,4 @@ keys = "accept-language,user-agent"
sdk_eligible_payment_methods = "card"

[locker_based_open_banking_connectors]
connector_list = ""
connector_list = ""
2 changes: 1 addition & 1 deletion config/deployments/sandbox.toml
Original file line number Diff line number Diff line change
Expand Up @@ -386,4 +386,4 @@ keys = "accept-language,user-agent"
sdk_eligible_payment_methods = "card"

[locker_based_open_banking_connectors]
connector_list = ""
connector_list = ""
4 changes: 3 additions & 1 deletion config/development.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ request_body_limit = 32768
admin_api_key = "test_admin"
master_enc_key = "73ad7bbbbc640c845a150f67d058b279849370cd2c1f3c67c4dd6c869213e13a"
jwt_secret = "secret"
recon_admin_api_key = "recon_test_admin"

[applepay_merchant_configs]
merchant_cert_key = "MERCHANT CERTIFICATE KEY"
Expand Down Expand Up @@ -727,3 +726,6 @@ encryption_key = "A8EF32E029BC3342E54BF2E172A4D7AA43E8EF9D2C3A624A9F04E2EF79DC69

[locker_based_open_banking_connectors]
connector_list = ""

[recipient_emails]
recon = "[email protected]"
4 changes: 3 additions & 1 deletion config/docker_compose.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ pool_size = 5
admin_api_key = "test_admin"
jwt_secret = "secret"
master_enc_key = "73ad7bbbbc640c845a150f67d058b279849370cd2c1f3c67c4dd6c869213e13a"
recon_admin_api_key = "recon_test_admin"

[user]
password_validity_in_days = 90
Expand Down Expand Up @@ -586,3 +585,6 @@ ach = { country = "US", currency = "USD" }

[locker_based_open_banking_connectors]
connector_list = ""

[recipient_emails]
recon = "[email protected]"
1 change: 0 additions & 1 deletion crates/api_models/src/recon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use crate::enums;

#[derive(serde::Deserialize, Debug, serde::Serialize)]
pub struct ReconUpdateMerchantRequest {
pub merchant_id: common_utils::id_type::MerchantId,
pub recon_status: enums::ReconStatus,
pub user_email: pii::Email,
}
Expand Down
3 changes: 3 additions & 0 deletions crates/api_models/src/user_role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub enum Permission {
PayoutRead,
WebhookEventWrite,
GenerateReport,
ReconAdmin,
}

#[derive(Clone, Debug, serde::Serialize, PartialEq, Eq, Hash)]
Expand All @@ -50,6 +51,7 @@ pub enum ParentGroup {
Merchant,
#[serde(rename = "OrganizationAccess")]
Organization,
Recon,
}

#[derive(Debug, serde::Serialize)]
Expand All @@ -67,6 +69,7 @@ pub enum PermissionModule {
SurchargeDecisionManager,
AccountCreate,
Payouts,
Recon,
}

#[derive(Debug, serde::Serialize)]
Expand Down
1 change: 1 addition & 0 deletions crates/common_enums/src/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2795,6 +2795,7 @@ pub enum PermissionGroup {
MerchantDetailsView,
MerchantDetailsManage,
OrganizationManage,
ReconOps,
}

/// Name of banks supported by Hyperswitch
Expand Down
5 changes: 2 additions & 3 deletions crates/router/src/configs/secrets_transformers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,17 +252,15 @@ impl SecretsHandler for settings::Secrets {
secret_management_client: &dyn SecretManagementInterface,
) -> CustomResult<SecretStateContainer<Self, RawSecret>, SecretsManagementError> {
let secrets = value.get_inner();
let (jwt_secret, admin_api_key, recon_admin_api_key, master_enc_key) = tokio::try_join!(
let (jwt_secret, admin_api_key, master_enc_key) = tokio::try_join!(
secret_management_client.get_secret(secrets.jwt_secret.clone()),
secret_management_client.get_secret(secrets.admin_api_key.clone()),
secret_management_client.get_secret(secrets.recon_admin_api_key.clone()),
secret_management_client.get_secret(secrets.master_enc_key.clone())
)?;

Ok(value.transition_state(|_| Self {
jwt_secret,
admin_api_key,
recon_admin_api_key,
master_enc_key,
}))
}
Expand Down Expand Up @@ -454,5 +452,6 @@ pub(crate) async fn fetch_raw_secrets(
user_auth_methods,
decision: conf.decision,
locker_based_open_banking_connectors: conf.locker_based_open_banking_connectors,
recipient_emails: conf.recipient_emails,
}
}
9 changes: 7 additions & 2 deletions crates/router/src/configs/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::{
#[cfg(feature = "olap")]
use analytics::{opensearch::OpenSearchConfig, ReportConfig};
use api_models::{enums, payment_methods::RequiredFieldInfo};
use common_utils::ext_traits::ConfigExt;
use common_utils::{ext_traits::ConfigExt, pii::Email};
use config::{Environment, File};
use error_stack::ResultExt;
#[cfg(feature = "email")]
Expand Down Expand Up @@ -120,6 +120,7 @@ pub struct Settings<S: SecretState> {
pub user_auth_methods: SecretStateContainer<UserAuthMethodSettings, S>,
pub decision: Option<DecisionConfig>,
pub locker_based_open_banking_connectors: LockerBasedRecipientConnectorList,
pub recipient_emails: RecipientMails,
}

#[derive(Debug, Deserialize, Clone, Default)]
Expand Down Expand Up @@ -513,7 +514,6 @@ pub struct RequiredFieldFinal {
pub struct Secrets {
pub jwt_secret: Secret<String>,
pub admin_api_key: Secret<String>,
pub recon_admin_api_key: Secret<String>,
pub master_enc_key: Secret<String>,
}

Expand Down Expand Up @@ -900,6 +900,11 @@ pub struct ServerTls {
pub certificate: PathBuf,
}

#[derive(Debug, Deserialize, Clone, Default)]
pub struct RecipientMails {
pub recon: Email,
}

fn deserialize_hashmap_inner<K, V>(
value: HashMap<String, String>,
) -> Result<HashMap<K, HashSet<V>>, String>
Expand Down
3 changes: 3 additions & 0 deletions crates/router/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,6 @@ pub const MAX_ALLOWED_AMOUNT: i64 = 999999999;
//payment attempt default unified error code and unified error message
pub const DEFAULT_UNIFIED_ERROR_CODE: &str = "UE_000";
pub const DEFAULT_UNIFIED_ERROR_MESSAGE: &str = "Something went wrong";

// Recon's feature tag
pub const RECON_FEATURE_TAG: &str = "RECONCILIATION AND SETTLEMENT";
2 changes: 2 additions & 0 deletions crates/router/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ pub mod payout_link;
pub mod payouts;
pub mod pm_auth;
pub mod poll;
#[cfg(feature = "recon")]
pub mod recon;
pub mod refunds;
pub mod routing;
pub mod surcharge_decision_config;
Expand Down
172 changes: 172 additions & 0 deletions crates/router/src/core/recon.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
use api_models::recon as recon_api;
use common_utils::ext_traits::AsyncExt;
use error_stack::ResultExt;
use masking::{ExposeInterface, PeekInterface, Secret};

use crate::{
consts,
core::errors::{self, RouterResponse, UserErrors},
services::{api as service_api, authentication, email::types as email_types},
types::{
api::{self as api_types, enums},
domain, storage,
transformers::ForeignTryFrom,
},
SessionState,
};

pub async fn send_recon_request(
state: SessionState,
user_with_auth_data: authentication::UserFromTokenWithAuthData,
) -> RouterResponse<recon_api::ReconStatusResponse> {
let user = user_with_auth_data.0;
let user_in_db = &user_with_auth_data.1.user;
let merchant_id = user.merchant_id;

let user_email = user_in_db.email.clone();
let email_contents = email_types::ProFeatureRequest {
feature_name: consts::RECON_FEATURE_TAG.to_string(),
merchant_id: merchant_id.clone(),
user_name: domain::UserName::new(user_in_db.name.clone())
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to form username")?,
user_email: domain::UserEmail::from_pii_email(user_email.clone())
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to convert recipient's email to UserEmail")?,
recipient_email: domain::UserEmail::from_pii_email(
state.conf.recipient_emails.recon.clone(),
)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to convert recipient's email to UserEmail")?,
settings: state.conf.clone(),
subject: format!(
"Dashboard Pro Feature Request by {}",
user_email.expose().peek()
),
};

state
.email_client
.compose_and_send_email(
Box::new(email_contents),
state.conf.proxy.https_url.as_ref(),
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to compose and send email for ProFeatureRequest [Recon]")
.async_and_then(|_| async {
let auth = user_with_auth_data.1;
let updated_merchant_account = storage::MerchantAccountUpdate::ReconUpdate {
recon_status: enums::ReconStatus::Requested,
};
let db = &*state.store;
let key_manager_state = &(&state).into();

let response = db
.update_merchant(
key_manager_state,
auth.merchant_account,
updated_merchant_account,
&auth.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,
},
))
})
.await
}

pub async fn generate_recon_token(
state: SessionState,
user: authentication::UserFromToken,
) -> RouterResponse<recon_api::ReconTokenResponse> {
let token = authentication::AuthToken::new_token(
user.user_id.clone(),
user.merchant_id.clone(),
user.role_id.clone(),
&state.conf,
user.org_id.clone(),
user.profile_id.clone(),
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable_lazy(|| {
format!(
"Failed to create recon token for params [user_id, org_id, mid, pid] [{}, {:?}, {:?}, {:?}]",
user.user_id, user.org_id, user.merchant_id, user.profile_id,
)
})?;

Ok(service_api::ApplicationResponse::Json(
recon_api::ReconTokenResponse {
token: token.into(),
},
))
}

pub async fn recon_merchant_account_update(
state: SessionState,
auth: authentication::AuthenticationData,
req: recon_api::ReconUpdateMerchantRequest,
) -> RouterResponse<api_types::MerchantAccountResponse> {
let db = &*state.store;
let key_manager_state = &(&state).into();

let updated_merchant_account = storage::MerchantAccountUpdate::ReconUpdate {
recon_status: req.recon_status,
};
let merchant_id = auth.merchant_account.get_id().clone();

let updated_merchant_account = db
.update_merchant(
key_manager_state,
auth.merchant_account,
updated_merchant_account,
&auth.key_store,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable_lazy(|| {
format!("Failed while updating merchant's recon status: {merchant_id:?}")
})?;

let user_email = &req.user_email.clone();
let email_contents = email_types::ReconActivation {
recipient_email: domain::UserEmail::from_pii_email(user_email.clone())
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to convert recipient's email to UserEmail from pii::Email")?,
user_name: domain::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 == enums::ReconStatus::Active {
let _ = 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(
api_types::MerchantAccountResponse::foreign_try_from(updated_merchant_account)
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "merchant_account",
})?,
))
}
Loading

0 comments on commit d9485a5

Please sign in to comment.