diff --git a/api-reference-v2/api-reference/payments/payments--get.mdx b/api-reference-v2/api-reference/payments/payments--get.mdx new file mode 100644 index 0000000000..7b5f7f68da --- /dev/null +++ b/api-reference-v2/api-reference/payments/payments--get.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /v2/payments/{id} +--- \ No newline at end of file diff --git a/api-reference-v2/mint.json b/api-reference-v2/mint.json index 9964806ec9..c0723a63f3 100644 --- a/api-reference-v2/mint.json +++ b/api-reference-v2/mint.json @@ -39,7 +39,8 @@ "api-reference/payments/payments--create-intent", "api-reference/payments/payments--get-intent", "api-reference/payments/payments--session-token", - "api-reference/payments/payments--confirm-intent" + "api-reference/payments/payments--confirm-intent", + "api-reference/payments/payments--get" ] }, { @@ -111,9 +112,7 @@ }, { "group": "Refunds", - "pages": [ - "api-reference/refunds/refunds--create" - ] + "pages": ["api-reference/refunds/refunds--create"] } ], "footerSocials": { diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index e36dc78de4..0385aa55a2 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -1967,6 +1967,56 @@ ] } }, + "/v2/payments/{id}": { + "get": { + "tags": [ + "Payments" + ], + "summary": "Payments - Get", + "description": "Retrieves a Payment. This API can also be used to get the status of a previously initiated payment or next action for an ongoing payment", + "operationId": "Retrieve a Payment", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The global payment id", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "force_sync", + "in": "query", + "description": "A boolean to indicate whether to force sync the payment status. Value can be true or false", + "required": true, + "schema": { + "$ref": "#/components/schemas/ForceSync" + } + } + ], + "responses": { + "200": { + "description": "Gets the payment with final status", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsRetrieveResponse" + } + } + } + }, + "404": { + "description": "No payment found with the given id" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, "/v2/refunds": { "post": { "tags": [ @@ -8030,6 +8080,13 @@ ], "description": "Possible field type of required fields in payment_method_data" }, + "ForceSync": { + "type": "string", + "enum": [ + "true", + "false" + ] + }, "FrmAction": { "type": "string", "enum": [ @@ -15072,6 +15129,105 @@ } } }, + "PaymentsRetrieveResponse": { + "type": "object", + "description": "Response for Payment Intent Confirm", + "required": [ + "id", + "status", + "amount", + "client_secret", + "created" + ], + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant.", + "example": "12345_pay_01926c58bc6e77c09e809964e72af8c8", + "maxLength": 64, + "minLength": 32 + }, + "status": { + "$ref": "#/components/schemas/IntentStatus" + }, + "amount": { + "$ref": "#/components/schemas/ConfirmIntentAmountDetailsResponse" + }, + "connector": { + "type": "string", + "description": "The connector used for the payment", + "example": "stripe", + "nullable": true + }, + "client_secret": { + "type": "string", + "description": "It's a token used for client side verification." + }, + "created": { + "type": "string", + "format": "date-time", + "description": "Time when the payment was created", + "example": "2022-09-10T10:11:12Z" + }, + "payment_method_data": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodDataResponseWithBilling" + } + ], + "nullable": true + }, + "payment_method_type": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethod" + } + ], + "nullable": true + }, + "payment_method_subtype": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodType" + } + ], + "nullable": true + }, + "connector_transaction_id": { + "type": "string", + "description": "A unique identifier for a payment provided by the connector", + "example": "993672945374576J", + "nullable": true + }, + "connector_reference_id": { + "type": "string", + "description": "reference(Identifier) to the payment at connector side", + "example": "993672945374576J", + "nullable": true + }, + "merchant_connector_id": { + "type": "string", + "description": "Identifier of the connector ( merchant connector account ) which was chosen to make the payment", + "nullable": true + }, + "browser_info": { + "allOf": [ + { + "$ref": "#/components/schemas/BrowserInformation" + } + ], + "nullable": true + }, + "error": { + "allOf": [ + { + "$ref": "#/components/schemas/ErrorDetails" + } + ], + "nullable": true + } + } + }, "PaymentsSessionRequest": { "type": "object" }, diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index f42d81c0be..7c09813ca6 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -125,6 +125,7 @@ Never share your secret api keys. Keep them guarded and secure. routes::payments::payments_create_intent, routes::payments::payments_get_intent, routes::payments::payments_confirm_intent, + routes::payments::payment_status, //Routes for refunds routes::refunds::refunds_create, @@ -431,6 +432,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payment_methods::SurchargePercentage, api_models::payment_methods::PaymentMethodCollectLinkRequest, api_models::payment_methods::PaymentMethodCollectLinkResponse, + api_models::payments::PaymentsRetrieveResponse, api_models::refunds::RefundListRequest, api_models::refunds::RefundListResponse, api_models::payments::AmountFilter, @@ -587,6 +589,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::ErrorDetails, common_utils::types::BrowserInformation, api_models::payments::ConfirmIntentAmountDetailsResponse, + routes::payments::ForceSync, )), modifiers(&SecurityAddon) )] diff --git a/crates/openapi/src/routes/payments.rs b/crates/openapi/src/routes/payments.rs index 77e5bea10f..2ffd4f4cdc 100644 --- a/crates/openapi/src/routes/payments.rs +++ b/crates/openapi/src/routes/payments.rs @@ -691,3 +691,33 @@ pub fn payments_get_intent() {} )] #[cfg(feature = "v2")] pub fn payments_confirm_intent() {} + +/// Payments - Get +/// +/// Retrieves a Payment. This API can also be used to get the status of a previously initiated payment or next action for an ongoing payment +#[utoipa::path( + get, + path = "/v2/payments/{id}", + params( + ("id" = String, Path, description = "The global payment id"), + ("force_sync" = ForceSync, Query, description = "A boolean to indicate whether to force sync the payment status. Value can be true or false") + ), + responses( + (status = 200, description = "Gets the payment with final status", body = PaymentsRetrieveResponse), + (status = 404, description = "No payment found with the given id") + ), + tag = "Payments", + operation_id = "Retrieve a Payment", + security(("api_key" = [])) +)] +#[cfg(feature = "v2")] +pub fn payment_status() {} + +#[derive(utoipa::ToSchema)] +#[schema(rename_all = "lowercase")] +pub(crate) enum ForceSync { + /// Force sync with the connector / processor to update the status + True, + /// Do not force sync with the connector / processor. Get the status which is available in the database + False, +} diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index 2c9aec39b0..e0808b9226 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -51,8 +51,8 @@ pub mod routes { impl Analytics { #[cfg(feature = "v2")] - pub fn server(_state: AppState) -> Scope { - todo!() + pub fn server(state: AppState) -> Scope { + web::scope("/analytics").app_data(web::Data::new(state)) } #[cfg(feature = "v1")] pub fn server(state: AppState) -> Scope { diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 68f2b9d634..ba27e8b00d 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -2969,19 +2969,11 @@ pub async fn retrieve_connector( #[cfg(all(feature = "olap", feature = "v2"))] pub async fn list_connectors_for_a_profile( state: SessionState, - merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, profile_id: id_type::ProfileId, ) -> RouterResponse> { let store = state.store.as_ref(); let key_manager_state = &(&state).into(); - let key_store = store - .get_merchant_key_store_by_merchant_id( - key_manager_state, - merchant_account.get_id(), - &store.get_master_key().to_vec().into(), - ) - .await - .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; let merchant_connector_accounts = store .list_connector_account_by_profile_id(key_manager_state, &profile_id, &key_store) diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 215a8b209c..cc9d5b04eb 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -125,15 +125,19 @@ pub fn mk_app( { // This is a more specific route as compared to `MerchantConnectorAccount` // so it is registered before `MerchantConnectorAccount`. - server_app = server_app - .service(routes::ProfileNew::server(state.clone())) - .service(routes::Profile::server(state.clone())) + #[cfg(feature = "v1")] + { + server_app = server_app + .service(routes::ProfileNew::server(state.clone())) + .service(routes::Forex::server(state.clone())); + } + + server_app = server_app.service(routes::Profile::server(state.clone())) } server_app = server_app .service(routes::Payments::server(state.clone())) .service(routes::Customers::server(state.clone())) .service(routes::Configs::server(state.clone())) - .service(routes::Forex::server(state.clone())) .service(routes::MerchantConnectorAccount::server(state.clone())); #[cfg(feature = "v1")] @@ -163,7 +167,6 @@ pub fn mk_app( .service(routes::Organization::server(state.clone())) .service(routes::MerchantAccount::server(state.clone())) .service(routes::ApiKeys::server(state.clone())) - .service(routes::Analytics::server(state.clone())) .service(routes::Routing::server(state.clone())); #[cfg(feature = "v1")] @@ -178,11 +181,12 @@ pub fn mk_app( .service(routes::User::server(state.clone())) .service(routes::ConnectorOnboarding::server(state.clone())) .service(routes::Verify::server(state.clone())) + .service(routes::Analytics::server(state.clone())) .service(routes::WebhookEvents::server(state.clone())); } } - #[cfg(feature = "payouts")] + #[cfg(all(feature = "payouts", feature = "v1"))] { server_app = server_app .service(routes::Payouts::server(state.clone())) @@ -195,7 +199,9 @@ pub fn mk_app( not(feature = "customer_v2") ))] { - server_app = server_app.service(routes::StripeApis::server(state.clone())); + server_app = server_app + .service(routes::StripeApis::server(state.clone())) + .service(routes::Cards::server(state.clone())); } #[cfg(all(feature = "recon", feature = "v1"))] @@ -203,7 +209,6 @@ pub fn mk_app( server_app = server_app.service(routes::Recon::server(state.clone())); } - server_app = server_app.service(routes::Cards::server(state.clone())); server_app = server_app.service(routes::Cache::server(state.clone())); server_app = server_app.service(routes::Health::server(state.clone())); diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 8baed5089e..0f1529a9bd 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -61,13 +61,11 @@ pub mod webhooks; #[cfg(feature = "dummy_connector")] pub use self::app::DummyConnector; -#[cfg(any(feature = "olap", feature = "oltp"))] -pub use self::app::Forex; #[cfg(all(feature = "olap", feature = "recon", feature = "v1"))] pub use self::app::Recon; pub use self::app::{ ApiKeys, AppState, ApplePayCertificatesMigration, Cache, Cards, Configs, ConnectorOnboarding, - Customers, Disputes, EphemeralKey, Files, Gsm, Health, Mandates, MerchantAccount, + Customers, Disputes, EphemeralKey, Files, Forex, Gsm, Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, PaymentMethods, Payments, Poll, Profile, ProfileNew, Refunds, SessionState, User, Webhooks, }; diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index 64a0630c40..80d22ad22b 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -458,14 +458,14 @@ pub async fn connector_retrieve( state, &req, payload, - |state, auth_data, req, _| { - retrieve_connector( - state, - auth_data.merchant_account, - auth_data.key_store, - req.id.clone(), - ) - }, + |state, + auth::AuthenticationData { + merchant_account, + key_store, + .. + }, + req, + _| { retrieve_connector(state, merchant_account, key_store, req.id.clone()) }, auth::auth_type( &auth::AdminApiAuthWithMerchantIdFromHeader, &auth::JWTAuthMerchantFromHeader { @@ -493,8 +493,8 @@ pub async fn connector_list( state, &req, profile_id.to_owned(), - |state, auth, _, _| { - list_connectors_for_a_profile(state, auth.merchant_account.clone(), profile_id.clone()) + |state, auth::AuthenticationData { key_store, .. }, _, _| { + list_connectors_for_a_profile(state, key_store, profile_id.clone()) }, auth::auth_type( &auth::AdminApiAuthWithMerchantIdFromHeader, @@ -793,14 +793,14 @@ pub async fn connector_delete( state, &req, payload, - |state, auth_data, req, _| { - delete_connector( - state, - auth_data.merchant_account, - auth_data.key_store, - req.id, - ) - }, + |state, + auth::AuthenticationData { + merchant_account, + key_store, + .. + }, + req, + _| { delete_connector(state, merchant_account, key_store, req.id) }, auth::auth_type( &auth::AdminApiAuthWithMerchantIdFromHeader, &auth::JWTAuthMerchantFromHeader { diff --git a/crates/router/src/routes/api_keys.rs b/crates/router/src/routes/api_keys.rs index 1a2f60bccc..a96521c3ac 100644 --- a/crates/router/src/routes/api_keys.rs +++ b/crates/router/src/routes/api_keys.rs @@ -56,8 +56,8 @@ pub async fn api_key_create( state, &req, payload, - |state, auth_data, payload, _| async { - api_keys::create_api_key(state, payload, auth_data.key_store).await + |state, auth::AuthenticationDataWithoutProfile { key_store, .. }, payload, _| async { + api_keys::create_api_key(state, payload, key_store).await }, auth::auth_type( &auth::AdminApiAuthWithMerchantIdFromHeader, @@ -86,10 +86,15 @@ pub async fn api_key_retrieve( state, &req, &key_id, - |state, auth_data, key_id, _| { + |state, + auth::AuthenticationDataWithoutProfile { + merchant_account, .. + }, + key_id, + _| { api_keys::retrieve_api_key( state, - auth_data.merchant_account.get_id().to_owned(), + merchant_account.get_id().to_owned(), key_id.to_owned(), ) }, @@ -190,8 +195,13 @@ pub async fn api_key_update( state, &req, payload, - |state, authentication_data, mut payload, _| { - payload.merchant_id = authentication_data.merchant_account.get_id().to_owned(); + |state, + auth::AuthenticationDataWithoutProfile { + merchant_account, .. + }, + mut payload, + _| { + payload.merchant_id = merchant_account.get_id().to_owned(); api_keys::update_api_key(state, payload) }, auth::auth_type( @@ -319,8 +329,13 @@ pub async fn api_key_list( state, &req, payload, - |state, authentication_data, payload, _| async move { - let merchant_id = authentication_data.merchant_account.get_id().to_owned(); + |state, + auth::AuthenticationDataWithoutProfile { + merchant_account, .. + }, + payload, + _| async move { + let merchant_id = merchant_account.get_id().to_owned(); api_keys::list_api_keys(state, merchant_id, payload.limit, payload.skip).await }, auth::auth_type( diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index ee6bd68ac2..d747f96b9f 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -674,9 +674,8 @@ impl Payments { #[cfg(any(feature = "olap", feature = "oltp"))] pub struct Forex; -#[cfg(any(feature = "olap", feature = "oltp"))] +#[cfg(all(any(feature = "olap", feature = "oltp"), feature = "v1"))] impl Forex { - #[cfg(feature = "v1")] pub fn server(state: AppState) -> Scope { web::scope("/forex") .app_data(web::Data::new(state.clone())) @@ -686,10 +685,6 @@ impl Forex { web::resource("/convert_from_minor").route(web::get().to(currency::convert_forex)), ) } - #[cfg(feature = "v2")] - pub fn server(state: AppState) -> Scope { - todo!() - } } #[cfg(feature = "olap")] @@ -1064,13 +1059,8 @@ impl Refunds { #[cfg(feature = "payouts")] pub struct Payouts; -#[cfg(feature = "payouts")] +#[cfg(all(feature = "payouts", feature = "v1"))] impl Payouts { - #[cfg(feature = "v2")] - pub fn server(state: AppState) -> Scope { - todo!() - } - #[cfg(feature = "v1")] pub fn server(state: AppState) -> Scope { let mut route = web::scope("/payouts").app_data(web::Data::new(state)); route = route.service(web::resource("/create").route(web::post().to(payouts_create))); @@ -1578,17 +1568,13 @@ impl Disputes { pub struct Cards; +#[cfg(feature = "v1")] impl Cards { - #[cfg(feature = "v1")] pub fn server(state: AppState) -> Scope { web::scope("/cards") .app_data(web::Data::new(state)) .service(web::resource("/{bin}").route(web::get().to(card_iin_info))) } - #[cfg(feature = "v2")] - pub fn server(state: AppState) -> Scope { - todo!() - } } pub struct Files; @@ -1647,9 +1633,8 @@ impl PaymentLink { #[cfg(feature = "payouts")] pub struct PayoutLink; -#[cfg(feature = "payouts")] +#[cfg(all(feature = "payouts", feature = "v1"))] impl PayoutLink { - #[cfg(feature = "v1")] pub fn server(state: AppState) -> Scope { let mut route = web::scope("/payout_link").app_data(web::Data::new(state)); route = route.service( @@ -1657,10 +1642,6 @@ impl PayoutLink { ); route } - #[cfg(feature = "v2")] - pub fn server(state: AppState) -> Scope { - todo!() - } } pub struct Profile; @@ -1789,7 +1770,7 @@ impl ProfileNew { } #[cfg(feature = "v2")] pub fn server(state: AppState) -> Scope { - todo!() + web::scope("/account/{account_id}/profile").app_data(web::Data::new(state)) } } diff --git a/crates/router/src/routes/profiles.rs b/crates/router/src/routes/profiles.rs index 6c2e5407cf..1b98160546 100644 --- a/crates/router/src/routes/profiles.rs +++ b/crates/router/src/routes/profiles.rs @@ -56,9 +56,13 @@ pub async fn profile_create( state, &req, payload, - |state, auth_data, req, _| { - create_profile(state, req, auth_data.merchant_account, auth_data.key_store) - }, + |state, + auth::AuthenticationDataWithoutProfile { + merchant_account, + key_store, + }, + req, + _| { create_profile(state, req, merchant_account, key_store) }, auth::auth_type( &auth::AdminApiAuthWithMerchantIdFromHeader, &auth::JWTAuthMerchantFromHeader { @@ -118,7 +122,9 @@ pub async fn profile_retrieve( state, &req, profile_id, - |state, auth_data, profile_id, _| retrieve_profile(state, profile_id, auth_data.key_store), + |state, auth::AuthenticationDataWithoutProfile { key_store, .. }, profile_id, _| { + retrieve_profile(state, profile_id, key_store) + }, auth::auth_type( &auth::AdminApiAuthWithMerchantIdFromHeader, &auth::JWTAuthMerchantFromHeader { @@ -181,7 +187,9 @@ pub async fn profile_update( state, &req, json_payload.into_inner(), - |state, auth_data, req, _| update_profile(state, &profile_id, auth_data.key_store, req), + |state, auth::AuthenticationDataWithoutProfile { key_store, .. }, req, _| { + update_profile(state, &profile_id, key_store, req) + }, auth::auth_type( &auth::AdminApiAuthWithMerchantIdFromHeader, &auth::JWTAuthMerchantFromHeader { @@ -217,6 +225,8 @@ pub async fn profile_delete( ) .await } + +#[cfg(feature = "v1")] #[instrument(skip_all, fields(flow = ?Flow::ProfileList))] pub async fn profiles_list( state: web::Data, @@ -233,7 +243,38 @@ pub async fn profiles_list( merchant_id.clone(), |state, _auth, merchant_id, _| list_profile(state, merchant_id, None), auth::auth_type( - &auth::AdminApiAuthWithMerchantIdFromHeader, + &auth::AdminApiAuthWithMerchantIdFromRoute(merchant_id.clone()), + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: permissions::Permission::MerchantAccountRead, + }, + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "v2")] +#[instrument(skip_all, fields(flow = ?Flow::ProfileList))] +pub async fn profiles_list( + state: web::Data, + req: HttpRequest, + path: web::Path, +) -> HttpResponse { + let flow = Flow::ProfileList; + let merchant_id = path.into_inner(); + + Box::pin(api::server_wrap( + flow, + state, + &req, + merchant_id.clone(), + |state, auth::AuthenticationDataWithoutProfile { .. }, merchant_id, _| { + list_profile(state, merchant_id, None) + }, + auth::auth_type( + &auth::AdminApiAuthWithMerchantIdFromRoute(merchant_id.clone()), &auth::JWTAuthMerchantFromRoute { merchant_id, required_permission: permissions::Permission::MerchantAccountRead, diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index a22d6bf1df..43f0448741 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -69,6 +69,12 @@ pub struct AuthenticationData { pub profile: domain::Profile, } +#[derive(Clone, Debug)] +pub struct AuthenticationDataWithoutProfile { + pub merchant_account: domain::MerchantAccount, + pub key_store: domain::MerchantKeyStore, +} + #[derive(Clone, Debug)] pub struct AuthenticationDataWithMultipleProfiles { pub merchant_account: domain::MerchantAccount, @@ -437,17 +443,6 @@ where .change_context(errors::ApiErrorResponse::Unauthorized) .attach_printable("Failed to fetch merchant key store for the merchant id")?; - let profile = state - .store() - .find_business_profile_by_merchant_id_profile_id( - key_manager_state, - &key_store, - &stored_api_key.merchant_id, - &profile_id, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; - let merchant = state .store() .find_merchant_account_by_merchant_id( @@ -458,6 +453,12 @@ where .await .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + let profile = state + .store() + .find_business_profile_by_profile_id(key_manager_state, &key_store, &profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + let auth = AuthenticationData { merchant_account: merchant, key_store, @@ -683,14 +684,14 @@ where let key_manager_state = &(&state.session_state()).into(); let profile = state .store() - .find_business_profile_by_merchant_id_profile_id( + .find_business_profile_by_profile_id( key_manager_state, &auth_data.key_store, - auth_data.merchant_account.get_id(), &profile_id, ) .await .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + let auth_data_v2 = AuthenticationData { merchant_account: auth_data.merchant_account, key_store: auth_data.key_store, @@ -1006,6 +1007,53 @@ where } } +#[cfg(feature = "v2")] +#[async_trait] +impl AuthenticateAndFetch + for AdminApiAuthWithMerchantIdFromRoute +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationDataWithoutProfile, AuthenticationType)> { + AdminApiAuth + .authenticate_and_fetch(request_headers, state) + .await?; + + let merchant_id = self.0.clone(); + + let key_manager_state = &(&state.session_state()).into(); + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + &merchant_id, + &state.store().get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + + let merchant = state + .store() + .find_merchant_account_by_merchant_id(key_manager_state, &merchant_id, &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + + let auth = AuthenticationDataWithoutProfile { + merchant_account: merchant, + key_store, + }; + + Ok(( + auth, + AuthenticationType::AdminApiAuthWithMerchantId { merchant_id }, + )) + } +} + /// A helper struct to extract headers from the request pub(crate) struct HeaderMapStruct<'a> { headers: &'a HeaderMap, @@ -1181,6 +1229,53 @@ where } } +#[cfg(feature = "v2")] +#[async_trait] +impl AuthenticateAndFetch + for AdminApiAuthWithMerchantIdFromHeader +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationDataWithoutProfile, AuthenticationType)> { + AdminApiAuth + .authenticate_and_fetch(request_headers, state) + .await?; + + let merchant_id = HeaderMapStruct::new(request_headers) + .get_id_type_from_header::(headers::X_MERCHANT_ID)?; + + let key_manager_state = &(&state.session_state()).into(); + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + &merchant_id, + &state.store().get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; + + let merchant = state + .store() + .find_merchant_account_by_merchant_id(key_manager_state, &merchant_id, &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + + let auth = AuthenticationDataWithoutProfile { + merchant_account: merchant, + key_store, + }; + Ok(( + auth, + AuthenticationType::AdminApiAuthWithMerchantId { merchant_id }, + )) + } +} + #[derive(Debug)] pub struct EphemeralKeyAuth; @@ -1827,6 +1922,72 @@ where } } +#[cfg(feature = "v2")] +#[async_trait] +impl AuthenticateAndFetch for JWTAuthMerchantFromHeader +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationDataWithoutProfile, AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + if payload.check_in_blacklist(state).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } + + let role_info = authorization::get_role_info(state, &payload).await?; + authorization::check_permission(&self.required_permission, &role_info)?; + + let merchant_id_from_header = HeaderMapStruct::new(request_headers) + .get_id_type_from_header::(headers::X_MERCHANT_ID)?; + + // Check if token has access to MerchantId that has been requested through headers + if payload.merchant_id != merchant_id_from_header { + return Err(report!(errors::ApiErrorResponse::InvalidJwtToken)); + } + + let key_manager_state = &(&state.session_state()).into(); + + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + &payload.merchant_id, + &state.store().get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InvalidJwtToken) + .attach_printable("Failed to fetch merchant key store for the merchant id")?; + + let merchant = state + .store() + .find_merchant_account_by_merchant_id( + key_manager_state, + &payload.merchant_id, + &key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InvalidJwtToken) + .attach_printable("Failed to fetch merchant account for the merchant id")?; + + let auth = AuthenticationDataWithoutProfile { + merchant_account: merchant, + key_store, + }; + + Ok(( + auth, + AuthenticationType::MerchantJwt { + merchant_id: payload.merchant_id, + user_id: Some(payload.user_id), + }, + )) + } +} + #[async_trait] impl AuthenticateAndFetch<(), A> for JWTAuthMerchantFromRoute where @@ -2000,6 +2161,68 @@ where )) } } + +#[cfg(feature = "v2")] +#[async_trait] +impl AuthenticateAndFetch for JWTAuthMerchantFromRoute +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationDataWithoutProfile, AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + + if payload.check_in_blacklist(state).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } + + if payload.merchant_id != self.merchant_id { + return Err(report!(errors::ApiErrorResponse::InvalidJwtToken)); + } + + let role_info = authorization::get_role_info(state, &payload).await?; + authorization::check_permission(&self.required_permission, &role_info)?; + + let key_manager_state = &(&state.session_state()).into(); + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + &payload.merchant_id, + &state.store().get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InvalidJwtToken) + .attach_printable("Failed to fetch merchant key store for the merchant id")?; + + let merchant = state + .store() + .find_merchant_account_by_merchant_id( + key_manager_state, + &payload.merchant_id, + &key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InvalidJwtToken) + .attach_printable("Failed to fetch merchant account for the merchant id")?; + + let auth = AuthenticationDataWithoutProfile { + merchant_account: merchant, + key_store, + }; + Ok(( + auth.clone(), + AuthenticationType::MerchantJwt { + merchant_id: auth.merchant_account.get_id().clone(), + user_id: Some(payload.user_id), + }, + )) + } +} + pub struct JWTAuthMerchantAndProfileFromRoute { pub merchant_id: id_type::MerchantId, pub profile_id: id_type::ProfileId,