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:
Mani Chandra
2023-11-13 11:17:35 +05:30
committed by GitHub
parent 0eb81f04b3
commit f88eee7362
34 changed files with 3489 additions and 67 deletions

View File

@ -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

View 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";

View File

@ -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;

View File

@ -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,

View 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,
)),
}
}
}

View 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())
}
}

View File

@ -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"))]

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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)))
}
}

View File

@ -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,
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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,

View 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
}

View File

@ -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")]

View File

@ -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> {

View 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)
}

View File

@ -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::*;

View 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)
}
}

View File

@ -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;

View File

@ -0,0 +1 @@
pub mod password;

File diff suppressed because it is too large Load Diff

View 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)
}