mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-31 01:57:45 +08:00
feat(router): Add new JWT authentication variants and use them (#2835)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
@ -1,3 +1,6 @@
|
||||
#[cfg(feature = "olap")]
|
||||
pub mod user;
|
||||
|
||||
// ID generation
|
||||
pub(crate) const ID_LENGTH: usize = 20;
|
||||
pub(crate) const MAX_ID_LENGTH: usize = 64;
|
||||
@ -52,3 +55,6 @@ pub const ROUTING_CONFIG_ID_LENGTH: usize = 10;
|
||||
|
||||
pub const LOCKER_REDIS_PREFIX: &str = "LOCKER_PM_TOKEN";
|
||||
pub const LOCKER_REDIS_EXPIRY_SECONDS: u32 = 60 * 15; // 15 minutes
|
||||
|
||||
#[cfg(any(feature = "olap", feature = "oltp"))]
|
||||
pub const JWT_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24 * 2; // 2 days
|
||||
|
||||
8
crates/router/src/consts/user.rs
Normal file
8
crates/router/src/consts/user.rs
Normal file
@ -0,0 +1,8 @@
|
||||
#[cfg(feature = "olap")]
|
||||
pub const MAX_NAME_LENGTH: usize = 70;
|
||||
#[cfg(feature = "olap")]
|
||||
pub const MAX_COMPANY_NAME_LENGTH: usize = 70;
|
||||
|
||||
// USER ROLES
|
||||
#[cfg(any(feature = "olap", feature = "oltp"))]
|
||||
pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin";
|
||||
@ -18,6 +18,8 @@ pub mod payments;
|
||||
pub mod payouts;
|
||||
pub mod refunds;
|
||||
pub mod routing;
|
||||
#[cfg(feature = "olap")]
|
||||
pub mod user;
|
||||
pub mod utils;
|
||||
#[cfg(all(feature = "olap", feature = "kms"))]
|
||||
pub mod verification;
|
||||
|
||||
@ -2,6 +2,8 @@ pub mod api_error_response;
|
||||
pub mod customers_error_response;
|
||||
pub mod error_handlers;
|
||||
pub mod transformers;
|
||||
#[cfg(feature = "olap")]
|
||||
pub mod user;
|
||||
pub mod utils;
|
||||
|
||||
use std::fmt::Display;
|
||||
@ -13,6 +15,8 @@ use diesel_models::errors as storage_errors;
|
||||
pub use redis_interface::errors::RedisError;
|
||||
use scheduler::errors as sch_errors;
|
||||
use storage_impl::errors as storage_impl_errors;
|
||||
#[cfg(feature = "olap")]
|
||||
pub use user::*;
|
||||
|
||||
pub use self::{
|
||||
api_error_response::ApiErrorResponse,
|
||||
|
||||
78
crates/router/src/core/errors/user.rs
Normal file
78
crates/router/src/core/errors/user.rs
Normal file
@ -0,0 +1,78 @@
|
||||
use common_utils::errors::CustomResult;
|
||||
|
||||
use crate::services::ApplicationResponse;
|
||||
|
||||
pub type UserResult<T> = CustomResult<T, UserErrors>;
|
||||
pub type UserResponse<T> = CustomResult<ApplicationResponse<T>, UserErrors>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum UserErrors {
|
||||
#[error("User InternalServerError")]
|
||||
InternalServerError,
|
||||
#[error("InvalidCredentials")]
|
||||
InvalidCredentials,
|
||||
#[error("UserExists")]
|
||||
UserExists,
|
||||
#[error("EmailParsingError")]
|
||||
EmailParsingError,
|
||||
#[error("NameParsingError")]
|
||||
NameParsingError,
|
||||
#[error("PasswordParsingError")]
|
||||
PasswordParsingError,
|
||||
#[error("CompanyNameParsingError")]
|
||||
CompanyNameParsingError,
|
||||
#[error("MerchantAccountCreationError: {0}")]
|
||||
MerchantAccountCreationError(String),
|
||||
#[error("InvalidEmailError")]
|
||||
InvalidEmailError,
|
||||
#[error("DuplicateOrganizationId")]
|
||||
DuplicateOrganizationId,
|
||||
}
|
||||
|
||||
impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorResponse> for UserErrors {
|
||||
fn switch(&self) -> api_models::errors::types::ApiErrorResponse {
|
||||
use api_models::errors::types::{ApiError, ApiErrorResponse as AER};
|
||||
let sub_code = "UR";
|
||||
match self {
|
||||
Self::InternalServerError => {
|
||||
AER::InternalServerError(ApiError::new("HE", 0, "Something Went Wrong", None))
|
||||
}
|
||||
Self::InvalidCredentials => AER::Unauthorized(ApiError::new(
|
||||
sub_code,
|
||||
1,
|
||||
"Incorrect email or password",
|
||||
None,
|
||||
)),
|
||||
Self::UserExists => AER::BadRequest(ApiError::new(
|
||||
sub_code,
|
||||
3,
|
||||
"An account already exists with this email",
|
||||
None,
|
||||
)),
|
||||
Self::EmailParsingError => {
|
||||
AER::BadRequest(ApiError::new(sub_code, 7, "Invalid Email", None))
|
||||
}
|
||||
Self::NameParsingError => {
|
||||
AER::BadRequest(ApiError::new(sub_code, 8, "Invalid Name", None))
|
||||
}
|
||||
Self::PasswordParsingError => {
|
||||
AER::BadRequest(ApiError::new(sub_code, 9, "Invalid Password", None))
|
||||
}
|
||||
Self::CompanyNameParsingError => {
|
||||
AER::BadRequest(ApiError::new(sub_code, 14, "Invalid Company Name", None))
|
||||
}
|
||||
Self::MerchantAccountCreationError(error_message) => {
|
||||
AER::InternalServerError(ApiError::new(sub_code, 15, error_message, None))
|
||||
}
|
||||
Self::InvalidEmailError => {
|
||||
AER::BadRequest(ApiError::new(sub_code, 16, "Invalid Email", None))
|
||||
}
|
||||
Self::DuplicateOrganizationId => AER::InternalServerError(ApiError::new(
|
||||
sub_code,
|
||||
21,
|
||||
"An Organization with the id already exists",
|
||||
None,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
81
crates/router/src/core/user.rs
Normal file
81
crates/router/src/core/user.rs
Normal file
@ -0,0 +1,81 @@
|
||||
use api_models::user as api;
|
||||
use diesel_models::enums::UserStatus;
|
||||
use error_stack::IntoReport;
|
||||
use masking::{ExposeInterface, Secret};
|
||||
use router_env::env;
|
||||
|
||||
use super::errors::{UserErrors, UserResponse};
|
||||
use crate::{
|
||||
consts::user as consts, routes::AppState, services::ApplicationResponse, types::domain,
|
||||
};
|
||||
|
||||
pub async fn connect_account(
|
||||
state: AppState,
|
||||
request: api::ConnectAccountRequest,
|
||||
) -> UserResponse<api::ConnectAccountResponse> {
|
||||
let find_user = state
|
||||
.store
|
||||
.find_user_by_email(request.email.clone().expose().expose().as_str())
|
||||
.await;
|
||||
|
||||
if let Ok(found_user) = find_user {
|
||||
let user_from_db: domain::UserFromStorage = found_user.into();
|
||||
|
||||
user_from_db.compare_password(request.password)?;
|
||||
|
||||
let user_role = user_from_db.get_role_from_db(state.clone()).await?;
|
||||
let jwt_token = user_from_db
|
||||
.get_jwt_auth_token(state.clone(), user_role.org_id)
|
||||
.await?;
|
||||
|
||||
return Ok(ApplicationResponse::Json(api::ConnectAccountResponse {
|
||||
token: Secret::new(jwt_token),
|
||||
merchant_id: user_role.merchant_id,
|
||||
name: user_from_db.get_name(),
|
||||
email: user_from_db.get_email(),
|
||||
verification_days_left: None,
|
||||
user_role: user_role.role_id,
|
||||
user_id: user_from_db.get_user_id().to_string(),
|
||||
}));
|
||||
} else if find_user
|
||||
.map_err(|e| e.current_context().is_db_not_found())
|
||||
.err()
|
||||
.unwrap_or(false)
|
||||
{
|
||||
if matches!(env::which(), env::Env::Production) {
|
||||
return Err(UserErrors::InvalidCredentials).into_report();
|
||||
}
|
||||
|
||||
let new_user = domain::NewUser::try_from(request)?;
|
||||
let _ = new_user
|
||||
.get_new_merchant()
|
||||
.get_new_organization()
|
||||
.insert_org_in_db(state.clone())
|
||||
.await?;
|
||||
let user_from_db = new_user
|
||||
.insert_user_and_merchant_in_db(state.clone())
|
||||
.await?;
|
||||
let user_role = new_user
|
||||
.insert_user_role_in_db(
|
||||
state.clone(),
|
||||
consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
|
||||
UserStatus::Active,
|
||||
)
|
||||
.await?;
|
||||
let jwt_token = user_from_db
|
||||
.get_jwt_auth_token(state.clone(), user_role.org_id)
|
||||
.await?;
|
||||
|
||||
return Ok(ApplicationResponse::Json(api::ConnectAccountResponse {
|
||||
token: Secret::new(jwt_token),
|
||||
merchant_id: user_role.merchant_id,
|
||||
name: user_from_db.get_name(),
|
||||
email: user_from_db.get_email(),
|
||||
verification_days_left: None,
|
||||
user_role: user_role.role_id,
|
||||
user_id: user_from_db.get_user_id().to_string(),
|
||||
}));
|
||||
} else {
|
||||
Err(UserErrors::InternalServerError.into())
|
||||
}
|
||||
}
|
||||
@ -146,6 +146,7 @@ pub fn mk_app(
|
||||
.service(routes::Analytics::server(state.clone()))
|
||||
.service(routes::Routing::server(state.clone()))
|
||||
.service(routes::Gsm::server(state.clone()))
|
||||
.service(routes::User::server(state.clone()))
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "olap", feature = "kms"))]
|
||||
|
||||
@ -23,6 +23,8 @@ pub mod payouts;
|
||||
pub mod refunds;
|
||||
#[cfg(feature = "olap")]
|
||||
pub mod routing;
|
||||
#[cfg(feature = "olap")]
|
||||
pub mod user;
|
||||
#[cfg(all(feature = "olap", feature = "kms"))]
|
||||
pub mod verification;
|
||||
pub mod webhooks;
|
||||
@ -38,7 +40,7 @@ pub use self::app::Verify;
|
||||
pub use self::app::{
|
||||
ApiKeys, AppState, BusinessProfile, Cache, Cards, Configs, Customers, Disputes, EphemeralKey,
|
||||
Files, Gsm, Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink,
|
||||
PaymentMethods, Payments, Refunds, Webhooks,
|
||||
PaymentMethods, Payments, Refunds, User, Webhooks,
|
||||
};
|
||||
#[cfg(feature = "stripe")]
|
||||
pub use super::compatibility::stripe::StripeApis;
|
||||
|
||||
@ -64,7 +64,10 @@ pub async fn retrieve_merchant_account(
|
||||
) -> HttpResponse {
|
||||
let flow = Flow::MerchantsAccountRetrieve;
|
||||
let merchant_id = mid.into_inner();
|
||||
let payload = web::Json(admin::MerchantId { merchant_id }).into_inner();
|
||||
let payload = web::Json(admin::MerchantId {
|
||||
merchant_id: merchant_id.to_owned(),
|
||||
})
|
||||
.into_inner();
|
||||
|
||||
api::server_wrap(
|
||||
flow,
|
||||
@ -72,7 +75,11 @@ pub async fn retrieve_merchant_account(
|
||||
&req,
|
||||
payload,
|
||||
|state, _, req| get_merchant_account(state, req),
|
||||
&auth::AdminApiAuth,
|
||||
auth::auth_type(
|
||||
&auth::AdminApiAuth,
|
||||
&auth::JWTAuthMerchantFromRoute { merchant_id },
|
||||
req.headers(),
|
||||
),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
.await
|
||||
@ -130,7 +137,13 @@ pub async fn update_merchant_account(
|
||||
&req,
|
||||
json_payload.into_inner(),
|
||||
|state, _, req| merchant_account_update(state, &merchant_id, req),
|
||||
&auth::AdminApiAuth,
|
||||
auth::auth_type(
|
||||
&auth::AdminApiAuth,
|
||||
&auth::JWTAuthMerchantFromRoute {
|
||||
merchant_id: merchant_id.clone(),
|
||||
},
|
||||
req.headers(),
|
||||
),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
.await
|
||||
@ -203,7 +216,13 @@ pub async fn payment_connector_create(
|
||||
&req,
|
||||
json_payload.into_inner(),
|
||||
|state, _, req| create_payment_connector(state, req, &merchant_id),
|
||||
&auth::AdminApiAuth,
|
||||
auth::auth_type(
|
||||
&auth::AdminApiAuth,
|
||||
&auth::JWTAuthMerchantFromRoute {
|
||||
merchant_id: merchant_id.clone(),
|
||||
},
|
||||
req.headers(),
|
||||
),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
.await
|
||||
@ -236,7 +255,7 @@ pub async fn payment_connector_retrieve(
|
||||
let flow = Flow::MerchantConnectorsRetrieve;
|
||||
let (merchant_id, merchant_connector_id) = path.into_inner();
|
||||
let payload = web::Json(admin::MerchantConnectorId {
|
||||
merchant_id,
|
||||
merchant_id: merchant_id.clone(),
|
||||
merchant_connector_id,
|
||||
})
|
||||
.into_inner();
|
||||
@ -249,7 +268,11 @@ pub async fn payment_connector_retrieve(
|
||||
|state, _, req| {
|
||||
retrieve_payment_connector(state, req.merchant_id, req.merchant_connector_id)
|
||||
},
|
||||
&auth::AdminApiAuth,
|
||||
auth::auth_type(
|
||||
&auth::AdminApiAuth,
|
||||
&auth::JWTAuthMerchantFromRoute { merchant_id },
|
||||
req.headers(),
|
||||
),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
.await
|
||||
@ -285,9 +308,13 @@ pub async fn payment_connector_list(
|
||||
flow,
|
||||
state,
|
||||
&req,
|
||||
merchant_id,
|
||||
merchant_id.to_owned(),
|
||||
|state, _, merchant_id| list_payment_connectors(state, merchant_id),
|
||||
&auth::AdminApiAuth,
|
||||
auth::auth_type(
|
||||
&auth::AdminApiAuth,
|
||||
&auth::JWTAuthMerchantFromRoute { merchant_id },
|
||||
req.headers(),
|
||||
),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
.await
|
||||
@ -328,7 +355,13 @@ pub async fn payment_connector_update(
|
||||
&req,
|
||||
json_payload.into_inner(),
|
||||
|state, _, req| update_payment_connector(state, &merchant_id, &merchant_connector_id, req),
|
||||
&auth::AdminApiAuth,
|
||||
auth::auth_type(
|
||||
&auth::AdminApiAuth,
|
||||
&auth::JWTAuthMerchantFromRoute {
|
||||
merchant_id: merchant_id.clone(),
|
||||
},
|
||||
req.headers(),
|
||||
),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
.await
|
||||
@ -362,7 +395,7 @@ pub async fn payment_connector_delete(
|
||||
let (merchant_id, merchant_connector_id) = path.into_inner();
|
||||
|
||||
let payload = web::Json(admin::MerchantConnectorId {
|
||||
merchant_id,
|
||||
merchant_id: merchant_id.clone(),
|
||||
merchant_connector_id,
|
||||
})
|
||||
.into_inner();
|
||||
@ -372,7 +405,11 @@ pub async fn payment_connector_delete(
|
||||
&req,
|
||||
payload,
|
||||
|state, _, req| delete_payment_connector(state, req.merchant_id, req.merchant_connector_id),
|
||||
&auth::AdminApiAuth,
|
||||
auth::auth_type(
|
||||
&auth::AdminApiAuth,
|
||||
&auth::JWTAuthMerchantFromRoute { merchant_id },
|
||||
req.headers(),
|
||||
),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
.await
|
||||
@ -419,7 +456,13 @@ pub async fn business_profile_create(
|
||||
&req,
|
||||
payload,
|
||||
|state, _, req| create_business_profile(state, req, &merchant_id),
|
||||
&auth::AdminApiAuth,
|
||||
auth::auth_type(
|
||||
&auth::AdminApiAuth,
|
||||
&auth::JWTAuthMerchantFromRoute {
|
||||
merchant_id: merchant_id.clone(),
|
||||
},
|
||||
req.headers(),
|
||||
),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
.await
|
||||
@ -431,7 +474,7 @@ pub async fn business_profile_retrieve(
|
||||
path: web::Path<(String, String)>,
|
||||
) -> HttpResponse {
|
||||
let flow = Flow::BusinessProfileRetrieve;
|
||||
let (_, profile_id) = path.into_inner();
|
||||
let (merchant_id, profile_id) = path.into_inner();
|
||||
|
||||
api::server_wrap(
|
||||
flow,
|
||||
@ -439,7 +482,11 @@ pub async fn business_profile_retrieve(
|
||||
&req,
|
||||
profile_id,
|
||||
|state, _, profile_id| retrieve_business_profile(state, profile_id),
|
||||
&auth::AdminApiAuth,
|
||||
auth::auth_type(
|
||||
&auth::AdminApiAuth,
|
||||
&auth::JWTAuthMerchantFromRoute { merchant_id },
|
||||
req.headers(),
|
||||
),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
.await
|
||||
@ -460,7 +507,13 @@ pub async fn business_profile_update(
|
||||
&req,
|
||||
json_payload.into_inner(),
|
||||
|state, _, req| update_business_profile(state, &profile_id, &merchant_id, req),
|
||||
&auth::AdminApiAuth,
|
||||
auth::auth_type(
|
||||
&auth::AdminApiAuth,
|
||||
&auth::JWTAuthMerchantFromRoute {
|
||||
merchant_id: merchant_id.clone(),
|
||||
},
|
||||
req.headers(),
|
||||
),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
.await
|
||||
@ -498,9 +551,13 @@ pub async fn business_profiles_list(
|
||||
flow,
|
||||
state,
|
||||
&req,
|
||||
merchant_id,
|
||||
merchant_id.clone(),
|
||||
|state, _, merchant_id| list_business_profile(state, merchant_id),
|
||||
&auth::AdminApiAuth,
|
||||
auth::auth_type(
|
||||
&auth::AdminApiAuth,
|
||||
&auth::JWTAuthMerchantFromRoute { merchant_id },
|
||||
req.headers(),
|
||||
),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
.await
|
||||
|
||||
@ -53,7 +53,13 @@ pub async fn api_key_create(
|
||||
)
|
||||
.await
|
||||
},
|
||||
&auth::AdminApiAuth,
|
||||
auth::auth_type(
|
||||
&auth::AdminApiAuth,
|
||||
&auth::JWTAuthMerchantFromRoute {
|
||||
merchant_id: merchant_id.clone(),
|
||||
},
|
||||
req.headers(),
|
||||
),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
.await
|
||||
@ -91,7 +97,13 @@ pub async fn api_key_retrieve(
|
||||
&req,
|
||||
(&merchant_id, &key_id),
|
||||
|state, _, (merchant_id, key_id)| api_keys::retrieve_api_key(state, merchant_id, key_id),
|
||||
&auth::AdminApiAuth,
|
||||
auth::auth_type(
|
||||
&auth::AdminApiAuth,
|
||||
&auth::JWTAuthMerchantFromRoute {
|
||||
merchant_id: merchant_id.clone(),
|
||||
},
|
||||
req.headers(),
|
||||
),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
.await
|
||||
@ -173,7 +185,13 @@ pub async fn api_key_revoke(
|
||||
&req,
|
||||
(&merchant_id, &key_id),
|
||||
|state, _, (merchant_id, key_id)| api_keys::revoke_api_key(state, merchant_id, key_id),
|
||||
&auth::AdminApiAuth,
|
||||
auth::auth_type(
|
||||
&auth::AdminApiAuth,
|
||||
&auth::JWTAuthMerchantFromRoute {
|
||||
merchant_id: merchant_id.clone(),
|
||||
},
|
||||
req.headers(),
|
||||
),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
.await
|
||||
@ -213,11 +231,15 @@ pub async fn api_key_list(
|
||||
flow,
|
||||
state,
|
||||
&req,
|
||||
(limit, offset, merchant_id),
|
||||
(limit, offset, merchant_id.clone()),
|
||||
|state, _, (limit, offset, merchant_id)| async move {
|
||||
api_keys::list_api_keys(state, merchant_id, limit, offset).await
|
||||
},
|
||||
&auth::AdminApiAuth,
|
||||
auth::auth_type(
|
||||
&auth::AdminApiAuth,
|
||||
&auth::JWTAuthMerchantFromRoute { merchant_id },
|
||||
req.headers(),
|
||||
),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
.await
|
||||
|
||||
@ -19,7 +19,7 @@ use super::routing as cloud_routing;
|
||||
#[cfg(all(feature = "olap", feature = "kms"))]
|
||||
use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_verified_domains};
|
||||
#[cfg(feature = "olap")]
|
||||
use super::{admin::*, api_keys::*, disputes::*, files::*, gsm::*};
|
||||
use super::{admin::*, api_keys::*, disputes::*, files::*, gsm::*, user::*};
|
||||
use super::{cache::*, health::*, payment_link::*};
|
||||
#[cfg(any(feature = "olap", feature = "oltp"))]
|
||||
use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*};
|
||||
@ -710,3 +710,17 @@ impl Verify {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct User;
|
||||
|
||||
#[cfg(feature = "olap")]
|
||||
impl User {
|
||||
pub fn server(state: AppState) -> Scope {
|
||||
web::scope("/user")
|
||||
.app_data(web::Data::new(state))
|
||||
.service(web::resource("/signin").route(web::post().to(user_connect_account)))
|
||||
.service(web::resource("/signup").route(web::post().to(user_connect_account)))
|
||||
.service(web::resource("/v2/signin").route(web::post().to(user_connect_account)))
|
||||
.service(web::resource("/v2/signup").route(web::post().to(user_connect_account)))
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@ pub enum ApiIdentifier {
|
||||
PaymentLink,
|
||||
Routing,
|
||||
Gsm,
|
||||
User,
|
||||
}
|
||||
|
||||
impl From<Flow> for ApiIdentifier {
|
||||
@ -134,6 +135,8 @@ impl From<Flow> for ApiIdentifier {
|
||||
| Flow::GsmRuleRetrieve
|
||||
| Flow::GsmRuleUpdate
|
||||
| Flow::GsmRuleDelete => Self::Gsm,
|
||||
|
||||
Flow::UserConnectAccount => Self::User,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ pub mod helpers;
|
||||
use actix_web::{web, Responder};
|
||||
use api_models::payments::HeaderPayload;
|
||||
use error_stack::report;
|
||||
use router_env::{instrument, tracing, types, Flow};
|
||||
use router_env::{env, instrument, tracing, types, Flow};
|
||||
|
||||
use crate::{
|
||||
self as app,
|
||||
@ -118,7 +118,10 @@ pub async fn payments_create(
|
||||
api::AuthFlow::Merchant,
|
||||
)
|
||||
},
|
||||
&auth::ApiKeyAuth,
|
||||
match env::which() {
|
||||
env::Env::Production => &auth::ApiKeyAuth,
|
||||
_ => auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
},
|
||||
locking_action,
|
||||
)
|
||||
.await
|
||||
@ -249,7 +252,11 @@ pub async fn payments_retrieve(
|
||||
HeaderPayload::default(),
|
||||
)
|
||||
},
|
||||
&*auth_type,
|
||||
auth::auth_type(
|
||||
&*auth_type,
|
||||
&auth::JWTAuth,
|
||||
req.headers(),
|
||||
),
|
||||
locking_action,
|
||||
)
|
||||
.await
|
||||
@ -828,7 +835,7 @@ pub async fn payments_list(
|
||||
&req,
|
||||
payload,
|
||||
|state, auth, req| payments::list_payments(state, auth.merchant_account, req),
|
||||
&auth::ApiKeyAuth,
|
||||
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
.await
|
||||
@ -848,7 +855,7 @@ pub async fn payments_list_by_filter(
|
||||
&req,
|
||||
payload,
|
||||
|state, auth, req| payments::apply_filters_on_payments(state, auth.merchant_account, req),
|
||||
&auth::ApiKeyAuth,
|
||||
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
.await
|
||||
@ -868,7 +875,7 @@ pub async fn get_filters_for_payments(
|
||||
&req,
|
||||
payload,
|
||||
|state, auth, req| payments::get_filters_for_payments(state, auth.merchant_account, req),
|
||||
&auth::ApiKeyAuth,
|
||||
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
.await
|
||||
|
||||
@ -37,7 +37,7 @@ pub async fn refunds_create(
|
||||
&req,
|
||||
json_payload.into_inner(),
|
||||
|state, auth, req| refund_create_core(state, auth.merchant_account, auth.key_store, req),
|
||||
&auth::ApiKeyAuth,
|
||||
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
.await
|
||||
@ -88,7 +88,7 @@ pub async fn refunds_retrieve(
|
||||
refund_retrieve_core,
|
||||
)
|
||||
},
|
||||
&auth::ApiKeyAuth,
|
||||
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
.await
|
||||
@ -202,7 +202,7 @@ pub async fn refunds_list(
|
||||
&req,
|
||||
payload.into_inner(),
|
||||
|state, auth, req| refund_list(state, auth.merchant_account, req),
|
||||
&auth::ApiKeyAuth,
|
||||
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
.await
|
||||
@ -235,7 +235,7 @@ pub async fn refunds_filter_list(
|
||||
&req,
|
||||
payload.into_inner(),
|
||||
|state, auth, req| refund_filter_list(state, auth.merchant_account, req),
|
||||
&auth::ApiKeyAuth,
|
||||
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
.await
|
||||
|
||||
@ -14,7 +14,7 @@ use router_env::{
|
||||
use crate::{
|
||||
core::{api_locking, routing},
|
||||
routes::AppState,
|
||||
services::{api as oss_api, authentication as oss_auth, authentication as auth},
|
||||
services::{api as oss_api, authentication as auth},
|
||||
};
|
||||
|
||||
#[cfg(feature = "olap")]
|
||||
@ -30,11 +30,11 @@ pub async fn routing_create_config(
|
||||
state,
|
||||
&req,
|
||||
json_payload.into_inner(),
|
||||
|state, auth: oss_auth::AuthenticationData, payload| {
|
||||
|state, auth: auth::AuthenticationData, payload| {
|
||||
routing::create_routing_config(state, auth.merchant_account, auth.key_store, payload)
|
||||
},
|
||||
#[cfg(not(feature = "release"))]
|
||||
auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
#[cfg(feature = "release")]
|
||||
&auth::JWTAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
@ -55,7 +55,7 @@ pub async fn routing_link_config(
|
||||
state,
|
||||
&req,
|
||||
path.into_inner(),
|
||||
|state, auth: oss_auth::AuthenticationData, algorithm_id| {
|
||||
|state, auth: auth::AuthenticationData, algorithm_id| {
|
||||
routing::link_routing_config(
|
||||
state,
|
||||
auth.merchant_account,
|
||||
@ -65,7 +65,7 @@ pub async fn routing_link_config(
|
||||
)
|
||||
},
|
||||
#[cfg(not(feature = "release"))]
|
||||
auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
#[cfg(feature = "release")]
|
||||
&auth::JWTAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
@ -87,11 +87,11 @@ pub async fn routing_retrieve_config(
|
||||
state,
|
||||
&req,
|
||||
algorithm_id,
|
||||
|state, auth: oss_auth::AuthenticationData, algorithm_id| {
|
||||
|state, auth: auth::AuthenticationData, algorithm_id| {
|
||||
routing::retrieve_routing_config(state, auth.merchant_account, algorithm_id)
|
||||
},
|
||||
#[cfg(not(feature = "release"))]
|
||||
auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
#[cfg(feature = "release")]
|
||||
&auth::JWTAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
@ -114,7 +114,7 @@ pub async fn routing_retrieve_dictionary(
|
||||
state,
|
||||
&req,
|
||||
query.into_inner(),
|
||||
|state, auth: oss_auth::AuthenticationData, query_params| {
|
||||
|state, auth: auth::AuthenticationData, query_params| {
|
||||
routing::retrieve_merchant_routing_dictionary(
|
||||
state,
|
||||
auth.merchant_account,
|
||||
@ -122,7 +122,7 @@ pub async fn routing_retrieve_dictionary(
|
||||
)
|
||||
},
|
||||
#[cfg(not(feature = "release"))]
|
||||
auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
#[cfg(feature = "release")]
|
||||
&auth::JWTAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
@ -138,11 +138,11 @@ pub async fn routing_retrieve_dictionary(
|
||||
state,
|
||||
&req,
|
||||
(),
|
||||
|state, auth: oss_auth::AuthenticationData, _| {
|
||||
|state, auth: auth::AuthenticationData, _| {
|
||||
routing::retrieve_merchant_routing_dictionary(state, auth.merchant_account)
|
||||
},
|
||||
#[cfg(not(feature = "release"))]
|
||||
auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
#[cfg(feature = "release")]
|
||||
&auth::JWTAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
@ -168,11 +168,11 @@ pub async fn routing_unlink_config(
|
||||
state,
|
||||
&req,
|
||||
payload.into_inner(),
|
||||
|state, auth: oss_auth::AuthenticationData, payload_req| {
|
||||
|state, auth: auth::AuthenticationData, payload_req| {
|
||||
routing::unlink_routing_config(state, auth.merchant_account, payload_req)
|
||||
},
|
||||
#[cfg(not(feature = "release"))]
|
||||
auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
#[cfg(feature = "release")]
|
||||
&auth::JWTAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
@ -188,11 +188,11 @@ pub async fn routing_unlink_config(
|
||||
state,
|
||||
&req,
|
||||
(),
|
||||
|state, auth: oss_auth::AuthenticationData, _| {
|
||||
|state, auth: auth::AuthenticationData, _| {
|
||||
routing::unlink_routing_config(state, auth.merchant_account, auth.key_store)
|
||||
},
|
||||
#[cfg(not(feature = "release"))]
|
||||
auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
#[cfg(feature = "release")]
|
||||
&auth::JWTAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
@ -213,11 +213,11 @@ pub async fn routing_update_default_config(
|
||||
state,
|
||||
&req,
|
||||
json_payload.into_inner(),
|
||||
|state, auth: oss_auth::AuthenticationData, updated_config| {
|
||||
|state, auth: auth::AuthenticationData, updated_config| {
|
||||
routing::update_default_routing_config(state, auth.merchant_account, updated_config)
|
||||
},
|
||||
#[cfg(not(feature = "release"))]
|
||||
auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
#[cfg(feature = "release")]
|
||||
&auth::JWTAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
@ -236,11 +236,11 @@ pub async fn routing_retrieve_default_config(
|
||||
state,
|
||||
&req,
|
||||
(),
|
||||
|state, auth: oss_auth::AuthenticationData, _| {
|
||||
|state, auth: auth::AuthenticationData, _| {
|
||||
routing::retrieve_default_routing_config(state, auth.merchant_account)
|
||||
},
|
||||
#[cfg(not(feature = "release"))]
|
||||
auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
#[cfg(feature = "release")]
|
||||
&auth::JWTAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
@ -268,7 +268,7 @@ pub async fn routing_retrieve_linked_config(
|
||||
routing::retrieve_linked_routing_config(state, auth.merchant_account, query_params)
|
||||
},
|
||||
#[cfg(not(feature = "release"))]
|
||||
auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
#[cfg(feature = "release")]
|
||||
&auth::JWTAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
@ -284,11 +284,11 @@ pub async fn routing_retrieve_linked_config(
|
||||
state,
|
||||
&req,
|
||||
(),
|
||||
|state, auth: oss_auth::AuthenticationData, _| {
|
||||
|state, auth: auth::AuthenticationData, _| {
|
||||
routing::retrieve_linked_routing_config(state, auth.merchant_account)
|
||||
},
|
||||
#[cfg(not(feature = "release"))]
|
||||
auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||
#[cfg(feature = "release")]
|
||||
&auth::JWTAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
|
||||
31
crates/router/src/routes/user.rs
Normal file
31
crates/router/src/routes/user.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use api_models::user as user_api;
|
||||
use router_env::Flow;
|
||||
|
||||
use super::AppState;
|
||||
use crate::{
|
||||
core::{api_locking, user},
|
||||
services::{
|
||||
api,
|
||||
authentication::{self as auth},
|
||||
},
|
||||
};
|
||||
|
||||
pub async fn user_connect_account(
|
||||
state: web::Data<AppState>,
|
||||
http_req: HttpRequest,
|
||||
json_payload: web::Json<user_api::ConnectAccountRequest>,
|
||||
) -> HttpResponse {
|
||||
let flow = Flow::UserConnectAccount;
|
||||
let req_payload = json_payload.into_inner();
|
||||
Box::pin(api::server_wrap(
|
||||
flow.clone(),
|
||||
state,
|
||||
&http_req,
|
||||
req_payload.clone(),
|
||||
|state, _, req_body| user::connect_account(state, req_body),
|
||||
&auth::NoAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
pub mod api;
|
||||
pub mod authentication;
|
||||
pub mod encryption;
|
||||
#[cfg(feature = "olap")]
|
||||
pub mod jwt;
|
||||
pub mod logger;
|
||||
|
||||
#[cfg(feature = "kms")]
|
||||
|
||||
@ -9,6 +9,10 @@ use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
|
||||
use masking::{PeekInterface, StrongSecret};
|
||||
use serde::Serialize;
|
||||
|
||||
#[cfg(feature = "olap")]
|
||||
use super::jwt;
|
||||
#[cfg(feature = "olap")]
|
||||
use crate::consts;
|
||||
use crate::{
|
||||
configs::settings,
|
||||
core::{
|
||||
@ -71,6 +75,37 @@ impl AuthenticationType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct AuthToken {
|
||||
pub user_id: String,
|
||||
pub merchant_id: String,
|
||||
pub role_id: String,
|
||||
pub exp: u64,
|
||||
pub org_id: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "olap")]
|
||||
impl AuthToken {
|
||||
pub async fn new_token(
|
||||
user_id: String,
|
||||
merchant_id: String,
|
||||
role_id: String,
|
||||
settings: &settings::Settings,
|
||||
org_id: String,
|
||||
) -> errors::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,
|
||||
role_id,
|
||||
exp,
|
||||
org_id,
|
||||
};
|
||||
jwt::generate_jwt(&token_payload, settings).await
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AuthInfo {
|
||||
fn get_merchant_id(&self) -> Option<&str>;
|
||||
}
|
||||
@ -366,14 +401,58 @@ where
|
||||
request_headers: &HeaderMap,
|
||||
state: &A,
|
||||
) -> RouterResult<((), AuthenticationType)> {
|
||||
let mut token = get_jwt(request_headers)?;
|
||||
token = strip_jwt_token(token)?;
|
||||
decode_jwt::<JwtAuthPayloadFetchUnit>(token, state)
|
||||
.await
|
||||
.map(|_| ((), AuthenticationType::NoAuth))
|
||||
let payload = parse_jwt_payload::<A, AuthToken>(request_headers, state).await?;
|
||||
Ok((
|
||||
(),
|
||||
AuthenticationType::MerchantJWT {
|
||||
merchant_id: payload.merchant_id,
|
||||
user_id: Some(payload.user_id),
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct JWTAuthMerchantFromRoute {
|
||||
pub merchant_id: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<A> AuthenticateAndFetch<(), A> for JWTAuthMerchantFromRoute
|
||||
where
|
||||
A: AppStateInfo + Sync,
|
||||
{
|
||||
async fn authenticate_and_fetch(
|
||||
&self,
|
||||
request_headers: &HeaderMap,
|
||||
state: &A,
|
||||
) -> RouterResult<((), AuthenticationType)> {
|
||||
let payload = parse_jwt_payload::<A, AuthToken>(request_headers, state).await?;
|
||||
|
||||
// Check if token has access to merchantID that has been requested through query param
|
||||
if payload.merchant_id != self.merchant_id {
|
||||
return Err(report!(errors::ApiErrorResponse::InvalidJwtToken));
|
||||
}
|
||||
Ok((
|
||||
(),
|
||||
AuthenticationType::MerchantJWT {
|
||||
merchant_id: payload.merchant_id,
|
||||
user_id: Some(payload.user_id),
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn parse_jwt_payload<A, T>(headers: &HeaderMap, state: &A) -> RouterResult<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
A: AppStateInfo + Sync,
|
||||
{
|
||||
let token = get_jwt_from_authorization_header(headers)?;
|
||||
let payload = decode_jwt(token, state).await?;
|
||||
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct JwtAuthPayloadFetchMerchantAccount {
|
||||
merchant_id: String,
|
||||
@ -389,9 +468,9 @@ where
|
||||
request_headers: &HeaderMap,
|
||||
state: &A,
|
||||
) -> RouterResult<(AuthenticationData, AuthenticationType)> {
|
||||
let mut token = get_jwt(request_headers)?;
|
||||
token = strip_jwt_token(token)?;
|
||||
let payload = decode_jwt::<JwtAuthPayloadFetchMerchantAccount>(token, state).await?;
|
||||
let payload =
|
||||
parse_jwt_payload::<A, JwtAuthPayloadFetchMerchantAccount>(request_headers, state)
|
||||
.await?;
|
||||
let key_store = state
|
||||
.store()
|
||||
.get_merchant_key_store_by_merchant_id(
|
||||
@ -595,14 +674,16 @@ pub fn get_header_value_by_key(key: String, headers: &HeaderMap) -> RouterResult
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub fn get_jwt(headers: &HeaderMap) -> RouterResult<&str> {
|
||||
pub fn get_jwt_from_authorization_header(headers: &HeaderMap) -> RouterResult<&str> {
|
||||
headers
|
||||
.get(crate::headers::AUTHORIZATION)
|
||||
.get_required_value(crate::headers::AUTHORIZATION)?
|
||||
.to_str()
|
||||
.into_report()
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to convert JWT token to string")
|
||||
.attach_printable("Failed to convert JWT token to string")?
|
||||
.strip_prefix("Bearer ")
|
||||
.ok_or(errors::ApiErrorResponse::InvalidJwtToken.into())
|
||||
}
|
||||
|
||||
pub fn strip_jwt_token(token: &str) -> RouterResult<&str> {
|
||||
|
||||
42
crates/router/src/services/jwt.rs
Normal file
42
crates/router/src/services/jwt.rs
Normal file
@ -0,0 +1,42 @@
|
||||
use common_utils::errors::CustomResult;
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use jsonwebtoken::{encode, EncodingKey, Header};
|
||||
use masking::PeekInterface;
|
||||
|
||||
use super::authentication;
|
||||
use crate::{configs::settings::Settings, core::errors::UserErrors};
|
||||
|
||||
pub fn generate_exp(
|
||||
exp_duration: std::time::Duration,
|
||||
) -> CustomResult<std::time::Duration, UserErrors> {
|
||||
std::time::SystemTime::now()
|
||||
.checked_add(exp_duration)
|
||||
.ok_or(UserErrors::InternalServerError)?
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.into_report()
|
||||
.change_context(UserErrors::InternalServerError)
|
||||
}
|
||||
|
||||
pub async fn generate_jwt<T>(
|
||||
claims_data: &T,
|
||||
settings: &Settings,
|
||||
) -> CustomResult<String, UserErrors>
|
||||
where
|
||||
T: serde::ser::Serialize,
|
||||
{
|
||||
let jwt_secret = authentication::get_jwt_secret(
|
||||
&settings.secrets,
|
||||
#[cfg(feature = "kms")]
|
||||
external_services::kms::get_kms_client(&settings.kms).await,
|
||||
)
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)
|
||||
.attach_printable("Failed to obtain JWT secret")?;
|
||||
encode(
|
||||
&Header::default(),
|
||||
claims_data,
|
||||
&EncodingKey::from_secret(jwt_secret.peek().as_bytes()),
|
||||
)
|
||||
.into_report()
|
||||
.change_context(UserErrors::InternalServerError)
|
||||
}
|
||||
@ -5,9 +5,13 @@ mod merchant_account;
|
||||
mod merchant_connector_account;
|
||||
mod merchant_key_store;
|
||||
pub mod types;
|
||||
#[cfg(feature = "olap")]
|
||||
pub mod user;
|
||||
|
||||
pub use address::*;
|
||||
pub use customer::*;
|
||||
pub use merchant_account::*;
|
||||
pub use merchant_connector_account::*;
|
||||
pub use merchant_key_store::*;
|
||||
#[cfg(feature = "olap")]
|
||||
pub use user::*;
|
||||
|
||||
483
crates/router/src/types/domain/user.rs
Normal file
483
crates/router/src/types/domain/user.rs
Normal file
@ -0,0 +1,483 @@
|
||||
use std::{collections::HashSet, ops, str::FromStr};
|
||||
|
||||
use api_models::{admin as admin_api, organization as api_org, user as user_api};
|
||||
use common_utils::pii;
|
||||
use diesel_models::{
|
||||
enums::UserStatus,
|
||||
organization as diesel_org,
|
||||
organization::Organization,
|
||||
user as storage_user,
|
||||
user_role::{UserRole, UserRoleNew},
|
||||
};
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use masking::{ExposeInterface, PeekInterface, Secret};
|
||||
use once_cell::sync::Lazy;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use crate::{
|
||||
consts::user as consts,
|
||||
core::{
|
||||
admin,
|
||||
errors::{UserErrors, UserResult},
|
||||
},
|
||||
db::StorageInterface,
|
||||
routes::AppState,
|
||||
services::authentication::AuthToken,
|
||||
types::transformers::ForeignFrom,
|
||||
utils::user::password,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UserName(Secret<String>);
|
||||
|
||||
impl UserName {
|
||||
pub fn new(name: Secret<String>) -> UserResult<Self> {
|
||||
let name = name.expose();
|
||||
let is_empty_or_whitespace = name.trim().is_empty();
|
||||
let is_too_long = name.graphemes(true).count() > consts::MAX_NAME_LENGTH;
|
||||
|
||||
let forbidden_characters = ['/', '(', ')', '"', '<', '>', '\\', '{', '}'];
|
||||
let contains_forbidden_characters = name.chars().any(|g| forbidden_characters.contains(&g));
|
||||
|
||||
if is_empty_or_whitespace || is_too_long || contains_forbidden_characters {
|
||||
Err(UserErrors::NameParsingError.into())
|
||||
} else {
|
||||
Ok(Self(name.into()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_secret(self) -> Secret<String> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<pii::Email> for UserName {
|
||||
type Error = error_stack::Report<UserErrors>;
|
||||
|
||||
fn try_from(value: pii::Email) -> UserResult<Self> {
|
||||
Self::new(Secret::new(
|
||||
value
|
||||
.peek()
|
||||
.split_once('@')
|
||||
.ok_or(UserErrors::InvalidEmailError)?
|
||||
.0
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UserEmail(pii::Email);
|
||||
|
||||
static BLOCKED_EMAIL: Lazy<HashSet<String>> = Lazy::new(|| {
|
||||
let blocked_emails_content = include_str!("../../utils/user/blocker_emails.txt");
|
||||
let blocked_emails: HashSet<String> = blocked_emails_content
|
||||
.lines()
|
||||
.map(|s| s.trim().to_owned())
|
||||
.collect();
|
||||
blocked_emails
|
||||
});
|
||||
|
||||
impl UserEmail {
|
||||
pub fn new(email: Secret<String, pii::EmailStrategy>) -> UserResult<Self> {
|
||||
let email_string = email.expose();
|
||||
let email =
|
||||
pii::Email::from_str(&email_string).change_context(UserErrors::EmailParsingError)?;
|
||||
|
||||
if validator::validate_email(&email_string) {
|
||||
let (_username, domain) = match email_string.as_str().split_once('@') {
|
||||
Some((u, d)) => (u, d),
|
||||
None => return Err(UserErrors::EmailParsingError.into()),
|
||||
};
|
||||
|
||||
if BLOCKED_EMAIL.contains(domain) {
|
||||
return Err(UserErrors::InvalidEmailError.into());
|
||||
}
|
||||
Ok(Self(email))
|
||||
} else {
|
||||
Err(UserErrors::EmailParsingError.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pii_email(email: pii::Email) -> UserResult<Self> {
|
||||
let email_string = email.peek();
|
||||
if validator::validate_email(email_string) {
|
||||
let (_username, domain) = match email_string.split_once('@') {
|
||||
Some((u, d)) => (u, d),
|
||||
None => return Err(UserErrors::EmailParsingError.into()),
|
||||
};
|
||||
if BLOCKED_EMAIL.contains(domain) {
|
||||
return Err(UserErrors::InvalidEmailError.into());
|
||||
}
|
||||
Ok(Self(email))
|
||||
} else {
|
||||
Err(UserErrors::EmailParsingError.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> pii::Email {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn get_secret(self) -> Secret<String, pii::EmailStrategy> {
|
||||
(*self.0).clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<pii::Email> for UserEmail {
|
||||
type Error = error_stack::Report<UserErrors>;
|
||||
|
||||
fn try_from(value: pii::Email) -> Result<Self, Self::Error> {
|
||||
Self::from_pii_email(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Deref for UserEmail {
|
||||
type Target = Secret<String, pii::EmailStrategy>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UserPassword(Secret<String>);
|
||||
|
||||
impl UserPassword {
|
||||
pub fn new(password: Secret<String>) -> UserResult<Self> {
|
||||
let password = password.expose();
|
||||
if password.is_empty() {
|
||||
Err(UserErrors::PasswordParsingError.into())
|
||||
} else {
|
||||
Ok(Self(password.into()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_secret(&self) -> Secret<String> {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UserCompanyName(String);
|
||||
|
||||
impl UserCompanyName {
|
||||
pub fn new(company_name: String) -> UserResult<Self> {
|
||||
let company_name = company_name.trim();
|
||||
let is_empty_or_whitespace = company_name.is_empty();
|
||||
let is_too_long = company_name.graphemes(true).count() > consts::MAX_COMPANY_NAME_LENGTH;
|
||||
|
||||
let is_all_valid_characters = company_name
|
||||
.chars()
|
||||
.all(|x| x.is_alphanumeric() || x.is_ascii_whitespace() || x == '_');
|
||||
if is_empty_or_whitespace || is_too_long || !is_all_valid_characters {
|
||||
Err(UserErrors::CompanyNameParsingError.into())
|
||||
} else {
|
||||
Ok(Self(company_name.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_secret(self) -> String {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NewUserOrganization(diesel_org::OrganizationNew);
|
||||
|
||||
impl NewUserOrganization {
|
||||
pub async fn insert_org_in_db(self, state: AppState) -> UserResult<Organization> {
|
||||
state
|
||||
.store
|
||||
.insert_organization(self.0)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
if e.current_context().is_db_unique_violation() {
|
||||
e.change_context(UserErrors::DuplicateOrganizationId)
|
||||
} else {
|
||||
e.change_context(UserErrors::InternalServerError)
|
||||
}
|
||||
})
|
||||
.attach_printable("Error while inserting organization")
|
||||
}
|
||||
|
||||
pub fn get_organization_id(&self) -> String {
|
||||
self.0.org_id.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<user_api::ConnectAccountRequest> for NewUserOrganization {
|
||||
fn from(_value: user_api::ConnectAccountRequest) -> Self {
|
||||
let new_organization = api_org::OrganizationNew::new(None);
|
||||
let db_organization = ForeignFrom::foreign_from(new_organization);
|
||||
Self(db_organization)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NewUserMerchant {
|
||||
merchant_id: String,
|
||||
company_name: Option<UserCompanyName>,
|
||||
new_organization: NewUserOrganization,
|
||||
}
|
||||
|
||||
impl NewUserMerchant {
|
||||
pub fn get_company_name(&self) -> Option<String> {
|
||||
self.company_name.clone().map(UserCompanyName::get_secret)
|
||||
}
|
||||
|
||||
pub fn get_merchant_id(&self) -> String {
|
||||
self.merchant_id.clone()
|
||||
}
|
||||
|
||||
pub fn get_new_organization(&self) -> NewUserOrganization {
|
||||
self.new_organization.clone()
|
||||
}
|
||||
|
||||
pub async fn check_if_already_exists_in_db(&self, state: AppState) -> UserResult<()> {
|
||||
if state
|
||||
.store
|
||||
.get_merchant_key_store_by_merchant_id(
|
||||
self.get_merchant_id().as_str(),
|
||||
&state.store.get_master_key().to_vec().into(),
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Err(UserErrors::MerchantAccountCreationError(format!(
|
||||
"Merchant with {} already exists",
|
||||
self.get_merchant_id()
|
||||
)))
|
||||
.into_report();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_new_merchant_and_insert_in_db(&self, state: AppState) -> UserResult<()> {
|
||||
self.check_if_already_exists_in_db(state.clone()).await?;
|
||||
Box::pin(admin::create_merchant_account(
|
||||
state.clone(),
|
||||
admin_api::MerchantAccountCreate {
|
||||
merchant_id: self.get_merchant_id(),
|
||||
metadata: None,
|
||||
locker_id: None,
|
||||
return_url: None,
|
||||
merchant_name: self.get_company_name().map(Secret::new),
|
||||
webhook_details: None,
|
||||
publishable_key: None,
|
||||
organization_id: Some(self.new_organization.get_organization_id()),
|
||||
merchant_details: None,
|
||||
routing_algorithm: None,
|
||||
parent_merchant_id: None,
|
||||
payment_link_config: None,
|
||||
sub_merchants_enabled: None,
|
||||
frm_routing_algorithm: None,
|
||||
intent_fulfillment_time: None,
|
||||
payout_routing_algorithm: None,
|
||||
primary_business_details: None,
|
||||
payment_response_hash_key: None,
|
||||
enable_payment_response_hash: None,
|
||||
redirect_to_merchant_with_http_post: None,
|
||||
},
|
||||
))
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)
|
||||
.attach_printable("Error while creating a merchant")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<user_api::ConnectAccountRequest> for NewUserMerchant {
|
||||
type Error = error_stack::Report<UserErrors>;
|
||||
|
||||
fn try_from(value: user_api::ConnectAccountRequest) -> UserResult<Self> {
|
||||
let merchant_id = format!("merchant_{}", common_utils::date_time::now_unix_timestamp());
|
||||
let new_organization = NewUserOrganization::from(value);
|
||||
|
||||
Ok(Self {
|
||||
company_name: None,
|
||||
merchant_id,
|
||||
new_organization,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NewUser {
|
||||
user_id: String,
|
||||
name: UserName,
|
||||
email: UserEmail,
|
||||
password: UserPassword,
|
||||
new_merchant: NewUserMerchant,
|
||||
}
|
||||
|
||||
impl NewUser {
|
||||
pub fn get_user_id(&self) -> String {
|
||||
self.user_id.clone()
|
||||
}
|
||||
|
||||
pub fn get_email(&self) -> UserEmail {
|
||||
self.email.clone()
|
||||
}
|
||||
|
||||
pub fn get_name(&self) -> Secret<String> {
|
||||
self.name.clone().get_secret()
|
||||
}
|
||||
|
||||
pub fn get_new_merchant(&self) -> NewUserMerchant {
|
||||
self.new_merchant.clone()
|
||||
}
|
||||
|
||||
pub async fn insert_user_in_db(
|
||||
&self,
|
||||
db: &dyn StorageInterface,
|
||||
) -> UserResult<UserFromStorage> {
|
||||
match db.insert_user(self.clone().try_into()?).await {
|
||||
Ok(user) => Ok(user.into()),
|
||||
Err(e) => {
|
||||
if e.current_context().is_db_unique_violation() {
|
||||
return Err(e.change_context(UserErrors::UserExists));
|
||||
} else {
|
||||
return Err(e.change_context(UserErrors::InternalServerError));
|
||||
}
|
||||
}
|
||||
}
|
||||
.attach_printable("Error while inserting user")
|
||||
}
|
||||
|
||||
pub async fn insert_user_and_merchant_in_db(
|
||||
&self,
|
||||
state: AppState,
|
||||
) -> UserResult<UserFromStorage> {
|
||||
let db = state.store.as_ref();
|
||||
let merchant_id = self.get_new_merchant().get_merchant_id();
|
||||
self.new_merchant
|
||||
.create_new_merchant_and_insert_in_db(state.clone())
|
||||
.await?;
|
||||
let created_user = self.insert_user_in_db(db).await;
|
||||
if created_user.is_err() {
|
||||
let _ = admin::merchant_account_delete(state, merchant_id).await;
|
||||
};
|
||||
created_user
|
||||
}
|
||||
|
||||
pub async fn insert_user_role_in_db(
|
||||
self,
|
||||
state: AppState,
|
||||
role_id: String,
|
||||
user_status: UserStatus,
|
||||
) -> UserResult<UserRole> {
|
||||
let now = common_utils::date_time::now();
|
||||
let user_id = self.get_user_id();
|
||||
|
||||
state
|
||||
.store
|
||||
.insert_user_role(UserRoleNew {
|
||||
merchant_id: self.get_new_merchant().get_merchant_id(),
|
||||
status: user_status,
|
||||
created_by: user_id.clone(),
|
||||
last_modified_by: user_id.clone(),
|
||||
user_id,
|
||||
role_id,
|
||||
created_at: now,
|
||||
last_modified_at: now,
|
||||
org_id: self
|
||||
.get_new_merchant()
|
||||
.get_new_organization()
|
||||
.get_organization_id(),
|
||||
})
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<NewUser> for storage_user::UserNew {
|
||||
type Error = error_stack::Report<UserErrors>;
|
||||
|
||||
fn try_from(value: NewUser) -> UserResult<Self> {
|
||||
let hashed_password = password::generate_password_hash(value.password.get_secret())?;
|
||||
Ok(Self {
|
||||
user_id: value.get_user_id(),
|
||||
name: value.get_name(),
|
||||
email: value.get_email().into_inner(),
|
||||
password: hashed_password,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<user_api::ConnectAccountRequest> for NewUser {
|
||||
type Error = error_stack::Report<UserErrors>;
|
||||
|
||||
fn try_from(value: user_api::ConnectAccountRequest) -> UserResult<Self> {
|
||||
let user_id = uuid::Uuid::new_v4().to_string();
|
||||
let email = value.email.clone().try_into()?;
|
||||
let name = UserName::try_from(value.email.clone())?;
|
||||
let password = UserPassword::new(value.password.clone())?;
|
||||
let new_merchant = NewUserMerchant::try_from(value)?;
|
||||
|
||||
Ok(Self {
|
||||
user_id,
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
new_merchant,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UserFromStorage(pub storage_user::User);
|
||||
|
||||
impl From<storage_user::User> for UserFromStorage {
|
||||
fn from(value: storage_user::User) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl UserFromStorage {
|
||||
pub fn get_user_id(&self) -> &str {
|
||||
self.0.user_id.as_str()
|
||||
}
|
||||
|
||||
pub fn compare_password(&self, candidate: Secret<String>) -> UserResult<()> {
|
||||
match password::is_correct_password(candidate, self.0.password.clone()) {
|
||||
Ok(true) => Ok(()),
|
||||
Ok(false) => Err(UserErrors::InvalidCredentials.into()),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_name(&self) -> Secret<String> {
|
||||
self.0.name.clone()
|
||||
}
|
||||
|
||||
pub fn get_email(&self) -> pii::Email {
|
||||
self.0.email.clone()
|
||||
}
|
||||
|
||||
pub async fn get_jwt_auth_token(&self, state: AppState, org_id: String) -> UserResult<String> {
|
||||
let role_id = self.get_role_from_db(state.clone()).await?.role_id;
|
||||
let merchant_id = state
|
||||
.store
|
||||
.find_user_role_by_user_id(self.get_user_id())
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)?
|
||||
.merchant_id;
|
||||
AuthToken::new_token(
|
||||
self.0.user_id.clone(),
|
||||
merchant_id,
|
||||
role_id,
|
||||
&state.conf,
|
||||
org_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_role_from_db(&self, state: AppState) -> UserResult<UserRole> {
|
||||
state
|
||||
.store
|
||||
.find_user_role_by_user_id(self.get_user_id())
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
pub mod custom_serde;
|
||||
pub mod db_utils;
|
||||
pub mod ext_traits;
|
||||
#[cfg(feature = "olap")]
|
||||
pub mod user;
|
||||
|
||||
#[cfg(feature = "kv_store")]
|
||||
pub mod storage_partitioning;
|
||||
|
||||
1
crates/router/src/utils/user.rs
Normal file
1
crates/router/src/utils/user.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod password;
|
||||
2349
crates/router/src/utils/user/blocker_emails.txt
Normal file
2349
crates/router/src/utils/user/blocker_emails.txt
Normal file
File diff suppressed because it is too large
Load Diff
43
crates/router/src/utils/user/password.rs
Normal file
43
crates/router/src/utils/user/password.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use argon2::{
|
||||
password_hash::{
|
||||
rand_core::OsRng, Error as argon2Err, PasswordHash, PasswordHasher, PasswordVerifier,
|
||||
SaltString,
|
||||
},
|
||||
Argon2,
|
||||
};
|
||||
use common_utils::errors::CustomResult;
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use masking::{ExposeInterface, Secret};
|
||||
|
||||
use crate::core::errors::UserErrors;
|
||||
|
||||
pub fn generate_password_hash(
|
||||
password: Secret<String>,
|
||||
) -> CustomResult<Secret<String>, UserErrors> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
|
||||
let argon2 = Argon2::default();
|
||||
let password_hash = argon2
|
||||
.hash_password(password.expose().as_bytes(), &salt)
|
||||
.into_report()
|
||||
.change_context(UserErrors::InternalServerError)?;
|
||||
Ok(Secret::new(password_hash.to_string()))
|
||||
}
|
||||
|
||||
pub fn is_correct_password(
|
||||
candidate: Secret<String>,
|
||||
password: Secret<String>,
|
||||
) -> CustomResult<bool, UserErrors> {
|
||||
let password = password.expose();
|
||||
let parsed_hash = PasswordHash::new(&password)
|
||||
.into_report()
|
||||
.change_context(UserErrors::InternalServerError)?;
|
||||
let result = Argon2::default().verify_password(candidate.expose().as_bytes(), &parsed_hash);
|
||||
match result {
|
||||
Ok(_) => Ok(true),
|
||||
Err(argon2Err::Password) => Ok(false),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
.into_report()
|
||||
.change_context(UserErrors::InternalServerError)
|
||||
}
|
||||
Reference in New Issue
Block a user